Files
homelable/frontend/src/utils/alignmentSettings.ts
T
Pouzor 004a8f19c1 feat(canvas): alignment guides + snap while dragging nodes
Draw.io / Figma style: while dragging a node, show dashed cyan guide
lines when its edges (left / center / right / top / middle / bottom)
align with another node's, and snap the position to the matched line
within a configurable threshold.

- utils/alignment.ts: pure snap math, returns delta + guide segments.
  Same-size boxes show all aligned guides simultaneously.
- canvas/AlignmentGuides.tsx: SVG overlay locked to the React Flow
  viewport (panned/zoomed correctly).
- hooks/useAlignmentGuides.ts: wires onNodeDrag/onNodeDragStop, applies
  snap via setNodes, listens for Alt to temporarily disable.
- utils/alignmentSettings.ts: localStorage-backed prefs (enabled,
  threshold 2-16px) with a tiny CustomEvent pub-sub so the SettingsPanel
  and the drag hook stay in sync without a global store.
- Sidebar settings panel: toggle + threshold slider.

Multi-selection drag uses the union bounding box. Children with
parentId are skipped for v1 to avoid mixing absolute and parent-relative
coordinates. Hold Alt to escape snap.
2026-05-11 00:35:50 +02:00

44 lines
1.5 KiB
TypeScript

// Persisted client-side settings for alignment guides.
// Kept in localStorage (per-user UI preference, not canvas data).
// Same-tab updates propagate via a CustomEvent so the drag hook and the
// settings panel can stay in sync without a global store.
export interface AlignmentSettings {
enabled: boolean
threshold: number
}
export const DEFAULT_ALIGNMENT_SETTINGS: AlignmentSettings = { enabled: true, threshold: 6 }
const KEY = 'homelable.alignmentGuides'
const EVENT = 'homelable:alignment-settings-changed'
export function readAlignmentSettings(): AlignmentSettings {
try {
const raw = localStorage.getItem(KEY)
if (!raw) return DEFAULT_ALIGNMENT_SETTINGS
const parsed = JSON.parse(raw) as Partial<AlignmentSettings>
return {
enabled: parsed.enabled ?? DEFAULT_ALIGNMENT_SETTINGS.enabled,
threshold: typeof parsed.threshold === 'number' ? parsed.threshold : DEFAULT_ALIGNMENT_SETTINGS.threshold,
}
} catch {
return DEFAULT_ALIGNMENT_SETTINGS
}
}
export function writeAlignmentSettings(s: AlignmentSettings): void {
try {
localStorage.setItem(KEY, JSON.stringify(s))
window.dispatchEvent(new CustomEvent<AlignmentSettings>(EVENT, { detail: s }))
} catch {
/* quota / SSR */
}
}
export function subscribeAlignmentSettings(listener: (s: AlignmentSettings) => void): () => void {
const handler = (e: Event) => listener((e as CustomEvent<AlignmentSettings>).detail)
window.addEventListener(EVENT, handler)
return () => window.removeEventListener(EVENT, handler)
}