@elytracms/studio 0.0.64 → 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 (123) 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.js +90 -8
  16. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  17. package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
  18. package/dist/components/builder/canvas-inline-editing.js +3 -1
  19. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  20. package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
  21. package/dist/components/builder/canvas-overlay-geometry.js +80 -0
  22. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
  23. package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
  24. package/dist/components/builder/canvas-rect-tracking.js +198 -0
  25. package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
  26. package/dist/components/builder/canvas.js +61 -12
  27. package/dist/components/builder/canvas.js.map +1 -1
  28. package/dist/components/builder/composition-field-editor.js +22 -3
  29. package/dist/components/builder/composition-field-editor.js.map +1 -1
  30. package/dist/components/builder/delete-key.d.ts +38 -0
  31. package/dist/components/builder/delete-key.js +46 -0
  32. package/dist/components/builder/delete-key.js.map +1 -0
  33. package/dist/components/builder/drag-session.d.ts +8 -0
  34. package/dist/components/builder/drag-session.js +15 -0
  35. package/dist/components/builder/drag-session.js.map +1 -1
  36. package/dist/components/builder/insert-affordance-context.d.ts +19 -0
  37. package/dist/components/builder/insert-affordance-context.js +7 -0
  38. package/dist/components/builder/insert-affordance-context.js.map +1 -0
  39. package/dist/components/builder/insert-context.d.ts +40 -0
  40. package/dist/components/builder/insert-context.js +86 -3
  41. package/dist/components/builder/insert-context.js.map +1 -1
  42. package/dist/components/builder/node-title.d.ts +60 -0
  43. package/dist/components/builder/node-title.js +135 -0
  44. package/dist/components/builder/node-title.js.map +1 -0
  45. package/dist/components/builder/page-tree.js +21 -3
  46. package/dist/components/builder/page-tree.js.map +1 -1
  47. package/dist/components/builder/prop-selection.d.ts +52 -0
  48. package/dist/components/builder/prop-selection.js +38 -0
  49. package/dist/components/builder/prop-selection.js.map +1 -0
  50. package/dist/components/builder/prop-sync-context.d.ts +49 -0
  51. package/dist/components/builder/prop-sync-context.js +23 -0
  52. package/dist/components/builder/prop-sync-context.js.map +1 -0
  53. package/dist/components/builder/property-inspector.js +23 -1
  54. package/dist/components/builder/property-inspector.js.map +1 -1
  55. package/dist/components/builder/reuse-context.js +1 -1
  56. package/dist/components/builder/reuse-context.js.map +1 -1
  57. package/dist/components/builder/selection-overlay.d.ts +79 -0
  58. package/dist/components/builder/selection-overlay.js +244 -0
  59. package/dist/components/builder/selection-overlay.js.map +1 -0
  60. package/dist/components/builder/spacing-handles.d.ts +57 -1
  61. package/dist/components/builder/spacing-handles.js +142 -26
  62. package/dist/components/builder/spacing-handles.js.map +1 -1
  63. package/dist/components/cms/asset-picker.js +11 -5
  64. package/dist/components/cms/asset-picker.js.map +1 -1
  65. package/dist/components/cms/rich-text/block-style.d.ts +48 -0
  66. package/dist/components/cms/rich-text/block-style.js +30 -0
  67. package/dist/components/cms/rich-text/block-style.js.map +1 -0
  68. package/dist/components/cms/rich-text/embed-picker.js +4 -3
  69. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  70. package/dist/components/cms/rich-text/extensions.d.ts +2 -0
  71. package/dist/components/cms/rich-text/extensions.js +4 -0
  72. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  73. package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
  74. package/dist/components/cms/rich-text/font-size-control.js +40 -0
  75. package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
  76. package/dist/components/cms/rich-text/font-size.d.ts +16 -0
  77. package/dist/components/cms/rich-text/font-size.js +40 -0
  78. package/dist/components/cms/rich-text/font-size.js.map +1 -0
  79. package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
  80. package/dist/components/cms/rich-text/toolbar-state.js +3 -0
  81. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
  82. package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
  83. package/dist/components/cms/rich-text/toolbar.js +17 -5
  84. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  85. package/dist/components/components/component-detail.js +3 -3
  86. package/dist/components/components/component-detail.js.map +1 -1
  87. package/dist/components/components/component-preview.js +1 -1
  88. package/dist/components/components/component-preview.js.map +1 -1
  89. package/dist/components/components/registry-browser.js +2 -2
  90. package/dist/components/components/registry-browser.js.map +1 -1
  91. package/dist/components/components/taxonomy.js +3 -2
  92. package/dist/components/components/taxonomy.js.map +1 -1
  93. package/dist/components/components/transfer-dialog.js +1 -1
  94. package/dist/components/components/transfer-dialog.js.map +1 -1
  95. package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
  96. package/dist/lib/assets/backfill-thumbs.js +120 -0
  97. package/dist/lib/assets/backfill-thumbs.js.map +1 -0
  98. package/dist/lib/assets/delete-asset.d.ts +2 -1
  99. package/dist/lib/assets/delete-asset.js +12 -0
  100. package/dist/lib/assets/delete-asset.js.map +1 -1
  101. package/dist/lib/assets/index.d.ts +2 -0
  102. package/dist/lib/assets/index.js +2 -0
  103. package/dist/lib/assets/index.js.map +1 -1
  104. package/dist/lib/assets/thumbnail.d.ts +145 -0
  105. package/dist/lib/assets/thumbnail.js +220 -0
  106. package/dist/lib/assets/thumbnail.js.map +1 -0
  107. package/dist/lib/assets/upload.d.ts +9 -0
  108. package/dist/lib/assets/upload.js +31 -0
  109. package/dist/lib/assets/upload.js.map +1 -1
  110. package/dist/lib/i18n/messages.de.js +16 -0
  111. package/dist/lib/i18n/messages.de.js.map +1 -1
  112. package/dist/lib/i18n/messages.en.d.ts +14 -0
  113. package/dist/lib/i18n/messages.en.js +16 -0
  114. package/dist/lib/i18n/messages.en.js.map +1 -1
  115. package/dist/lib/persistence/convex-adapter.js +4 -0
  116. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  117. package/dist/lib/persona/labels.d.ts +9 -5
  118. package/dist/lib/persona/labels.js +13 -8
  119. package/dist/lib/persona/labels.js.map +1 -1
  120. package/dist/lib/references/references-store.js +7 -1
  121. package/dist/lib/references/references-store.js.map +1 -1
  122. package/package.json +2 -2
  123. package/src/styles/studio.css +13 -0
@@ -1 +1 @@
1
- {"version":3,"file":"composition-field-editor.js","sourceRoot":"","sources":["../../../src/components/builder/composition-field-editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport { AlertTriangle, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react'\nimport { DEFAULT_SLOT, blockRootsOf } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { BlocksFieldDef } from '@elytracms/core/cms-core'\nimport { Button, Panel, Row, Spacer, Text, Toolbar, ToolbarGroup, ToolbarSeparator } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { BuilderProvider, useBuilder } from './builder-store'\nimport { useCanvasViewports } from './canvas-breakpoints'\nimport type { ViewportOption } from './canvas-breakpoints'\nimport { BuilderCanvas } from './canvas'\nimport { PageTree } from './page-tree'\nimport { PropertyInspector } from './property-inspector'\nimport { InspectorSidebar } from '../inspector/inspector-sidebar'\nimport type { InspectorPane } from '../inspector/inspector-sidebar'\nimport { InsertMenu } from './insert-menu'\nimport type { InsertContext } from './insert-menu'\nimport { resolveInsertContext } from './insert-context'\nimport { ReuseProvider } from './reuse-context'\n\n/**\n * EC-188 — the block-field surface: one canvas engine mounted on a composition\n * **field value** (AD-12), not the project graph.\n *\n * The composition tree is wrapped in a synthetic single-page graph and edited by\n * a SCOPED `BuilderProvider` with **no `projectId`** (so the graph persister is a\n * no-op — builder-store.tsx). Every committed edit is lifted out via `onChange`\n * into the CMS document draft; the document's own Save persists it (the tree is\n * stored as document data, `v.any()`). This reuses the real canvas + inspector +\n * blocks panel unchanged — the same EC-158–164 mechanics — without touching the\n * project-wide builder store. Surfaced on a dedicated \"Canvas\" fieldset tab in\n * the CMS detail editor so the page builder gets full screen real estate rather\n * than being crammed into the field list.\n */\n\nconst SYNTH_ROOT_ID = 'composition.root'\n/** The implicit root container the field's blocks compose inside (like a page root). */\nconst SYNTH_ROOT_COMPONENT = 'base.Stack'\n\n/**\n * Wrap the field value's blocks into a synthetic root container (EC-287). The\n * builder store edits this single root-bearing composition directly; its\n * children are the field's blocks, lifted back out via `onChange`.\n */\nfunction buildSyntheticRoot(value: unknown): ComponentNode {\n return {\n id: SYNTH_ROOT_ID,\n componentId: SYNTH_ROOT_COMPONENT,\n slots: { [DEFAULT_SLOT]: blockRootsOf(value) },\n }\n}\n\n/** Unwrap the synthetic root's children back to a field value per cardinality. */\nfunction unwrapValue(blocks: readonly ComponentNode[], multiple: boolean): unknown {\n return multiple ? [...blocks] : blocks[0]\n}\n\nexport function CompositionFieldEditor({\n field,\n value,\n onChange,\n readOnly,\n trailingPanes,\n}: {\n field: BlocksFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n /** Inspector panes the document editor appends after the canvas-supplied\n * `Properties` pane (EC-224 — the frame-owned `Versions` pane). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n // Build the synthetic root ONCE from the value at mount. The editor is\n // uncontrolled thereafter — it is the source of truth while open and lifts\n // changes out via onChange; re-opening the tab remounts from the current draft.\n // (BuilderProvider reads `composition` only in its lazy initial state, so later\n // prop changes are ignored regardless.)\n const initialRoot = useMemo(() => buildSyntheticRoot(value), [])\n\n return (\n <BuilderProvider composition={initialRoot}>\n <CompositionLift multiple={field.multiple} onChange={onChange} readOnly={readOnly} />\n <ReuseProvider>\n <CompositionEditorShell\n readOnly={readOnly}\n fieldAllow={field.allow}\n trailingPanes={trailingPanes}\n />\n </ReuseProvider>\n </BuilderProvider>\n )\n}\n\n/**\n * Lifts committed edits out of the scoped store into the CMS draft. The first\n * render establishes the (container-normalized) baseline so simply opening the\n * tab never marks the document dirty; every subsequent change to the block list\n * emits the unwrapped value.\n */\nfunction CompositionLift({\n multiple,\n onChange,\n readOnly,\n}: {\n multiple: boolean\n onChange: (value: unknown) => void\n readOnly: boolean\n}) {\n const builder = useBuilder()\n const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n const baselineRef = useRef<readonly ComponentNode[] | null>(null)\n\n useEffect(() => {\n // EC-226: never lift edits out of a READ-ONLY canvas (the Published view).\n // The canvas's keyboard listeners (clipboard paste/cut) aren't blocked by the\n // visual `pointer-events-none`, so without this a Cmd+V could mutate the\n // graph; refusing to lift keeps the document draft intact regardless. The\n // editor remounts per perspective, so any transient in-canvas change is\n // discarded on switch.\n if (readOnly) return\n if (baselineRef.current === null) {\n baselineRef.current = blocks\n return\n }\n if (blocks !== baselineRef.current) {\n baselineRef.current = blocks\n onChangeRef.current(unwrapValue(blocks, multiple))\n }\n }, [blocks, multiple, readOnly])\n\n return null\n}\n\nconst EMPTY_BLOCKS: readonly ComponentNode[] = []\n\n/** A breakpoint preview-width toggle (EC-211/EC-250). Switching the viewport\n * resizes only the preview frame (not an undoable edit, writes no graph command).\n * The label is the breakpoint name (`Full`/`sm`/`md`/…) with the real px width in\n * the tooltip — so the switcher reflects the site's actual screens, not a fixed\n * desktop/tablet/phone triple. */\nfunction ViewportButton({ option }: { option: ViewportOption }) {\n const builder = useBuilder()\n const active = builder.viewport === option.key\n return (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.viewportPreviewAria', { label: option.label })}\n aria-pressed={active}\n title={\n option.px !== undefined\n ? `${option.label} · ${option.px}px`\n : translate('builder.canvas.viewportWidthTitle', { label: option.label })\n }\n onClick={() => builder.dispatch({ type: 'viewport', viewport: option.key })}\n className={cn('px-2 text-xs', active && 'bg-accent text-accent-foreground')}\n >\n {option.label}\n </Button>\n )\n}\n\n/** The centered breakpoint switcher (EC-250): the resolved viewport set, plus a\n * visible validation issue when a project declared breakpoints but none are usable. */\nfunction BreakpointSwitcher() {\n const { viewports, issue } = useCanvasViewports()\n return (\n <Row gap=\"1\" align=\"center\">\n <ToolbarGroup>\n {viewports.map((v) => (\n <ViewportButton key={v.key} option={v} />\n ))}\n </ToolbarGroup>\n {issue ? (\n <Row gap=\"1\" align=\"center\" className=\"text-amber-600 dark:text-amber-400\" title={issue}>\n <AlertTriangle aria-hidden className=\"size-3.5\" />\n <Text size=\"xs\">{translate('builder.canvas.breakpointsIssueLabel')}</Text>\n </Row>\n ) : null}\n </Row>\n )\n}\n\n/**\n * The slim editor surface — the canvas + layers + inspector, plus Insert and\n * undo/redo. It deliberately drops the project-builder *lifecycle* chrome\n * (publish, versions, AI, page SEO): the document owns the lifecycle, not the\n * field. It keeps spatial/preview controls — including the breakpoint\n * preview-width switcher (EC-211), a canvas concern, not a lifecycle one.\n */\nfunction CompositionEditorShell({\n readOnly,\n fieldAllow,\n trailingPanes,\n}: {\n readOnly: boolean\n /** The field's top-level component vocabulary (EC-188) — the leash applied to\n * inserts at the composition root so forbidden components aren't even offered. */\n fieldAllow?: readonly string[]\n /** Document-level panes appended after `Properties` (EC-224). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n const builder = useBuilder()\n const dispatch = builder.dispatch\n const [insertContext, setInsertContext] = useState<InsertContext | null>(null)\n const [insertOpen, setInsertOpen] = useState(false)\n const rootId = builder.present.id\n\n // The canvas SUPPLIES the `Properties` pane (EC-224); the document editor\n // appends its own (`Versions`). Properties dims under read-only just as the\n // whole inspector column used to — but the appended panes stay interactive.\n const propertiesPane = useMemo<InspectorPane>(\n () => ({\n id: 'properties',\n label: translate('builder.inspector.propertiesPane'),\n icon: <SlidersHorizontal className=\"size-3.5\" />,\n render: () => (\n <div\n className={cn(\n 'h-full min-h-0 overflow-hidden',\n readOnly && 'pointer-events-none opacity-60',\n )}\n >\n <PropertyInspector />\n </div>\n ),\n }),\n [readOnly],\n )\n const panes = useMemo<InspectorPane[]>(\n () => [propertiesPane, ...(trailingPanes ?? [])],\n [propertiesPane, trailingPanes],\n )\n\n // The single insert entry point for BOTH the toolbar and the page tree's\n // \"Add child\" (EC-383). It selects its target first so `builder.selectedId`\n // is the one source of truth for where an insert lands: the toolbar resolves\n // `builder.selectedId ?? rootId` while Add-child passes an explicit row, and\n // without this sync clicking Add-child (which otherwise leaves selection put)\n // could target a different node than the toolbar — surfacing as a mismatched\n // (even empty \"No allowed components match\") insert dialog. Both now resolve\n // the identical target + allow set for the same selection.\n function openInsert(nodeId: string) {\n const ctx = resolveInsertContext(builder, nodeId, fieldAllow)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n }\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <div className=\"shrink-0 border-b border-border\">\n <Toolbar>\n <ToolbarGroup>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.undo')}\n title={translate('builder.canvas.undo')}\n disabled={readOnly || !builder.canUndo}\n onClick={() => dispatch({ type: 'undo' })}\n >\n <Undo2 />\n </Button>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.redo')}\n title={translate('builder.canvas.redo')}\n disabled={readOnly || !builder.canRedo}\n onClick={() => dispatch({ type: 'redo' })}\n >\n <Redo2 />\n </Button>\n </ToolbarGroup>\n <ToolbarSeparator />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={readOnly}\n onClick={() => openInsert(builder.selectedId ?? rootId)}\n >\n <Plus /> {translate('builder.canvas.insert')}\n </Button>\n {/* Breakpoint preview-width switcher (EC-211/EC-250), centered over the\n canvas via flanking spacers. A preview control, not an edit — stays\n active even in read-only published preview. */}\n <Spacer />\n <BreakpointSwitcher />\n <Spacer />\n </Toolbar>\n </div>\n\n <div className=\"grid min-h-0 flex-1 grid-cols-[minmax(200px,240px)_1fr_minmax(280px,320px)] gap-px bg-border\">\n <Panel\n className={cn('rounded-none border-0', readOnly && 'pointer-events-none opacity-60')}\n >\n <PageTree onAddChild={openInsert} />\n </Panel>\n <div\n className={cn(\n 'flex min-h-0 flex-col bg-background',\n readOnly && 'pointer-events-none opacity-60',\n )}\n >\n {/* EC-300/AD-16: the read-only Published view resolves references against\n the published perspective (honest preview); the editable Draft view\n resolves drafts (a just-made reusable shows immediately). */}\n <BuilderCanvas perspective={readOnly ? 'published' : 'draft'} />\n </div>\n <InspectorSidebar panes={panes} />\n </div>\n\n <InsertMenu context={insertContext} open={insertOpen} onOpenChange={setInsertOpen} />\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"composition-field-editor.js","sourceRoot":"","sources":["../../../src/components/builder/composition-field-editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport { AlertTriangle, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react'\nimport { DEFAULT_SLOT, blockRootsOf } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { BlocksFieldDef } from '@elytracms/core/cms-core'\nimport { Button, Panel, Row, Spacer, Text, Toolbar, ToolbarGroup, ToolbarSeparator } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { BuilderProvider, useBuilder } from './builder-store'\nimport { useCanvasViewports } from './canvas-breakpoints'\nimport type { ViewportOption } from './canvas-breakpoints'\nimport { BuilderCanvas } from './canvas'\nimport { PageTree } from './page-tree'\nimport { PropertyInspector } from './property-inspector'\nimport { InspectorSidebar } from '../inspector/inspector-sidebar'\nimport type { InspectorPane } from '../inspector/inspector-sidebar'\nimport { InsertMenu } from './insert-menu'\nimport type { InsertContext } from './insert-menu'\nimport { insertablePositions, resolveInsertContext, resolveInsertContextAt } from './insert-context'\nimport type { InsertPosition } from './insert-context'\nimport { InsertAffordanceProvider } from './insert-affordance-context'\nimport type { InsertAffordanceApi } from './insert-affordance-context'\nimport { PropSyncProvider } from './prop-sync-context'\nimport { ReuseProvider } from './reuse-context'\n\n/**\n * EC-188 — the block-field surface: one canvas engine mounted on a composition\n * **field value** (AD-12), not the project graph.\n *\n * The composition tree is wrapped in a synthetic single-page graph and edited by\n * a SCOPED `BuilderProvider` with **no `projectId`** (so the graph persister is a\n * no-op — builder-store.tsx). Every committed edit is lifted out via `onChange`\n * into the CMS document draft; the document's own Save persists it (the tree is\n * stored as document data, `v.any()`). This reuses the real canvas + inspector +\n * blocks panel unchanged — the same EC-158–164 mechanics — without touching the\n * project-wide builder store. Surfaced on a dedicated \"Canvas\" fieldset tab in\n * the CMS detail editor so the page builder gets full screen real estate rather\n * than being crammed into the field list.\n */\n\nconst SYNTH_ROOT_ID = 'composition.root'\n/** The implicit root container the field's blocks compose inside (like a page root). */\nconst SYNTH_ROOT_COMPONENT = 'base.Stack'\n\n/**\n * Wrap the field value's blocks into a synthetic root container (EC-287). The\n * builder store edits this single root-bearing composition directly; its\n * children are the field's blocks, lifted back out via `onChange`.\n */\nfunction buildSyntheticRoot(value: unknown): ComponentNode {\n return {\n id: SYNTH_ROOT_ID,\n componentId: SYNTH_ROOT_COMPONENT,\n slots: { [DEFAULT_SLOT]: blockRootsOf(value) },\n }\n}\n\n/** Unwrap the synthetic root's children back to a field value per cardinality. */\nfunction unwrapValue(blocks: readonly ComponentNode[], multiple: boolean): unknown {\n return multiple ? [...blocks] : blocks[0]\n}\n\nexport function CompositionFieldEditor({\n field,\n value,\n onChange,\n readOnly,\n trailingPanes,\n}: {\n field: BlocksFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n /** Inspector panes the document editor appends after the canvas-supplied\n * `Properties` pane (EC-224 — the frame-owned `Versions` pane). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n // Build the synthetic root ONCE from the value at mount. The editor is\n // uncontrolled thereafter — it is the source of truth while open and lifts\n // changes out via onChange; re-opening the tab remounts from the current draft.\n // (BuilderProvider reads `composition` only in its lazy initial state, so later\n // prop changes are ignored regardless.)\n const initialRoot = useMemo(() => buildSyntheticRoot(value), [])\n\n return (\n <BuilderProvider composition={initialRoot}>\n <CompositionLift multiple={field.multiple} onChange={onChange} readOnly={readOnly} />\n <ReuseProvider>\n {/* EC-408: the field↔component selection-sync channel wraps BOTH the canvas\n and the inspector (they reach each other through nothing else). */}\n <PropSyncProvider>\n <CompositionEditorShell readOnly={readOnly} fieldAllow={field.allow} trailingPanes={trailingPanes} />\n </PropSyncProvider>\n </ReuseProvider>\n </BuilderProvider>\n )\n}\n\n/**\n * Lifts committed edits out of the scoped store into the CMS draft. The first\n * render establishes the (container-normalized) baseline so simply opening the\n * tab never marks the document dirty; every subsequent change to the block list\n * emits the unwrapped value.\n */\nfunction CompositionLift({\n multiple,\n onChange,\n readOnly,\n}: {\n multiple: boolean\n onChange: (value: unknown) => void\n readOnly: boolean\n}) {\n const builder = useBuilder()\n const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n const baselineRef = useRef<readonly ComponentNode[] | null>(null)\n\n useEffect(() => {\n // EC-226: never lift edits out of a READ-ONLY canvas (the Published view).\n // The canvas's keyboard listeners (clipboard paste/cut) aren't blocked by the\n // visual `pointer-events-none`, so without this a Cmd+V could mutate the\n // graph; refusing to lift keeps the document draft intact regardless. The\n // editor remounts per perspective, so any transient in-canvas change is\n // discarded on switch.\n if (readOnly) return\n if (baselineRef.current === null) {\n baselineRef.current = blocks\n return\n }\n if (blocks !== baselineRef.current) {\n baselineRef.current = blocks\n onChangeRef.current(unwrapValue(blocks, multiple))\n }\n }, [blocks, multiple, readOnly])\n\n return null\n}\n\nconst EMPTY_BLOCKS: readonly ComponentNode[] = []\n\n/** A breakpoint preview-width toggle (EC-211/EC-250). Switching the viewport\n * resizes only the preview frame (not an undoable edit, writes no graph command).\n * The label is the breakpoint name (`Full`/`sm`/`md`/…) with the real px width in\n * the tooltip — so the switcher reflects the site's actual screens, not a fixed\n * desktop/tablet/phone triple. */\nfunction ViewportButton({ option }: { option: ViewportOption }) {\n const builder = useBuilder()\n const active = builder.viewport === option.key\n return (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.viewportPreviewAria', { label: option.label })}\n aria-pressed={active}\n title={\n option.px !== undefined\n ? `${option.label} · ${option.px}px`\n : translate('builder.canvas.viewportWidthTitle', { label: option.label })\n }\n onClick={() => builder.dispatch({ type: 'viewport', viewport: option.key })}\n className={cn('px-2 text-xs', active && 'bg-accent text-accent-foreground')}\n >\n {option.label}\n </Button>\n )\n}\n\n/** The centered breakpoint switcher (EC-250): the resolved viewport set, plus a\n * visible validation issue when a project declared breakpoints but none are usable. */\nfunction BreakpointSwitcher() {\n const { viewports, issue } = useCanvasViewports()\n return (\n <Row gap=\"1\" align=\"center\">\n <ToolbarGroup>\n {viewports.map((v) => (\n <ViewportButton key={v.key} option={v} />\n ))}\n </ToolbarGroup>\n {issue ? (\n <Row gap=\"1\" align=\"center\" className=\"text-amber-600 dark:text-amber-400\" title={issue}>\n <AlertTriangle aria-hidden className=\"size-3.5\" />\n <Text size=\"xs\">{translate('builder.canvas.breakpointsIssueLabel')}</Text>\n </Row>\n ) : null}\n </Row>\n )\n}\n\n/**\n * The slim editor surface — the canvas + layers + inspector, plus Insert and\n * undo/redo. It deliberately drops the project-builder *lifecycle* chrome\n * (publish, versions, AI, page SEO): the document owns the lifecycle, not the\n * field. It keeps spatial/preview controls — including the breakpoint\n * preview-width switcher (EC-211), a canvas concern, not a lifecycle one.\n */\nfunction CompositionEditorShell({\n readOnly,\n fieldAllow,\n trailingPanes,\n}: {\n readOnly: boolean\n /** The field's top-level component vocabulary (EC-188) — the leash applied to\n * inserts at the composition root so forbidden components aren't even offered. */\n fieldAllow?: readonly string[]\n /** Document-level panes appended after `Properties` (EC-224). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n const builder = useBuilder()\n const dispatch = builder.dispatch\n const [insertContext, setInsertContext] = useState<InsertContext | null>(null)\n const [insertOpen, setInsertOpen] = useState(false)\n const rootId = builder.present.id\n\n // The canvas SUPPLIES the `Properties` pane (EC-224); the document editor\n // appends its own (`Versions`). Properties dims under read-only just as the\n // whole inspector column used to — but the appended panes stay interactive.\n const propertiesPane = useMemo<InspectorPane>(\n () => ({\n id: 'properties',\n label: translate('builder.inspector.propertiesPane'),\n icon: <SlidersHorizontal className=\"size-3.5\" />,\n render: () => (\n <div className={cn('h-full min-h-0 overflow-hidden', readOnly && 'pointer-events-none opacity-60')}>\n <PropertyInspector />\n </div>\n ),\n }),\n [readOnly],\n )\n const panes = useMemo<InspectorPane[]>(\n () => [propertiesPane, ...(trailingPanes ?? [])],\n [propertiesPane, trailingPanes],\n )\n\n // The single insert entry point for BOTH the toolbar and the page tree's\n // \"Add child\" (EC-383). It selects its target first so `builder.selectedId`\n // is the one source of truth for where an insert lands: the toolbar resolves\n // `builder.selectedId ?? rootId` while Add-child passes an explicit row, and\n // without this sync clicking Add-child (which otherwise leaves selection put)\n // could target a different node than the toolbar — surfacing as a mismatched\n // (even empty \"No allowed components match\") insert dialog. Both now resolve\n // the identical target + allow set for the same selection.\n function openInsert(nodeId: string) {\n const ctx = resolveInsertContext(builder, nodeId, fieldAllow)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n }\n\n // EC-405: the in-canvas \"+\" affordances resolve the SAME slot rules through the\n // shared position-carrying insert-context resolution, and open the panel at the\n // clicked edge (an explicit position that survives to the insertion, rather than\n // the panel re-deriving it from the selection). Re-memoized on every store change\n // so the closures read the live graph.\n const affordanceApi = useMemo<InsertAffordanceApi>(\n () => ({\n positionsFor: (nodeId: string) => insertablePositions(builder, nodeId, fieldAllow),\n openAt: (nodeId: string, position: InsertPosition) => {\n const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n },\n }),\n [builder, fieldAllow, dispatch],\n )\n\n return (\n <InsertAffordanceProvider value={affordanceApi}>\n <div className=\"flex h-full min-h-0 flex-col\">\n <div className=\"shrink-0 border-b border-border\">\n <Toolbar>\n <ToolbarGroup>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.undo')}\n title={translate('builder.canvas.undo')}\n disabled={readOnly || !builder.canUndo}\n onClick={() => dispatch({ type: 'undo' })}\n >\n <Undo2 />\n </Button>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.redo')}\n title={translate('builder.canvas.redo')}\n disabled={readOnly || !builder.canRedo}\n onClick={() => dispatch({ type: 'redo' })}\n >\n <Redo2 />\n </Button>\n </ToolbarGroup>\n <ToolbarSeparator />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={readOnly}\n onClick={() => openInsert(builder.selectedId ?? rootId)}\n >\n <Plus /> {translate('builder.canvas.insert')}\n </Button>\n {/* Breakpoint preview-width switcher (EC-211/EC-250), centered over the\n canvas via flanking spacers. A preview control, not an edit — stays\n active even in read-only published preview. */}\n <Spacer />\n <BreakpointSwitcher />\n <Spacer />\n </Toolbar>\n </div>\n\n <div className=\"grid min-h-0 flex-1 grid-cols-[minmax(200px,240px)_1fr_minmax(280px,320px)] gap-px bg-border\">\n <Panel className={cn('rounded-none border-0', readOnly && 'pointer-events-none opacity-60')}>\n <PageTree onAddChild={openInsert} />\n </Panel>\n <div className={cn('flex min-h-0 flex-col bg-background', readOnly && 'pointer-events-none opacity-60')}>\n {/* EC-300/AD-16: the read-only Published view resolves references against\n the published perspective (honest preview); the editable Draft view\n resolves drafts (a just-made reusable shows immediately). */}\n <BuilderCanvas perspective={readOnly ? 'published' : 'draft'} />\n </div>\n <InspectorSidebar panes={panes} />\n </div>\n\n <InsertMenu context={insertContext} open={insertOpen} onOpenChange={setInsertOpen} />\n </div>\n </InsertAffordanceProvider>\n )\n}\n"]}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Delete/Backspace removes the selected node (EC-404, scope 3) — the gap the
3
+ * findings batch surfaced: the studio had no keyboard remove at all except cut
4
+ * (⌘X). This sits beside the clipboard listeners (`attachClipboardListeners`),
5
+ * wired on the same two surfaces (the studio window for the page tree, the
6
+ * iframe document for the canvas), and reuses the SAME editing-context guard so
7
+ * pressing Backspace while typing in an input or an inline text/rich-text editor
8
+ * edits the text — never deletes the block.
9
+ */
10
+ /**
11
+ * True when a key event's target owns its own text-editing behavior — a form
12
+ * field or any contenteditable (which includes the EC-158/EC-159 inline
13
+ * editors). The single guard for the Delete/Backspace remove, kept identical to
14
+ * the clipboard shortcuts' scope so the two can never disagree.
15
+ */
16
+ export declare function isEditingContext(target: EventTarget | null): boolean;
17
+ export interface DeleteKeyListenerOptions {
18
+ /** The current selection — the remove subject. */
19
+ getSelectedId: () => string | null;
20
+ /** Dispatch the remove (the existing `remove` tree action; one undo restores). */
21
+ remove: (id: string) => void;
22
+ /** The id that must never be removed (the composition root). */
23
+ getRootId: () => string | null;
24
+ /** Extra suppression (e.g. a read-only published preview). */
25
+ isDisabled?: () => boolean;
26
+ }
27
+ interface ListenerTarget {
28
+ addEventListener(type: string, listener: (e: Event) => void): void;
29
+ removeEventListener(type: string, listener: (e: Event) => void): void;
30
+ }
31
+ /**
32
+ * Wire Delete/Backspace → remove the selected node. Inert when the event
33
+ * targets an editing context, when nothing (or the root) is selected, or when
34
+ * disabled. Modifier combinations are ignored so it never shadows a browser
35
+ * shortcut (⌘Delete etc.).
36
+ */
37
+ export declare function attachDeleteKeyListener(target: ListenerTarget, opts: DeleteKeyListenerOptions): () => void;
38
+ export {};
@@ -0,0 +1,46 @@
1
+ import { clipboardEventBlocked } from './clipboard-actions.js';
2
+ /**
3
+ * Delete/Backspace removes the selected node (EC-404, scope 3) — the gap the
4
+ * findings batch surfaced: the studio had no keyboard remove at all except cut
5
+ * (⌘X). This sits beside the clipboard listeners (`attachClipboardListeners`),
6
+ * wired on the same two surfaces (the studio window for the page tree, the
7
+ * iframe document for the canvas), and reuses the SAME editing-context guard so
8
+ * pressing Backspace while typing in an input or an inline text/rich-text editor
9
+ * edits the text — never deletes the block.
10
+ */
11
+ /**
12
+ * True when a key event's target owns its own text-editing behavior — a form
13
+ * field or any contenteditable (which includes the EC-158/EC-159 inline
14
+ * editors). The single guard for the Delete/Backspace remove, kept identical to
15
+ * the clipboard shortcuts' scope so the two can never disagree.
16
+ */
17
+ export function isEditingContext(target) {
18
+ return clipboardEventBlocked(target);
19
+ }
20
+ /**
21
+ * Wire Delete/Backspace → remove the selected node. Inert when the event
22
+ * targets an editing context, when nothing (or the root) is selected, or when
23
+ * disabled. Modifier combinations are ignored so it never shadows a browser
24
+ * shortcut (⌘Delete etc.).
25
+ */
26
+ export function attachDeleteKeyListener(target, opts) {
27
+ const onKeyDown = (e) => {
28
+ const event = e;
29
+ if (event.key !== 'Delete' && event.key !== 'Backspace')
30
+ return;
31
+ if (event.metaKey || event.ctrlKey || event.altKey)
32
+ return;
33
+ if (opts.isDisabled?.() ?? false)
34
+ return;
35
+ if (isEditingContext(event.target))
36
+ return;
37
+ const selectedId = opts.getSelectedId();
38
+ if (!selectedId || selectedId === opts.getRootId())
39
+ return;
40
+ event.preventDefault();
41
+ opts.remove(selectedId);
42
+ };
43
+ target.addEventListener('keydown', onKeyDown);
44
+ return () => target.removeEventListener('keydown', onKeyDown);
45
+ }
46
+ //# sourceMappingURL=delete-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete-key.js","sourceRoot":"","sources":["../../../src/components/builder/delete-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAE3D;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAA0B;IACzD,OAAO,qBAAqB,CAAC,MAAM,CAAC,CAAA;AACtC,CAAC;AAkBD;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAsB,EACtB,IAA8B;IAE9B,MAAM,SAAS,GAAG,CAAC,CAAQ,EAAQ,EAAE;QACnC,MAAM,KAAK,GAAG,CAAkB,CAAA;QAChC,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW;YAAE,OAAM;QAC/D,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,MAAM;YAAE,OAAM;QAC1D,IAAI,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,KAAK;YAAE,OAAM;QACxC,IAAI,gBAAgB,CAAC,KAAK,CAAC,MAAM,CAAC;YAAE,OAAM;QAC1C,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,CAAA;QACvC,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,IAAI,CAAC,SAAS,EAAE;YAAE,OAAM;QAC1D,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACzB,CAAC,CAAA;IACD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;IAC7C,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;AAC/D,CAAC","sourcesContent":["import { clipboardEventBlocked } from './clipboard-actions'\n\n/**\n * Delete/Backspace removes the selected node (EC-404, scope 3) — the gap the\n * findings batch surfaced: the studio had no keyboard remove at all except cut\n * (⌘X). This sits beside the clipboard listeners (`attachClipboardListeners`),\n * wired on the same two surfaces (the studio window for the page tree, the\n * iframe document for the canvas), and reuses the SAME editing-context guard so\n * pressing Backspace while typing in an input or an inline text/rich-text editor\n * edits the text — never deletes the block.\n */\n\n/**\n * True when a key event's target owns its own text-editing behavior — a form\n * field or any contenteditable (which includes the EC-158/EC-159 inline\n * editors). The single guard for the Delete/Backspace remove, kept identical to\n * the clipboard shortcuts' scope so the two can never disagree.\n */\nexport function isEditingContext(target: EventTarget | null): boolean {\n return clipboardEventBlocked(target)\n}\n\nexport interface DeleteKeyListenerOptions {\n /** The current selection — the remove subject. */\n getSelectedId: () => string | null\n /** Dispatch the remove (the existing `remove` tree action; one undo restores). */\n remove: (id: string) => void\n /** The id that must never be removed (the composition root). */\n getRootId: () => string | null\n /** Extra suppression (e.g. a read-only published preview). */\n isDisabled?: () => boolean\n}\n\ninterface ListenerTarget {\n addEventListener(type: string, listener: (e: Event) => void): void\n removeEventListener(type: string, listener: (e: Event) => void): void\n}\n\n/**\n * Wire Delete/Backspace → remove the selected node. Inert when the event\n * targets an editing context, when nothing (or the root) is selected, or when\n * disabled. Modifier combinations are ignored so it never shadows a browser\n * shortcut (⌘Delete etc.).\n */\nexport function attachDeleteKeyListener(\n target: ListenerTarget,\n opts: DeleteKeyListenerOptions,\n): () => void {\n const onKeyDown = (e: Event): void => {\n const event = e as KeyboardEvent\n if (event.key !== 'Delete' && event.key !== 'Backspace') return\n if (event.metaKey || event.ctrlKey || event.altKey) return\n if (opts.isDisabled?.() ?? false) return\n if (isEditingContext(event.target)) return\n const selectedId = opts.getSelectedId()\n if (!selectedId || selectedId === opts.getRootId()) return\n event.preventDefault()\n opts.remove(selectedId)\n }\n target.addEventListener('keydown', onKeyDown)\n return () => target.removeEventListener('keydown', onKeyDown)\n}\n"]}
@@ -26,6 +26,7 @@ export interface DragSession {
26
26
  buildNode?: () => ComponentNode;
27
27
  }
28
28
  type EndListener = (outcome: DragOutcome, session: DragSession) => void;
29
+ type StartListener = (session: DragSession) => void;
29
30
  /** Begin a drag session and stamp the dataTransfer (Firefox requires data to drag). */
30
31
  export declare function beginDrag(session: DragSession, dataTransfer: DataTransfer | null): void;
31
32
  /** The live session, or null when nothing Elytra-owned is being dragged. */
@@ -38,4 +39,11 @@ export declare function activeDrag(): DragSession | null;
38
39
  export declare function endDrag(outcome: DragOutcome): void;
39
40
  /** Subscribe to session end (indicator cleanup, blocks-panel close-on-drop). */
40
41
  export declare function onDragSessionEnd(listener: EndListener): () => void;
42
+ /**
43
+ * Subscribe to session start — fired for EVERY drag source (canvas node, tree
44
+ * row, blocks-panel tile). The canvas insertion affordances (EC-405) use this to
45
+ * hide while a drag is live: the DnD before/after/into indicators own the "where
46
+ * it lands" story during a drag, so a competing "+" would be noise.
47
+ */
48
+ export declare function onDragSessionStart(listener: StartListener): () => void;
41
49
  export {};
@@ -15,6 +15,7 @@
15
15
  export const ELYTRA_DND_MIME = 'application/x-elytra-canvas';
16
16
  let active = null;
17
17
  const endListeners = new Set();
18
+ const startListeners = new Set();
18
19
  /** Begin a drag session and stamp the dataTransfer (Firefox requires data to drag). */
19
20
  export function beginDrag(session, dataTransfer) {
20
21
  active = session;
@@ -23,6 +24,8 @@ export function beginDrag(session, dataTransfer) {
23
24
  dataTransfer.setData('text/plain', session.label);
24
25
  dataTransfer.effectAllowed = session.subject.kind === 'node' ? 'move' : 'copy';
25
26
  }
27
+ for (const listener of [...startListeners])
28
+ listener(session);
26
29
  }
27
30
  /** The live session, or null when nothing Elytra-owned is being dragged. */
28
31
  export function activeDrag() {
@@ -48,4 +51,16 @@ export function onDragSessionEnd(listener) {
48
51
  endListeners.delete(listener);
49
52
  };
50
53
  }
54
+ /**
55
+ * Subscribe to session start — fired for EVERY drag source (canvas node, tree
56
+ * row, blocks-panel tile). The canvas insertion affordances (EC-405) use this to
57
+ * hide while a drag is live: the DnD before/after/into indicators own the "where
58
+ * it lands" story during a drag, so a competing "+" would be noise.
59
+ */
60
+ export function onDragSessionStart(listener) {
61
+ startListeners.add(listener);
62
+ return () => {
63
+ startListeners.delete(listener);
64
+ };
65
+ }
51
66
  //# sourceMappingURL=drag-session.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"drag-session.js","sourceRoot":"","sources":["../../../src/components/builder/drag-session.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AAEH,6EAA6E;AAC7E,MAAM,CAAC,MAAM,eAAe,GAAG,6BAA6B,CAAA;AAgB5D,IAAI,MAAM,GAAuB,IAAI,CAAA;AACrC,MAAM,YAAY,GAAG,IAAI,GAAG,EAAe,CAAA;AAE3C,uFAAuF;AACvF,MAAM,UAAU,SAAS,CAAC,OAAoB,EAAE,YAAiC;IAC/E,MAAM,GAAG,OAAO,CAAA;IAChB,IAAI,YAAY,EAAE,CAAC;QACjB,YAAY,CAAC,OAAO,CAAC,eAAe,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;QACpD,YAAY,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;QACjD,YAAY,CAAC,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;IAChF,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,UAAU;IACxB,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,OAAoB;IAC1C,IAAI,CAAC,MAAM;QAAE,OAAM;IACnB,MAAM,OAAO,GAAG,MAAM,CAAA;IACtB,MAAM,GAAG,IAAI,CAAA;IACb,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,YAAY,CAAC;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;AACtE,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,gBAAgB,CAAC,QAAqB;IACpD,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC1B,OAAO,GAAG,EAAE;QACV,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC/B,CAAC,CAAA;AACH,CAAC","sourcesContent":["import type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { DragSubject } from '@elytracms/studio/lib/dnd/drop-rules'\n\n/**\n * The one live drag session (EC-160). HTML5 `dataTransfer` payloads are\n * unreadable during `dragover`, so the canvas, page tree, and blocks panel\n * share this module-level session instead: the source begins it, every\n * surface reads it to resolve drop eligibility (via the pure rules in\n * `lib/dnd/drop-rules`), and exactly one place ends it — the drop handler\n * (`dropped`) or the source's `dragend` (`cancelled`). `endDrag` is\n * idempotent, so the source's trailing `dragend` after a successful drop\n * cannot double-commit or double-notify.\n *\n * The iframe canvas shares the parent window's JS realm (listeners are wired\n * from the parent), so one module instance covers all surfaces.\n */\n\n/** Custom MIME marking Elytra drags (lets surfaces ignore foreign drags). */\nexport const ELYTRA_DND_MIME = 'application/x-elytra-canvas'\n\nexport type DragSource = 'canvas' | 'tree' | 'blocks'\nexport type DragOutcome = 'dropped' | 'cancelled'\n\nexport interface DragSession {\n subject: DragSubject\n source: DragSource\n /** Human label (drag text fallback). */\n label: string\n /** For `component` subjects: builds the subtree to insert (ids reassigned by the store). */\n buildNode?: () => ComponentNode\n}\n\ntype EndListener = (outcome: DragOutcome, session: DragSession) => void\n\nlet active: DragSession | null = null\nconst endListeners = new Set<EndListener>()\n\n/** Begin a drag session and stamp the dataTransfer (Firefox requires data to drag). */\nexport function beginDrag(session: DragSession, dataTransfer: DataTransfer | null): void {\n active = session\n if (dataTransfer) {\n dataTransfer.setData(ELYTRA_DND_MIME, session.label)\n dataTransfer.setData('text/plain', session.label)\n dataTransfer.effectAllowed = session.subject.kind === 'node' ? 'move' : 'copy'\n }\n}\n\n/** The live session, or null when nothing Elytra-owned is being dragged. */\nexport function activeDrag(): DragSession | null {\n return active\n}\n\n/**\n * End the live session (no-op when none is active) and notify subscribers.\n * Call with `dropped` from the drop handler that dispatched the action, and\n * with `cancelled` from the source's `dragend`.\n */\nexport function endDrag(outcome: DragOutcome): void {\n if (!active) return\n const session = active\n active = null\n for (const listener of [...endListeners]) listener(outcome, session)\n}\n\n/** Subscribe to session end (indicator cleanup, blocks-panel close-on-drop). */\nexport function onDragSessionEnd(listener: EndListener): () => void {\n endListeners.add(listener)\n return () => {\n endListeners.delete(listener)\n }\n}\n"]}
1
+ {"version":3,"file":"drag-session.js","sourceRoot":"","sources":["../../../src/components/builder/drag-session.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AAEH,6EAA6E;AAC7E,MAAM,CAAC,MAAM,eAAe,GAAG,6BAA6B,CAAA;AAiB5D,IAAI,MAAM,GAAuB,IAAI,CAAA;AACrC,MAAM,YAAY,GAAG,IAAI,GAAG,EAAe,CAAA;AAC3C,MAAM,cAAc,GAAG,IAAI,GAAG,EAAiB,CAAA;AAE/C,uFAAuF;AACvF,MAAM,UAAU,SAAS,CAAC,OAAoB,EAAE,YAAiC;IAC/E,MAAM,GAAG,OAAO,CAAA;IAChB,IAAI,YAAY,EAAE,CAAC;QACjB,YAAY,CAAC,OAAO,CAAC,eAAe,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;QACpD,YAAY,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;QACjD,YAAY,CAAC,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;IAChF,CAAC;IACD,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,cAAc,CAAC;QAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;AAC/D,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,UAAU;IACxB,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,OAAoB;IAC1C,IAAI,CAAC,MAAM;QAAE,OAAM;IACnB,MAAM,OAAO,GAAG,MAAM,CAAA;IACtB,MAAM,GAAG,IAAI,CAAA;IACb,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,YAAY,CAAC;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;AACtE,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,gBAAgB,CAAC,QAAqB;IACpD,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC1B,OAAO,GAAG,EAAE;QACV,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC/B,CAAC,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAuB;IACxD,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC5B,OAAO,GAAG,EAAE;QACV,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IACjC,CAAC,CAAA;AACH,CAAC","sourcesContent":["import type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { DragSubject } from '@elytracms/studio/lib/dnd/drop-rules'\n\n/**\n * The one live drag session (EC-160). HTML5 `dataTransfer` payloads are\n * unreadable during `dragover`, so the canvas, page tree, and blocks panel\n * share this module-level session instead: the source begins it, every\n * surface reads it to resolve drop eligibility (via the pure rules in\n * `lib/dnd/drop-rules`), and exactly one place ends it — the drop handler\n * (`dropped`) or the source's `dragend` (`cancelled`). `endDrag` is\n * idempotent, so the source's trailing `dragend` after a successful drop\n * cannot double-commit or double-notify.\n *\n * The iframe canvas shares the parent window's JS realm (listeners are wired\n * from the parent), so one module instance covers all surfaces.\n */\n\n/** Custom MIME marking Elytra drags (lets surfaces ignore foreign drags). */\nexport const ELYTRA_DND_MIME = 'application/x-elytra-canvas'\n\nexport type DragSource = 'canvas' | 'tree' | 'blocks'\nexport type DragOutcome = 'dropped' | 'cancelled'\n\nexport interface DragSession {\n subject: DragSubject\n source: DragSource\n /** Human label (drag text fallback). */\n label: string\n /** For `component` subjects: builds the subtree to insert (ids reassigned by the store). */\n buildNode?: () => ComponentNode\n}\n\ntype EndListener = (outcome: DragOutcome, session: DragSession) => void\ntype StartListener = (session: DragSession) => void\n\nlet active: DragSession | null = null\nconst endListeners = new Set<EndListener>()\nconst startListeners = new Set<StartListener>()\n\n/** Begin a drag session and stamp the dataTransfer (Firefox requires data to drag). */\nexport function beginDrag(session: DragSession, dataTransfer: DataTransfer | null): void {\n active = session\n if (dataTransfer) {\n dataTransfer.setData(ELYTRA_DND_MIME, session.label)\n dataTransfer.setData('text/plain', session.label)\n dataTransfer.effectAllowed = session.subject.kind === 'node' ? 'move' : 'copy'\n }\n for (const listener of [...startListeners]) listener(session)\n}\n\n/** The live session, or null when nothing Elytra-owned is being dragged. */\nexport function activeDrag(): DragSession | null {\n return active\n}\n\n/**\n * End the live session (no-op when none is active) and notify subscribers.\n * Call with `dropped` from the drop handler that dispatched the action, and\n * with `cancelled` from the source's `dragend`.\n */\nexport function endDrag(outcome: DragOutcome): void {\n if (!active) return\n const session = active\n active = null\n for (const listener of [...endListeners]) listener(outcome, session)\n}\n\n/** Subscribe to session end (indicator cleanup, blocks-panel close-on-drop). */\nexport function onDragSessionEnd(listener: EndListener): () => void {\n endListeners.add(listener)\n return () => {\n endListeners.delete(listener)\n }\n}\n\n/**\n * Subscribe to session start — fired for EVERY drag source (canvas node, tree\n * row, blocks-panel tile). The canvas insertion affordances (EC-405) use this to\n * hide while a drag is live: the DnD before/after/into indicators own the \"where\n * it lands\" story during a drag, so a competing \"+\" would be noise.\n */\nexport function onDragSessionStart(listener: StartListener): () => void {\n startListeners.add(listener)\n return () => {\n startListeners.delete(listener)\n }\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import type { InsertPosition } from './insert-context.js';
2
+ /**
3
+ * Bridges the in-canvas insertion affordances (EC-405) to the insert panel state
4
+ * that lives on the composition-field editor. The `SelectionOverlay` (portaled
5
+ * inside the canvas iframe) asks this context which positions are insertable for a
6
+ * node and opens the panel at a chosen one; the editor owns the panel's
7
+ * `open`/`context` state and the field-level `allow` leash. A separate module (not
8
+ * the editor) so the deep overlay can consume it without a canvas ↔ editor import
9
+ * cycle, and so any canvas mount WITHOUT an editor above it (component preview)
10
+ * simply reads `null` and shows no affordances — the `ReuseProvider` pattern.
11
+ */
12
+ export interface InsertAffordanceApi {
13
+ /** The affordance positions (before/after/into) that may actually be inserted at `nodeId`. */
14
+ positionsFor(nodeId: string): InsertPosition[];
15
+ /** Select `nodeId` and open the blocks panel targeted at that exact position. */
16
+ openAt(nodeId: string, position: InsertPosition): void;
17
+ }
18
+ export declare const InsertAffordanceProvider: import("react").Provider<InsertAffordanceApi | null>;
19
+ export declare function useInsertAffordance(): InsertAffordanceApi | null;
@@ -0,0 +1,7 @@
1
+ import { createContext, useContext } from 'react';
2
+ const InsertAffordanceContext = createContext(null);
3
+ export const InsertAffordanceProvider = InsertAffordanceContext.Provider;
4
+ export function useInsertAffordance() {
5
+ return useContext(InsertAffordanceContext);
6
+ }
7
+ //# sourceMappingURL=insert-affordance-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insert-affordance-context.js","sourceRoot":"","sources":["../../../src/components/builder/insert-affordance-context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAoBjD,MAAM,uBAAuB,GAAG,aAAa,CAA6B,IAAI,CAAC,CAAA;AAE/E,MAAM,CAAC,MAAM,wBAAwB,GAAG,uBAAuB,CAAC,QAAQ,CAAA;AAExE,MAAM,UAAU,mBAAmB;IACjC,OAAO,UAAU,CAAC,uBAAuB,CAAC,CAAA;AAC5C,CAAC","sourcesContent":["import { createContext, useContext } from 'react'\nimport type { InsertPosition } from './insert-context'\n\n/**\n * Bridges the in-canvas insertion affordances (EC-405) to the insert panel state\n * that lives on the composition-field editor. The `SelectionOverlay` (portaled\n * inside the canvas iframe) asks this context which positions are insertable for a\n * node and opens the panel at a chosen one; the editor owns the panel's\n * `open`/`context` state and the field-level `allow` leash. A separate module (not\n * the editor) so the deep overlay can consume it without a canvas ↔ editor import\n * cycle, and so any canvas mount WITHOUT an editor above it (component preview)\n * simply reads `null` and shows no affordances — the `ReuseProvider` pattern.\n */\nexport interface InsertAffordanceApi {\n /** The affordance positions (before/after/into) that may actually be inserted at `nodeId`. */\n positionsFor(nodeId: string): InsertPosition[]\n /** Select `nodeId` and open the blocks panel targeted at that exact position. */\n openAt(nodeId: string, position: InsertPosition): void\n}\n\nconst InsertAffordanceContext = createContext<InsertAffordanceApi | null>(null)\n\nexport const InsertAffordanceProvider = InsertAffordanceContext.Provider\n\nexport function useInsertAffordance(): InsertAffordanceApi | null {\n return useContext(InsertAffordanceContext)\n}\n"]}
@@ -23,3 +23,43 @@ import type { InsertContext } from './insert-menu.js';
23
23
  export declare function resolveInsertContext(builder: {
24
24
  present: ComponentNode;
25
25
  }, nodeId: string, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertContext | null;
26
+ /**
27
+ * An in-canvas insertion position relative to a reference node (EC-405) — the
28
+ * DnD position vocabulary (`lib/dnd/drop-rules`), narrowed to the three the
29
+ * insertion affordances offer: `before`/`after` a sibling, or `into` an (empty)
30
+ * container. `left`/`right` (the wrap-into-columns gesture) are DnD-only.
31
+ */
32
+ export type InsertPosition = 'before' | 'after' | 'into';
33
+ /**
34
+ * Resolve an `InsertContext` for an EXPLICIT position relative to `nodeId`
35
+ * (EC-405) — the position-carrying twin of {@link resolveInsertContext}, so an
36
+ * affordance click at a chosen edge lands the insert at exactly that spot rather
37
+ * than the panel re-deriving it from the selection.
38
+ *
39
+ * The slot resolution reuses the shared DnD rules — `into` uses
40
+ * {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the
41
+ * node accepts no children), `before`/`after` use the reference's parent slot at
42
+ * `index`/`index + 1` — so this surface and drag-insert can never disagree about
43
+ * where a position lands or what it allows. `fieldAllow` (EC-188) still constrains
44
+ * ONLY a direct child of the composition root, mirroring `resolveInsertContext`.
45
+ */
46
+ export declare function resolveInsertContextAt(builder: {
47
+ present: ComponentNode;
48
+ }, nodeId: string, position: InsertPosition, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertContext | null;
49
+ /**
50
+ * Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):
51
+ * a position is offered only when it structurally exists AND at least one registry
52
+ * component may land there (the SAME slot-rule filter the tree/DnD and the blocks
53
+ * panel apply — {@link hasInsertableComponent} over the resolved `slotAllow`), so a
54
+ * blocked spot shows no dead button.
55
+ *
56
+ * An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there
57
+ * is "add the first child", and an empty container often renders zero-height, so
58
+ * its own before/after edges are degenerate and would pile the three "+" buttons
59
+ * onto one point. A filled container and every leaf get before/after instead (each
60
+ * child already carries its own edge affordances, so a filled container needs no
61
+ * center "+").
62
+ */
63
+ export declare function insertablePositions(builder: {
64
+ present: ComponentNode;
65
+ }, nodeId: string, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertPosition[];
@@ -1,7 +1,8 @@
1
1
  import { getNode, getParentInfo } from '@elytracms/core/project-graph';
2
2
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
3
3
  import { shortId } from '@elytracms/studio/lib/persona/labels';
4
- import { intersectAllow } from './blocks-model.js';
4
+ import { primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
5
+ import { hasInsertableComponent, intersectAllow } from './blocks-model.js';
5
6
  /**
6
7
  * Where an insert lands: the canvas insert-context resolution, shared by every
7
8
  * surface that mounts the canvas engine.
@@ -27,8 +28,7 @@ export function resolveInsertContext(builder, nodeId, fieldAllow, studio = getSt
27
28
  if (!node)
28
29
  return null;
29
30
  const manifest = studio.registry.getManifest(node.componentId);
30
- const childSlot = manifest?.slots.find((s) => s.name === 'children') ??
31
- manifest?.slots.find((s) => s.name !== 'outlet');
31
+ const childSlot = manifest?.slots.find((s) => s.name === 'children') ?? manifest?.slots.find((s) => s.name !== 'outlet');
32
32
  const ctx = childSlot
33
33
  ? {
34
34
  target: { parentId: nodeId, slot: childSlot.name },
@@ -58,4 +58,87 @@ function resolveSiblingContext(studio, root, nodeId) {
58
58
  }
59
59
  return { target: { parentId: nodeId, slot: 'children' }, destinationLabel: 'root' };
60
60
  }
61
+ /**
62
+ * Resolve an `InsertContext` for an EXPLICIT position relative to `nodeId`
63
+ * (EC-405) — the position-carrying twin of {@link resolveInsertContext}, so an
64
+ * affordance click at a chosen edge lands the insert at exactly that spot rather
65
+ * than the panel re-deriving it from the selection.
66
+ *
67
+ * The slot resolution reuses the shared DnD rules — `into` uses
68
+ * {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the
69
+ * node accepts no children), `before`/`after` use the reference's parent slot at
70
+ * `index`/`index + 1` — so this surface and drag-insert can never disagree about
71
+ * where a position lands or what it allows. `fieldAllow` (EC-188) still constrains
72
+ * ONLY a direct child of the composition root, mirroring `resolveInsertContext`.
73
+ */
74
+ export function resolveInsertContextAt(builder, nodeId, position, fieldAllow, studio = getStudioRegistry()) {
75
+ const root = builder.present;
76
+ const node = getNode(root, nodeId);
77
+ if (!node)
78
+ return null;
79
+ let ctx;
80
+ if (position === 'into') {
81
+ const manifest = studio.registry.getManifest(node.componentId);
82
+ const slot = primaryInsertSlot(manifest);
83
+ if (!slot)
84
+ return null; // the node accepts no children — no `into` position
85
+ ctx = {
86
+ target: {
87
+ parentId: nodeId,
88
+ slot: slot.name,
89
+ index: (node.slots?.[slot.name] ?? []).length,
90
+ },
91
+ slotAllow: slot.allow,
92
+ destinationLabel: `${shortId(node.componentId)} · ${slot.name}`,
93
+ };
94
+ }
95
+ else {
96
+ const info = getParentInfo(root, nodeId);
97
+ if (!info)
98
+ return null; // the root has no siblings — no before/after position
99
+ const pManifest = studio.registry.getManifest(info.parent.componentId);
100
+ ctx = {
101
+ target: {
102
+ parentId: info.parent.id,
103
+ slot: info.slot,
104
+ index: info.index + (position === 'after' ? 1 : 0),
105
+ },
106
+ slotAllow: pManifest?.slots.find((s) => s.name === info.slot)?.allow,
107
+ destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,
108
+ };
109
+ }
110
+ if (fieldAllow && fieldAllow.length > 0 && ctx.target.parentId === root.id) {
111
+ return { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, fieldAllow) };
112
+ }
113
+ return ctx;
114
+ }
115
+ /**
116
+ * Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):
117
+ * a position is offered only when it structurally exists AND at least one registry
118
+ * component may land there (the SAME slot-rule filter the tree/DnD and the blocks
119
+ * panel apply — {@link hasInsertableComponent} over the resolved `slotAllow`), so a
120
+ * blocked spot shows no dead button.
121
+ *
122
+ * An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there
123
+ * is "add the first child", and an empty container often renders zero-height, so
124
+ * its own before/after edges are degenerate and would pile the three "+" buttons
125
+ * onto one point. A filled container and every leaf get before/after instead (each
126
+ * child already carries its own edge affordances, so a filled container needs no
127
+ * center "+").
128
+ */
129
+ export function insertablePositions(builder, nodeId, fieldAllow, studio = getStudioRegistry()) {
130
+ // Empty insertable container → into only.
131
+ const into = resolveInsertContextAt(builder, nodeId, 'into', fieldAllow, studio);
132
+ if (into && (into.target.index ?? 0) === 0 && hasInsertableComponent(studio, into.slotAllow)) {
133
+ return ['into'];
134
+ }
135
+ // Otherwise the sibling edges that accept something.
136
+ const out = [];
137
+ for (const position of ['before', 'after']) {
138
+ const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow, studio);
139
+ if (ctx && hasInsertableComponent(studio, ctx.slotAllow))
140
+ out.push(position);
141
+ }
142
+ return out;
143
+ }
61
144
  //# sourceMappingURL=insert-context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"insert-context.js","sourceRoot":"","sources":["../../../src/components/builder/insert-context.ts"],"names":[],"mappings":"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","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\nimport { intersectAllow } from './blocks-model'\nimport type { InsertContext } from './insert-menu'\n\n/**\n * Where an insert lands: the canvas insert-context resolution, shared by every\n * surface that mounts the canvas engine.\n *\n * Extracted from the retired standalone `BuilderShell` (EC-210) so the in-CMS\n * Canvas editor (`composition-field-editor.tsx`, EC-188/AD-12) can own it without\n * importing the dead shell. No UI here — pure graph/registry resolution.\n */\n\n/**\n * Resolve where an insert lands: into the node's child slot, else as a sibling.\n *\n * `fieldAllow` (EC-188) is the composition field's TOP-LEVEL vocabulary\n * (`field.allow`). It constrains ONLY inserts that land as a direct child of the\n * active page root — the top level of a composition field value — where it is\n * intersected into `slotAllow` so forbidden components are never even offered,\n * mirroring the top-level leash in `validateCompositionValue`. Deeper inserts keep\n * following each component's own slot `allow`. Callers that omit `fieldAllow` get\n * an unconstrained top level.\n */\nexport function resolveInsertContext(\n builder: { present: ComponentNode },\n nodeId: string,\n fieldAllow?: readonly string[],\n studio: StudioRegistry = getStudioRegistry(),\n): InsertContext | null {\n const root = builder.present\n const node = getNode(root, nodeId)\n if (!node) return null\n const manifest = studio.registry.getManifest(node.componentId)\n const childSlot =\n manifest?.slots.find((s) => s.name === 'children') ??\n manifest?.slots.find((s) => s.name !== 'outlet')\n const ctx: InsertContext = childSlot\n ? {\n target: { parentId: nodeId, slot: childSlot.name },\n slotAllow: manifest!.slots.find((s) => s.name === childSlot.name)?.allow,\n destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,\n }\n : resolveSiblingContext(studio, root, nodeId)\n\n // The field vocabulary governs only the top level (a direct child of the page\n // root). `length > 0` matches the validator, which treats an empty allow as\n // \"allow any\" (validate.ts).\n if (fieldAllow && fieldAllow.length > 0 && ctx.target.parentId === root.id) {\n return { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, fieldAllow) }\n }\n return ctx\n}\n\n/** Sibling-or-root insert context for a node with no insertable child slot. */\nfunction resolveSiblingContext(\n studio: StudioRegistry,\n root: ComponentNode,\n nodeId: string,\n): InsertContext {\n const info = getParentInfo(root, nodeId)\n if (info) {\n const pManifest = studio.registry.getManifest(info.parent.componentId)\n const allow = pManifest?.slots.find((s) => s.name === info.slot)?.allow\n return {\n target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n slotAllow: allow,\n destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,\n }\n }\n return { target: { parentId: nodeId, slot: 'children' }, destinationLabel: 'root' }\n}\n"]}
1
+ {"version":3,"file":"insert-context.js","sourceRoot":"","sources":["../../../src/components/builder/insert-context.ts"],"names":[],"mappings":"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","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\nimport { primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { hasInsertableComponent, intersectAllow } from './blocks-model'\nimport type { InsertContext } from './insert-menu'\n\n/**\n * Where an insert lands: the canvas insert-context resolution, shared by every\n * surface that mounts the canvas engine.\n *\n * Extracted from the retired standalone `BuilderShell` (EC-210) so the in-CMS\n * Canvas editor (`composition-field-editor.tsx`, EC-188/AD-12) can own it without\n * importing the dead shell. No UI here — pure graph/registry resolution.\n */\n\n/**\n * Resolve where an insert lands: into the node's child slot, else as a sibling.\n *\n * `fieldAllow` (EC-188) is the composition field's TOP-LEVEL vocabulary\n * (`field.allow`). It constrains ONLY inserts that land as a direct child of the\n * active page root — the top level of a composition field value — where it is\n * intersected into `slotAllow` so forbidden components are never even offered,\n * mirroring the top-level leash in `validateCompositionValue`. Deeper inserts keep\n * following each component's own slot `allow`. Callers that omit `fieldAllow` get\n * an unconstrained top level.\n */\nexport function resolveInsertContext(\n builder: { present: ComponentNode },\n nodeId: string,\n fieldAllow?: readonly string[],\n studio: StudioRegistry = getStudioRegistry(),\n): InsertContext | null {\n const root = builder.present\n const node = getNode(root, nodeId)\n if (!node) return null\n const manifest = studio.registry.getManifest(node.componentId)\n const childSlot =\n manifest?.slots.find((s) => s.name === 'children') ?? manifest?.slots.find((s) => s.name !== 'outlet')\n const ctx: InsertContext = childSlot\n ? {\n target: { parentId: nodeId, slot: childSlot.name },\n slotAllow: manifest!.slots.find((s) => s.name === childSlot.name)?.allow,\n destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,\n }\n : resolveSiblingContext(studio, root, nodeId)\n\n // The field vocabulary governs only the top level (a direct child of the page\n // root). `length > 0` matches the validator, which treats an empty allow as\n // \"allow any\" (validate.ts).\n if (fieldAllow && fieldAllow.length > 0 && ctx.target.parentId === root.id) {\n return { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, fieldAllow) }\n }\n return ctx\n}\n\n/** Sibling-or-root insert context for a node with no insertable child slot. */\nfunction resolveSiblingContext(studio: StudioRegistry, root: ComponentNode, nodeId: string): InsertContext {\n const info = getParentInfo(root, nodeId)\n if (info) {\n const pManifest = studio.registry.getManifest(info.parent.componentId)\n const allow = pManifest?.slots.find((s) => s.name === info.slot)?.allow\n return {\n target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n slotAllow: allow,\n destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,\n }\n }\n return { target: { parentId: nodeId, slot: 'children' }, destinationLabel: 'root' }\n}\n\n/**\n * An in-canvas insertion position relative to a reference node (EC-405) — the\n * DnD position vocabulary (`lib/dnd/drop-rules`), narrowed to the three the\n * insertion affordances offer: `before`/`after` a sibling, or `into` an (empty)\n * container. `left`/`right` (the wrap-into-columns gesture) are DnD-only.\n */\nexport type InsertPosition = 'before' | 'after' | 'into'\n\n/**\n * Resolve an `InsertContext` for an EXPLICIT position relative to `nodeId`\n * (EC-405) — the position-carrying twin of {@link resolveInsertContext}, so an\n * affordance click at a chosen edge lands the insert at exactly that spot rather\n * than the panel re-deriving it from the selection.\n *\n * The slot resolution reuses the shared DnD rules — `into` uses\n * {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the\n * node accepts no children), `before`/`after` use the reference's parent slot at\n * `index`/`index + 1` — so this surface and drag-insert can never disagree about\n * where a position lands or what it allows. `fieldAllow` (EC-188) still constrains\n * ONLY a direct child of the composition root, mirroring `resolveInsertContext`.\n */\nexport function resolveInsertContextAt(\n builder: { present: ComponentNode },\n nodeId: string,\n position: InsertPosition,\n fieldAllow?: readonly string[],\n studio: StudioRegistry = getStudioRegistry(),\n): InsertContext | null {\n const root = builder.present\n const node = getNode(root, nodeId)\n if (!node) return null\n\n let ctx: InsertContext\n if (position === 'into') {\n const manifest = studio.registry.getManifest(node.componentId)\n const slot = primaryInsertSlot(manifest)\n if (!slot) return null // the node accepts no children — no `into` position\n ctx = {\n target: {\n parentId: nodeId,\n slot: slot.name,\n index: (node.slots?.[slot.name] ?? []).length,\n },\n slotAllow: slot.allow,\n destinationLabel: `${shortId(node.componentId)} · ${slot.name}`,\n }\n } else {\n const info = getParentInfo(root, nodeId)\n if (!info) return null // the root has no siblings — no before/after position\n const pManifest = studio.registry.getManifest(info.parent.componentId)\n ctx = {\n target: {\n parentId: info.parent.id,\n slot: info.slot,\n index: info.index + (position === 'after' ? 1 : 0),\n },\n slotAllow: pManifest?.slots.find((s) => s.name === info.slot)?.allow,\n destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,\n }\n }\n\n if (fieldAllow && fieldAllow.length > 0 && ctx.target.parentId === root.id) {\n return { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, fieldAllow) }\n }\n return ctx\n}\n\n/**\n * Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):\n * a position is offered only when it structurally exists AND at least one registry\n * component may land there (the SAME slot-rule filter the tree/DnD and the blocks\n * panel apply — {@link hasInsertableComponent} over the resolved `slotAllow`), so a\n * blocked spot shows no dead button.\n *\n * An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there\n * is \"add the first child\", and an empty container often renders zero-height, so\n * its own before/after edges are degenerate and would pile the three \"+\" buttons\n * onto one point. A filled container and every leaf get before/after instead (each\n * child already carries its own edge affordances, so a filled container needs no\n * center \"+\").\n */\nexport function insertablePositions(\n builder: { present: ComponentNode },\n nodeId: string,\n fieldAllow?: readonly string[],\n studio: StudioRegistry = getStudioRegistry(),\n): InsertPosition[] {\n // Empty insertable container → into only.\n const into = resolveInsertContextAt(builder, nodeId, 'into', fieldAllow, studio)\n if (into && (into.target.index ?? 0) === 0 && hasInsertableComponent(studio, into.slotAllow)) {\n return ['into']\n }\n // Otherwise the sibling edges that accept something.\n const out: InsertPosition[] = []\n for (const position of ['before', 'after'] as const) {\n const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow, studio)\n if (ctx && hasInsertableComponent(studio, ctx.slotAllow)) out.push(position)\n }\n return out\n}\n"]}
@@ -0,0 +1,60 @@
1
+ import type { ComponentNode } from '@elytracms/core/project-graph';
2
+ import type { ComponentManifest } from '@elytracms/core/component-registry';
3
+ /**
4
+ * Node display titles (EC-407) — the ONE helper behind both the page-tree row and
5
+ * the EC-404 selection-overlay frame label.
6
+ *
7
+ * A placed instance shows a derived **instance title** (its own text content, e.g. a
8
+ * heading's words) as the primary label, with the localized **component name** as
9
+ * muted secondary context; a node with no derivable instance title shows the
10
+ * component name alone. This replaces the old tree row's redundant `shortId` (mono
11
+ * id) + `title` pair — the raw id stays one hover away in the inspector header +
12
+ * the tree row `title=` attribute, so agent-inspectability is not lost.
13
+ *
14
+ * Placement: this lives in the studio (not `component-registry`) because the
15
+ * cheap rich-text head reuses `@elytracms/core/rich-text` `richTextPlainText`,
16
+ * which the DAG bars `component-registry` from importing. The pure derivation is
17
+ * mirrored from the CMS `resolveDocumentTitle` pattern (declared field/function +
18
+ * fallback), not shared with it.
19
+ */
20
+ /**
21
+ * Prop names, in priority order, whose value best titles an instance (EC-407). The
22
+ * name-affinity phase of {@link deriveInstanceTitle}: identity-ish props first
23
+ * (`title`/`heading`/`label`), then generic text (`text`/`name`/`value`), then media
24
+ * captions (`caption`/`alt`, so an Image titles by its alt text, not its URL `src`).
25
+ */
26
+ export declare const INSTANCE_TITLE_AFFINITY: readonly ["title", "heading", "label", "text", "name", "value", "caption", "alt"];
27
+ /** Max characters of a derived instance title before it is truncated for a row/frame. */
28
+ export declare const INSTANCE_TITLE_MAX = 64;
29
+ export interface NodeDisplayTitle {
30
+ /** The main label a surface shows: the instance title when derivable, else the component name. */
31
+ primary: string;
32
+ /** The localized component name — shown as muted secondary context ONLY when an instance title distinguishes `primary`. */
33
+ secondary?: string;
34
+ /** The localized component TYPE name (always present). */
35
+ componentName: string;
36
+ /** The derived per-instance title (already truncated), when one could be derived. */
37
+ instanceTitle?: string;
38
+ }
39
+ /**
40
+ * Derive a placed instance's raw (untruncated) title from its effective prop values
41
+ * (EC-407), or `undefined` when nothing text-ish is set. Pure; mirrors the CMS
42
+ * `resolveDocumentTitle` precedence:
43
+ *
44
+ * 1. A declared `nodeTitle` **function** — composed from the values (throws are
45
+ * swallowed so a bad pattern never breaks the tree).
46
+ * 2. A declared `nodeTitle` **prop name** — that prop's display text.
47
+ * 3. **Name-affinity heuristic** — the first {@link INSTANCE_TITLE_AFFINITY} prop
48
+ * that carries display text.
49
+ * 4. **Fallback** — the first declared text-ish prop (declaration order) with a
50
+ * non-URL value.
51
+ */
52
+ export declare function deriveInstanceTitle(manifest: ComponentManifest | undefined, node: ComponentNode): string | undefined;
53
+ /**
54
+ * The unified node label (EC-407) feeding the page-tree row AND the EC-404 overlay
55
+ * frame. `primary` is the derived instance title when one exists (truncated), else
56
+ * the localized component name; `secondary` is the localized component name, present
57
+ * only when an instance title distinguishes the row. `locale` is optional — omit it
58
+ * to resolve the component name against the active studio-UI locale.
59
+ */
60
+ export declare function nodeDisplayTitle(manifest: ComponentManifest | undefined, node: ComponentNode, locale?: string): NodeDisplayTitle;