import { useEffect, useMemo, useState } from 'react'; import { AlignCenter, AlignLeft, AlignRight, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, } from 'lucide-react'; import { type BranchStyle, type FontFamilyId, type IconPosition, type NodeShape, type ShadowStyle, type TextAlign, type VerticalAlign, } from '@/types/map'; import { Button } from '@/components/Button'; import { IconButton } from '@/components/IconButton'; import { CloseIcon, CopyIcon, LinkIcon, TrashIcon } from '@/components/Icons'; import { getTheme, paletteForDepth, themeMetrics } from '@/features/editor/themes'; import { FONT_FAMILY_OPTIONS, LAYOUT_LIMITS, SHADOW_OPTIONS, resolveNodeAppearance, } from '@/features/editor/nodeStyleResolver'; import { BUILTIN_ICONS, BUILTIN_ICON_IDS, EMOJI_ICONS } from '@/features/editor/nodeIcons'; import { PanelSection } from '@/features/editor/panel/PanelSection'; import { ColorField, NumberField, SegmentedControl, SelectField, ToggleRow, } from '@/features/editor/panel/controls'; import { sharedValue } from '@/features/editor/panel/sharedValue'; import { useEditorStore } from '@/stores/editorStore'; import { useUiStore } from '@/stores/uiStore'; import { computeDepths } from '@/utils/tree'; import { sanitizeUrl } from '@/utils/url'; const SHAPES: Array<{ id: NodeShape; label: string }> = [ { id: 'rectangle', label: 'Rectangle' }, { id: 'rounded', label: 'Arrondi' }, { id: 'capsule', label: 'Capsule' }, { id: 'circle', label: 'Cercle' }, ]; const BRANCH_STYLES: Array<{ id: BranchStyle | 'theme'; label: string }> = [ { id: 'theme', label: 'Theme' }, { id: 'bezier', label: 'Bezier' }, { id: 'curved', label: 'Courbe' }, { id: 'straight', label: 'Droite' }, ]; const HORIZONTAL_ALIGN = [ { value: 'left' as TextAlign, label: 'Aligner a gauche', icon: }, { value: 'center' as TextAlign, label: 'Centrer', icon: }, { value: 'right' as TextAlign, label: 'Aligner a droite', icon: }, ]; const VERTICAL_ALIGN = [ { value: 'top' as VerticalAlign, label: 'Aligner en haut', icon: , }, { value: 'middle' as VerticalAlign, label: 'Centrer verticalement', icon: , }, { value: 'bottom' as VerticalAlign, label: 'Aligner en bas', icon: , }, ]; const ICON_POSITIONS: Array<{ id: IconPosition; label: string }> = [ { id: 'left', label: 'Gauche' }, { id: 'right', label: 'Droite' }, { id: 'top', label: 'Au-dessus' }, ]; const ICON_SIZES = [16, 20, 24, 32]; export function PropertiesPanel() { const map = useEditorStore((state) => state.map); const selectedIds = useEditorStore((state) => state.selectedNodeIds); const updateNode = useEditorStore((state) => state.updateNode); const updateNodeText = useEditorStore((state) => state.updateNodeText); const updateNodesStyle = useEditorStore((state) => state.updateNodesStyle); const updateNodesLayout = useEditorStore((state) => state.updateNodesLayout); const updateNodesIconStyle = useEditorStore((state) => state.updateNodesIconStyle); const updateNodesBranch = useEditorStore((state) => state.updateNodesBranch); const setNodesIcon = useEditorStore((state) => state.setNodesIcon); const resetNodesStyle = useEditorStore((state) => state.resetNodesStyle); const fitNodesToContent = useEditorStore((state) => state.fitNodesToContent); const copyStyle = useEditorStore((state) => state.copyStyle); const pasteStyle = useEditorStore((state) => state.pasteStyle); const hasStyleClipboard = useEditorStore((state) => state.styleClipboard !== null); const detachNode = useEditorStore((state) => state.detachNode); const deleteNodes = useEditorStore((state) => state.deleteNodes); const setPropertiesPanelOpen = useUiStore((state) => state.setPropertiesPanelOpen); const pushToast = useUiStore((state) => state.pushToast); const nodes = useMemo(() => { if (!map) return []; const wanted = new Set(selectedIds); return map.nodes.filter((node) => wanted.has(node.id)); }, [map, selectedIds]); const single = nodes.length === 1 ? nodes[0] : undefined; const ids = useMemo(() => nodes.map((node) => node.id), [nodes]); const [text, setText] = useState(''); const [url, setUrl] = useState(''); const [note, setNote] = useState(''); useEffect(() => { setText(single?.text ?? ''); setUrl(single?.url ?? ''); setNote(single?.note ?? ''); }, [single?.id, single?.text, single?.url, single?.note]); const theme = getTheme(map?.settings.theme); const metrics = themeMetrics(theme); const depths = useMemo(() => (map ? computeDepths(map.nodes) : new Map()), [map]); const referenceDepth = single ? (depths.get(single.id) ?? 0) : 0; const palette = paletteForDepth(theme, referenceDepth); const reference = single ? resolveNodeAppearance(single, referenceDepth, theme) : undefined; const header = (

Proprietes {nodes.length > 1 ? ( {nodes.length} noeuds ) : null}

} onClick={() => setPropertiesPanelOpen(false)} />
); if (nodes.length === 0) { return ( ); } const styleOf = sharedValue; const commitUrl = () => { if (!single) return; const trimmed = url.trim(); if (!trimmed) { updateNode(single.id, { url: undefined }); return; } const safe = sanitizeUrl(trimmed); if (!safe) { pushToast('Cette adresse est invalide. Utilisez un lien http(s) ou mailto.', 'error'); setUrl(single.url ?? ''); return; } setUrl(safe); updateNode(single.id, { url: safe }); }; return (