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:
@@ -1,27 +1,17 @@
|
||||
import { type Edge, type Node } from '@xyflow/react';
|
||||
import { Position } from '@xyflow/react';
|
||||
import { type BranchStyle, type MindMap, type MindMapNode, type NodeShape } from '@/types/map';
|
||||
import { type MapTheme, paletteForDepth } from '@/features/editor/themes';
|
||||
import { type BranchStyle, type MindMap, type MindMapNode } from '@/types/map';
|
||||
import { type MapTheme } from '@/features/editor/themes';
|
||||
import {
|
||||
resolveNodeAppearance,
|
||||
type NodeAppearance,
|
||||
} from '@/features/editor/nodeStyleResolver';
|
||||
import { isHorizontalLayout, layoutDirection } from '@/features/editor/layout';
|
||||
import { buildChildrenMap, computeDepths, getHiddenNodeIds } from '@/utils/tree';
|
||||
import { buildChildrenMap, buildNodeMap, computeDepths, getHiddenNodeIds } from '@/utils/tree';
|
||||
import { type NodeSize } from '@/utils/nodeSize';
|
||||
|
||||
/** Style CSS resolu d'un noeud : theme + personnalisations explicites. */
|
||||
export interface NodeAppearance {
|
||||
background: string;
|
||||
color: string;
|
||||
borderColor: string;
|
||||
borderWidth: number;
|
||||
borderRadius: string;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
fontWeight: number;
|
||||
fontStyle: 'normal' | 'italic';
|
||||
textAlign: 'left' | 'center' | 'right';
|
||||
boxShadow: string;
|
||||
width?: number;
|
||||
circle: boolean;
|
||||
}
|
||||
export { resolveNodeAppearance };
|
||||
export type { NodeAppearance };
|
||||
|
||||
export interface MindNodeData extends Record<string, unknown> {
|
||||
node: MindMapNode;
|
||||
@@ -38,51 +28,6 @@ export interface MindNodeData extends Record<string, unknown> {
|
||||
|
||||
export type MindFlowNode = Node<MindNodeData, 'mind'>;
|
||||
|
||||
function radiusForShape(shape: NodeShape, themeRadius: number): string {
|
||||
switch (shape) {
|
||||
case 'rectangle':
|
||||
return '3px';
|
||||
case 'capsule':
|
||||
return '999px';
|
||||
case 'circle':
|
||||
return '50%';
|
||||
case 'rounded':
|
||||
default:
|
||||
return `${themeRadius}px`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fusionne le theme et les personnalisations du noeud.
|
||||
* Une valeur definie explicitement sur le noeud gagne toujours : changer de
|
||||
* theme ne detruit donc jamais une personnalisation.
|
||||
*/
|
||||
export function resolveNodeAppearance(
|
||||
node: MindMapNode,
|
||||
depth: number,
|
||||
theme: MapTheme,
|
||||
): NodeAppearance {
|
||||
const palette = paletteForDepth(theme, depth);
|
||||
const style = node.style ?? {};
|
||||
const shape = style.shape ?? theme.nodeShape;
|
||||
|
||||
return {
|
||||
background: style.backgroundColor ?? palette.background,
|
||||
color: style.textColor ?? palette.text,
|
||||
borderColor: style.borderColor ?? palette.border,
|
||||
borderWidth: theme.borderWidth,
|
||||
borderRadius: radiusForShape(shape, theme.nodeRadius),
|
||||
fontSize: style.fontSize ?? (depth === 0 ? 16 : 14),
|
||||
fontFamily: theme.fontFamily,
|
||||
fontWeight: style.bold ? 700 : depth === 0 ? 600 : 500,
|
||||
fontStyle: style.italic ? 'italic' : 'normal',
|
||||
textAlign: style.textAlign ?? 'left',
|
||||
boxShadow: theme.shadow,
|
||||
...(style.width ? { width: style.width } : {}),
|
||||
circle: shape === 'circle',
|
||||
};
|
||||
}
|
||||
|
||||
function handlePositions(layout: MindMap['settings']['layout']): {
|
||||
source: Position;
|
||||
target: Position;
|
||||
@@ -163,20 +108,31 @@ function countBranch(children: Map<string, string[]>, rootId: string): number {
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Les liens heritent du style de branche de la carte, sauf si le noeud enfant
|
||||
* definit sa propre personnalisation (`node.branch`).
|
||||
*/
|
||||
export function toFlowEdges(map: MindMap, options: AdapterOptions): Edge[] {
|
||||
const hidden = getHiddenNodeIds(map.nodes);
|
||||
const edgeType = EDGE_TYPE_BY_BRANCH[map.settings.branchStyle] ?? 'default';
|
||||
const nodeMap = buildNodeMap(map.nodes);
|
||||
const defaultType = EDGE_TYPE_BY_BRANCH[map.settings.branchStyle] ?? 'default';
|
||||
|
||||
return map.edges.map((edge) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
type: edgeType,
|
||||
hidden: hidden.has(edge.source) || hidden.has(edge.target),
|
||||
deletable: !options.readOnly,
|
||||
style: {
|
||||
stroke: edge.color ?? options.theme.branchColor,
|
||||
strokeWidth: options.theme.branchWidth,
|
||||
},
|
||||
}));
|
||||
return map.edges.map((edge) => {
|
||||
const target = nodeMap.get(edge.target);
|
||||
const branch = target?.branch ?? {};
|
||||
const type = branch.style ? (EDGE_TYPE_BY_BRANCH[branch.style] ?? defaultType) : defaultType;
|
||||
|
||||
return {
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
type,
|
||||
hidden: hidden.has(edge.source) || hidden.has(edge.target),
|
||||
deletable: !options.readOnly,
|
||||
style: {
|
||||
stroke: branch.color ?? edge.color ?? options.theme.branchColor,
|
||||
strokeWidth: branch.width ?? options.theme.branchWidth,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user