All checks were successful
Refonte du système de templates de lore en assemblage de blocs (façon Lore
d'Amsel), plus le support des cartes DungeonDraft (.dd2vtt) sur les scènes.
Templates de lore — éditeur de blocs en grille :
- Builder partagé `block-grid-builder` : palette de types de blocs à glisser sur
une grille 12 colonnes, placement LIBRE (x/y) et redimensionnement en largeur
ET hauteur par poignées, renommage en place. Remplace la liste + flèches de
template-create/template-edit (dédupliqués).
- Rendu : page-view (aperçu) honore largeur ET hauteur (grille à lignes fixes) ;
page-edit garde les colonnes (côte-à-côte) avec des hauteurs naturelles pour
l'édition. Repli empilé pour les templates sans mise en page.
- Modèle : `TemplateField` gagne `id` (clé stable) et `pos {x,y,w,h}`. Les valeurs
de page sont ancrées sur l'`id` (repli sur le nom) → renommer un bloc ne perd
plus son contenu. Sérialisation JSON additive, aucune migration ; correction au
passage de la non-relecture de `foundryPath` dans le converter.
Bloc image :
- Nouveau composant `image-block` (remplace la galerie pour les pages de lore) :
plusieurs images plein cadre (carrousel), déplacement (recadrage) et zoom
persistés PAR image et PAR page. Rendu `object-fit: contain` (image entière,
bordures noires) avec zoom 0,4×–4×.
- Stockage : `Page.imageFraming` (fieldKey → imageId → {x,y,scale}), colonne
`image_framing` (migration V8), DTO/mapper + passthrough export/import.
Scènes — source de carte au choix :
- Sélecteur Dungeon Alchemist (image/vidéo + .json, inchangé) / DungeonDraft
(.dd2vtt). Le .dd2vtt embarque l'image en base64 : extraction à l'upload
(→ média) + sidecar allégé (→ données) pour que l'export Foundry et le module
fonctionnent sans changement. Mode déduit à la relecture de l'extension.
Front-only, sans migration.
i18n fr/en, tests (converters back + helper front) et specs e2e mis à jour.
304 lines
6.0 KiB
SCSS
304 lines
6.0 KiB
SCSS
.page {
|
|
padding: 2rem 3rem;
|
|
max-width: 1000px;
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 2rem;
|
|
|
|
h1 {
|
|
font-size: 1.6rem;
|
|
font-weight: 700;
|
|
color: white;
|
|
margin: 0 0 0.25rem;
|
|
}
|
|
.subtitle {
|
|
color: #9ca3af;
|
|
font-size: 0.85rem;
|
|
margin: 0;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
gap: 0.6rem;
|
|
}
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: white;
|
|
margin: 1.5rem 0 0.25rem;
|
|
border-top: 1px solid #1e1e3a;
|
|
padding-top: 1rem;
|
|
}
|
|
|
|
.edit-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
// Conteneur des champs dynamiques.
|
|
// - Sans mise en page (hasLayout=false) : `display: contents` -> les .field
|
|
// restent des enfants flex directs de .edit-form (même gap, rendu inchangé).
|
|
// - Avec mise en page : grille 12 colonnes ; chaque .field est placé via
|
|
// [style.grid-column] / [style.grid-row].
|
|
.edit-fields {
|
|
display: contents;
|
|
|
|
// En saisie, on honore la LARGEUR (colonnes, pour le côte-à-côte) mais la
|
|
// hauteur reste NATURELLE (les widgets — textarea, tableau, image — ont besoin
|
|
// de respirer). Les blocs s'écoulent par ligne dans l'ordre (ligne, colonne).
|
|
&.edit-fields--grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(12, 1fr);
|
|
grid-auto-flow: row;
|
|
gap: 1.25rem 1rem;
|
|
align-items: start;
|
|
}
|
|
}
|
|
|
|
// Repli mobile : la grille se réempile en une colonne, en conservant l'espacement.
|
|
@media (max-width: 820px) {
|
|
.edit-fields.edit-fields--grid {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.25rem;
|
|
}
|
|
}
|
|
|
|
.field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.4rem;
|
|
|
|
label {
|
|
font-size: 0.82rem;
|
|
font-weight: 500;
|
|
color: #d1d5db;
|
|
}
|
|
|
|
input, textarea, select {
|
|
background: #1a1a2e;
|
|
border: 1px solid #2a2a3d;
|
|
color: white;
|
|
padding: 0.7rem 0.9rem;
|
|
border-radius: 6px;
|
|
font-size: 0.9rem;
|
|
font-family: inherit;
|
|
|
|
&:focus {
|
|
outline: none;
|
|
border-color: #6c63ff;
|
|
}
|
|
}
|
|
|
|
textarea { resize: vertical; }
|
|
}
|
|
|
|
.hint {
|
|
font-size: 0.76rem;
|
|
color: #6b7280;
|
|
margin: 0;
|
|
}
|
|
|
|
.btn-primary, .btn-secondary, .btn-danger, .btn-ai {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
padding: 0.6rem 1.1rem;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 0.88rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: background 0.15s, opacity 0.15s;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #6c63ff;
|
|
color: white;
|
|
|
|
&:hover:not(:disabled) { background: #5a52e0; }
|
|
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #2a2a3d;
|
|
color: #d1d5db;
|
|
|
|
&:hover { background: #363650; }
|
|
}
|
|
|
|
.btn-danger {
|
|
background: #3f1f1f;
|
|
color: #fca5a5;
|
|
|
|
&:hover { background: #5a2a2a; }
|
|
}
|
|
|
|
.btn-ai {
|
|
background: transparent;
|
|
color: #a5b4fc;
|
|
border: 1px solid #2a2a3d;
|
|
|
|
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
&:hover:not(:disabled) { background: #1f2937; }
|
|
&.active {
|
|
background: #1f2937;
|
|
border-color: #6c63ff;
|
|
color: white;
|
|
}
|
|
}
|
|
|
|
// Grille de saisie d'un champ Tableau (KEY_VALUE_LIST) — même esthétique
|
|
// que la grille des fiches de personnage (dynamic-fields-form).
|
|
.kv-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
|
gap: 8px;
|
|
|
|
.kv-cell {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 3px;
|
|
padding: 8px 6px;
|
|
background: rgba(255, 255, 255, 0.03);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 4px;
|
|
|
|
.kv-label {
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: #9ca3af;
|
|
}
|
|
|
|
input {
|
|
width: 100%;
|
|
text-align: center;
|
|
padding: 4px 6px;
|
|
background: rgba(0, 0, 0, 0.25);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 3px;
|
|
color: white;
|
|
font-family: 'Cinzel', 'EB Garamond', Georgia, serif;
|
|
font-weight: 700;
|
|
font-size: 1.05rem;
|
|
|
|
&:focus { outline: none; border-color: #6c63ff; }
|
|
}
|
|
}
|
|
|
|
.kv-empty {
|
|
padding: 8px;
|
|
font-size: 0.8rem;
|
|
color: #6b7280;
|
|
font-style: italic;
|
|
margin: 0;
|
|
}
|
|
}
|
|
|
|
// Éditeur d'un champ Tableau (TABLE) : colonnes du template, lignes libres.
|
|
.table-edit-wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.table-edit {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
|
|
th {
|
|
text-align: left;
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: #99f6e4;
|
|
padding: 6px 8px;
|
|
border-bottom: 1px solid rgba(20, 184, 166, 0.35);
|
|
}
|
|
|
|
td {
|
|
padding: 4px 4px;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
|
|
input {
|
|
width: 100%;
|
|
padding: 6px 8px;
|
|
background: rgba(0, 0, 0, 0.25);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 3px;
|
|
color: white;
|
|
font-size: 0.88rem;
|
|
|
|
&:focus { outline: none; border-color: #14b8a6; }
|
|
}
|
|
}
|
|
|
|
.table-edit-actions-col {
|
|
width: 34px;
|
|
text-align: center;
|
|
}
|
|
|
|
.btn-row-delete {
|
|
background: transparent;
|
|
border: none;
|
|
color: #6b7280;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 4px;
|
|
|
|
&:hover { color: #f87171; }
|
|
}
|
|
}
|
|
|
|
.btn-row-add {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.3rem;
|
|
background: transparent;
|
|
border: 1px dashed rgba(20, 184, 166, 0.5);
|
|
border-radius: 5px;
|
|
color: #5eead4;
|
|
font-size: 0.8rem;
|
|
padding: 0.35rem 0.7rem;
|
|
cursor: pointer;
|
|
|
|
&:hover { background: rgba(20, 184, 166, 0.08); }
|
|
}
|
|
|
|
.ai-error-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
background: #3f1f1f;
|
|
color: #fca5a5;
|
|
border: 1px solid #7f1d1d;
|
|
border-radius: 6px;
|
|
padding: 0.7rem 1rem;
|
|
margin-bottom: 1.25rem;
|
|
font-size: 0.88rem;
|
|
|
|
.ai-error-dismiss {
|
|
background: transparent;
|
|
border: none;
|
|
color: #fca5a5;
|
|
font-size: 1.2rem;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
padding: 0 0.25rem;
|
|
|
|
&:hover { color: white; }
|
|
}
|
|
}
|