Amélioration de l'UI : meilleur affichage des images que ce soit dans la partie lore ou la partie campagne (partie campagne : visualisation scrapbooking). Possibilité de réordonner les champs dans les templates...
Passage v0.3.0
This commit is contained in:
@@ -65,6 +65,7 @@
|
||||
<app-image-gallery
|
||||
[imageIds]="imageValues[field.name] || []"
|
||||
[editable]="true"
|
||||
[layout]="field.layout ?? 'GALLERY'"
|
||||
(imageIdsChange)="imageValues[field.name] = $event">
|
||||
</app-image-gallery>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,10 @@
|
||||
</section>
|
||||
<section class="view-section" *ngIf="field.type === 'IMAGE'">
|
||||
<h2 class="view-section-title">{{ field.name }}</h2>
|
||||
<app-image-gallery [imageIds]="imageIdsOf(field.name)"></app-image-gallery>
|
||||
<app-image-gallery
|
||||
[imageIds]="imageIdsOf(field.name)"
|
||||
[layout]="field.layout ?? 'GALLERY'">
|
||||
</app-image-gallery>
|
||||
</section>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@@ -37,7 +37,21 @@
|
||||
<label class="section-label">Champs du template *</label>
|
||||
|
||||
<ul class="fields-list">
|
||||
<li class="field-row" *ngFor="let f of fields; let i = index">
|
||||
<li class="field-row" *ngFor="let f of fields; let i = index; let first = first; let last = last">
|
||||
<div class="reorder-stack">
|
||||
<button type="button" class="btn-icon btn-reorder"
|
||||
(click)="moveField(i, -1)"
|
||||
[disabled]="first"
|
||||
aria-label="Monter d'un cran" title="Monter">
|
||||
<lucide-icon [img]="ChevronUp" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
<button type="button" class="btn-icon btn-reorder"
|
||||
(click)="moveField(i, 1)"
|
||||
[disabled]="last"
|
||||
aria-label="Descendre d'un cran" title="Descendre">
|
||||
<lucide-icon [img]="ChevronDown" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
<span class="field-chip" [class.field-chip-image]="f.type === 'IMAGE'">
|
||||
<lucide-icon [img]="f.type === 'IMAGE' ? ImageIcon : Type" [size]="12"></lucide-icon>
|
||||
{{ f.name }}
|
||||
@@ -49,6 +63,17 @@
|
||||
[title]="f.type === 'TEXT' ? 'Transformer en champ Image' : 'Transformer en champ Texte'">
|
||||
{{ f.type === 'TEXT' ? 'Texte' : 'Image' }}
|
||||
</button>
|
||||
<select *ngIf="f.type === 'IMAGE'"
|
||||
class="layout-select"
|
||||
[ngModel]="f.layout ?? 'GALLERY'"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
(ngModelChange)="setFieldLayout(i, $event)"
|
||||
title="Mise en page des images">
|
||||
<option value="GALLERY">Grille</option>
|
||||
<option value="HERO">Heros</option>
|
||||
<option value="MASONRY">Mosaique</option>
|
||||
<option value="CAROUSEL">Carrousel</option>
|
||||
</select>
|
||||
<button type="button" class="btn-icon" (click)="removeField(i)" aria-label="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
|
||||
@@ -124,7 +124,8 @@
|
||||
&:hover { background: #363650; color: white; }
|
||||
}
|
||||
|
||||
.type-select {
|
||||
.type-select,
|
||||
.layout-select {
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #2a2a3d;
|
||||
color: white;
|
||||
@@ -137,6 +138,12 @@
|
||||
&:focus { outline: none; border-color: #6c63ff; }
|
||||
}
|
||||
|
||||
.layout-select {
|
||||
height: 28px;
|
||||
font-size: 0.72rem;
|
||||
padding: 0 0.45rem;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
background: #1a1a2e;
|
||||
@@ -153,6 +160,35 @@
|
||||
}
|
||||
|
||||
&.add-row { margin-top: 0.5rem; }
|
||||
|
||||
.reorder-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.btn-reorder {
|
||||
width: 22px;
|
||||
height: 16px;
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
border: 1px solid #2a2a3d;
|
||||
border-radius: 3px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #2a2a3d;
|
||||
color: white;
|
||||
border-color: #6c63ff;
|
||||
}
|
||||
|
||||
&:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { LucideAngularModule, Plus, Trash2, Type, Image as ImageIcon } from 'lucide-angular';
|
||||
import { LucideAngularModule, Plus, Trash2, Type, Image as ImageIcon, ChevronUp, ChevronDown } from 'lucide-angular';
|
||||
import { LoreService } from '../../services/lore.service';
|
||||
import { TemplateService } from '../../services/template.service';
|
||||
import { PageService } from '../../services/page.service';
|
||||
import { LayoutService } from '../../services/layout.service';
|
||||
import { LoreNode } from '../../services/lore.model';
|
||||
import { FieldType, TemplateField } from '../../services/template.model';
|
||||
import { FieldType, ImageLayout, TemplateField } from '../../services/template.model';
|
||||
import { loadLoreSidebarData, buildLoreSidebarConfig } from '../lore-sidebar.helper';
|
||||
|
||||
/**
|
||||
@@ -29,6 +29,8 @@ export class TemplateCreateComponent implements OnInit, OnDestroy {
|
||||
readonly Trash2 = Trash2;
|
||||
readonly Type = Type;
|
||||
readonly ImageIcon = ImageIcon;
|
||||
readonly ChevronUp = ChevronUp;
|
||||
readonly ChevronDown = ChevronDown;
|
||||
|
||||
form: FormGroup;
|
||||
loreId = '';
|
||||
@@ -75,7 +77,10 @@ export class TemplateCreateComponent implements OnInit, OnDestroy {
|
||||
if (!name) return;
|
||||
// Unicite par nom (on ignore le type pour eviter des collisions d'affichage).
|
||||
if (this.fields.some(f => f.name === name)) return;
|
||||
this.fields = [...this.fields, { name, type: this.newFieldType }];
|
||||
const newField: TemplateField = this.newFieldType === 'IMAGE'
|
||||
? { name, type: 'IMAGE', layout: 'GALLERY' }
|
||||
: { name, type: 'TEXT' };
|
||||
this.fields = [...this.fields, newField];
|
||||
this.newFieldName = '';
|
||||
// Le type reste sur la derniere valeur choisie : pratique pour enchainer
|
||||
// plusieurs champs du meme type.
|
||||
@@ -85,12 +90,33 @@ export class TemplateCreateComponent implements OnInit, OnDestroy {
|
||||
this.fields = this.fields.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
/** Deplace un champ d'un cran vers le haut ou le bas. No-op aux bords. */
|
||||
moveField(index: number, direction: -1 | 1): void {
|
||||
const target = index + direction;
|
||||
if (target < 0 || target >= this.fields.length) return;
|
||||
const next = [...this.fields];
|
||||
[next[index], next[target]] = [next[target], next[index]];
|
||||
this.fields = next;
|
||||
}
|
||||
|
||||
/** Bascule le type d'un champ existant (TEXT <-> IMAGE). */
|
||||
toggleFieldType(index: number): void {
|
||||
const field = this.fields[index];
|
||||
if (!field) return;
|
||||
const nextType: FieldType = field.type === 'TEXT' ? 'IMAGE' : 'TEXT';
|
||||
this.fields = this.fields.map((f, i) => i === index ? { ...f, type: nextType } : f);
|
||||
this.fields = this.fields.map((f, i) => {
|
||||
if (i !== index) return f;
|
||||
return nextType === 'IMAGE'
|
||||
? { name: f.name, type: 'IMAGE', layout: f.layout ?? 'GALLERY' }
|
||||
: { name: f.name, type: 'TEXT' };
|
||||
});
|
||||
}
|
||||
|
||||
/** Met a jour le layout d'un champ IMAGE. */
|
||||
setFieldLayout(index: number, layout: ImageLayout): void {
|
||||
this.fields = this.fields.map((f, i) =>
|
||||
i === index && f.type === 'IMAGE' ? { ...f, layout } : f
|
||||
);
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
|
||||
@@ -43,7 +43,21 @@
|
||||
<label class="section-label">Champs du template</label>
|
||||
|
||||
<ul class="fields-list">
|
||||
<li class="field-row" *ngFor="let f of fields; let i = index">
|
||||
<li class="field-row" *ngFor="let f of fields; let i = index; let first = first; let last = last">
|
||||
<div class="reorder-stack">
|
||||
<button type="button" class="btn-icon-ghost btn-reorder"
|
||||
(click)="moveField(i, -1)"
|
||||
[disabled]="first"
|
||||
aria-label="Monter d'un cran" title="Monter">
|
||||
<lucide-icon [img]="ChevronUp" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
<button type="button" class="btn-icon-ghost btn-reorder"
|
||||
(click)="moveField(i, 1)"
|
||||
[disabled]="last"
|
||||
aria-label="Descendre d'un cran" title="Descendre">
|
||||
<lucide-icon [img]="ChevronDown" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
<span class="field-chip" [class.field-chip-image]="f.type === 'IMAGE'">
|
||||
<lucide-icon [img]="f.type === 'IMAGE' ? ImageIcon : Type" [size]="12"></lucide-icon>
|
||||
{{ f.name }}
|
||||
@@ -54,6 +68,17 @@
|
||||
[title]="f.type === 'TEXT' ? 'Transformer en champ Image' : 'Transformer en champ Texte'">
|
||||
{{ f.type === 'TEXT' ? 'Texte' : 'Image' }}
|
||||
</button>
|
||||
<select *ngIf="f.type === 'IMAGE'"
|
||||
class="layout-select"
|
||||
[ngModel]="f.layout ?? 'GALLERY'"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
(ngModelChange)="setFieldLayout(i, $event)"
|
||||
title="Mise en page des images">
|
||||
<option value="GALLERY">Grille</option>
|
||||
<option value="HERO">Heros</option>
|
||||
<option value="MASONRY">Mosaique</option>
|
||||
<option value="CAROUSEL">Carrousel</option>
|
||||
</select>
|
||||
<button type="button" class="btn-icon-ghost" (click)="removeField(i)" aria-label="Supprimer">
|
||||
<lucide-icon [img]="X" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
|
||||
@@ -125,7 +125,8 @@
|
||||
&:hover { color: #a5b4fc; background: #1f1b3a; }
|
||||
}
|
||||
|
||||
.type-select {
|
||||
.type-select,
|
||||
.layout-select {
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #2a2a3d;
|
||||
color: white;
|
||||
@@ -138,6 +139,12 @@
|
||||
&:focus { outline: none; border-color: #6c63ff; }
|
||||
}
|
||||
|
||||
.layout-select {
|
||||
height: 28px;
|
||||
font-size: 0.72rem;
|
||||
padding: 0 0.45rem;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
background: #1a1a2e;
|
||||
@@ -167,6 +174,35 @@
|
||||
&:focus { border: none; }
|
||||
}
|
||||
}
|
||||
|
||||
.reorder-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.btn-reorder {
|
||||
width: 22px;
|
||||
height: 16px;
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
border: 1px solid #2a2a3d;
|
||||
border-radius: 3px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #2a2a3d;
|
||||
color: white;
|
||||
border-color: #6c63ff;
|
||||
}
|
||||
|
||||
&:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon-ghost {
|
||||
|
||||
@@ -3,14 +3,14 @@ import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { LucideAngularModule, Plus, X, Trash2, Type, Image as ImageIcon } from 'lucide-angular';
|
||||
import { LucideAngularModule, Plus, X, Trash2, Type, Image as ImageIcon, ChevronUp, ChevronDown } from 'lucide-angular';
|
||||
import { LoreService } from '../../services/lore.service';
|
||||
import { TemplateService } from '../../services/template.service';
|
||||
import { PageService } from '../../services/page.service';
|
||||
import { LayoutService } from '../../services/layout.service';
|
||||
import { PageTitleService } from '../../services/page-title.service';
|
||||
import { LoreNode } from '../../services/lore.model';
|
||||
import { FieldType, Template, TemplateField } from '../../services/template.model';
|
||||
import { FieldType, ImageLayout, Template, TemplateField } from '../../services/template.model';
|
||||
import { loadLoreSidebarData, buildLoreSidebarConfig } from '../lore-sidebar.helper';
|
||||
|
||||
/**
|
||||
@@ -30,6 +30,8 @@ export class TemplateEditComponent implements OnInit, OnDestroy {
|
||||
readonly Trash2 = Trash2;
|
||||
readonly Type = Type;
|
||||
readonly ImageIcon = ImageIcon;
|
||||
readonly ChevronUp = ChevronUp;
|
||||
readonly ChevronDown = ChevronDown;
|
||||
|
||||
form: FormGroup;
|
||||
loreId = '';
|
||||
@@ -75,10 +77,12 @@ export class TemplateEditComponent implements OnInit, OnDestroy {
|
||||
this.template = template;
|
||||
// Copie defensive + normalisation du type (defaut TEXT si inconnu/manquant,
|
||||
// utile pour les templates legacy cote frontend meme si le backend le fait aussi).
|
||||
this.fields = (template.fields ?? []).map(f => ({
|
||||
name: f.name,
|
||||
type: f.type === 'IMAGE' ? 'IMAGE' : 'TEXT'
|
||||
}));
|
||||
this.fields = (template.fields ?? []).map(f => {
|
||||
const type: FieldType = f.type === 'IMAGE' ? 'IMAGE' : 'TEXT';
|
||||
return type === 'IMAGE'
|
||||
? { name: f.name, type, layout: f.layout ?? 'GALLERY' }
|
||||
: { name: f.name, type };
|
||||
});
|
||||
this.form.patchValue({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
@@ -91,7 +95,10 @@ export class TemplateEditComponent implements OnInit, OnDestroy {
|
||||
const name = this.newFieldName.trim();
|
||||
if (!name) return;
|
||||
if (this.fields.some(f => f.name === name)) return;
|
||||
this.fields = [...this.fields, { name, type: this.newFieldType }];
|
||||
const newField: TemplateField = this.newFieldType === 'IMAGE'
|
||||
? { name, type: 'IMAGE', layout: 'GALLERY' }
|
||||
: { name, type: 'TEXT' };
|
||||
this.fields = [...this.fields, newField];
|
||||
this.newFieldName = '';
|
||||
}
|
||||
|
||||
@@ -99,12 +106,33 @@ export class TemplateEditComponent implements OnInit, OnDestroy {
|
||||
this.fields = this.fields.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
/** Deplace un champ d'un cran vers le haut ou le bas. No-op aux bords. */
|
||||
moveField(index: number, direction: -1 | 1): void {
|
||||
const target = index + direction;
|
||||
if (target < 0 || target >= this.fields.length) return;
|
||||
const next = [...this.fields];
|
||||
[next[index], next[target]] = [next[target], next[index]];
|
||||
this.fields = next;
|
||||
}
|
||||
|
||||
/** Bascule le type d'un champ (TEXT <-> IMAGE). */
|
||||
toggleFieldType(index: number): void {
|
||||
const field = this.fields[index];
|
||||
if (!field) return;
|
||||
const nextType: FieldType = field.type === 'TEXT' ? 'IMAGE' : 'TEXT';
|
||||
this.fields = this.fields.map((f, i) => i === index ? { ...f, type: nextType } : f);
|
||||
this.fields = this.fields.map((f, i) => {
|
||||
if (i !== index) return f;
|
||||
return nextType === 'IMAGE'
|
||||
? { name: f.name, type: 'IMAGE', layout: f.layout ?? 'GALLERY' }
|
||||
: { name: f.name, type: 'TEXT' };
|
||||
});
|
||||
}
|
||||
|
||||
/** Met a jour le layout d'un champ IMAGE. */
|
||||
setFieldLayout(index: number, layout: ImageLayout): void {
|
||||
this.fields = this.fields.map((f, i) =>
|
||||
i === index && f.type === 'IMAGE' ? { ...f, layout } : f
|
||||
);
|
||||
}
|
||||
|
||||
save(): void {
|
||||
|
||||
Reference in New Issue
Block a user