'use client' import { useScene, type ZoneNode } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { Check, Pencil } from 'lucide-react' import { useCallback, useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { useShallow } from 'zustand/react/shallow' import useEditor from '@/store/use-editor' // ─── Per-zone label editor ──────────────────────────────────────────────────── function ZoneLabelEditor({ zoneId }: { zoneId: ZoneNode['id'] }) { const zone = useScene((s) => s.nodes[zoneId] as ZoneNode | undefined) const updateNode = useScene((s) => s.updateNode) const setSelection = useViewer((s) => s.setSelection) const [editing, setEditing] = useState(false) const [value, setValue] = useState('') const inputRef = useRef(null) const [labelEl, setLabelEl] = useState(null) // Keep a ref so the click handler never has a stale zone name const zoneNameRef = useRef(zone?.name ?? '') useEffect(() => { zoneNameRef.current = zone?.name ?? '' }, [zone?.name]) // Setup: find the label element, enable pointer events, and hide the // zone-renderer's own text node (children[0]) — we replace it via portal. useEffect(() => { const el = document.getElementById(`${zoneId}-label`) if (!el) return setLabelEl(el) el.style.pointerEvents = 'auto' const textEl = el.children[0] as HTMLElement | undefined if (textEl) textEl.style.display = 'none' return () => { el.style.pointerEvents = '' if (textEl) textEl.style.display = '' } }, [zoneId]) // Focus + select-all when entering edit mode useEffect(() => { if (editing) { inputRef.current?.focus() inputRef.current?.select() } }, [editing]) const save = useCallback(() => { const trimmed = value.trim() if (trimmed !== (zone?.name ?? '')) { updateNode(zoneId, { name: trimmed || undefined }) } setEditing(false) }, [value, zone?.name, updateNode, zoneId]) const cancel = useCallback(() => { setValue(zone?.name ?? '') setEditing(false) }, [zone?.name]) if (!labelEl) return null const shadowColor = zone?.color ?? '#6366f1' const textShadow = [ `-1px -1px 0 ${shadowColor}`, ` 1px -1px 0 ${shadowColor}`, `-1px 1px 0 ${shadowColor}`, ` 1px 1px 0 ${shadowColor}`, ].join(',') // order: -1 puts this flex item before children[0] (hidden) and children[1] (pin) const sharedStyle: React.CSSProperties = { order: -1, color: 'white', textShadow, fontSize: 14, fontFamily: 'sans-serif', userSelect: 'none', display: 'inline-flex', alignItems: 'center', gap: 4, whiteSpace: 'nowrap', } return createPortal( editing ? (
e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} > setValue(e.target.value)} onKeyDown={(e) => { e.stopPropagation() if (e.key === 'Enter') { e.preventDefault(); save() } if (e.key === 'Escape') { e.preventDefault(); cancel() } }} onBlur={save} onClick={(e) => e.stopPropagation()} style={{ width: `${Math.max((value || zone?.name || '').length + 1, 4)}ch`, border: 'none', borderBottom: `1px solid ${shadowColor}`, background: 'transparent', color: 'white', textShadow, outline: 'none', padding: 0, margin: 0, fontSize: 'inherit', lineHeight: 'inherit', fontFamily: 'inherit', textAlign: 'center', }} />
) : ( ), labelEl, ) } // ─── System: rendered in the main React tree (outside Canvas) ───────────────── export function ZoneLabelEditorSystem() { const zoneIds = useScene( useShallow((s) => Object.values(s.nodes) .filter((n) => n.type === 'zone') .map((n) => n.id as ZoneNode['id']), ), ) const structureLayer = useEditor((s) => s.structureLayer) const mode = useEditor((s) => s.mode) if (structureLayer !== 'zones' || mode !== 'select') return null return ( <> {zoneIds.map((id) => ( ))} ) }