@elytracms/studio 0.0.63 → 0.0.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/convex/assets.ts +11 -0
  2. package/convex/delivery.ts +5 -0
  3. package/convex/schema.ts +23 -0
  4. package/dist/components/assets/asset-thumb.d.ts +4 -2
  5. package/dist/components/assets/asset-thumb.js +26 -4
  6. package/dist/components/assets/asset-thumb.js.map +1 -1
  7. package/dist/components/assets/assets-module.js +48 -2
  8. package/dist/components/assets/assets-module.js.map +1 -1
  9. package/dist/components/assets/assets-store.d.ts +11 -1
  10. package/dist/components/assets/assets-store.js +18 -1
  11. package/dist/components/assets/assets-store.js.map +1 -1
  12. package/dist/components/builder/blocks-model.d.ts +11 -0
  13. package/dist/components/builder/blocks-model.js +26 -8
  14. package/dist/components/builder/blocks-model.js.map +1 -1
  15. package/dist/components/builder/canvas-chrome-css.d.ts +54 -0
  16. package/dist/components/builder/canvas-chrome-css.js +246 -0
  17. package/dist/components/builder/canvas-chrome-css.js.map +1 -0
  18. package/dist/components/builder/canvas-chrome.d.ts +122 -18
  19. package/dist/components/builder/canvas-chrome.js +148 -82
  20. package/dist/components/builder/canvas-chrome.js.map +1 -1
  21. package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
  22. package/dist/components/builder/canvas-inline-editing.js +3 -1
  23. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  24. package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
  25. package/dist/components/builder/canvas-overlay-geometry.js +80 -0
  26. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
  27. package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
  28. package/dist/components/builder/canvas-rect-tracking.js +198 -0
  29. package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
  30. package/dist/components/builder/canvas-render-context.d.ts +13 -0
  31. package/dist/components/builder/canvas-render-context.js +7 -5
  32. package/dist/components/builder/canvas-render-context.js.map +1 -1
  33. package/dist/components/builder/canvas-stylesheet.d.ts +1 -1
  34. package/dist/components/builder/canvas-stylesheet.js +16 -9
  35. package/dist/components/builder/canvas-stylesheet.js.map +1 -1
  36. package/dist/components/builder/canvas-theme-notice.d.ts +6 -0
  37. package/dist/components/builder/canvas-theme-notice.js +46 -0
  38. package/dist/components/builder/canvas-theme-notice.js.map +1 -0
  39. package/dist/components/builder/canvas.js +78 -22
  40. package/dist/components/builder/canvas.js.map +1 -1
  41. package/dist/components/builder/composition-field-editor.js +22 -3
  42. package/dist/components/builder/composition-field-editor.js.map +1 -1
  43. package/dist/components/builder/delete-key.d.ts +38 -0
  44. package/dist/components/builder/delete-key.js +46 -0
  45. package/dist/components/builder/delete-key.js.map +1 -0
  46. package/dist/components/builder/drag-session.d.ts +8 -0
  47. package/dist/components/builder/drag-session.js +15 -0
  48. package/dist/components/builder/drag-session.js.map +1 -1
  49. package/dist/components/builder/insert-affordance-context.d.ts +19 -0
  50. package/dist/components/builder/insert-affordance-context.js +7 -0
  51. package/dist/components/builder/insert-affordance-context.js.map +1 -0
  52. package/dist/components/builder/insert-context.d.ts +40 -0
  53. package/dist/components/builder/insert-context.js +86 -3
  54. package/dist/components/builder/insert-context.js.map +1 -1
  55. package/dist/components/builder/node-title.d.ts +60 -0
  56. package/dist/components/builder/node-title.js +135 -0
  57. package/dist/components/builder/node-title.js.map +1 -0
  58. package/dist/components/builder/page-tree.js +21 -3
  59. package/dist/components/builder/page-tree.js.map +1 -1
  60. package/dist/components/builder/prop-selection.d.ts +52 -0
  61. package/dist/components/builder/prop-selection.js +38 -0
  62. package/dist/components/builder/prop-selection.js.map +1 -0
  63. package/dist/components/builder/prop-sync-context.d.ts +49 -0
  64. package/dist/components/builder/prop-sync-context.js +23 -0
  65. package/dist/components/builder/prop-sync-context.js.map +1 -0
  66. package/dist/components/builder/property-inspector.js +23 -1
  67. package/dist/components/builder/property-inspector.js.map +1 -1
  68. package/dist/components/builder/reuse-context.js +1 -1
  69. package/dist/components/builder/reuse-context.js.map +1 -1
  70. package/dist/components/builder/rich-text-toolbar.d.ts +23 -0
  71. package/dist/components/builder/rich-text-toolbar.js +24 -17
  72. package/dist/components/builder/rich-text-toolbar.js.map +1 -1
  73. package/dist/components/builder/selection-overlay.d.ts +79 -0
  74. package/dist/components/builder/selection-overlay.js +244 -0
  75. package/dist/components/builder/selection-overlay.js.map +1 -0
  76. package/dist/components/builder/spacing-handles.d.ts +88 -1
  77. package/dist/components/builder/spacing-handles.js +191 -63
  78. package/dist/components/builder/spacing-handles.js.map +1 -1
  79. package/dist/components/cms/asset-picker.js +11 -5
  80. package/dist/components/cms/asset-picker.js.map +1 -1
  81. package/dist/components/cms/rich-text/block-style.d.ts +48 -0
  82. package/dist/components/cms/rich-text/block-style.js +30 -0
  83. package/dist/components/cms/rich-text/block-style.js.map +1 -0
  84. package/dist/components/cms/rich-text/embed-picker.js +4 -3
  85. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  86. package/dist/components/cms/rich-text/extensions.d.ts +2 -0
  87. package/dist/components/cms/rich-text/extensions.js +4 -0
  88. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  89. package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
  90. package/dist/components/cms/rich-text/font-size-control.js +40 -0
  91. package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
  92. package/dist/components/cms/rich-text/font-size.d.ts +16 -0
  93. package/dist/components/cms/rich-text/font-size.js +40 -0
  94. package/dist/components/cms/rich-text/font-size.js.map +1 -0
  95. package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
  96. package/dist/components/cms/rich-text/toolbar-state.js +3 -0
  97. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
  98. package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
  99. package/dist/components/cms/rich-text/toolbar.js +17 -5
  100. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  101. package/dist/components/components/component-detail.js +3 -3
  102. package/dist/components/components/component-detail.js.map +1 -1
  103. package/dist/components/components/component-preview.d.ts +2 -1
  104. package/dist/components/components/component-preview.js +25 -26
  105. package/dist/components/components/component-preview.js.map +1 -1
  106. package/dist/components/components/registry-browser.js +2 -2
  107. package/dist/components/components/registry-browser.js.map +1 -1
  108. package/dist/components/components/taxonomy.js +3 -2
  109. package/dist/components/components/taxonomy.js.map +1 -1
  110. package/dist/components/components/transfer-dialog.js +1 -1
  111. package/dist/components/components/transfer-dialog.js.map +1 -1
  112. package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
  113. package/dist/lib/assets/backfill-thumbs.js +120 -0
  114. package/dist/lib/assets/backfill-thumbs.js.map +1 -0
  115. package/dist/lib/assets/delete-asset.d.ts +2 -1
  116. package/dist/lib/assets/delete-asset.js +12 -0
  117. package/dist/lib/assets/delete-asset.js.map +1 -1
  118. package/dist/lib/assets/index.d.ts +2 -0
  119. package/dist/lib/assets/index.js +2 -0
  120. package/dist/lib/assets/index.js.map +1 -1
  121. package/dist/lib/assets/thumbnail.d.ts +145 -0
  122. package/dist/lib/assets/thumbnail.js +220 -0
  123. package/dist/lib/assets/thumbnail.js.map +1 -0
  124. package/dist/lib/assets/upload.d.ts +9 -0
  125. package/dist/lib/assets/upload.js +31 -0
  126. package/dist/lib/assets/upload.js.map +1 -1
  127. package/dist/lib/i18n/messages.de.js +20 -0
  128. package/dist/lib/i18n/messages.de.js.map +1 -1
  129. package/dist/lib/i18n/messages.en.d.ts +18 -0
  130. package/dist/lib/i18n/messages.en.js +21 -0
  131. package/dist/lib/i18n/messages.en.js.map +1 -1
  132. package/dist/lib/persistence/backend-mode.d.ts +7 -0
  133. package/dist/lib/persistence/backend-mode.js +9 -0
  134. package/dist/lib/persistence/backend-mode.js.map +1 -1
  135. package/dist/lib/persistence/convex-adapter.js +4 -0
  136. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  137. package/dist/lib/persistence/index.d.ts +1 -1
  138. package/dist/lib/persistence/index.js +1 -1
  139. package/dist/lib/persistence/index.js.map +1 -1
  140. package/dist/lib/persona/labels.d.ts +9 -5
  141. package/dist/lib/persona/labels.js +13 -8
  142. package/dist/lib/persona/labels.js.map +1 -1
  143. package/dist/lib/references/references-store.js +7 -1
  144. package/dist/lib/references/references-store.js.map +1 -1
  145. package/package.json +2 -2
  146. package/src/styles/studio.css +13 -0
@@ -5,17 +5,19 @@ import { getParentInfo } from '@elytracms/core/project-graph';
5
5
  import { canNestInto, containerFlowAxis, dropIndicatorFor, dropPositionForPointer, resolveDrop, } from '@elytracms/studio/lib/dnd/drop-rules';
6
6
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
7
7
  import { shortId } from '@elytracms/studio/lib/persona/labels';
8
- import { translate } from '@elytracms/studio/lib/i18n';
8
+ import { translate, useUiLocale } from '@elytracms/studio/lib/i18n';
9
9
  import { useData } from '@elytracms/studio/lib/data';
10
10
  import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Row, Spacer, Text, } from '@elytracms/studio/ui';
11
11
  import { cn } from '@elytracms/studio/lib/utils';
12
12
  import { useCollab } from '../collab/collab-context.js';
13
13
  import { useBuilder } from './builder-store.js';
14
14
  import { attachClipboardListeners, useStudioClipboard } from './clipboard-actions.js';
15
+ import { attachDeleteKeyListener } from './delete-key.js';
16
+ import { nodeDisplayTitle } from './node-title.js';
15
17
  import { useReuse } from './reuse-context.js';
16
18
  import { activeDrag, beginDrag, endDrag, onDragSessionEnd } from './drag-session.js';
17
19
  function flatten(node, depth, rows, meta) {
18
- rows.push({ id: node.id, componentId: node.componentId, depth, ...meta, condition: node.condition });
20
+ rows.push({ id: node.id, componentId: node.componentId, node, depth, ...meta, condition: node.condition });
19
21
  for (const children of Object.values(node.slots ?? {})) {
20
22
  children.forEach((child, i) => flatten(child, depth + 1, rows, {
21
23
  isFirst: i === 0,
@@ -40,6 +42,7 @@ export function PageTree({ onAddChild }) {
40
42
  const studio = getStudioRegistry();
41
43
  const data = useData();
42
44
  const collab = useCollab();
45
+ const uiLocale = useUiLocale();
43
46
  const rows = [];
44
47
  flatten(builder.present, 0, rows, { isFirst: true, isLast: true, isRoot: true });
45
48
  // Copy/cut/paste (EC-175): keyboard shortcuts for the whole studio window
@@ -59,6 +62,20 @@ export function PageTree({ onAddChild }) {
59
62
  getSelection: () => window.getSelection(),
60
63
  });
61
64
  }, [clipboard, readOnly]);
65
+ // Delete/Backspace removes the selected node from the tree side too (EC-404),
66
+ // beside the clipboard shortcuts. Inert in the read-only published preview and
67
+ // while focus is in a form field / inline editor (isEditingContext).
68
+ const { dispatch } = builder;
69
+ const rootId = builder.present.id;
70
+ useEffect(() => {
71
+ if (readOnly)
72
+ return;
73
+ return attachDeleteKeyListener(window, {
74
+ getSelectedId: () => selectedIdRef.current,
75
+ remove: (id) => dispatch({ type: 'remove', id }),
76
+ getRootId: () => rootId,
77
+ });
78
+ }, [readOnly, rootId, dispatch]);
62
79
  // Drag & drop (EC-160): eligibility comes from the same pure rule set the
63
80
  // canvas uses (`lib/dnd/drop-rules`); the tree only renders the resolution.
64
81
  // One eligible drop dispatches exactly one move/insert action.
@@ -165,6 +182,7 @@ export function PageTree({ onAddChild }) {
165
182
  clearDrop();
166
183
  }, children: rows.map((row) => {
167
184
  const manifest = studio.registry.getManifest(row.componentId);
185
+ const title = nodeDisplayTitle(manifest, row.node, uiLocale);
168
186
  const selected = row.id === builder.selectedId;
169
187
  const hovered = row.id === builder.hoveredId;
170
188
  // EC-088: indicate condition-gated nodes; flag the currently-hidden ones.
@@ -179,7 +197,7 @@ export function PageTree({ onAddChild }) {
179
197
  ? 'bg-muted text-foreground'
180
198
  : 'text-muted-foreground', drop?.rowId === row.id && drop.indicator
181
199
  ? ROW_DROP_CLASSES[drop.indicator]
182
- : undefined), children: [_jsxs("button", { type: "button", onClick: () => builder.dispatch({ type: 'select', id: row.id }), className: "flex min-w-0 flex-1 items-center gap-1.5 py-1", style: { paddingLeft: `${6 + row.depth * 14}px` }, children: [_jsx("span", { className: cn('truncate font-mono', condState === 'hidden' && 'line-through opacity-70'), children: shortId(row.componentId) }), manifest?.title ? (_jsx("span", { className: "truncate text-muted-foreground/70", children: manifest.title })) : null, condState === 'hidden' ? (_jsx(EyeOff, { className: "size-3 shrink-0 text-warning", "aria-label": translate('builder.pageTree.hiddenByCondition') })) : condState === 'visible' ? (_jsx(Filter, { className: "size-3 shrink-0 text-muted-foreground/60", "aria-label": translate('builder.pageTree.conditional') })) : null] }), _jsxs("div", { className: cn('flex shrink-0 items-center gap-0.5', selected
200
+ : undefined), children: [_jsxs("button", { type: "button", onClick: () => builder.dispatch({ type: 'select', id: row.id }), title: row.componentId, className: "flex min-w-0 flex-1 items-center gap-1.5 py-1", style: { paddingLeft: `${6 + row.depth * 14}px` }, children: [_jsx("span", { className: cn('truncate', condState === 'hidden' && 'line-through opacity-70'), children: title.primary }), title.secondary ? (_jsx("span", { className: "truncate text-muted-foreground/70", children: title.secondary })) : null, condState === 'hidden' ? (_jsx(EyeOff, { className: "size-3 shrink-0 text-warning", "aria-label": translate('builder.pageTree.hiddenByCondition') })) : condState === 'visible' ? (_jsx(Filter, { className: "size-3 shrink-0 text-muted-foreground/60", "aria-label": translate('builder.pageTree.conditional') })) : null] }), _jsxs("div", { className: cn('flex shrink-0 items-center gap-0.5', selected
183
201
  ? 'opacity-100'
184
202
  : 'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100'), children: [_jsx(IconAction, { label: translate('builder.pageTree.addChild'), onClick: () => onAddChild(row.id), children: _jsx(Plus, {}) }), _jsx(RowActionsMenu, { row: row, builder: builder, clipboard: clipboard })] })] }, row.id));
185
203
  }) })] }));
@@ -1 +1 @@
1
- {"version":3,"file":"page-tree.js","sourceRoot":"","sources":["../../../src/components/builder/page-tree.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n Boxes,\n ChevronUp,\n ChevronDown,\n ClipboardCopy,\n ClipboardPaste,\n ClipboardX,\n Copy,\n EyeOff,\n Filter,\n MoreHorizontal,\n Plus,\n Trash2,\n Unlink,\n} from 'lucide-react'\nimport { useEffect, useRef, useState } from 'react'\nimport type { Condition, ComponentNode } from '@elytracms/core/project-graph'\nimport { getParentInfo } from '@elytracms/core/project-graph'\nimport {\n canNestInto,\n containerFlowAxis,\n dropIndicatorFor,\n dropPositionForPointer,\n resolveDrop,\n} from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { DropIndicator, DropPlan, DropPosition } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useData } from '@elytracms/studio/lib/data'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Row,\n Spacer,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useCollab } from '../collab/collab-context'\nimport { useBuilder } from './builder-store'\nimport { attachClipboardListeners, useStudioClipboard } from './clipboard-actions'\nimport { useReuse } from './reuse-context'\nimport { activeDrag, beginDrag, endDrag, onDragSessionEnd } from './drag-session'\n\ninterface TreeRow {\n id: string\n componentId: string\n depth: number\n isRoot: boolean\n isFirst: boolean\n isLast: boolean\n condition?: Condition\n}\n\nfunction flatten(\n node: ComponentNode,\n depth: number,\n rows: TreeRow[],\n meta: { isFirst: boolean; isLast: boolean; isRoot: boolean },\n): void {\n rows.push({ id: node.id, componentId: node.componentId, depth, ...meta, condition: node.condition })\n for (const children of Object.values(node.slots ?? {})) {\n children.forEach((child, i) =>\n flatten(child, depth + 1, rows, {\n isFirst: i === 0,\n isLast: i === children.length - 1,\n isRoot: false,\n }),\n )\n }\n}\n\n/** Tailwind classes per drop indicator state (EC-160/EC-162) — rejection is visible. */\nconst ROW_DROP_CLASSES: Record<Exclude<DropIndicator, null>, string> = {\n into: 'ring-1 ring-inset ring-emerald-400 bg-emerald-400/10',\n before: 'shadow-[inset_0_2px_0_0_#38bdf8]',\n after: 'shadow-[inset_0_-2px_0_0_#38bdf8]',\n // Wrap-into-columns gesture (EC-162): the tree can execute wrap plans, the\n // bands themselves are a canvas gesture (rows have no spatial \"beside\").\n left: 'shadow-[inset_2px_0_0_0_#a78bfa] bg-violet-400/10',\n right: 'shadow-[inset_-2px_0_0_0_#a78bfa] bg-violet-400/10',\n blocked: 'ring-1 ring-inset ring-red-500/80 bg-red-500/10 cursor-not-allowed',\n}\n\nexport function PageTree({ onAddChild }: { onAddChild: (parentId: string) => void }) {\n const builder = useBuilder()\n const studio = getStudioRegistry()\n const data = useData()\n const collab = useCollab()\n const rows: TreeRow[] = []\n flatten(builder.present, 0, rows, { isFirst: true, isLast: true, isRoot: true })\n\n // Copy/cut/paste (EC-175): keyboard shortcuts for the whole studio window\n // (the canvas iframe wires its own document) plus per-row affordances. The\n // shared listener keeps shortcuts inert in form fields/inline editors, and\n // the published preview is read-only.\n const clipboard = useStudioClipboard()\n const readOnly = collab.previewMode === 'published'\n const selectedIdRef = useRef(builder.selectedId)\n selectedIdRef.current = builder.selectedId\n useEffect(() => {\n if (readOnly) return\n return attachClipboardListeners(window, {\n clipboard,\n getSelectedId: () => selectedIdRef.current,\n getSelection: () => window.getSelection(),\n })\n }, [clipboard, readOnly])\n\n // Drag & drop (EC-160): eligibility comes from the same pure rule set the\n // canvas uses (`lib/dnd/drop-rules`); the tree only renders the resolution.\n // One eligible drop dispatches exactly one move/insert action.\n // The current plan lives in a ref: `drop` fires synchronously after the\n // last `dragover`, before React flushes indicator state.\n const dropPlanRef = useRef<DropPlan | null>(null)\n const [drop, setDrop] = useState<{ rowId: string; indicator: DropIndicator } | null>(null)\n const clearDrop = () => {\n dropPlanRef.current = null\n setDrop(null)\n }\n useEffect(() => onDragSessionEnd(() => clearDrop()), [])\n const getManifest = (id: string) => studio.registry.getManifest(id)\n\n function handleRowDragStart(row: TreeRow, e: React.DragEvent) {\n if (row.isRoot) {\n e.preventDefault()\n return\n }\n beginDrag(\n {\n subject: { kind: 'node', nodeId: row.id, componentId: row.componentId },\n source: 'tree',\n label: shortId(row.componentId),\n },\n e.dataTransfer,\n )\n }\n\n function handleRowDragOver(row: TreeRow, e: React.DragEvent) {\n const drag = activeDrag()\n if (!drag) return\n let position: DropPosition = 'into'\n if (!row.isRoot) {\n const rect = e.currentTarget.getBoundingClientRect()\n // The hit-band axis follows the parent container's flow (EC-162):\n // children of horizontal Stacks/Grids reorder along the row's x axis.\n const flow = containerFlowAxis(getParentInfo(builder.present, row.id)?.parent)\n const ratio =\n flow === 'horizontal'\n ? rect.width > 0\n ? (e.clientX - rect.left) / rect.width\n : 0.5\n : rect.height > 0\n ? (e.clientY - rect.top) / rect.height\n : 0.5\n position = dropPositionForPointer({\n ratio,\n nestable: canNestInto(getManifest, row.componentId),\n edges: true,\n })\n }\n const resolution = resolveDrop({\n root: builder.present,\n getManifest,\n subject: drag.subject,\n location: { nodeId: row.id, position },\n })\n const indicator = dropIndicatorFor(resolution, position)\n dropPlanRef.current = resolution.eligible ? resolution : null\n setDrop({ rowId: row.id, indicator })\n if (resolution.eligible) {\n e.preventDefault()\n e.dataTransfer.dropEffect = drag.subject.kind === 'node' ? 'move' : 'copy'\n }\n }\n\n function handleRowDrop(e: React.DragEvent) {\n const drag = activeDrag()\n if (!drag) return\n const plan = dropPlanRef.current\n clearDrop()\n if (!plan) return\n e.preventDefault()\n // Exactly one dispatch per completed drop (one undo step, one command).\n if (plan.kind === 'move') {\n builder.dispatch({ type: 'move', id: plan.nodeId, target: plan.target })\n } else if (plan.kind === 'wrap') {\n // EC-162: shared plan vocabulary — the tree executes wrap plans too.\n const subject =\n plan.nodeId !== undefined\n ? { kind: 'node' as const, nodeId: plan.nodeId }\n : drag.buildNode\n ? { kind: 'component' as const, node: drag.buildNode() }\n : null\n if (!subject) {\n endDrag('cancelled')\n return\n }\n builder.dispatch({ type: 'wrap', referenceId: plan.referenceId, side: plan.side, subject })\n } else if (drag.buildNode) {\n builder.dispatch({ type: 'insert', target: plan.target, node: drag.buildNode() })\n } else {\n endDrag('cancelled')\n return\n }\n endDrag('dropped')\n }\n\n function handleRowDragEnd() {\n clearDrop()\n endDrag('cancelled')\n }\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <Row gap=\"2\" className=\"h-10 shrink-0 border-b border-border px-3\">\n <Text size=\"sm\" weight=\"semibold\">\n {translate('builder.pageTree.title')}\n </Text>\n <Spacer />\n <Text size=\"xs\" tone=\"subtle\" mono>\n {rows.length}\n </Text>\n </Row>\n <div\n className=\"min-h-0 flex-1 overflow-auto p-1.5\"\n onMouseLeave={() => builder.dispatch({ type: 'hover', id: null })}\n onDragLeave={(e) => {\n // Clear the indicator when the drag exits the tree panel.\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) clearDrop()\n }}\n >\n {rows.map((row) => {\n const manifest = studio.registry.getManifest(row.componentId)\n const selected = row.id === builder.selectedId\n const hovered = row.id === builder.hoveredId\n // EC-088: indicate condition-gated nodes; flag the currently-hidden ones.\n let condState: 'none' | 'visible' | 'hidden' = 'none'\n if (row.condition) {\n const r = data.resolveCondition(row.condition)\n condState = r.kind === 'condition' && r.pass ? 'visible' : 'hidden'\n }\n return (\n <div\n key={row.id}\n onMouseEnter={() => builder.dispatch({ type: 'hover', id: row.id })}\n draggable={!row.isRoot}\n onDragStart={(e) => handleRowDragStart(row, e)}\n onDragOver={(e) => handleRowDragOver(row, e)}\n onDrop={handleRowDrop}\n onDragEnd={handleRowDragEnd}\n data-tree-row={row.id}\n data-tree-drop={drop?.rowId === row.id ? (drop.indicator ?? undefined) : undefined}\n className={cn(\n 'group flex items-center rounded-md pr-1 text-left text-xs transition-colors',\n selected\n ? 'bg-accent text-accent-foreground'\n : hovered\n ? 'bg-muted text-foreground'\n : 'text-muted-foreground',\n drop?.rowId === row.id && drop.indicator\n ? ROW_DROP_CLASSES[drop.indicator]\n : undefined,\n )}\n >\n <button\n type=\"button\"\n onClick={() => builder.dispatch({ type: 'select', id: row.id })}\n className=\"flex min-w-0 flex-1 items-center gap-1.5 py-1\"\n style={{ paddingLeft: `${6 + row.depth * 14}px` }}\n >\n <span className={cn('truncate font-mono', condState === 'hidden' && 'line-through opacity-70')}>\n {shortId(row.componentId)}\n </span>\n {manifest?.title ? (\n <span className=\"truncate text-muted-foreground/70\">{manifest.title}</span>\n ) : null}\n {condState === 'hidden' ? (\n <EyeOff\n className=\"size-3 shrink-0 text-warning\"\n aria-label={translate('builder.pageTree.hiddenByCondition')}\n />\n ) : condState === 'visible' ? (\n <Filter\n className=\"size-3 shrink-0 text-muted-foreground/60\"\n aria-label={translate('builder.pageTree.conditional')}\n />\n ) : null}\n </button>\n\n <div\n className={cn(\n 'flex shrink-0 items-center gap-0.5',\n selected\n ? 'opacity-100'\n : 'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100',\n )}\n >\n {/* Add child stays inline — the one constructive, high-frequency\n action. Everything else collapses into the ••• menu so labels\n stop cropping on the 200–240px panel (EC-211). */}\n <IconAction label={translate('builder.pageTree.addChild')} onClick={() => onAddChild(row.id)}>\n <Plus />\n </IconAction>\n <RowActionsMenu row={row} builder={builder} clipboard={clipboard} />\n </div>\n </div>\n )\n })}\n </div>\n </div>\n )\n}\n\n/** Right-aligned keyboard-shortcut hint in a menu item — informational only; the\n * shortcut fires globally via the clipboard listeners regardless of the menu. */\nfunction MenuShortcut({ children }: { children: React.ReactNode }) {\n return <span className=\"ml-auto pl-6 text-xs tracking-widest text-muted-foreground\">{children}</span>\n}\n\n/**\n * The per-row \"•••\" action menu (EC-211). Copy/Paste on every row; Cut, Move,\n * Duplicate, Delete for non-root rows. Collapsing the old inline icon cluster\n * (up to 8 buttons, cropping on the 200–240px panel) into a menu changes no\n * behavior — same clipboard fragment + dispatch paths, same Cmd/Ctrl shortcuts.\n * Delete stays a one-click (undoable) destructive item, consistent with the rest\n * of the studio.\n */\nfunction RowActionsMenu({\n row,\n builder,\n clipboard,\n}: {\n row: { id: string; isRoot: boolean; isFirst: boolean; isLast: boolean }\n builder: ReturnType<typeof useBuilder>\n clipboard: ReturnType<typeof useStudioClipboard>\n}) {\n // EC-282 in-canvas reuse gestures, present only where a reuse capability is\n // configured (and the row is eligible). Null outside the composition editor.\n const reuse = useReuse()\n const canMakeReusable = !row.isRoot && (reuse?.canMakeReusable(row.id) ?? false)\n const canDetach = reuse?.canDetach(row.id) ?? false\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.pageTree.moreActions')}\n title={translate('builder.pageTree.moreActions')}\n className=\"size-6\"\n >\n <MoreHorizontal />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => clipboard.copyNode(row.id)}>\n <ClipboardCopy /> {translate('common.copy')} <MenuShortcut>⌘C</MenuShortcut>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => void clipboard.pasteFromClipboard(row.id)}>\n <ClipboardPaste /> {translate('builder.clipboard.paste')} <MenuShortcut>⌘V</MenuShortcut>\n </DropdownMenuItem>\n {!row.isRoot ? (\n <>\n <DropdownMenuItem onClick={() => clipboard.cutNode(row.id)}>\n <ClipboardX /> {translate('builder.clipboard.cut')} <MenuShortcut>⌘X</MenuShortcut>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n disabled={row.isFirst}\n onClick={() => builder.dispatch({ type: 'reorder', id: row.id, delta: -1 })}\n >\n <ChevronUp /> {translate('builder.pageTree.moveUp')}\n </DropdownMenuItem>\n <DropdownMenuItem\n disabled={row.isLast}\n onClick={() => builder.dispatch({ type: 'reorder', id: row.id, delta: 1 })}\n >\n <ChevronDown /> {translate('builder.pageTree.moveDown')}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => builder.dispatch({ type: 'duplicate', id: row.id })}>\n <Copy /> {translate('common.duplicate')}\n </DropdownMenuItem>\n {canMakeReusable || canDetach ? <DropdownMenuSeparator /> : null}\n {canMakeReusable ? (\n <DropdownMenuItem onClick={() => reuse?.startMakeReusable(row.id)}>\n <Boxes /> {translate('builder.reuse.makeReusable')}\n </DropdownMenuItem>\n ) : null}\n {canDetach ? (\n <DropdownMenuItem onClick={() => reuse?.detach(row.id)}>\n <Unlink /> {translate('builder.reuse.detachToCopy')}\n </DropdownMenuItem>\n ) : null}\n <DropdownMenuSeparator />\n <DropdownMenuItem\n variant=\"destructive\"\n onClick={() => builder.dispatch({ type: 'remove', id: row.id })}\n >\n <Trash2 /> {translate('common.delete')}\n </DropdownMenuItem>\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\nfunction IconAction({\n label,\n onClick,\n disabled,\n destructive,\n children,\n}: {\n label: string\n onClick: () => void\n disabled?: boolean\n destructive?: boolean\n children: React.ReactNode\n}) {\n return (\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={label}\n title={label}\n disabled={disabled}\n onClick={onClick}\n className={cn('size-6', destructive && 'hover:text-destructive')}\n >\n {children}\n </Button>\n )\n}\n"]}
1
+ {"version":3,"file":"page-tree.js","sourceRoot":"","sources":["../../../src/components/builder/page-tree.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,KAAK,EACL,SAAS,EACT,WAAW,EACX,aAAa,EACb,cAAc,EACd,UAAU,EACV,IAAI,EACJ,MAAM,EACN,MAAM,EACN,cAAc,EACd,IAAI,EACJ,MAAM,EACN,MAAM,GACP,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAC7D,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,WAAW,GACZ,MAAM,sCAAsC,CAAA;AAE7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAC9D,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AACpD,OAAO,EACL,MAAM,EACN,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,GAAG,EACH,MAAM,EACN,IAAI,GACL,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAClF,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAcjF,SAAS,OAAO,CACd,IAAmB,EACnB,KAAa,EACb,IAAe,EACf,IAA4D;IAE5D,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAA;IAC1G,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;QACvD,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAC5B,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,EAAE;YAC9B,OAAO,EAAE,CAAC,KAAK,CAAC;YAChB,MAAM,EAAE,CAAC,KAAK,QAAQ,CAAC,MAAM,GAAG,CAAC;YACjC,MAAM,EAAE,KAAK;SACd,CAAC,CACH,CAAA;IACH,CAAC;AACH,CAAC;AAED,wFAAwF;AACxF,MAAM,gBAAgB,GAAiD;IACrE,IAAI,EAAE,sDAAsD;IAC5D,MAAM,EAAE,kCAAkC;IAC1C,KAAK,EAAE,mCAAmC;IAC1C,2EAA2E;IAC3E,yEAAyE;IACzE,IAAI,EAAE,mDAAmD;IACzD,KAAK,EAAE,oDAAoD;IAC3D,OAAO,EAAE,oEAAoE;CAC9E,CAAA;AAED,MAAM,UAAU,QAAQ,CAAC,EAAE,UAAU,EAA8C;IACjF,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,IAAI,GAAG,OAAO,EAAE,CAAA;IACtB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAC1B,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,MAAM,IAAI,GAAc,EAAE,CAAA;IAC1B,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IAEhF,0EAA0E;IAC1E,2EAA2E;IAC3E,2EAA2E;IAC3E,sCAAsC;IACtC,MAAM,SAAS,GAAG,kBAAkB,EAAE,CAAA;IACtC,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,KAAK,WAAW,CAAA;IACnD,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IAChD,aAAa,CAAC,OAAO,GAAG,OAAO,CAAC,UAAU,CAAA;IAC1C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,QAAQ;YAAE,OAAM;QACpB,OAAO,wBAAwB,CAAC,MAAM,EAAE;YACtC,SAAS;YACT,aAAa,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,OAAO;YAC1C,YAAY,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;SAC1C,CAAC,CAAA;IACJ,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAA;IAEzB,8EAA8E;IAC9E,+EAA+E;IAC/E,qEAAqE;IACrE,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAA;IAC5B,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,CAAA;IACjC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,QAAQ;YAAE,OAAM;QACpB,OAAO,uBAAuB,CAAC,MAAM,EAAE;YACrC,aAAa,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,OAAO;YAC1C,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;YAChD,SAAS,EAAE,GAAG,EAAE,CAAC,MAAM;SACxB,CAAC,CAAA;IACJ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;IAEhC,0EAA0E;IAC1E,4EAA4E;IAC5E,+DAA+D;IAC/D,wEAAwE;IACxE,yDAAyD;IACzD,MAAM,WAAW,GAAG,MAAM,CAAkB,IAAI,CAAC,CAAA;IACjD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAqD,IAAI,CAAC,CAAA;IAC1F,MAAM,SAAS,GAAG,GAAG,EAAE;QACrB,WAAW,CAAC,OAAO,GAAG,IAAI,CAAA;QAC1B,OAAO,CAAC,IAAI,CAAC,CAAA;IACf,CAAC,CAAA;IACD,SAAS,CAAC,GAAG,EAAE,CAAC,gBAAgB,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;IACxD,MAAM,WAAW,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;IAEnE,SAAS,kBAAkB,CAAC,GAAY,EAAE,CAAkB;QAC1D,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;YACf,CAAC,CAAC,cAAc,EAAE,CAAA;YAClB,OAAM;QACR,CAAC;QACD,SAAS,CACP;YACE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE;YACvE,MAAM,EAAE,MAAM;YACd,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;SAChC,EACD,CAAC,CAAC,YAAY,CACf,CAAA;IACH,CAAC;IAED,SAAS,iBAAiB,CAAC,GAAY,EAAE,CAAkB;QACzD,MAAM,IAAI,GAAG,UAAU,EAAE,CAAA;QACzB,IAAI,CAAC,IAAI;YAAE,OAAM;QACjB,IAAI,QAAQ,GAAiB,MAAM,CAAA;QACnC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,GAAG,CAAC,CAAC,aAAa,CAAC,qBAAqB,EAAE,CAAA;YACpD,kEAAkE;YAClE,sEAAsE;YACtE,MAAM,IAAI,GAAG,iBAAiB,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAA;YAC9E,MAAM,KAAK,GACT,IAAI,KAAK,YAAY;gBACnB,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC;oBACd,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK;oBACtC,CAAC,CAAC,GAAG;gBACP,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;oBACf,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM;oBACtC,CAAC,CAAC,GAAG,CAAA;YACX,QAAQ,GAAG,sBAAsB,CAAC;gBAChC,KAAK;gBACL,QAAQ,EAAE,WAAW,CAAC,WAAW,EAAE,GAAG,CAAC,WAAW,CAAC;gBACnD,KAAK,EAAE,IAAI;aACZ,CAAC,CAAA;QACJ,CAAC;QACD,MAAM,UAAU,GAAG,WAAW,CAAC;YAC7B,IAAI,EAAE,OAAO,CAAC,OAAO;YACrB,WAAW;YACX,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,QAAQ,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE;SACvC,CAAC,CAAA;QACF,MAAM,SAAS,GAAG,gBAAgB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;QACxD,WAAW,CAAC,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAA;QAC7D,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,CAAA;QACrC,IAAI,UAAU,CAAC,QAAQ,EAAE,CAAC;YACxB,CAAC,CAAC,cAAc,EAAE,CAAA;YAClB,CAAC,CAAC,YAAY,CAAC,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;QAC5E,CAAC;IACH,CAAC;IAED,SAAS,aAAa,CAAC,CAAkB;QACvC,MAAM,IAAI,GAAG,UAAU,EAAE,CAAA;QACzB,IAAI,CAAC,IAAI;YAAE,OAAM;QACjB,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAA;QAChC,SAAS,EAAE,CAAA;QACX,IAAI,CAAC,IAAI;YAAE,OAAM;QACjB,CAAC,CAAC,cAAc,EAAE,CAAA;QAClB,wEAAwE;QACxE,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACzB,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;QAC1E,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAChC,qEAAqE;YACrE,MAAM,OAAO,GACX,IAAI,CAAC,MAAM,KAAK,SAAS;gBACvB,CAAC,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE;gBAChD,CAAC,CAAC,IAAI,CAAC,SAAS;oBACd,CAAC,CAAC,EAAE,IAAI,EAAE,WAAoB,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE;oBACxD,CAAC,CAAC,IAAI,CAAA;YACZ,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,CAAC,WAAW,CAAC,CAAA;gBACpB,OAAM;YACR,CAAC;YACD,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;QAC7F,CAAC;aAAM,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAC1B,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;QACnF,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,WAAW,CAAC,CAAA;YACpB,OAAM;QACR,CAAC;QACD,OAAO,CAAC,SAAS,CAAC,CAAA;IACpB,CAAC;IAED,SAAS,gBAAgB;QACvB,SAAS,EAAE,CAAA;QACX,OAAO,CAAC,WAAW,CAAC,CAAA;IACtB,CAAC;IAED,OAAO,CACL,eAAK,SAAS,EAAC,8BAA8B,aAC3C,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,2CAA2C,aAChE,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,UAAU,YAC9B,SAAS,CAAC,wBAAwB,CAAC,GAC/B,EACP,KAAC,MAAM,KAAG,EACV,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,QAAQ,EAAC,IAAI,kBAC/B,IAAI,CAAC,MAAM,GACP,IACH,EACN,cACE,SAAS,EAAC,oCAAoC,EAC9C,YAAY,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EACjE,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE;oBACjB,0DAA0D;oBAC1D,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC,aAA4B,CAAC;wBAAE,SAAS,EAAE,CAAA;gBAC5E,CAAC,YAEA,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;oBAChB,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;oBAC7D,MAAM,KAAK,GAAG,gBAAgB,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;oBAC5D,MAAM,QAAQ,GAAG,GAAG,CAAC,EAAE,KAAK,OAAO,CAAC,UAAU,CAAA;oBAC9C,MAAM,OAAO,GAAG,GAAG,CAAC,EAAE,KAAK,OAAO,CAAC,SAAS,CAAA;oBAC5C,0EAA0E;oBAC1E,IAAI,SAAS,GAAkC,MAAM,CAAA;oBACrD,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;wBAClB,MAAM,CAAC,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;wBAC9C,SAAS,GAAG,CAAC,CAAC,IAAI,KAAK,WAAW,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAA;oBACrE,CAAC;oBACD,OAAO,CACL,eAEE,YAAY,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,EACnE,SAAS,EAAE,CAAC,GAAG,CAAC,MAAM,EACtB,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,CAAC,EAC9C,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,GAAG,EAAE,CAAC,CAAC,EAC5C,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,gBAAgB,mBACZ,GAAG,CAAC,EAAE,oBACL,IAAI,EAAE,KAAK,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,EAClF,SAAS,EAAE,EAAE,CACX,6EAA6E,EAC7E,QAAQ;4BACN,CAAC,CAAC,kCAAkC;4BACpC,CAAC,CAAC,OAAO;gCACP,CAAC,CAAC,0BAA0B;gCAC5B,CAAC,CAAC,uBAAuB,EAC7B,IAAI,EAAE,KAAK,KAAK,GAAG,CAAC,EAAE,IAAI,IAAI,CAAC,SAAS;4BACtC,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC;4BAClC,CAAC,CAAC,SAAS,CACd,aAED,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,EAC/D,KAAK,EAAE,GAAG,CAAC,WAAW,EACtB,SAAS,EAAC,+CAA+C,EACzD,KAAK,EAAE,EAAE,WAAW,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,KAAK,GAAG,EAAE,IAAI,EAAE,aAKjD,eAAM,SAAS,EAAE,EAAE,CAAC,UAAU,EAAE,SAAS,KAAK,QAAQ,IAAI,yBAAyB,CAAC,YACjF,KAAK,CAAC,OAAO,GACT,EACN,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CACjB,eAAM,SAAS,EAAC,mCAAmC,YAAE,KAAK,CAAC,SAAS,GAAQ,CAC7E,CAAC,CAAC,CAAC,IAAI,EACP,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,CACxB,KAAC,MAAM,IACL,SAAS,EAAC,8BAA8B,gBAC5B,SAAS,CAAC,oCAAoC,CAAC,GAC3D,CACH,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,CAC5B,KAAC,MAAM,IACL,SAAS,EAAC,0CAA0C,gBACxC,SAAS,CAAC,8BAA8B,CAAC,GACrD,CACH,CAAC,CAAC,CAAC,IAAI,IACD,EAET,eACE,SAAS,EAAE,EAAE,CACX,oCAAoC,EACpC,QAAQ;oCACN,CAAC,CAAC,aAAa;oCACf,CAAC,CAAC,kEAAkE,CACvE,aAKD,KAAC,UAAU,IAAC,KAAK,EAAE,SAAS,CAAC,2BAA2B,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,YAC1F,KAAC,IAAI,KAAG,GACG,EACb,KAAC,cAAc,IAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,GAAI,IAChE,KAjED,GAAG,CAAC,EAAE,CAkEP,CACP,CAAA;gBACH,CAAC,CAAC,GACE,IACF,CACP,CAAA;AACH,CAAC;AAED;iFACiF;AACjF,SAAS,YAAY,CAAC,EAAE,QAAQ,EAAiC;IAC/D,OAAO,eAAM,SAAS,EAAC,4DAA4D,YAAE,QAAQ,GAAQ,CAAA;AACvG,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,EACtB,GAAG,EACH,OAAO,EACP,SAAS,GAKV;IACC,4EAA4E;IAC5E,6EAA6E;IAC7E,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAA;IACxB,MAAM,eAAe,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,CAAA;IAChF,MAAM,SAAS,GAAG,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK,CAAA;IACnD,OAAO,CACL,MAAC,YAAY,eACX,KAAC,mBAAmB,IAAC,OAAO,kBAC1B,KAAC,MAAM,IACL,IAAI,EAAC,SAAS,EACd,OAAO,EAAC,OAAO,gBACH,SAAS,CAAC,8BAA8B,CAAC,EACrD,KAAK,EAAE,SAAS,CAAC,8BAA8B,CAAC,EAChD,SAAS,EAAC,QAAQ,YAElB,KAAC,cAAc,KAAG,GACX,GACW,EACtB,MAAC,mBAAmB,IAAC,KAAK,EAAC,KAAK,aAC9B,MAAC,gBAAgB,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,aACzD,KAAC,aAAa,KAAG,OAAE,SAAS,CAAC,aAAa,CAAC,OAAE,KAAC,YAAY,0BAAkB,IAC3D,EACnB,MAAC,gBAAgB,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC,aACxE,KAAC,cAAc,KAAG,OAAE,SAAS,CAAC,yBAAyB,CAAC,OAAE,KAAC,YAAY,0BAAkB,IACxE,EAClB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CACb,8BACE,MAAC,gBAAgB,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,aACxD,KAAC,UAAU,KAAG,OAAE,SAAS,CAAC,uBAAuB,CAAC,OAAE,KAAC,YAAY,0BAAkB,IAClE,EACnB,KAAC,qBAAqB,KAAG,EACzB,MAAC,gBAAgB,IACf,QAAQ,EAAE,GAAG,CAAC,OAAO,EACrB,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,aAE3E,KAAC,SAAS,KAAG,OAAE,SAAS,CAAC,yBAAyB,CAAC,IAClC,EACnB,MAAC,gBAAgB,IACf,QAAQ,EAAE,GAAG,CAAC,MAAM,EACpB,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,aAE1E,KAAC,WAAW,KAAG,OAAE,SAAS,CAAC,2BAA2B,CAAC,IACtC,EACnB,MAAC,gBAAgB,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,aAClF,KAAC,IAAI,KAAG,OAAE,SAAS,CAAC,kBAAkB,CAAC,IACtB,EAClB,eAAe,IAAI,SAAS,CAAC,CAAC,CAAC,KAAC,qBAAqB,KAAG,CAAC,CAAC,CAAC,IAAI,EAC/D,eAAe,CAAC,CAAC,CAAC,CACjB,MAAC,gBAAgB,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,aAC/D,KAAC,KAAK,KAAG,OAAE,SAAS,CAAC,4BAA4B,CAAC,IACjC,CACpB,CAAC,CAAC,CAAC,IAAI,EACP,SAAS,CAAC,CAAC,CAAC,CACX,MAAC,gBAAgB,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,aACpD,KAAC,MAAM,KAAG,OAAE,SAAS,CAAC,4BAA4B,CAAC,IAClC,CACpB,CAAC,CAAC,CAAC,IAAI,EACR,KAAC,qBAAqB,KAAG,EACzB,MAAC,gBAAgB,IACf,OAAO,EAAC,aAAa,EACrB,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,aAE/D,KAAC,MAAM,KAAG,OAAE,SAAS,CAAC,eAAe,CAAC,IACrB,IAClB,CACJ,CAAC,CAAC,CAAC,IAAI,IACY,IACT,CAChB,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAClB,KAAK,EACL,OAAO,EACP,QAAQ,EACR,WAAW,EACX,QAAQ,GAOT;IACC,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAC,SAAS,EACd,OAAO,EAAC,OAAO,gBACH,KAAK,EACjB,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,EAAE,CAAC,QAAQ,EAAE,WAAW,IAAI,wBAAwB,CAAC,YAE/D,QAAQ,GACF,CACV,CAAA;AACH,CAAC","sourcesContent":["import {\n Boxes,\n ChevronUp,\n ChevronDown,\n ClipboardCopy,\n ClipboardPaste,\n ClipboardX,\n Copy,\n EyeOff,\n Filter,\n MoreHorizontal,\n Plus,\n Trash2,\n Unlink,\n} from 'lucide-react'\nimport { useEffect, useRef, useState } from 'react'\nimport type { Condition, ComponentNode } from '@elytracms/core/project-graph'\nimport { getParentInfo } from '@elytracms/core/project-graph'\nimport {\n canNestInto,\n containerFlowAxis,\n dropIndicatorFor,\n dropPositionForPointer,\n resolveDrop,\n} from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { DropIndicator, DropPlan, DropPosition } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\nimport { translate, useUiLocale } from '@elytracms/studio/lib/i18n'\nimport { useData } from '@elytracms/studio/lib/data'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Row,\n Spacer,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useCollab } from '../collab/collab-context'\nimport { useBuilder } from './builder-store'\nimport { attachClipboardListeners, useStudioClipboard } from './clipboard-actions'\nimport { attachDeleteKeyListener } from './delete-key'\nimport { nodeDisplayTitle } from './node-title'\nimport { useReuse } from './reuse-context'\nimport { activeDrag, beginDrag, endDrag, onDragSessionEnd } from './drag-session'\n\ninterface TreeRow {\n id: string\n componentId: string\n /** The node itself — EC-407 derives the instance title from its prop values. */\n node: ComponentNode\n depth: number\n isRoot: boolean\n isFirst: boolean\n isLast: boolean\n condition?: Condition\n}\n\nfunction flatten(\n node: ComponentNode,\n depth: number,\n rows: TreeRow[],\n meta: { isFirst: boolean; isLast: boolean; isRoot: boolean },\n): void {\n rows.push({ id: node.id, componentId: node.componentId, node, depth, ...meta, condition: node.condition })\n for (const children of Object.values(node.slots ?? {})) {\n children.forEach((child, i) =>\n flatten(child, depth + 1, rows, {\n isFirst: i === 0,\n isLast: i === children.length - 1,\n isRoot: false,\n }),\n )\n }\n}\n\n/** Tailwind classes per drop indicator state (EC-160/EC-162) — rejection is visible. */\nconst ROW_DROP_CLASSES: Record<Exclude<DropIndicator, null>, string> = {\n into: 'ring-1 ring-inset ring-emerald-400 bg-emerald-400/10',\n before: 'shadow-[inset_0_2px_0_0_#38bdf8]',\n after: 'shadow-[inset_0_-2px_0_0_#38bdf8]',\n // Wrap-into-columns gesture (EC-162): the tree can execute wrap plans, the\n // bands themselves are a canvas gesture (rows have no spatial \"beside\").\n left: 'shadow-[inset_2px_0_0_0_#a78bfa] bg-violet-400/10',\n right: 'shadow-[inset_-2px_0_0_0_#a78bfa] bg-violet-400/10',\n blocked: 'ring-1 ring-inset ring-red-500/80 bg-red-500/10 cursor-not-allowed',\n}\n\nexport function PageTree({ onAddChild }: { onAddChild: (parentId: string) => void }) {\n const builder = useBuilder()\n const studio = getStudioRegistry()\n const data = useData()\n const collab = useCollab()\n const uiLocale = useUiLocale()\n const rows: TreeRow[] = []\n flatten(builder.present, 0, rows, { isFirst: true, isLast: true, isRoot: true })\n\n // Copy/cut/paste (EC-175): keyboard shortcuts for the whole studio window\n // (the canvas iframe wires its own document) plus per-row affordances. The\n // shared listener keeps shortcuts inert in form fields/inline editors, and\n // the published preview is read-only.\n const clipboard = useStudioClipboard()\n const readOnly = collab.previewMode === 'published'\n const selectedIdRef = useRef(builder.selectedId)\n selectedIdRef.current = builder.selectedId\n useEffect(() => {\n if (readOnly) return\n return attachClipboardListeners(window, {\n clipboard,\n getSelectedId: () => selectedIdRef.current,\n getSelection: () => window.getSelection(),\n })\n }, [clipboard, readOnly])\n\n // Delete/Backspace removes the selected node from the tree side too (EC-404),\n // beside the clipboard shortcuts. Inert in the read-only published preview and\n // while focus is in a form field / inline editor (isEditingContext).\n const { dispatch } = builder\n const rootId = builder.present.id\n useEffect(() => {\n if (readOnly) return\n return attachDeleteKeyListener(window, {\n getSelectedId: () => selectedIdRef.current,\n remove: (id) => dispatch({ type: 'remove', id }),\n getRootId: () => rootId,\n })\n }, [readOnly, rootId, dispatch])\n\n // Drag & drop (EC-160): eligibility comes from the same pure rule set the\n // canvas uses (`lib/dnd/drop-rules`); the tree only renders the resolution.\n // One eligible drop dispatches exactly one move/insert action.\n // The current plan lives in a ref: `drop` fires synchronously after the\n // last `dragover`, before React flushes indicator state.\n const dropPlanRef = useRef<DropPlan | null>(null)\n const [drop, setDrop] = useState<{ rowId: string; indicator: DropIndicator } | null>(null)\n const clearDrop = () => {\n dropPlanRef.current = null\n setDrop(null)\n }\n useEffect(() => onDragSessionEnd(() => clearDrop()), [])\n const getManifest = (id: string) => studio.registry.getManifest(id)\n\n function handleRowDragStart(row: TreeRow, e: React.DragEvent) {\n if (row.isRoot) {\n e.preventDefault()\n return\n }\n beginDrag(\n {\n subject: { kind: 'node', nodeId: row.id, componentId: row.componentId },\n source: 'tree',\n label: shortId(row.componentId),\n },\n e.dataTransfer,\n )\n }\n\n function handleRowDragOver(row: TreeRow, e: React.DragEvent) {\n const drag = activeDrag()\n if (!drag) return\n let position: DropPosition = 'into'\n if (!row.isRoot) {\n const rect = e.currentTarget.getBoundingClientRect()\n // The hit-band axis follows the parent container's flow (EC-162):\n // children of horizontal Stacks/Grids reorder along the row's x axis.\n const flow = containerFlowAxis(getParentInfo(builder.present, row.id)?.parent)\n const ratio =\n flow === 'horizontal'\n ? rect.width > 0\n ? (e.clientX - rect.left) / rect.width\n : 0.5\n : rect.height > 0\n ? (e.clientY - rect.top) / rect.height\n : 0.5\n position = dropPositionForPointer({\n ratio,\n nestable: canNestInto(getManifest, row.componentId),\n edges: true,\n })\n }\n const resolution = resolveDrop({\n root: builder.present,\n getManifest,\n subject: drag.subject,\n location: { nodeId: row.id, position },\n })\n const indicator = dropIndicatorFor(resolution, position)\n dropPlanRef.current = resolution.eligible ? resolution : null\n setDrop({ rowId: row.id, indicator })\n if (resolution.eligible) {\n e.preventDefault()\n e.dataTransfer.dropEffect = drag.subject.kind === 'node' ? 'move' : 'copy'\n }\n }\n\n function handleRowDrop(e: React.DragEvent) {\n const drag = activeDrag()\n if (!drag) return\n const plan = dropPlanRef.current\n clearDrop()\n if (!plan) return\n e.preventDefault()\n // Exactly one dispatch per completed drop (one undo step, one command).\n if (plan.kind === 'move') {\n builder.dispatch({ type: 'move', id: plan.nodeId, target: plan.target })\n } else if (plan.kind === 'wrap') {\n // EC-162: shared plan vocabulary — the tree executes wrap plans too.\n const subject =\n plan.nodeId !== undefined\n ? { kind: 'node' as const, nodeId: plan.nodeId }\n : drag.buildNode\n ? { kind: 'component' as const, node: drag.buildNode() }\n : null\n if (!subject) {\n endDrag('cancelled')\n return\n }\n builder.dispatch({ type: 'wrap', referenceId: plan.referenceId, side: plan.side, subject })\n } else if (drag.buildNode) {\n builder.dispatch({ type: 'insert', target: plan.target, node: drag.buildNode() })\n } else {\n endDrag('cancelled')\n return\n }\n endDrag('dropped')\n }\n\n function handleRowDragEnd() {\n clearDrop()\n endDrag('cancelled')\n }\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <Row gap=\"2\" className=\"h-10 shrink-0 border-b border-border px-3\">\n <Text size=\"sm\" weight=\"semibold\">\n {translate('builder.pageTree.title')}\n </Text>\n <Spacer />\n <Text size=\"xs\" tone=\"subtle\" mono>\n {rows.length}\n </Text>\n </Row>\n <div\n className=\"min-h-0 flex-1 overflow-auto p-1.5\"\n onMouseLeave={() => builder.dispatch({ type: 'hover', id: null })}\n onDragLeave={(e) => {\n // Clear the indicator when the drag exits the tree panel.\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) clearDrop()\n }}\n >\n {rows.map((row) => {\n const manifest = studio.registry.getManifest(row.componentId)\n const title = nodeDisplayTitle(manifest, row.node, uiLocale)\n const selected = row.id === builder.selectedId\n const hovered = row.id === builder.hoveredId\n // EC-088: indicate condition-gated nodes; flag the currently-hidden ones.\n let condState: 'none' | 'visible' | 'hidden' = 'none'\n if (row.condition) {\n const r = data.resolveCondition(row.condition)\n condState = r.kind === 'condition' && r.pass ? 'visible' : 'hidden'\n }\n return (\n <div\n key={row.id}\n onMouseEnter={() => builder.dispatch({ type: 'hover', id: row.id })}\n draggable={!row.isRoot}\n onDragStart={(e) => handleRowDragStart(row, e)}\n onDragOver={(e) => handleRowDragOver(row, e)}\n onDrop={handleRowDrop}\n onDragEnd={handleRowDragEnd}\n data-tree-row={row.id}\n data-tree-drop={drop?.rowId === row.id ? (drop.indicator ?? undefined) : undefined}\n className={cn(\n 'group flex items-center rounded-md pr-1 text-left text-xs transition-colors',\n selected\n ? 'bg-accent text-accent-foreground'\n : hovered\n ? 'bg-muted text-foreground'\n : 'text-muted-foreground',\n drop?.rowId === row.id && drop.indicator\n ? ROW_DROP_CLASSES[drop.indicator]\n : undefined,\n )}\n >\n <button\n type=\"button\"\n onClick={() => builder.dispatch({ type: 'select', id: row.id })}\n title={row.componentId}\n className=\"flex min-w-0 flex-1 items-center gap-1.5 py-1\"\n style={{ paddingLeft: `${6 + row.depth * 14}px` }}\n >\n {/* EC-407: instance title (its own text content) is primary; the\n localized component name is secondary context. The raw mono id\n left the row — it stays in the `title=` above + inspector header. */}\n <span className={cn('truncate', condState === 'hidden' && 'line-through opacity-70')}>\n {title.primary}\n </span>\n {title.secondary ? (\n <span className=\"truncate text-muted-foreground/70\">{title.secondary}</span>\n ) : null}\n {condState === 'hidden' ? (\n <EyeOff\n className=\"size-3 shrink-0 text-warning\"\n aria-label={translate('builder.pageTree.hiddenByCondition')}\n />\n ) : condState === 'visible' ? (\n <Filter\n className=\"size-3 shrink-0 text-muted-foreground/60\"\n aria-label={translate('builder.pageTree.conditional')}\n />\n ) : null}\n </button>\n\n <div\n className={cn(\n 'flex shrink-0 items-center gap-0.5',\n selected\n ? 'opacity-100'\n : 'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100',\n )}\n >\n {/* Add child stays inline — the one constructive, high-frequency\n action. Everything else collapses into the ••• menu so labels\n stop cropping on the 200–240px panel (EC-211). */}\n <IconAction label={translate('builder.pageTree.addChild')} onClick={() => onAddChild(row.id)}>\n <Plus />\n </IconAction>\n <RowActionsMenu row={row} builder={builder} clipboard={clipboard} />\n </div>\n </div>\n )\n })}\n </div>\n </div>\n )\n}\n\n/** Right-aligned keyboard-shortcut hint in a menu item — informational only; the\n * shortcut fires globally via the clipboard listeners regardless of the menu. */\nfunction MenuShortcut({ children }: { children: React.ReactNode }) {\n return <span className=\"ml-auto pl-6 text-xs tracking-widest text-muted-foreground\">{children}</span>\n}\n\n/**\n * The per-row \"•••\" action menu (EC-211). Copy/Paste on every row; Cut, Move,\n * Duplicate, Delete for non-root rows. Collapsing the old inline icon cluster\n * (up to 8 buttons, cropping on the 200–240px panel) into a menu changes no\n * behavior — same clipboard fragment + dispatch paths, same Cmd/Ctrl shortcuts.\n * Delete stays a one-click (undoable) destructive item, consistent with the rest\n * of the studio.\n */\nfunction RowActionsMenu({\n row,\n builder,\n clipboard,\n}: {\n row: { id: string; isRoot: boolean; isFirst: boolean; isLast: boolean }\n builder: ReturnType<typeof useBuilder>\n clipboard: ReturnType<typeof useStudioClipboard>\n}) {\n // EC-282 in-canvas reuse gestures, present only where a reuse capability is\n // configured (and the row is eligible). Null outside the composition editor.\n const reuse = useReuse()\n const canMakeReusable = !row.isRoot && (reuse?.canMakeReusable(row.id) ?? false)\n const canDetach = reuse?.canDetach(row.id) ?? false\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.pageTree.moreActions')}\n title={translate('builder.pageTree.moreActions')}\n className=\"size-6\"\n >\n <MoreHorizontal />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => clipboard.copyNode(row.id)}>\n <ClipboardCopy /> {translate('common.copy')} <MenuShortcut>⌘C</MenuShortcut>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => void clipboard.pasteFromClipboard(row.id)}>\n <ClipboardPaste /> {translate('builder.clipboard.paste')} <MenuShortcut>⌘V</MenuShortcut>\n </DropdownMenuItem>\n {!row.isRoot ? (\n <>\n <DropdownMenuItem onClick={() => clipboard.cutNode(row.id)}>\n <ClipboardX /> {translate('builder.clipboard.cut')} <MenuShortcut>⌘X</MenuShortcut>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n disabled={row.isFirst}\n onClick={() => builder.dispatch({ type: 'reorder', id: row.id, delta: -1 })}\n >\n <ChevronUp /> {translate('builder.pageTree.moveUp')}\n </DropdownMenuItem>\n <DropdownMenuItem\n disabled={row.isLast}\n onClick={() => builder.dispatch({ type: 'reorder', id: row.id, delta: 1 })}\n >\n <ChevronDown /> {translate('builder.pageTree.moveDown')}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => builder.dispatch({ type: 'duplicate', id: row.id })}>\n <Copy /> {translate('common.duplicate')}\n </DropdownMenuItem>\n {canMakeReusable || canDetach ? <DropdownMenuSeparator /> : null}\n {canMakeReusable ? (\n <DropdownMenuItem onClick={() => reuse?.startMakeReusable(row.id)}>\n <Boxes /> {translate('builder.reuse.makeReusable')}\n </DropdownMenuItem>\n ) : null}\n {canDetach ? (\n <DropdownMenuItem onClick={() => reuse?.detach(row.id)}>\n <Unlink /> {translate('builder.reuse.detachToCopy')}\n </DropdownMenuItem>\n ) : null}\n <DropdownMenuSeparator />\n <DropdownMenuItem\n variant=\"destructive\"\n onClick={() => builder.dispatch({ type: 'remove', id: row.id })}\n >\n <Trash2 /> {translate('common.delete')}\n </DropdownMenuItem>\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\nfunction IconAction({\n label,\n onClick,\n disabled,\n destructive,\n children,\n}: {\n label: string\n onClick: () => void\n disabled?: boolean\n destructive?: boolean\n children: React.ReactNode\n}) {\n return (\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={label}\n title={label}\n disabled={disabled}\n onClick={onClick}\n className={cn('size-6', destructive && 'hover:text-destructive')}\n >\n {children}\n </Button>\n )\n}\n"]}
@@ -0,0 +1,52 @@
1
+ import type { ComponentManifest } from '@elytracms/core/component-registry';
2
+ /**
3
+ * EC-408 — the click-resolution core: given a click inside a selected node's
4
+ * wrapper, which prop (if any) does the clicked element map to, and should the
5
+ * inspector focus that prop's control?
6
+ *
7
+ * Pure and framework-free (the `findInlineTextHost` house pattern): a minimal
8
+ * ancestor-walk over an injected element face, so the three rules are unit-pinned
9
+ * without a DOM:
10
+ * 1. A `data-ec-prop` marker INSIDE the selected node's wrapper wins.
11
+ * 2. A marker OUTSIDE the wrapper (belonging to a parent node's render) is
12
+ * ignored — the walk stops at the wrapper and never climbs past it.
13
+ * 3. A marker inside a NESTED node's wrapper is ignored — the nested
14
+ * `data-ec-node-id` scopes out its own markers (that node is what a click
15
+ * there selects, resolved by its own wrapper).
16
+ * The inline-editable deferral (rule from scope 3) is a separate manifest check:
17
+ * an `inlineEditable` prop keeps inline editing (double-click) as its primary
18
+ * interaction, so a single click never steals focus to the inspector field.
19
+ */
20
+ /** The minimal element face the ancestor walk needs (a real `Element` satisfies it). */
21
+ export interface PropClickElement {
22
+ getAttribute(name: string): string | null;
23
+ readonly parentElement: PropClickElement | null;
24
+ }
25
+ /**
26
+ * The `data-ec-prop` name at `target` that belongs to `wrapper` (the selected
27
+ * node's editable wrapper), or `null`. Walks ancestors from `target` up to
28
+ * `wrapper`: returns the first `data-ec-prop` found, but stops (returns `null`)
29
+ * the moment it crosses into a NESTED node wrapper before finding one, and never
30
+ * climbs above `wrapper`.
31
+ */
32
+ export declare function propMarkerName(target: PropClickElement | null, wrapper: PropClickElement): string | null;
33
+ /** The outcome of resolving a sub-part click. */
34
+ export interface PropClickResolution {
35
+ /** The prop the clicked element maps to, or `null` (unannotated / marker outside). */
36
+ propName: string | null;
37
+ /**
38
+ * Whether to focus that prop's inspector control. `false` when there is no
39
+ * marker, or when the prop is inline-editable (inline editing stays primary).
40
+ */
41
+ focusField: boolean;
42
+ }
43
+ /**
44
+ * Resolve what a sub-part click should do: find the node-scoped `data-ec-prop`
45
+ * marker, then decide whether to focus its inspector control. Inline-editable
46
+ * props defer to inline editing; everything else focuses the field (scope 3).
47
+ */
48
+ export declare function resolvePropClick(args: {
49
+ target: PropClickElement | null;
50
+ wrapper: PropClickElement;
51
+ manifest: ComponentManifest | undefined;
52
+ }): PropClickResolution;
@@ -0,0 +1,38 @@
1
+ import { inlineEditablePropNames } from '@elytracms/core/component-registry';
2
+ /**
3
+ * The `data-ec-prop` name at `target` that belongs to `wrapper` (the selected
4
+ * node's editable wrapper), or `null`. Walks ancestors from `target` up to
5
+ * `wrapper`: returns the first `data-ec-prop` found, but stops (returns `null`)
6
+ * the moment it crosses into a NESTED node wrapper before finding one, and never
7
+ * climbs above `wrapper`.
8
+ */
9
+ export function propMarkerName(target, wrapper) {
10
+ let el = target;
11
+ while (el) {
12
+ // A nested node wrapper below the selected one scopes out its own markers.
13
+ if (el !== wrapper && el.getAttribute('data-ec-node-id') !== null)
14
+ return null;
15
+ const prop = el.getAttribute('data-ec-prop');
16
+ if (prop !== null)
17
+ return prop;
18
+ if (el === wrapper)
19
+ return null;
20
+ el = el.parentElement;
21
+ }
22
+ return null;
23
+ }
24
+ /**
25
+ * Resolve what a sub-part click should do: find the node-scoped `data-ec-prop`
26
+ * marker, then decide whether to focus its inspector control. Inline-editable
27
+ * props defer to inline editing; everything else focuses the field (scope 3).
28
+ */
29
+ export function resolvePropClick(args) {
30
+ const propName = propMarkerName(args.target, args.wrapper);
31
+ if (propName === null)
32
+ return { propName: null, focusField: false };
33
+ const inlineEditable = args.manifest
34
+ ? inlineEditablePropNames(args.manifest).includes(propName)
35
+ : false;
36
+ return { propName, focusField: !inlineEditable };
37
+ }
38
+ //# sourceMappingURL=prop-selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prop-selection.js","sourceRoot":"","sources":["../../../src/components/builder/prop-selection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,oCAAoC,CAAA;AA4B5E;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,MAA+B,EAC/B,OAAyB;IAEzB,IAAI,EAAE,GAA4B,MAAM,CAAA;IACxC,OAAO,EAAE,EAAE,CAAC;QACV,2EAA2E;QAC3E,IAAI,EAAE,KAAK,OAAO,IAAI,EAAE,CAAC,YAAY,CAAC,iBAAiB,CAAC,KAAK,IAAI;YAAE,OAAO,IAAI,CAAA;QAC9E,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,cAAc,CAAC,CAAA;QAC5C,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAA;QAC9B,IAAI,EAAE,KAAK,OAAO;YAAE,OAAO,IAAI,CAAA;QAC/B,EAAE,GAAG,EAAE,CAAC,aAAa,CAAA;IACvB,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAaD;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAIhC;IACC,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;IAC1D,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAA;IACnE,MAAM,cAAc,GAAG,IAAI,CAAC,QAAQ;QAClC,CAAC,CAAC,uBAAuB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC3D,CAAC,CAAC,KAAK,CAAA;IACT,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,cAAc,EAAE,CAAA;AAClD,CAAC","sourcesContent":["import { inlineEditablePropNames } from '@elytracms/core/component-registry'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\n\n/**\n * EC-408 — the click-resolution core: given a click inside a selected node's\n * wrapper, which prop (if any) does the clicked element map to, and should the\n * inspector focus that prop's control?\n *\n * Pure and framework-free (the `findInlineTextHost` house pattern): a minimal\n * ancestor-walk over an injected element face, so the three rules are unit-pinned\n * without a DOM:\n * 1. A `data-ec-prop` marker INSIDE the selected node's wrapper wins.\n * 2. A marker OUTSIDE the wrapper (belonging to a parent node's render) is\n * ignored — the walk stops at the wrapper and never climbs past it.\n * 3. A marker inside a NESTED node's wrapper is ignored — the nested\n * `data-ec-node-id` scopes out its own markers (that node is what a click\n * there selects, resolved by its own wrapper).\n * The inline-editable deferral (rule from scope 3) is a separate manifest check:\n * an `inlineEditable` prop keeps inline editing (double-click) as its primary\n * interaction, so a single click never steals focus to the inspector field.\n */\n\n/** The minimal element face the ancestor walk needs (a real `Element` satisfies it). */\nexport interface PropClickElement {\n getAttribute(name: string): string | null\n readonly parentElement: PropClickElement | null\n}\n\n/**\n * The `data-ec-prop` name at `target` that belongs to `wrapper` (the selected\n * node's editable wrapper), or `null`. Walks ancestors from `target` up to\n * `wrapper`: returns the first `data-ec-prop` found, but stops (returns `null`)\n * the moment it crosses into a NESTED node wrapper before finding one, and never\n * climbs above `wrapper`.\n */\nexport function propMarkerName(\n target: PropClickElement | null,\n wrapper: PropClickElement,\n): string | null {\n let el: PropClickElement | null = target\n while (el) {\n // A nested node wrapper below the selected one scopes out its own markers.\n if (el !== wrapper && el.getAttribute('data-ec-node-id') !== null) return null\n const prop = el.getAttribute('data-ec-prop')\n if (prop !== null) return prop\n if (el === wrapper) return null\n el = el.parentElement\n }\n return null\n}\n\n/** The outcome of resolving a sub-part click. */\nexport interface PropClickResolution {\n /** The prop the clicked element maps to, or `null` (unannotated / marker outside). */\n propName: string | null\n /**\n * Whether to focus that prop's inspector control. `false` when there is no\n * marker, or when the prop is inline-editable (inline editing stays primary).\n */\n focusField: boolean\n}\n\n/**\n * Resolve what a sub-part click should do: find the node-scoped `data-ec-prop`\n * marker, then decide whether to focus its inspector control. Inline-editable\n * props defer to inline editing; everything else focuses the field (scope 3).\n */\nexport function resolvePropClick(args: {\n target: PropClickElement | null\n wrapper: PropClickElement\n manifest: ComponentManifest | undefined\n}): PropClickResolution {\n const propName = propMarkerName(args.target, args.wrapper)\n if (propName === null) return { propName: null, focusField: false }\n const inlineEditable = args.manifest\n ? inlineEditablePropNames(args.manifest).includes(propName)\n : false\n return { propName, focusField: !inlineEditable }\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import type { ReactNode } from 'react';
2
+ /**
3
+ * EC-408 — the field↔component selection-sync channel. A small shared context
4
+ * (the `InsertAffordanceProvider` / `ReuseProvider` pattern) bridging the three
5
+ * surfaces that live in one editor tree but reach each other through nothing
6
+ * else: the in-iframe canvas click listener, the property inspector's prop
7
+ * controls, and the portaled selection overlay.
8
+ *
9
+ * Two directions ride it:
10
+ * - FORWARD (canvas → inspector): a sub-part click issues a one-shot
11
+ * {@link PropSyncApi.requestFocus}; the matching inspector control scrolls
12
+ * into view and takes focus. A monotonic `nonce` distinguishes repeat
13
+ * requests for the same prop so re-clicking re-focuses.
14
+ * - REVERSE (inspector → overlay): a prop control's focus sets
15
+ * {@link PropSyncApi.highlighted}; the overlay draws a quiet highlight on
16
+ * the matching `[data-ec-prop]` element. Blur clears it. No marker → the
17
+ * overlay simply measures nothing and draws nothing.
18
+ *
19
+ * A canvas mounted WITHOUT this provider (component preview) reads `null` and
20
+ * both directions are inert — today's node-level behavior, unchanged.
21
+ */
22
+ /** A prop identified by its owning node. */
23
+ export interface PropPath {
24
+ nodeId: string;
25
+ propName: string;
26
+ }
27
+ /** A one-shot focus request (nonce distinguishes repeats for the same prop). */
28
+ export interface PropFocusRequest extends PropPath {
29
+ nonce: number;
30
+ }
31
+ export interface PropSyncApi {
32
+ /** REVERSE: the prop control currently focused, or `null`. */
33
+ highlighted: PropPath | null;
34
+ /** Set/clear the focused prop (drives the overlay highlight). */
35
+ setHighlighted(path: PropPath | null): void;
36
+ /** FORWARD: the pending focus request, or `null`. */
37
+ focusRequest: PropFocusRequest | null;
38
+ /** Ask the inspector to scroll to + focus the given prop's control. */
39
+ requestFocus(path: PropPath): void;
40
+ }
41
+ /**
42
+ * Owns the shared sync state and exposes the api to both directions. Mounted
43
+ * around the canvas + inspector (the composition-field editor shell).
44
+ */
45
+ export declare function PropSyncProvider({ children }: {
46
+ children: ReactNode;
47
+ }): import("react").JSX.Element;
48
+ /** The sync api, or `null` outside a provider (component preview). */
49
+ export declare function usePropSync(): PropSyncApi | null;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
3
+ const PropSyncContext = createContext(null);
4
+ /**
5
+ * Owns the shared sync state and exposes the api to both directions. Mounted
6
+ * around the canvas + inspector (the composition-field editor shell).
7
+ */
8
+ export function PropSyncProvider({ children }) {
9
+ const [highlighted, setHighlighted] = useState(null);
10
+ const [focusRequest, setFocusRequest] = useState(null);
11
+ const nonceRef = useRef(0);
12
+ const requestFocus = useCallback((path) => {
13
+ nonceRef.current += 1;
14
+ setFocusRequest({ ...path, nonce: nonceRef.current });
15
+ }, []);
16
+ const api = useMemo(() => ({ highlighted, setHighlighted, focusRequest, requestFocus }), [highlighted, focusRequest, requestFocus]);
17
+ return _jsx(PropSyncContext.Provider, { value: api, children: children });
18
+ }
19
+ /** The sync api, or `null` outside a provider (component preview). */
20
+ export function usePropSync() {
21
+ return useContext(PropSyncContext);
22
+ }
23
+ //# sourceMappingURL=prop-sync-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prop-sync-context.js","sourceRoot":"","sources":["../../../src/components/builder/prop-sync-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AA8CzF,MAAM,eAAe,GAAG,aAAa,CAAqB,IAAI,CAAC,CAAA;AAE/D;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAE,QAAQ,EAA2B;IACpE,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAkB,IAAI,CAAC,CAAA;IACrE,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAA0B,IAAI,CAAC,CAAA;IAC/E,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IAE1B,MAAM,YAAY,GAAG,WAAW,CAAC,CAAC,IAAc,EAAE,EAAE;QAClD,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAA;QACrB,eAAe,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;IACvD,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,GAAG,GAAG,OAAO,CACjB,GAAG,EAAE,CAAC,CAAC,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC,EACnE,CAAC,WAAW,EAAE,YAAY,EAAE,YAAY,CAAC,CAC1C,CAAA;IAED,OAAO,KAAC,eAAe,CAAC,QAAQ,IAAC,KAAK,EAAE,GAAG,YAAG,QAAQ,GAA4B,CAAA;AACpF,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,WAAW;IACzB,OAAO,UAAU,CAAC,eAAe,CAAC,CAAA;AACpC,CAAC","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\n\n/**\n * EC-408 — the field↔component selection-sync channel. A small shared context\n * (the `InsertAffordanceProvider` / `ReuseProvider` pattern) bridging the three\n * surfaces that live in one editor tree but reach each other through nothing\n * else: the in-iframe canvas click listener, the property inspector's prop\n * controls, and the portaled selection overlay.\n *\n * Two directions ride it:\n * - FORWARD (canvas → inspector): a sub-part click issues a one-shot\n * {@link PropSyncApi.requestFocus}; the matching inspector control scrolls\n * into view and takes focus. A monotonic `nonce` distinguishes repeat\n * requests for the same prop so re-clicking re-focuses.\n * - REVERSE (inspector → overlay): a prop control's focus sets\n * {@link PropSyncApi.highlighted}; the overlay draws a quiet highlight on\n * the matching `[data-ec-prop]` element. Blur clears it. No marker → the\n * overlay simply measures nothing and draws nothing.\n *\n * A canvas mounted WITHOUT this provider (component preview) reads `null` and\n * both directions are inert — today's node-level behavior, unchanged.\n */\n\n/** A prop identified by its owning node. */\nexport interface PropPath {\n nodeId: string\n propName: string\n}\n\n/** A one-shot focus request (nonce distinguishes repeats for the same prop). */\nexport interface PropFocusRequest extends PropPath {\n nonce: number\n}\n\nexport interface PropSyncApi {\n /** REVERSE: the prop control currently focused, or `null`. */\n highlighted: PropPath | null\n /** Set/clear the focused prop (drives the overlay highlight). */\n setHighlighted(path: PropPath | null): void\n /** FORWARD: the pending focus request, or `null`. */\n focusRequest: PropFocusRequest | null\n /** Ask the inspector to scroll to + focus the given prop's control. */\n requestFocus(path: PropPath): void\n}\n\nconst PropSyncContext = createContext<PropSyncApi | null>(null)\n\n/**\n * Owns the shared sync state and exposes the api to both directions. Mounted\n * around the canvas + inspector (the composition-field editor shell).\n */\nexport function PropSyncProvider({ children }: { children: ReactNode }) {\n const [highlighted, setHighlighted] = useState<PropPath | null>(null)\n const [focusRequest, setFocusRequest] = useState<PropFocusRequest | null>(null)\n const nonceRef = useRef(0)\n\n const requestFocus = useCallback((path: PropPath) => {\n nonceRef.current += 1\n setFocusRequest({ ...path, nonce: nonceRef.current })\n }, [])\n\n const api = useMemo<PropSyncApi>(\n () => ({ highlighted, setHighlighted, focusRequest, requestFocus }),\n [highlighted, focusRequest, requestFocus],\n )\n\n return <PropSyncContext.Provider value={api}>{children}</PropSyncContext.Provider>\n}\n\n/** The sync api, or `null` outside a provider (component preview). */\nexport function usePropSync(): PropSyncApi | null {\n return useContext(PropSyncContext)\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
2
3
  import { staticProp } from '@elytracms/core/project-graph';
3
4
  import { effectivePropValues, manifestSlots } from '@elytracms/core/component-registry';
4
5
  import { isFieldHidden } from '@elytracms/core/cms-core';
@@ -10,6 +11,7 @@ import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
10
11
  import { PropValueControl } from '../cms/prop-fields.js';
11
12
  import { useBuilder } from './builder-store.js';
12
13
  import { LayoutSection, layoutManagedPropNames } from './layout-inspector.js';
14
+ import { usePropSync } from './prop-sync-context.js';
13
15
  export function PropertyInspector() {
14
16
  const builder = useBuilder();
15
17
  const studio = getStudioRegistry();
@@ -55,10 +57,30 @@ function SettingsTab({ node, manifest, }) {
55
57
  }
56
58
  function PropField({ node, name, spec }) {
57
59
  const builder = useBuilder();
60
+ const sync = usePropSync();
61
+ const anchorRef = useRef(null);
58
62
  const value = node.props?.[name];
59
63
  const id = `prop-${node.id}-${name}`;
60
64
  // Coalesce rapid edits to the same prop into one undo step (EC-190).
61
65
  const coalesceKey = `${node.id}:${name}`;
62
- return (_jsx(Field, { label: localizedLabel(spec.form?.label, name), htmlFor: id, hint: localizedLabel(spec.form?.description) || undefined, children: _jsx(PropValueControl, { id: id, name: name, spec: spec, value: value, onSet: (v) => builder.dispatch({ type: 'setProp', id: node.id, name, value: staticProp(v), coalesceKey }), onClear: () => builder.dispatch({ type: 'removeProp', id: node.id, name }) }) }));
66
+ // EC-408 FORWARD: a sub-part click that resolved to THIS prop scrolls its
67
+ // control into view and focuses the first focusable element. Keyed on the
68
+ // request object (a fresh nonce every request) so re-clicking re-focuses.
69
+ const request = sync?.focusRequest;
70
+ useEffect(() => {
71
+ if (!request || request.nodeId !== node.id || request.propName !== name)
72
+ return;
73
+ const anchor = anchorRef.current;
74
+ if (!anchor)
75
+ return;
76
+ anchor.scrollIntoView({ block: 'nearest' });
77
+ anchor
78
+ .querySelector('input, textarea, select, button, [contenteditable="true"], [tabindex]')
79
+ ?.focus();
80
+ }, [request, node.id, name]);
81
+ return (_jsx("div", { ref: anchorRef, "data-ec-prop-anchor": name, onFocus: () => sync?.setHighlighted({ nodeId: node.id, propName: name }), onBlur: (e) => {
82
+ if (!e.currentTarget.contains(e.relatedTarget))
83
+ sync?.setHighlighted(null);
84
+ }, children: _jsx(Field, { label: localizedLabel(spec.form?.label, name), htmlFor: id, hint: localizedLabel(spec.form?.description) || undefined, children: _jsx(PropValueControl, { id: id, name: name, spec: spec, value: value, onSet: (v) => builder.dispatch({ type: 'setProp', id: node.id, name, value: staticProp(v), coalesceKey }), onClear: () => builder.dispatch({ type: 'removeProp', id: node.id, name }) }) }) }));
63
85
  }
64
86
  //# sourceMappingURL=property-inspector.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"property-inspector.js","sourceRoot":"","sources":["../../../src/components/builder/property-inspector.tsx"],"names":[],"mappings":"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","sourcesContent":["import { staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { ComponentManifest, PropField } from '@elytracms/core/component-registry'\nimport { effectivePropValues, manifestSlots } from '@elytracms/core/component-registry'\nimport { isFieldHidden } from '@elytracms/core/cms-core'\nimport type { FieldConditionContext, FieldDef } from '@elytracms/core/cms-core'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport {\n Badge,\n Code,\n Field,\n InspectorSection,\n PanelHeader,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { humanizeName } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { PropValueControl } from '../cms/prop-fields'\nimport { useBuilder } from './builder-store'\nimport { LayoutSection, layoutManagedPropNames } from './layout-inspector'\n\nexport function PropertyInspector() {\n const builder = useBuilder()\n const studio = getStudioRegistry()\n const node = builder.selectedNode\n\n if (!node) {\n return (\n <div className=\"grid h-full place-items-center p-6 text-center\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('builder.inspector.emptySelection')}\n </Text>\n </div>\n )\n }\n\n const manifest = studio.registry.getManifest(node.componentId)\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <PanelHeader>\n <Text size=\"sm\" weight=\"semibold\">\n {translate('builder.inspector.title')}\n </Text>\n <Spacer />\n <Code tone=\"muted\" title={node.componentId}>\n {componentDisplayName(node.componentId, manifest)}\n </Code>\n </PanelHeader>\n\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <SettingsTab node={node} manifest={manifest} />\n </div>\n </div>\n )\n}\n\n/**\n * The prop rows the inspector renders for a node (EC-393). Manifest props MINUS:\n * layout-managed props (they render in the Layout section, EC-162); `blocks` props\n * (they are child regions shown in Slots, EC-190); and any prop hidden RIGHT NOW by\n * its `form.hidden` — static `true`, or a predicate over the SIBLING prop values.\n *\n * The predicate's context is the component instance's EFFECTIVE prop values\n * (`effectivePropValues` — static values with manifest defaults filled), the prop\n * analogue of the document form's active-locale draft values (`isFieldHidden` there).\n * So a conditional row shows/hides live as a sibling prop changes and unmounts\n * exactly like a document-form field. Pure (no store) so the behaviour is testable.\n */\nexport function visibleInspectorProps(\n manifest: ComponentManifest,\n node: ComponentNode,\n layoutManaged: ReadonlySet<string>,\n): [string, PropField][] {\n const ctx: FieldConditionContext = { values: effectivePropValues(manifest, node) }\n return Object.entries(manifest.props).filter(\n ([name, spec]) =>\n !layoutManaged.has(name) &&\n spec.type !== 'blocks' &&\n !isFieldHidden({ ...spec, name } as FieldDef, ctx),\n )\n}\n\n/** The full property surface: node identity, static props, condition, slots. */\nfunction SettingsTab({\n node,\n manifest,\n}: {\n node: ComponentNode\n manifest: ComponentManifest | undefined\n}) {\n // Container layout props render in the dedicated Layout section (EC-162),\n // not as a second set of generic controls.\n const layoutManaged = new Set(layoutManagedPropNames(node))\n // EC-190: `blocks` props are slots; EC-393: `form.hidden` props are omitted —\n // evaluated against the node's effective sibling prop values, live as they change.\n const props = manifest ? visibleInspectorProps(manifest, node, layoutManaged) : []\n const slots = manifest ? manifestSlots(manifest) : []\n\n return (\n <>\n {/* EC-192: the editor-facing inspector no longer renders the raw node id or\n namespaced component id (the persona promise). The component's human name\n is in the panel header (with the manifest id in its `title`); the canvas\n and tree provide selection. */}\n {/* Contextual layout inspector for Stack/Grid containers (EC-162). */}\n <LayoutSection node={node} />\n\n {!manifest ? (\n <div className=\"p-3\">\n <Badge variant=\"error\">{translate('builder.inspector.unknownComponent')}</Badge>\n <Text size=\"xs\" tone=\"muted\" className=\"mt-2\">\n {translate('builder.inspector.noManifest')}\n </Text>\n </div>\n ) : (\n <InspectorSection title={translate('builder.inspector.props')}>\n {props.length === 0 ? (\n <Text size=\"xs\" tone=\"subtle\">\n {translate('builder.inspector.noEditableProps')}\n </Text>\n ) : (\n <Stack gap=\"4\">\n {props.map(([name, spec]) => (\n // Key by node id so per-node local control state never leaks\n // across a selection change to a same-named prop.\n <PropField key={`${node.id}:${name}`} node={node} name={name} spec={spec} />\n ))}\n </Stack>\n )}\n </InspectorSection>\n )}\n\n {slots.length > 0 ? (\n <InspectorSection title={translate('builder.inspector.slots')}>\n <Stack gap=\"1\">\n {slots.map((slot) => {\n const count = node.slots?.[slot.name]?.length ?? 0\n const missing = slot.required && count === 0\n return (\n <Row key={slot.name} gap=\"2\">\n <Text size=\"xs\" title={slot.name}>\n {humanizeName(slot.name)}\n </Text>\n {slot.required ? (\n <Badge variant={missing ? 'error' : 'outline'}>{translate('common.required')}</Badge>\n ) : null}\n <Spacer />\n <Text size=\"xs\" tone=\"subtle\" mono>\n {count}\n </Text>\n </Row>\n )\n })}\n </Stack>\n <Text size=\"xs\" tone=\"subtle\" className=\"mt-2\">\n {translate('builder.inspector.slotsHint')}\n </Text>\n </InspectorSection>\n ) : null}\n </>\n )\n}\n\nfunction PropField({ node, name, spec }: { node: ComponentNode; name: string; spec: PropField }) {\n const builder = useBuilder()\n const value = node.props?.[name]\n const id = `prop-${node.id}-${name}`\n // Coalesce rapid edits to the same prop into one undo step (EC-190).\n const coalesceKey = `${node.id}:${name}`\n\n return (\n // EC-393: honor `form.description` as the field hint, at parity with the document\n // form (`detail-editor.tsx` passes the same to its `Field`).\n <Field\n label={localizedLabel(spec.form?.label, name)}\n htmlFor={id}\n hint={localizedLabel(spec.form?.description) || undefined}\n >\n <PropValueControl\n id={id}\n name={name}\n spec={spec}\n value={value}\n onSet={(v) => builder.dispatch({ type: 'setProp', id: node.id, name, value: staticProp(v), coalesceKey })}\n onClear={() => builder.dispatch({ type: 'removeProp', id: node.id, name })}\n />\n </Field>\n )\n}\n\n"]}
1
+ {"version":3,"file":"property-inspector.js","sourceRoot":"","sources":["../../../src/components/builder/property-inspector.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef } from 'react'\nimport { staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { ComponentManifest, PropField } from '@elytracms/core/component-registry'\nimport { effectivePropValues, manifestSlots } from '@elytracms/core/component-registry'\nimport { isFieldHidden } from '@elytracms/core/cms-core'\nimport type { FieldConditionContext, FieldDef } from '@elytracms/core/cms-core'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport {\n Badge,\n Code,\n Field,\n InspectorSection,\n PanelHeader,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { humanizeName } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { PropValueControl } from '../cms/prop-fields'\nimport { useBuilder } from './builder-store'\nimport { LayoutSection, layoutManagedPropNames } from './layout-inspector'\nimport { usePropSync } from './prop-sync-context'\n\nexport function PropertyInspector() {\n const builder = useBuilder()\n const studio = getStudioRegistry()\n const node = builder.selectedNode\n\n if (!node) {\n return (\n <div className=\"grid h-full place-items-center p-6 text-center\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('builder.inspector.emptySelection')}\n </Text>\n </div>\n )\n }\n\n const manifest = studio.registry.getManifest(node.componentId)\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <PanelHeader>\n <Text size=\"sm\" weight=\"semibold\">\n {translate('builder.inspector.title')}\n </Text>\n <Spacer />\n <Code tone=\"muted\" title={node.componentId}>\n {componentDisplayName(node.componentId, manifest)}\n </Code>\n </PanelHeader>\n\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <SettingsTab node={node} manifest={manifest} />\n </div>\n </div>\n )\n}\n\n/**\n * The prop rows the inspector renders for a node (EC-393). Manifest props MINUS:\n * layout-managed props (they render in the Layout section, EC-162); `blocks` props\n * (they are child regions shown in Slots, EC-190); and any prop hidden RIGHT NOW by\n * its `form.hidden` — static `true`, or a predicate over the SIBLING prop values.\n *\n * The predicate's context is the component instance's EFFECTIVE prop values\n * (`effectivePropValues` — static values with manifest defaults filled), the prop\n * analogue of the document form's active-locale draft values (`isFieldHidden` there).\n * So a conditional row shows/hides live as a sibling prop changes and unmounts\n * exactly like a document-form field. Pure (no store) so the behaviour is testable.\n */\nexport function visibleInspectorProps(\n manifest: ComponentManifest,\n node: ComponentNode,\n layoutManaged: ReadonlySet<string>,\n): [string, PropField][] {\n const ctx: FieldConditionContext = { values: effectivePropValues(manifest, node) }\n return Object.entries(manifest.props).filter(\n ([name, spec]) =>\n !layoutManaged.has(name) &&\n spec.type !== 'blocks' &&\n !isFieldHidden({ ...spec, name } as FieldDef, ctx),\n )\n}\n\n/** The full property surface: node identity, static props, condition, slots. */\nfunction SettingsTab({\n node,\n manifest,\n}: {\n node: ComponentNode\n manifest: ComponentManifest | undefined\n}) {\n // Container layout props render in the dedicated Layout section (EC-162),\n // not as a second set of generic controls.\n const layoutManaged = new Set(layoutManagedPropNames(node))\n // EC-190: `blocks` props are slots; EC-393: `form.hidden` props are omitted —\n // evaluated against the node's effective sibling prop values, live as they change.\n const props = manifest ? visibleInspectorProps(manifest, node, layoutManaged) : []\n const slots = manifest ? manifestSlots(manifest) : []\n\n return (\n <>\n {/* EC-192: the editor-facing inspector no longer renders the raw node id or\n namespaced component id (the persona promise). The component's human name\n is in the panel header (with the manifest id in its `title`); the canvas\n and tree provide selection. */}\n {/* Contextual layout inspector for Stack/Grid containers (EC-162). */}\n <LayoutSection node={node} />\n\n {!manifest ? (\n <div className=\"p-3\">\n <Badge variant=\"error\">{translate('builder.inspector.unknownComponent')}</Badge>\n <Text size=\"xs\" tone=\"muted\" className=\"mt-2\">\n {translate('builder.inspector.noManifest')}\n </Text>\n </div>\n ) : (\n <InspectorSection title={translate('builder.inspector.props')}>\n {props.length === 0 ? (\n <Text size=\"xs\" tone=\"subtle\">\n {translate('builder.inspector.noEditableProps')}\n </Text>\n ) : (\n <Stack gap=\"4\">\n {props.map(([name, spec]) => (\n // Key by node id so per-node local control state never leaks\n // across a selection change to a same-named prop.\n <PropField key={`${node.id}:${name}`} node={node} name={name} spec={spec} />\n ))}\n </Stack>\n )}\n </InspectorSection>\n )}\n\n {slots.length > 0 ? (\n <InspectorSection title={translate('builder.inspector.slots')}>\n <Stack gap=\"1\">\n {slots.map((slot) => {\n const count = node.slots?.[slot.name]?.length ?? 0\n const missing = slot.required && count === 0\n return (\n <Row key={slot.name} gap=\"2\">\n <Text size=\"xs\" title={slot.name}>\n {humanizeName(slot.name)}\n </Text>\n {slot.required ? (\n <Badge variant={missing ? 'error' : 'outline'}>{translate('common.required')}</Badge>\n ) : null}\n <Spacer />\n <Text size=\"xs\" tone=\"subtle\" mono>\n {count}\n </Text>\n </Row>\n )\n })}\n </Stack>\n <Text size=\"xs\" tone=\"subtle\" className=\"mt-2\">\n {translate('builder.inspector.slotsHint')}\n </Text>\n </InspectorSection>\n ) : null}\n </>\n )\n}\n\nfunction PropField({ node, name, spec }: { node: ComponentNode; name: string; spec: PropField }) {\n const builder = useBuilder()\n const sync = usePropSync()\n const anchorRef = useRef<HTMLDivElement>(null)\n const value = node.props?.[name]\n const id = `prop-${node.id}-${name}`\n // Coalesce rapid edits to the same prop into one undo step (EC-190).\n const coalesceKey = `${node.id}:${name}`\n\n // EC-408 FORWARD: a sub-part click that resolved to THIS prop scrolls its\n // control into view and focuses the first focusable element. Keyed on the\n // request object (a fresh nonce every request) so re-clicking re-focuses.\n const request = sync?.focusRequest\n useEffect(() => {\n if (!request || request.nodeId !== node.id || request.propName !== name) return\n const anchor = anchorRef.current\n if (!anchor) return\n anchor.scrollIntoView({ block: 'nearest' })\n anchor\n .querySelector<HTMLElement>('input, textarea, select, button, [contenteditable=\"true\"], [tabindex]')\n ?.focus()\n }, [request, node.id, name])\n\n return (\n // EC-408: the stable per-prop anchor. Focus (bubbling) inside any control\n // highlights the matching sub-part on the canvas (REVERSE); blur that leaves\n // the field clears it.\n <div\n ref={anchorRef}\n data-ec-prop-anchor={name}\n onFocus={() => sync?.setHighlighted({ nodeId: node.id, propName: name })}\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) sync?.setHighlighted(null)\n }}\n >\n {/* EC-393: honor `form.description` as the field hint, at parity with the document\n form (`detail-editor.tsx` passes the same to its `Field`). */}\n <Field\n label={localizedLabel(spec.form?.label, name)}\n htmlFor={id}\n hint={localizedLabel(spec.form?.description) || undefined}\n >\n <PropValueControl\n id={id}\n name={name}\n spec={spec}\n value={value}\n onSet={(v) => builder.dispatch({ type: 'setProp', id: node.id, name, value: staticProp(v), coalesceKey })}\n onClear={() => builder.dispatch({ type: 'removeProp', id: node.id, name })}\n />\n </Field>\n </div>\n )\n}\n\n"]}
@@ -38,7 +38,7 @@ export function ReuseProvider({ children }) {
38
38
  const node = getNode(builder.present, nodeId);
39
39
  if (!node)
40
40
  return;
41
- const manifestTitle = manifests.find((m) => m.id === node.componentId)?.title;
41
+ const manifestTitle = localizedLabel(manifests.find((m) => m.id === node.componentId)?.title) || undefined;
42
42
  const collection = capability ? cms.getCollection(capability.collectionId) : undefined;
43
43
  const defaultName = manifestTitle ?? (localizedLabel(collection?.form?.labelSingular) || localizedLabel(collection?.form?.label) || translate('builder.reuse.defaultElementName'));
44
44
  setPending({ nodeId, defaultName });