Phase 1.1 : mise en page et edition visuelle des noeuds
Evolution incrementale : aucun changement d'architecture, aucune fonctionnalite retiree, compatibilite .mmap v1 et IndexedDB preservee. Modele (format v2) - MindMapNode gagne `layout`, `iconStyle` et `branch`, tous optionnels. - NodeStyle gagne fontFamily, verticalAlign, lineHeight et shadow. - MapSettings gagne layoutSpacing, snapToGrid, snapGrid et alignmentGuides. - Un champ absent signifie "herite du theme" : rien n'est ecrit dans le document pour materialiser un defaut, donc les cartes deja stockees restent lisibles telles quelles. Migration v1 -> v2 : style.width devient layout.width, toujours lu en repli par le resolveur. Mise en page - Nouveau resolveur de style qui fusionne theme et personnalisations. - Taille automatique fidele au contenu, taille personnalisee bornee, redimensionnement a la souris via NodeResizer. - Padding horizontal et vertical, retour a la ligne, largeur maximale du texte, interligne, police, alignement horizontal et vertical, ombre. - Icones : emojis conserves, bibliotheque Lucide ajoutee, position, taille et espacement configurables. Multi-selection - Barre contextuelle d'alignement (6 axes) et de distribution (2 axes). - Application d'un reglage a toute la selection, avec etat "Mixte". Style - Copier / coller / reinitialiser le style, au clavier et par clic droit. Auto-layout - Espacement parent/enfant et entre branches configurable, presets Compact, Normal et Aere. Le layout reste declenche uniquement a la demande. Historique - Une seule entree par redimensionnement complet, par alignement, par distribution, par application multiple et par collage de style. Panneau Proprietes - Reorganise en cinq sections repliables dont l'etat est conserve pendant la session. Vraies icones d'alignement, tooltips et aria-labels partout. Corrections - Largeur des noeuds : word-break et overflow-wrap:anywhere ramenaient la largeur min-content a un caractere, ce qui faisait s'effondrer tous les noeuds automatiques. - Largeur auto en max-content bornee, pour qu'une phrase longue occupe la largeur maximale au lieu de former une colonne etroite. Tests - 85 tests vitest (43 -> 85) et 31 tests pytest (25 -> 31). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,12 @@ import {
|
||||
ZoomOutIcon,
|
||||
} from '@/components/Icons';
|
||||
import { ExportMenu } from '@/features/export/ExportMenu';
|
||||
import { LAYOUT_OPTIONS } from '@/features/editor/layout';
|
||||
import {
|
||||
LAYOUT_OPTIONS,
|
||||
SPACING_LIMITS,
|
||||
SPACING_PRESETS,
|
||||
resolveSpacing,
|
||||
} from '@/features/editor/layout';
|
||||
import { THEMES } from '@/features/editor/themes';
|
||||
import { selectCanRedo, selectCanUndo, useEditorStore } from '@/stores/editorStore';
|
||||
import { useUiStore } from '@/stores/uiStore';
|
||||
@@ -71,6 +76,7 @@ export function EditorToolbar({ onOpenShare, onToggleSearch }: EditorToolbarProp
|
||||
|
||||
const { zoomIn, zoomOut } = useReactFlow();
|
||||
const [name, setName] = useState(map?.name ?? '');
|
||||
const spacing = resolveSpacing(map?.settings.layoutSpacing);
|
||||
|
||||
useEffect(() => setName(map?.name ?? ''), [map?.id, map?.name]);
|
||||
|
||||
@@ -160,6 +166,71 @@ export function EditorToolbar({ onOpenShare, onToggleSearch }: EditorToolbarProp
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<MenuSeparator />
|
||||
<MenuLabel>Espacement</MenuLabel>
|
||||
<div className="flex gap-1 px-2.5 pb-2">
|
||||
{SPACING_PRESETS.map((preset) => (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => updateSettings({ layoutSpacing: preset.spacing })}
|
||||
aria-pressed={
|
||||
spacing.parentGap === preset.spacing.parentGap &&
|
||||
spacing.siblingGap === preset.spacing.siblingGap
|
||||
}
|
||||
className={`flex-1 rounded-md border px-2 py-1 text-xs font-medium transition-colors ${
|
||||
spacing.parentGap === preset.spacing.parentGap &&
|
||||
spacing.siblingGap === preset.spacing.siblingGap
|
||||
? 'border-indigo-400 bg-indigo-50 text-indigo-700 dark:bg-indigo-950/50 dark:text-indigo-300'
|
||||
: 'border-zinc-200 text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800'
|
||||
}`}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 px-2.5 pb-2">
|
||||
<label className="block text-[11px] text-zinc-500" htmlFor="spacing-parent">
|
||||
Distance parent / enfant : {spacing.parentGap} px
|
||||
</label>
|
||||
<input
|
||||
id="spacing-parent"
|
||||
type="range"
|
||||
min={SPACING_LIMITS.parentGap.min}
|
||||
max={SPACING_LIMITS.parentGap.max}
|
||||
step={5}
|
||||
value={spacing.parentGap}
|
||||
onChange={(event) =>
|
||||
updateSettings({
|
||||
layoutSpacing: { ...spacing, parentGap: Number(event.target.value) },
|
||||
})
|
||||
}
|
||||
className="h-1.5 w-full accent-indigo-600"
|
||||
/>
|
||||
|
||||
<label className="block text-[11px] text-zinc-500" htmlFor="spacing-sibling">
|
||||
Espacement entre branches : {spacing.siblingGap} px
|
||||
</label>
|
||||
<input
|
||||
id="spacing-sibling"
|
||||
type="range"
|
||||
min={SPACING_LIMITS.siblingGap.min}
|
||||
max={SPACING_LIMITS.siblingGap.max}
|
||||
step={5}
|
||||
value={spacing.siblingGap}
|
||||
onChange={(event) =>
|
||||
updateSettings({
|
||||
layoutSpacing: { ...spacing, siblingGap: Number(event.target.value) },
|
||||
})
|
||||
}
|
||||
className="h-1.5 w-full accent-indigo-600"
|
||||
/>
|
||||
<p className="text-[11px] leading-snug text-zinc-400">
|
||||
Changer l'espacement ne deplace rien : relancez l'organisation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<MenuSeparator />
|
||||
<MenuItem
|
||||
label="Reorganiser avec la disposition actuelle"
|
||||
@@ -211,6 +282,19 @@ export function EditorToolbar({ onOpenShare, onToggleSearch }: EditorToolbarProp
|
||||
label={map.settings.showGrid ? 'Masquer la grille' : 'Afficher la grille'}
|
||||
onSelect={() => updateSettings({ showGrid: !map.settings.showGrid })}
|
||||
/>
|
||||
<MenuItem
|
||||
label="Aimanter sur la grille"
|
||||
active={map.settings.snapToGrid === true}
|
||||
hint={`${map.settings.snapGrid ?? 20} px`}
|
||||
onSelect={() => updateSettings({ snapToGrid: !map.settings.snapToGrid })}
|
||||
/>
|
||||
<MenuItem
|
||||
label="Guides d'alignement"
|
||||
active={map.settings.alignmentGuides !== false}
|
||||
onSelect={() =>
|
||||
updateSettings({ alignmentGuides: map.settings.alignmentGuides === false })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
Reference in New Issue
Block a user