@elytracms/studio 0.0.35 → 0.0.36
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.
- package/dist/components/assets/asset-selection.d.ts +1 -1
- package/dist/components/assets/asset-selection.js +2 -2
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +1 -1
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +7 -7
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/inline-edit.js +4 -3
- package/dist/components/builder/inline-edit.js.map +1 -1
- package/dist/components/builder/insert-context.js +1 -1
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/layout-inspector.js +2 -2
- package/dist/components/builder/layout-inspector.js.map +1 -1
- package/dist/components/builder/node-factory.js +7 -7
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/builder/page-tree.js +1 -1
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +2 -2
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +2 -2
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.js +5 -4
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/cell.js +1 -1
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/editable-cell.js +4 -4
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.js +9 -9
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/relation-picker.d.ts +2 -2
- package/dist/components/cms/relation-picker.js +5 -5
- package/dist/components/cms/relation-picker.js.map +1 -1
- package/dist/components/components/component-detail.js +1 -1
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/components-index.js +1 -1
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/registry-browser.js +2 -2
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/extensions/extensions-view.js +1 -1
- package/dist/components/extensions/extensions-view.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +1 -1
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/dnd/drop-rules.js +2 -2
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/dnd/gap-drag.js +2 -2
- package/dist/lib/dnd/gap-drag.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +2 -2
- package/dist/lib/persona/labels.js +3 -3
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/reference-model.js +1 -1
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/registry/preview.js +15 -15
- package/dist/lib/registry/preview.js.map +1 -1
- package/dist/lib/registry/project-components.js +6 -13
- package/dist/lib/registry/project-components.js.map +1 -1
- package/package.json +2 -2
|
@@ -8,4 +8,4 @@ export declare function singleAssetIdOf(value: unknown): string | null;
|
|
|
8
8
|
* - `many`: the picked id is appended once (already-present ids are a
|
|
9
9
|
* no-op); `null` leaves the list untouched.
|
|
10
10
|
*/
|
|
11
|
-
export declare function applyAssetSelection(current: unknown, picked: string | null,
|
|
11
|
+
export declare function applyAssetSelection(current: unknown, picked: string | null, multiple: boolean): unknown;
|
|
@@ -27,8 +27,8 @@ export function singleAssetIdOf(value) {
|
|
|
27
27
|
* - `many`: the picked id is appended once (already-present ids are a
|
|
28
28
|
* no-op); `null` leaves the list untouched.
|
|
29
29
|
*/
|
|
30
|
-
export function applyAssetSelection(current, picked,
|
|
31
|
-
if (
|
|
30
|
+
export function applyAssetSelection(current, picked, multiple) {
|
|
31
|
+
if (!multiple)
|
|
32
32
|
return picked ?? undefined;
|
|
33
33
|
const ids = assetIdsOf(current);
|
|
34
34
|
if (picked === null || ids.includes(picked))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-selection.js","sourceRoot":"","sources":["../../../src/components/assets/asset-selection.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAEtD,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACpC,OAAO,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,EAAE,KAAK,SAAS,CAAC,CAAA;AAC9E,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;IACjC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IACvC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;IAC9D,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAgB,EAChB,MAAqB,EACrB,
|
|
1
|
+
{"version":3,"file":"asset-selection.js","sourceRoot":"","sources":["../../../src/components/assets/asset-selection.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAEtD,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACpC,OAAO,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,EAAE,KAAK,SAAS,CAAC,CAAA;AAC9E,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;IACjC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IACvC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;IAC9D,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAgB,EAChB,MAAqB,EACrB,QAAiB;IAEjB,IAAI,CAAC,QAAQ;QAAE,OAAO,MAAM,IAAI,SAAS,CAAA;IACzC,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAA;IAC/B,IAAI,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,GAAG,CAAA;IACvD,OAAO,CAAC,GAAG,GAAG,EAAE,MAAM,CAAC,CAAA;AACzB,CAAC","sourcesContent":["/**\n * Pure value logic for CMS `asset` fields (EC-075 → EC-152): a field stores the\n * asset *id* (cardinality `one`) or an array of ids (`many`). These helpers\n * normalize stored values and apply a picker selection without the UI owning the\n * rules. EC-277: reads tolerate self-typed `{ type:'asset', id }` envelopes AND\n * legacy bare ids (the field is self-typed on save by the operations write lens).\n */\nimport { readAssetId } from '@elytracms/core/cms-core'\n\n/** The id list a `many` asset field currently holds (unreadable entries dropped). */\nexport function assetIdsOf(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n return value.map(readAssetId).filter((id): id is string => id !== undefined)\n}\n\n/** The single id a `one` asset field currently holds, if any. */\nexport function singleAssetIdOf(value: unknown): string | null {\n const direct = readAssetId(value)\n if (direct !== undefined) return direct\n if (Array.isArray(value)) return readAssetId(value[0]) ?? null\n return null\n}\n\n/**\n * Apply a picker selection to the stored field value.\n * - `one`: the picked id replaces the value; `null` clears it (`undefined`).\n * - `many`: the picked id is appended once (already-present ids are a\n * no-op); `null` leaves the list untouched.\n */\nexport function applyAssetSelection(\n current: unknown,\n picked: string | null,\n multiple: boolean,\n): unknown {\n if (!multiple) return picked ?? undefined\n const ids = assetIdsOf(current)\n if (picked === null || ids.includes(picked)) return ids\n return [...ids, picked]\n}\n"]}
|
|
@@ -47,7 +47,7 @@ export function clipboardEventBlocked(target) {
|
|
|
47
47
|
return el.isContentEditable === true;
|
|
48
48
|
}
|
|
49
49
|
function shortId(componentId) {
|
|
50
|
-
return componentId.replace('base.
|
|
50
|
+
return componentId.replace('base.', '').replace('project.', '');
|
|
51
51
|
}
|
|
52
52
|
/** Mirrors the builder reducer's fresh-node-id counter (`node-<n>`). */
|
|
53
53
|
function counterFrom(start) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clipboard-actions.js","sourceRoot":"","sources":["../../../src/components/builder/clipboard-actions.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useRef, useSyncExternalStore } from 'react'\nimport { AlertTriangle, CircleCheck, Crosshair, X } from 'lucide-react'\nimport { cloneWithNewIds } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { buildFragmentFromComposition } from '@elytracms/core/clipboard'\nimport type { ClipboardFragment, ParseFragmentResult } from '@elytracms/core/clipboard'\nimport {\n readFragmentFromClipboard,\n readFragmentFromDataTransfer,\n resolvePasteTarget,\n diffPastedSubtree,\n pasteSummaryMessage,\n writeFragmentToClipboard,\n writeFragmentToDataTransfer,\n} from '@elytracms/studio/lib/clipboard'\nimport type { AsyncClipboard, PasteIssue } from '@elytracms/studio/lib/clipboard'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useData } from '@elytracms/studio/lib/data'\nimport { useCollab } from '../collab/collab-context'\nimport { Badge, Row, Text } from '@elytracms/studio/ui'\nimport { useBuilder } from './builder-store'\n\n/**\n * Copy/cut/paste everywhere (EC-175, vision 3.3/3.7).\n *\n * One hook ({@link useStudioClipboard}) backs every affordance — canvas\n * keyboard shortcuts (wired inside the iframe by the canvas), page-tree\n * shortcuts and row buttons, and the blocks panel's \"copy exact\n * configuration\" — so they all produce/consume the same EC-174 fragment via\n * the OS clipboard (`lib/clipboard/os-clipboard`), never an app-internal\n * buffer. That is what makes paste work across documents, pages, projects,\n * and browser tabs.\n *\n * Dispatch contract: a paste is exactly ONE `insert` action (the fragment\n * root is a single subtree) → one history entry → one mirrored\n * `graph.insertNode` command — one undo step removes everything the paste\n * inserted. A cut is copy + exactly one `remove`; one undo restores it.\n *\n * Paste is PERMISSIVE: missing components and unresolvable bindings insert\n * anyway (the renderer's fallbacks keep them visible) and surface in a\n * notice — \"Pasted — 2 bindings unresolved, 1 component missing\" — whose\n * issues deep-link to the inserted nodes. Only structurally invalid clipboard\n * content is reported as a non-blocking error, never a crash or silent no-op.\n */\n\n// ---------------------------------------------------------------------------\n// Notice store — module-level pub/sub (the drag-session pattern) so any\n// surface can publish and the canvas overlay renders it.\n// ---------------------------------------------------------------------------\n\nexport type ClipboardNotice =\n | { kind: 'copied'; label: string }\n | { kind: 'cut'; label: string }\n | { kind: 'pasted'; summary: string; destinationLabel: string; issues: PasteIssue[] }\n | { kind: 'error'; message: string }\n\nlet currentNotice: ClipboardNotice | null = null\nconst noticeListeners = new Set<() => void>()\n\nexport function publishClipboardNotice(notice: ClipboardNotice | null): void {\n currentNotice = notice\n for (const listener of [...noticeListeners]) listener()\n}\n\nfunction subscribeNotice(listener: () => void): () => void {\n noticeListeners.add(listener)\n return () => noticeListeners.delete(listener)\n}\n\nfunction getNotice(): ClipboardNotice | null {\n return currentNotice\n}\n\nexport function useClipboardNotice(): ClipboardNotice | null {\n return useSyncExternalStore(subscribeNotice, getNotice, getNotice)\n}\n\n// ---------------------------------------------------------------------------\n// Shortcut scoping\n// ---------------------------------------------------------------------------\n\n/**\n * Whether a clipboard shortcut/event must stay inert for this target: form\n * fields and any contenteditable (which includes the EC-158/EC-159 inline\n * editors) own their own native clipboard behaviour.\n */\nexport function clipboardEventBlocked(target: EventTarget | null): boolean {\n const el = target as HTMLElement | null\n if (!el) return false\n const tag = el.tagName\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true\n if (typeof el.closest === 'function' && el.closest('[contenteditable]')) return true\n return el.isContentEditable === true\n}\n\n// ---------------------------------------------------------------------------\n// The clipboard surface\n// ---------------------------------------------------------------------------\n\nexport interface StudioClipboard {\n /**\n * Copy a canvas/tree node's subtree. Writes synchronously when a\n * ClipboardEvent's DataTransfer is given, else via the async Clipboard API\n * (`asyncClipboard` lets the canvas pass the iframe's own navigator —\n * Chromium scopes clipboard access to the *focused* document).\n */\n copyNode(nodeId: string, dt?: DataTransfer | null, asyncClipboard?: AsyncClipboard): void\n /** Copy, then remove the source in exactly one dispatch (one undo restores). */\n cutNode(nodeId: string, dt?: DataTransfer | null, asyncClipboard?: AsyncClipboard): void\n /** Copy a prebuilt fragment (blocks-panel \"copy exact configuration\"). */\n copyFragment(fragment: ClipboardFragment, label: string): void\n /** Paste from a paste event's DataTransfer (no permissions needed). */\n pasteFromDataTransfer(dt: DataTransfer, anchorId?: string | null): void\n /** Paste via the async Clipboard API (explicit affordances + shortcuts). */\n pasteFromClipboard(anchorId?: string | null, asyncClipboard?: AsyncClipboard): Promise<void>\n}\n\nfunction shortId(componentId: string): string {\n return componentId.replace('base.primitives.', '').replace('project.', '')\n}\n\n/** Mirrors the builder reducer's fresh-node-id counter (`node-<n>`). */\nfunction counterFrom(start: number): () => string {\n let n = start\n return () => {\n n += 1\n return `node-${n}`\n }\n}\n\nexport function useStudioClipboard(): StudioClipboard {\n const builder = useBuilder()\n const data = useData()\n const collab = useCollab()\n const studio = getStudioRegistry()\n\n // Event listeners outlive renders; refs keep them on the current state.\n const stateRef = useRef({ builder, data, projectId: collab.projectId })\n stateRef.current = { builder, data, projectId: collab.projectId }\n\n return useMemo<StudioClipboard>(() => {\n const fragmentFor = (nodeId: string): { fragment: ClipboardFragment; label: string } | null => {\n const { builder: b, projectId } = stateRef.current\n const built = buildFragmentFromComposition(b.present, nodeId, { source: { projectId } })\n if (!built.ok) {\n publishClipboardNotice({ kind: 'error', message: built.error.message })\n return null\n }\n return { fragment: built.fragment, label: shortId(built.fragment.payload.root.componentId) }\n }\n\n const write = (\n fragment: ClipboardFragment,\n label: string,\n kind: 'copied' | 'cut',\n dt?: DataTransfer | null,\n asyncClipboard?: AsyncClipboard,\n ): void => {\n if (dt) {\n writeFragmentToDataTransfer(dt, fragment)\n publishClipboardNotice({ kind, label })\n return\n }\n writeFragmentToClipboard(asyncClipboard ?? navigator.clipboard, fragment)\n .then(() => publishClipboardNotice({ kind, label }))\n .catch((cause: unknown) => {\n const detail = cause instanceof Error ? cause.message : String(cause)\n publishClipboardNotice({ kind: 'error', message: translate('builder.clipboard.writeError', { detail }) })\n })\n }\n\n const currentRoot = (): ComponentNode | undefined => {\n const { builder: b } = stateRef.current\n return b.present\n }\n\n const applyPaste = (result: ParseFragmentResult, anchorId?: string | null): void => {\n const { builder: b, data: d } = stateRef.current\n if (!result.ok) {\n publishClipboardNotice({ kind: 'error', message: result.error.message })\n return\n }\n const root = currentRoot()\n if (!root) return\n const { target, destinationLabel } = resolvePasteTarget({\n root,\n anchorId: anchorId !== undefined ? anchorId : b.selectedId,\n getManifest: (id) => studio.registry.getManifest(id),\n })\n // Mirror the reducer's deterministic re-id so the issue deep links point\n // at exactly the node ids the inserted subtree will carry.\n const inserted = cloneWithNewIds(result.fragment.payload.root, counterFrom(b.nodeSeq))\n // ONE dispatch: one history entry, one graph.insertNode command, one undo.\n b.dispatch({ type: 'insert', target, node: result.fragment.payload.root })\n const diff = diffPastedSubtree({\n node: inserted,\n hasComponent: (id) => studio.registry.getManifest(id) !== undefined,\n knownSourceIds: d.knownSourceIds,\n })\n publishClipboardNotice({\n kind: 'pasted',\n summary: pasteSummaryMessage(diff),\n destinationLabel,\n issues: diff.issues,\n })\n }\n\n return {\n copyNode(nodeId, dt, asyncClipboard) {\n const built = fragmentFor(nodeId)\n if (built) write(built.fragment, built.label, 'copied', dt, asyncClipboard)\n },\n cutNode(nodeId, dt, asyncClipboard) {\n const { builder: b } = stateRef.current\n const root = currentRoot()\n if (root && nodeId === root.id) {\n publishClipboardNotice({ kind: 'error', message: translate('builder.clipboard.rootCannotBeCut') })\n return\n }\n const built = fragmentFor(nodeId)\n if (!built) return\n write(built.fragment, built.label, 'cut', dt, asyncClipboard)\n // Exactly one structural dispatch: a single undo restores the cut node.\n b.dispatch({ type: 'remove', id: nodeId })\n },\n copyFragment(fragment, label) {\n write(fragment, label, 'copied')\n },\n pasteFromDataTransfer(dt, anchorId) {\n applyPaste(readFragmentFromDataTransfer(dt), anchorId)\n },\n async pasteFromClipboard(anchorId, asyncClipboard) {\n applyPaste(await readFragmentFromClipboard(asyncClipboard ?? navigator.clipboard), anchorId)\n },\n }\n }, [studio])\n}\n\n// ---------------------------------------------------------------------------\n// Listener wiring — one implementation for the page-tree window and the\n// canvas iframe document (events never cross the frame boundary).\n// ---------------------------------------------------------------------------\n\nexport interface ClipboardListenerOptions {\n clipboard: StudioClipboard\n /** Current selection: the copy/cut subject and the paste anchor. */\n getSelectedId: () => string | null\n /** Extra guard (e.g. the canvas's live inline-edit session). */\n isBlocked?: (target: EventTarget | null) => boolean\n /** The async Clipboard API of the listening document's own window. */\n asyncClipboard?: AsyncClipboard\n /** The listening document's selection — real text selections stay native. */\n getSelection?: () => Selection | null\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 Cmd/Ctrl+C/X/V plus native copy/cut/paste ClipboardEvents.\n *\n * Shortcuts are handled on `keydown` (a selected node has no text selection,\n * so the browser would not fire native clipboard events for it); the\n * `preventDefault` there suppresses the native event, so the ClipboardEvent\n * listeners only ever see *other* triggers — context-menu copy/paste and\n * synthetic events — and the two paths can never double-handle one gesture.\n * Everything stays inert in form fields and inside the inline editors.\n */\nexport function attachClipboardListeners(target: ListenerTarget, opts: ClipboardListenerOptions): () => void {\n const blocked = (t: EventTarget | null): boolean =>\n clipboardEventBlocked(t) || (opts.isBlocked?.(t) ?? false)\n const hasTextSelection = (): boolean => {\n const selection = opts.getSelection?.()\n return selection !== null && selection !== undefined && !selection.isCollapsed\n }\n\n const onKeyDown = (e: Event) => {\n const event = e as KeyboardEvent\n if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return\n const key = event.key.toLowerCase()\n if (key !== 'c' && key !== 'x' && key !== 'v') return\n if (blocked(event.target)) return\n if ((key === 'c' || key === 'x') && hasTextSelection()) return\n const selectedId = opts.getSelectedId()\n if (key === 'v') {\n event.preventDefault()\n void opts.clipboard.pasteFromClipboard(selectedId, opts.asyncClipboard)\n return\n }\n if (!selectedId) return\n event.preventDefault()\n if (key === 'c') opts.clipboard.copyNode(selectedId, null, opts.asyncClipboard)\n else opts.clipboard.cutNode(selectedId, null, opts.asyncClipboard)\n }\n\n const onCopyOrCut = (e: Event) => {\n const event = e as ClipboardEvent\n if (blocked(event.target) || hasTextSelection()) return\n const selectedId = opts.getSelectedId()\n if (!selectedId || !event.clipboardData) return\n event.preventDefault()\n if (event.type === 'copy') opts.clipboard.copyNode(selectedId, event.clipboardData)\n else opts.clipboard.cutNode(selectedId, event.clipboardData)\n }\n\n const onPaste = (e: Event) => {\n const event = e as ClipboardEvent\n if (blocked(event.target) || !event.clipboardData) return\n event.preventDefault()\n opts.clipboard.pasteFromDataTransfer(event.clipboardData, opts.getSelectedId())\n }\n\n target.addEventListener('keydown', onKeyDown)\n target.addEventListener('copy', onCopyOrCut)\n target.addEventListener('cut', onCopyOrCut)\n target.addEventListener('paste', onPaste)\n return () => {\n target.removeEventListener('keydown', onKeyDown)\n target.removeEventListener('copy', onCopyOrCut)\n target.removeEventListener('cut', onCopyOrCut)\n target.removeEventListener('paste', onPaste)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Notice overlay — rendered by the canvas; issues deep-link to their nodes.\n// ---------------------------------------------------------------------------\n\nexport function ClipboardNoticeOverlay() {\n const notice = useClipboardNotice()\n const builder = useBuilder()\n if (!notice) return null\n\n const tone =\n notice.kind === 'error' ? 'error' : notice.kind === 'pasted' && notice.issues.length > 0 ? 'warning' : 'success'\n\n return (\n <div\n data-testid=\"clipboard-notice\"\n data-notice-kind={notice.kind}\n className=\"absolute bottom-4 left-1/2 z-20 w-max max-w-[min(420px,90%)] -translate-x-1/2\"\n >\n <div className=\"rounded-lg border border-border bg-popover p-2 text-popover-foreground shadow-lg\">\n <Row gap=\"2\" align=\"start\">\n {tone === 'error' ? (\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-destructive\" aria-hidden />\n ) : tone === 'warning' ? (\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-warning\" aria-hidden />\n ) : (\n <CircleCheck className=\"mt-0.5 size-3.5 shrink-0 text-success\" aria-hidden />\n )}\n <Text size=\"xs\" className=\"min-w-0 flex-1\">\n {notice.kind === 'copied'\n ? translate('builder.clipboard.copied', { label: notice.label })\n : notice.kind === 'cut'\n ? translate('builder.clipboard.cutNotice', { label: notice.label })\n : notice.kind === 'pasted'\n ? translate('builder.clipboard.pastedInto', {\n summary: notice.summary,\n destination: notice.destinationLabel,\n })\n : notice.message}\n </Text>\n <button\n type=\"button\"\n aria-label={translate('common.dismiss')}\n onClick={() => publishClipboardNotice(null)}\n className=\"shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground\"\n >\n <X className=\"size-3.5\" />\n </button>\n </Row>\n {notice.kind === 'pasted' && notice.issues.length > 0 ? (\n <div className=\"mt-1.5 flex flex-col gap-1\">\n {notice.issues.map((issue, i) => (\n <button\n key={`${issue.nodeId}-${i}`}\n type=\"button\"\n data-testid=\"clipboard-notice-issue\"\n onClick={() => builder.dispatch({ type: 'select', id: issue.nodeId })}\n className=\"flex w-full items-center gap-2 rounded-md p-1 text-left transition-colors hover:bg-muted\"\n >\n <Badge variant=\"warning\" className=\"shrink-0\">\n {issue.kind === 'missing-component'\n ? translate('builder.clipboard.issueComponent')\n : translate('builder.clipboard.issueBinding')}\n </Badge>\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1 truncate\">\n {issue.message}\n </Text>\n <Crosshair className=\"size-3.5 shrink-0 text-muted-foreground\" aria-hidden />\n </button>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"clipboard-actions.js","sourceRoot":"","sources":["../../../src/components/builder/clipboard-actions.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useRef, useSyncExternalStore } from 'react'\nimport { AlertTriangle, CircleCheck, Crosshair, X } from 'lucide-react'\nimport { cloneWithNewIds } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { buildFragmentFromComposition } from '@elytracms/core/clipboard'\nimport type { ClipboardFragment, ParseFragmentResult } from '@elytracms/core/clipboard'\nimport {\n readFragmentFromClipboard,\n readFragmentFromDataTransfer,\n resolvePasteTarget,\n diffPastedSubtree,\n pasteSummaryMessage,\n writeFragmentToClipboard,\n writeFragmentToDataTransfer,\n} from '@elytracms/studio/lib/clipboard'\nimport type { AsyncClipboard, PasteIssue } from '@elytracms/studio/lib/clipboard'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useData } from '@elytracms/studio/lib/data'\nimport { useCollab } from '../collab/collab-context'\nimport { Badge, Row, Text } from '@elytracms/studio/ui'\nimport { useBuilder } from './builder-store'\n\n/**\n * Copy/cut/paste everywhere (EC-175, vision 3.3/3.7).\n *\n * One hook ({@link useStudioClipboard}) backs every affordance — canvas\n * keyboard shortcuts (wired inside the iframe by the canvas), page-tree\n * shortcuts and row buttons, and the blocks panel's \"copy exact\n * configuration\" — so they all produce/consume the same EC-174 fragment via\n * the OS clipboard (`lib/clipboard/os-clipboard`), never an app-internal\n * buffer. That is what makes paste work across documents, pages, projects,\n * and browser tabs.\n *\n * Dispatch contract: a paste is exactly ONE `insert` action (the fragment\n * root is a single subtree) → one history entry → one mirrored\n * `graph.insertNode` command — one undo step removes everything the paste\n * inserted. A cut is copy + exactly one `remove`; one undo restores it.\n *\n * Paste is PERMISSIVE: missing components and unresolvable bindings insert\n * anyway (the renderer's fallbacks keep them visible) and surface in a\n * notice — \"Pasted — 2 bindings unresolved, 1 component missing\" — whose\n * issues deep-link to the inserted nodes. Only structurally invalid clipboard\n * content is reported as a non-blocking error, never a crash or silent no-op.\n */\n\n// ---------------------------------------------------------------------------\n// Notice store — module-level pub/sub (the drag-session pattern) so any\n// surface can publish and the canvas overlay renders it.\n// ---------------------------------------------------------------------------\n\nexport type ClipboardNotice =\n | { kind: 'copied'; label: string }\n | { kind: 'cut'; label: string }\n | { kind: 'pasted'; summary: string; destinationLabel: string; issues: PasteIssue[] }\n | { kind: 'error'; message: string }\n\nlet currentNotice: ClipboardNotice | null = null\nconst noticeListeners = new Set<() => void>()\n\nexport function publishClipboardNotice(notice: ClipboardNotice | null): void {\n currentNotice = notice\n for (const listener of [...noticeListeners]) listener()\n}\n\nfunction subscribeNotice(listener: () => void): () => void {\n noticeListeners.add(listener)\n return () => noticeListeners.delete(listener)\n}\n\nfunction getNotice(): ClipboardNotice | null {\n return currentNotice\n}\n\nexport function useClipboardNotice(): ClipboardNotice | null {\n return useSyncExternalStore(subscribeNotice, getNotice, getNotice)\n}\n\n// ---------------------------------------------------------------------------\n// Shortcut scoping\n// ---------------------------------------------------------------------------\n\n/**\n * Whether a clipboard shortcut/event must stay inert for this target: form\n * fields and any contenteditable (which includes the EC-158/EC-159 inline\n * editors) own their own native clipboard behaviour.\n */\nexport function clipboardEventBlocked(target: EventTarget | null): boolean {\n const el = target as HTMLElement | null\n if (!el) return false\n const tag = el.tagName\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true\n if (typeof el.closest === 'function' && el.closest('[contenteditable]')) return true\n return el.isContentEditable === true\n}\n\n// ---------------------------------------------------------------------------\n// The clipboard surface\n// ---------------------------------------------------------------------------\n\nexport interface StudioClipboard {\n /**\n * Copy a canvas/tree node's subtree. Writes synchronously when a\n * ClipboardEvent's DataTransfer is given, else via the async Clipboard API\n * (`asyncClipboard` lets the canvas pass the iframe's own navigator —\n * Chromium scopes clipboard access to the *focused* document).\n */\n copyNode(nodeId: string, dt?: DataTransfer | null, asyncClipboard?: AsyncClipboard): void\n /** Copy, then remove the source in exactly one dispatch (one undo restores). */\n cutNode(nodeId: string, dt?: DataTransfer | null, asyncClipboard?: AsyncClipboard): void\n /** Copy a prebuilt fragment (blocks-panel \"copy exact configuration\"). */\n copyFragment(fragment: ClipboardFragment, label: string): void\n /** Paste from a paste event's DataTransfer (no permissions needed). */\n pasteFromDataTransfer(dt: DataTransfer, anchorId?: string | null): void\n /** Paste via the async Clipboard API (explicit affordances + shortcuts). */\n pasteFromClipboard(anchorId?: string | null, asyncClipboard?: AsyncClipboard): Promise<void>\n}\n\nfunction shortId(componentId: string): string {\n return componentId.replace('base.', '').replace('project.', '')\n}\n\n/** Mirrors the builder reducer's fresh-node-id counter (`node-<n>`). */\nfunction counterFrom(start: number): () => string {\n let n = start\n return () => {\n n += 1\n return `node-${n}`\n }\n}\n\nexport function useStudioClipboard(): StudioClipboard {\n const builder = useBuilder()\n const data = useData()\n const collab = useCollab()\n const studio = getStudioRegistry()\n\n // Event listeners outlive renders; refs keep them on the current state.\n const stateRef = useRef({ builder, data, projectId: collab.projectId })\n stateRef.current = { builder, data, projectId: collab.projectId }\n\n return useMemo<StudioClipboard>(() => {\n const fragmentFor = (nodeId: string): { fragment: ClipboardFragment; label: string } | null => {\n const { builder: b, projectId } = stateRef.current\n const built = buildFragmentFromComposition(b.present, nodeId, { source: { projectId } })\n if (!built.ok) {\n publishClipboardNotice({ kind: 'error', message: built.error.message })\n return null\n }\n return { fragment: built.fragment, label: shortId(built.fragment.payload.root.componentId) }\n }\n\n const write = (\n fragment: ClipboardFragment,\n label: string,\n kind: 'copied' | 'cut',\n dt?: DataTransfer | null,\n asyncClipboard?: AsyncClipboard,\n ): void => {\n if (dt) {\n writeFragmentToDataTransfer(dt, fragment)\n publishClipboardNotice({ kind, label })\n return\n }\n writeFragmentToClipboard(asyncClipboard ?? navigator.clipboard, fragment)\n .then(() => publishClipboardNotice({ kind, label }))\n .catch((cause: unknown) => {\n const detail = cause instanceof Error ? cause.message : String(cause)\n publishClipboardNotice({ kind: 'error', message: translate('builder.clipboard.writeError', { detail }) })\n })\n }\n\n const currentRoot = (): ComponentNode | undefined => {\n const { builder: b } = stateRef.current\n return b.present\n }\n\n const applyPaste = (result: ParseFragmentResult, anchorId?: string | null): void => {\n const { builder: b, data: d } = stateRef.current\n if (!result.ok) {\n publishClipboardNotice({ kind: 'error', message: result.error.message })\n return\n }\n const root = currentRoot()\n if (!root) return\n const { target, destinationLabel } = resolvePasteTarget({\n root,\n anchorId: anchorId !== undefined ? anchorId : b.selectedId,\n getManifest: (id) => studio.registry.getManifest(id),\n })\n // Mirror the reducer's deterministic re-id so the issue deep links point\n // at exactly the node ids the inserted subtree will carry.\n const inserted = cloneWithNewIds(result.fragment.payload.root, counterFrom(b.nodeSeq))\n // ONE dispatch: one history entry, one graph.insertNode command, one undo.\n b.dispatch({ type: 'insert', target, node: result.fragment.payload.root })\n const diff = diffPastedSubtree({\n node: inserted,\n hasComponent: (id) => studio.registry.getManifest(id) !== undefined,\n knownSourceIds: d.knownSourceIds,\n })\n publishClipboardNotice({\n kind: 'pasted',\n summary: pasteSummaryMessage(diff),\n destinationLabel,\n issues: diff.issues,\n })\n }\n\n return {\n copyNode(nodeId, dt, asyncClipboard) {\n const built = fragmentFor(nodeId)\n if (built) write(built.fragment, built.label, 'copied', dt, asyncClipboard)\n },\n cutNode(nodeId, dt, asyncClipboard) {\n const { builder: b } = stateRef.current\n const root = currentRoot()\n if (root && nodeId === root.id) {\n publishClipboardNotice({ kind: 'error', message: translate('builder.clipboard.rootCannotBeCut') })\n return\n }\n const built = fragmentFor(nodeId)\n if (!built) return\n write(built.fragment, built.label, 'cut', dt, asyncClipboard)\n // Exactly one structural dispatch: a single undo restores the cut node.\n b.dispatch({ type: 'remove', id: nodeId })\n },\n copyFragment(fragment, label) {\n write(fragment, label, 'copied')\n },\n pasteFromDataTransfer(dt, anchorId) {\n applyPaste(readFragmentFromDataTransfer(dt), anchorId)\n },\n async pasteFromClipboard(anchorId, asyncClipboard) {\n applyPaste(await readFragmentFromClipboard(asyncClipboard ?? navigator.clipboard), anchorId)\n },\n }\n }, [studio])\n}\n\n// ---------------------------------------------------------------------------\n// Listener wiring — one implementation for the page-tree window and the\n// canvas iframe document (events never cross the frame boundary).\n// ---------------------------------------------------------------------------\n\nexport interface ClipboardListenerOptions {\n clipboard: StudioClipboard\n /** Current selection: the copy/cut subject and the paste anchor. */\n getSelectedId: () => string | null\n /** Extra guard (e.g. the canvas's live inline-edit session). */\n isBlocked?: (target: EventTarget | null) => boolean\n /** The async Clipboard API of the listening document's own window. */\n asyncClipboard?: AsyncClipboard\n /** The listening document's selection — real text selections stay native. */\n getSelection?: () => Selection | null\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 Cmd/Ctrl+C/X/V plus native copy/cut/paste ClipboardEvents.\n *\n * Shortcuts are handled on `keydown` (a selected node has no text selection,\n * so the browser would not fire native clipboard events for it); the\n * `preventDefault` there suppresses the native event, so the ClipboardEvent\n * listeners only ever see *other* triggers — context-menu copy/paste and\n * synthetic events — and the two paths can never double-handle one gesture.\n * Everything stays inert in form fields and inside the inline editors.\n */\nexport function attachClipboardListeners(target: ListenerTarget, opts: ClipboardListenerOptions): () => void {\n const blocked = (t: EventTarget | null): boolean =>\n clipboardEventBlocked(t) || (opts.isBlocked?.(t) ?? false)\n const hasTextSelection = (): boolean => {\n const selection = opts.getSelection?.()\n return selection !== null && selection !== undefined && !selection.isCollapsed\n }\n\n const onKeyDown = (e: Event) => {\n const event = e as KeyboardEvent\n if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return\n const key = event.key.toLowerCase()\n if (key !== 'c' && key !== 'x' && key !== 'v') return\n if (blocked(event.target)) return\n if ((key === 'c' || key === 'x') && hasTextSelection()) return\n const selectedId = opts.getSelectedId()\n if (key === 'v') {\n event.preventDefault()\n void opts.clipboard.pasteFromClipboard(selectedId, opts.asyncClipboard)\n return\n }\n if (!selectedId) return\n event.preventDefault()\n if (key === 'c') opts.clipboard.copyNode(selectedId, null, opts.asyncClipboard)\n else opts.clipboard.cutNode(selectedId, null, opts.asyncClipboard)\n }\n\n const onCopyOrCut = (e: Event) => {\n const event = e as ClipboardEvent\n if (blocked(event.target) || hasTextSelection()) return\n const selectedId = opts.getSelectedId()\n if (!selectedId || !event.clipboardData) return\n event.preventDefault()\n if (event.type === 'copy') opts.clipboard.copyNode(selectedId, event.clipboardData)\n else opts.clipboard.cutNode(selectedId, event.clipboardData)\n }\n\n const onPaste = (e: Event) => {\n const event = e as ClipboardEvent\n if (blocked(event.target) || !event.clipboardData) return\n event.preventDefault()\n opts.clipboard.pasteFromDataTransfer(event.clipboardData, opts.getSelectedId())\n }\n\n target.addEventListener('keydown', onKeyDown)\n target.addEventListener('copy', onCopyOrCut)\n target.addEventListener('cut', onCopyOrCut)\n target.addEventListener('paste', onPaste)\n return () => {\n target.removeEventListener('keydown', onKeyDown)\n target.removeEventListener('copy', onCopyOrCut)\n target.removeEventListener('cut', onCopyOrCut)\n target.removeEventListener('paste', onPaste)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Notice overlay — rendered by the canvas; issues deep-link to their nodes.\n// ---------------------------------------------------------------------------\n\nexport function ClipboardNoticeOverlay() {\n const notice = useClipboardNotice()\n const builder = useBuilder()\n if (!notice) return null\n\n const tone =\n notice.kind === 'error' ? 'error' : notice.kind === 'pasted' && notice.issues.length > 0 ? 'warning' : 'success'\n\n return (\n <div\n data-testid=\"clipboard-notice\"\n data-notice-kind={notice.kind}\n className=\"absolute bottom-4 left-1/2 z-20 w-max max-w-[min(420px,90%)] -translate-x-1/2\"\n >\n <div className=\"rounded-lg border border-border bg-popover p-2 text-popover-foreground shadow-lg\">\n <Row gap=\"2\" align=\"start\">\n {tone === 'error' ? (\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-destructive\" aria-hidden />\n ) : tone === 'warning' ? (\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-warning\" aria-hidden />\n ) : (\n <CircleCheck className=\"mt-0.5 size-3.5 shrink-0 text-success\" aria-hidden />\n )}\n <Text size=\"xs\" className=\"min-w-0 flex-1\">\n {notice.kind === 'copied'\n ? translate('builder.clipboard.copied', { label: notice.label })\n : notice.kind === 'cut'\n ? translate('builder.clipboard.cutNotice', { label: notice.label })\n : notice.kind === 'pasted'\n ? translate('builder.clipboard.pastedInto', {\n summary: notice.summary,\n destination: notice.destinationLabel,\n })\n : notice.message}\n </Text>\n <button\n type=\"button\"\n aria-label={translate('common.dismiss')}\n onClick={() => publishClipboardNotice(null)}\n className=\"shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground\"\n >\n <X className=\"size-3.5\" />\n </button>\n </Row>\n {notice.kind === 'pasted' && notice.issues.length > 0 ? (\n <div className=\"mt-1.5 flex flex-col gap-1\">\n {notice.issues.map((issue, i) => (\n <button\n key={`${issue.nodeId}-${i}`}\n type=\"button\"\n data-testid=\"clipboard-notice-issue\"\n onClick={() => builder.dispatch({ type: 'select', id: issue.nodeId })}\n className=\"flex w-full items-center gap-2 rounded-md p-1 text-left transition-colors hover:bg-muted\"\n >\n <Badge variant=\"warning\" className=\"shrink-0\">\n {issue.kind === 'missing-component'\n ? translate('builder.clipboard.issueComponent')\n : translate('builder.clipboard.issueBinding')}\n </Badge>\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1 truncate\">\n {issue.message}\n </Text>\n <Crosshair className=\"size-3.5 shrink-0 text-muted-foreground\" aria-hidden />\n </button>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n"]}
|
|
@@ -33,7 +33,7 @@ import { ReuseProvider } from './reuse-context';
|
|
|
33
33
|
const COMPOSITION_SCOPE = { active: true };
|
|
34
34
|
const SYNTH_ROOT_ID = 'composition.root';
|
|
35
35
|
/** The implicit root container the field's blocks compose inside (like a page root). */
|
|
36
|
-
const SYNTH_ROOT_COMPONENT = 'base.
|
|
36
|
+
const SYNTH_ROOT_COMPONENT = 'base.Stack';
|
|
37
37
|
/**
|
|
38
38
|
* Wrap the field value's blocks into a synthetic root container (EC-287). The
|
|
39
39
|
* builder store edits this single root-bearing composition directly; its
|
|
@@ -47,8 +47,8 @@ function buildSyntheticRoot(value) {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
/** Unwrap the synthetic root's children back to a field value per cardinality. */
|
|
50
|
-
function unwrapValue(blocks,
|
|
51
|
-
return
|
|
50
|
+
function unwrapValue(blocks, multiple) {
|
|
51
|
+
return multiple ? [...blocks] : blocks[0];
|
|
52
52
|
}
|
|
53
53
|
export function CompositionFieldEditor({ field, value, onChange, readOnly, trailingPanes, }) {
|
|
54
54
|
// Build the synthetic root ONCE from the value at mount. The editor is
|
|
@@ -57,7 +57,7 @@ export function CompositionFieldEditor({ field, value, onChange, readOnly, trail
|
|
|
57
57
|
// (BuilderProvider reads `composition` only in its lazy initial state, so later
|
|
58
58
|
// prop changes are ignored regardless.)
|
|
59
59
|
const initialRoot = useMemo(() => buildSyntheticRoot(value), []);
|
|
60
|
-
return (_jsxs(BuilderProvider, { composition: initialRoot, children: [_jsx(CompositionLift, {
|
|
60
|
+
return (_jsxs(BuilderProvider, { composition: initialRoot, children: [_jsx(CompositionLift, { multiple: field.multiple, onChange: onChange, readOnly: readOnly }), _jsx(ReuseProvider, { children: _jsx(CompositionScopeProvider, { value: COMPOSITION_SCOPE, children: _jsx(CompositionEditorShell, { readOnly: readOnly, fieldAllow: field.allow, trailingPanes: trailingPanes }) }) })] }));
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* Lifts committed edits out of the scoped store into the CMS draft. The first
|
|
@@ -65,7 +65,7 @@ export function CompositionFieldEditor({ field, value, onChange, readOnly, trail
|
|
|
65
65
|
* tab never marks the document dirty; every subsequent change to the block list
|
|
66
66
|
* emits the unwrapped value.
|
|
67
67
|
*/
|
|
68
|
-
function CompositionLift({
|
|
68
|
+
function CompositionLift({ multiple, onChange, readOnly, }) {
|
|
69
69
|
const builder = useBuilder();
|
|
70
70
|
const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS;
|
|
71
71
|
const onChangeRef = useRef(onChange);
|
|
@@ -86,9 +86,9 @@ function CompositionLift({ cardinality, onChange, readOnly, }) {
|
|
|
86
86
|
}
|
|
87
87
|
if (blocks !== baselineRef.current) {
|
|
88
88
|
baselineRef.current = blocks;
|
|
89
|
-
onChangeRef.current(unwrapValue(blocks,
|
|
89
|
+
onChangeRef.current(unwrapValue(blocks, multiple));
|
|
90
90
|
}
|
|
91
|
-
}, [blocks,
|
|
91
|
+
}, [blocks, multiple, readOnly]);
|
|
92
92
|
return null;
|
|
93
93
|
}
|
|
94
94
|
const EMPTY_BLOCKS = [];
|
|
@@ -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, Cardinality } 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 { CompositionScopeProvider } from './composition-scope'\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\n/** Stable scope value (a fresh object each render would re-render consumers). */\nconst COMPOSITION_SCOPE = { active: true } as const\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.primitives.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[], cardinality: Cardinality): unknown {\n return cardinality === 'one' ? blocks[0] : [...blocks]\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 cardinality={field.cardinality} onChange={onChange} readOnly={readOnly} />\n <ReuseProvider>\n <CompositionScopeProvider value={COMPOSITION_SCOPE}>\n <CompositionEditorShell\n readOnly={readOnly}\n fieldAllow={field.allow}\n trailingPanes={trailingPanes}\n />\n </CompositionScopeProvider>\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 cardinality,\n onChange,\n readOnly,\n}: {\n cardinality: Cardinality\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, cardinality))\n }\n }, [blocks, cardinality, 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 function openInsert(nodeId: string) {\n const ctx = resolveInsertContext(builder, nodeId, fieldAllow)\n if (ctx) {\n setInsertContext(ctx)\n setInsertOpen(true)\n }\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 { resolveInsertContext } from './insert-context'\nimport { CompositionScopeProvider } from './composition-scope'\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\n/** Stable scope value (a fresh object each render would re-render consumers). */\nconst COMPOSITION_SCOPE = { active: true } as const\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 <CompositionScopeProvider value={COMPOSITION_SCOPE}>\n <CompositionEditorShell\n readOnly={readOnly}\n fieldAllow={field.allow}\n trailingPanes={trailingPanes}\n />\n </CompositionScopeProvider>\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 function openInsert(nodeId: string) {\n const ctx = resolveInsertContext(builder, nodeId, fieldAllow)\n if (ctx) {\n setInsertContext(ctx)\n setInsertOpen(true)\n }\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,4 +1,5 @@
|
|
|
1
1
|
import { inlineEditablePropNames } from '@elytracms/core/component-registry';
|
|
2
|
+
import { isStaticProp } from '@elytracms/core/project-graph';
|
|
2
3
|
import { parseRichTextValue, richTextDocsEquivalent } from '@elytracms/core/rich-text';
|
|
3
4
|
export function createInlineEditSession(args) {
|
|
4
5
|
let active = true;
|
|
@@ -42,7 +43,7 @@ export function resolveInlineEditableProp(manifest, node) {
|
|
|
42
43
|
return { propName, value: fallback };
|
|
43
44
|
continue;
|
|
44
45
|
}
|
|
45
|
-
if (current
|
|
46
|
+
if (isStaticProp(current) && typeof current.value === 'string') {
|
|
46
47
|
return { propName, value: current.value };
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -65,9 +66,9 @@ export function resolveInlineRichTextProp(manifest, node) {
|
|
|
65
66
|
continue;
|
|
66
67
|
const current = node.props?.[propName];
|
|
67
68
|
const raw = current === undefined ? manifest.props[propName]?.default : undefined;
|
|
68
|
-
if (current !== undefined && current
|
|
69
|
+
if (current !== undefined && !isStaticProp(current))
|
|
69
70
|
continue;
|
|
70
|
-
const parsed = parseRichTextValue(current
|
|
71
|
+
const parsed = parseRichTextValue(isStaticProp(current) ? current.value : raw);
|
|
71
72
|
if (parsed.ok)
|
|
72
73
|
return { propName, value: parsed.value };
|
|
73
74
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inline-edit.js","sourceRoot":"","sources":["../../../src/components/builder/inline-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { inlineEditablePropNames } from '@elytracms/core/component-registry'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { parseRichTextValue, richTextDocsEquivalent } from '@elytracms/core/rich-text'\nimport type { RichTextDoc, RichTextValue } from '@elytracms/core/rich-text'\n\n/**\n * Inline text editing on the canvas (EC-158) — framework-free core.\n *\n * The canvas wires DOM events (dblclick/keydown/blur) to this module; the\n * module owns the editing-session state machine and the\n * one-gesture-one-command guarantee. A session finishes exactly once: the\n * first `commit()`/`cancel()` wins and every later call returns `null`, so\n * overlapping DOM signals (Enter immediately followed by blur, blur racing a\n * teardown) can never dispatch a second operations command or stack a second\n * undo step.\n *\n * Locale semantics (EC-176): inline editing targets STATIC graph props only,\n * and static graph props are locale-independent by design — the canonical\n * graph stores one value per prop, not per-locale variants. Bound props are\n * never inline-editable (below), so fallback-shown BOUND text on the canvas\n * cannot be edited in place either: translating localized content happens in\n * the CMS surfaces, where the active-locale variant is created explicitly.\n * Nothing here needs the ambient locale.\n */\n\n/** The single graph mutation a finished session may produce. */\nexport interface InlineEditCommit {\n kind: 'commit'\n nodeId: string\n propName: string\n /** The edited text to write as the prop's static value. */\n value: string\n}\n\n/** A cancelled session: restore this text on the DOM, dispatch nothing. */\nexport interface InlineEditCancel {\n kind: 'cancel'\n /** The original rendered value to restore. */\n value: string\n}\n\nexport interface InlineEditSession {\n readonly nodeId: string\n readonly propName: string\n readonly original: string\n /** False once the session committed or cancelled. */\n readonly active: boolean\n /**\n * Finish by committing `text`. Returns the one commit outcome, or `null`\n * when the session already finished or the text is unchanged (an unchanged\n * commit finishes the session but dispatches nothing — graph untouched).\n */\n commit(text: string): InlineEditCommit | null\n /** Finish by cancelling. Returns the restore outcome once, then `null`. */\n cancel(): InlineEditCancel | null\n}\n\nexport function createInlineEditSession(args: {\n nodeId: string\n propName: string\n original: string\n}): InlineEditSession {\n let active = true\n return {\n nodeId: args.nodeId,\n propName: args.propName,\n original: args.original,\n get active() {\n return active\n },\n commit(text: string): InlineEditCommit | null {\n if (!active) return null\n active = false\n if (text === args.original) return null\n return { kind: 'commit', nodeId: args.nodeId, propName: args.propName, value: text }\n },\n cancel(): InlineEditCancel | null {\n if (!active) return null\n active = false\n return { kind: 'cancel', value: args.original }\n },\n }\n}\n\n/** A node prop that can be edited in place right now. */\nexport interface InlineEditableProp {\n propName: string\n /** The current static string value (or string default when unset). */\n value: string\n}\n\n/**\n * Resolve which prop of `node` an inline-edit gesture should target: the\n * first manifest prop flagged `inlineEditable` whose *current* value is a\n * plain static string (or unset with a string default). Bound props and\n * non-string values are never inline-editable — they stay inspector-only.\n */\nexport function resolveInlineEditableProp(\n manifest: ComponentManifest | undefined,\n node: ComponentNode,\n): InlineEditableProp | null {\n if (!manifest) return null\n for (const propName of inlineEditablePropNames(manifest)) {\n const current = node.props?.[propName]\n if (current === undefined) {\n const fallback = manifest.props[propName]?.default\n if (typeof fallback === 'string') return { propName, value: fallback }\n continue\n }\n if (current.kind === 'static' && typeof current.value === 'string') {\n return { propName, value: current.value }\n }\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Rich-text inline editing (EC-159) — the same session contract over the\n// `@elytracms/core/rich-text` storage format.\n// ---------------------------------------------------------------------------\n\n/** A node prop carrying rich-text content that can be edited in place now. */\nexport interface InlineRichTextProp {\n propName: string\n /** The current stored value (or the manifest default when unset). */\n value: RichTextValue\n}\n\n/**\n * Resolve which rich-text prop of `node` an inline-edit gesture should\n * target: the first manifest prop flagged `inlineEditable` whose inspector\n * control is `richtext` and whose *current* value is a static, schema-valid\n * rich-text value (or unset with a valid rich-text default). Bound props are\n * never inline-editable — translating/bound content stays inspector- and\n * CMS-owned, exactly like EC-158's plain-text rule.\n */\nexport function resolveInlineRichTextProp(\n manifest: ComponentManifest | undefined,\n node: ComponentNode,\n): InlineRichTextProp | null {\n if (!manifest) return null\n for (const propName of inlineEditablePropNames(manifest)) {\n // EC-190: a rich-text prop is the `richText` field-def type (was control.kind 'richtext').\n if (manifest.props[propName]?.type !== 'richText') continue\n const current = node.props?.[propName]\n const raw = current === undefined ? manifest.props[propName]?.default : undefined\n if (current !== undefined && current.kind !== 'static') continue\n const parsed = parseRichTextValue(current?.kind === 'static' ? current.value : raw)\n if (parsed.ok) return { propName, value: parsed.value }\n }\n return null\n}\n\n/** The single graph mutation a finished rich-text session may produce. */\nexport interface InlineRichTextCommit {\n kind: 'commit'\n nodeId: string\n propName: string\n /** The edited content to write as the prop's static value. */\n value: RichTextValue\n}\n\nexport interface InlineRichTextSession {\n readonly nodeId: string\n readonly propName: string\n readonly original: RichTextValue\n /** False once the session committed or cancelled. */\n readonly active: boolean\n /**\n * Finish by committing the parsed-back document. Returns the one commit\n * outcome, or `null` when the session already finished or the document is\n * equivalent to the original. An equivalent commit finishes the session and\n * dispatches nothing — the stored value is never rewritten, so untouched\n * content round-trips byte-identical.\n */\n commit(doc: RichTextDoc): InlineRichTextCommit | null\n /** Finish by cancelling. Returns `true` exactly once (restore the DOM). */\n cancel(): boolean\n}\n\n/**\n * One rich-text editing session (EC-159), with EC-158's one-finish guarantee:\n * the first `commit()`/`cancel()` wins and every later call is inert, so\n * overlapping DOM signals can never dispatch a second operations command or\n * stack a second undo step. The committed value keeps the original's stored\n * schema `version` stamp — only the document tree changes.\n */\nexport function createInlineRichTextSession(args: {\n nodeId: string\n propName: string\n original: RichTextValue\n}): InlineRichTextSession {\n let active = true\n return {\n nodeId: args.nodeId,\n propName: args.propName,\n original: args.original,\n get active() {\n return active\n },\n commit(doc: RichTextDoc): InlineRichTextCommit | null {\n if (!active) return null\n active = false\n if (richTextDocsEquivalent(doc, args.original.doc)) return null\n return {\n kind: 'commit',\n nodeId: args.nodeId,\n propName: args.propName,\n value: { version: args.original.version, doc },\n }\n },\n cancel(): boolean {\n if (!active) return false\n active = false\n return true\n },\n }\n}\n\n/**\n * The structural slice of `Element` the host finder needs — kept minimal so\n * the descent logic is unit-testable without a DOM.\n */\nexport interface InlineHostElement {\n readonly childElementCount: number\n readonly firstElementChild: InlineHostElement | null\n readonly textContent: string | null\n getAttribute(name: string): string | null\n}\n\n/**\n * Find the rendered element inside a node's `.ec-editable` wrapper that hosts\n * the editable text. Descends single-child chains as long as no text is lost,\n * never crossing into another node's wrapper, and only accepts a host whose\n * text content is *exactly* the stored prop value — if the component renders\n * the prop transformed (or alongside other text), inline editing is skipped\n * and the inspector remains the editing surface. That exactness is what makes\n * edit-mode typography pixel-identical to render state: we edit the rendered\n * element itself.\n */\nexport function findInlineTextHost(\n wrapper: InlineHostElement,\n expected: string,\n): InlineHostElement | null {\n let el = wrapper.firstElementChild\n if (!el) return null\n if (el.getAttribute('data-ec-node-id') !== null) return null\n while (\n el.childElementCount === 1 &&\n el.firstElementChild!.getAttribute('data-ec-node-id') === null &&\n el.firstElementChild!.textContent === el.textContent\n ) {\n el = el.firstElementChild!\n }\n return (el.textContent ?? '') === expected ? el : null\n}\n"]}
|
|
1
|
+
{"version":3,"file":"inline-edit.js","sourceRoot":"","sources":["../../../src/components/builder/inline-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { inlineEditablePropNames } from '@elytracms/core/component-registry'\nimport { isStaticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { parseRichTextValue, richTextDocsEquivalent } from '@elytracms/core/rich-text'\nimport type { RichTextDoc, RichTextValue } from '@elytracms/core/rich-text'\n\n/**\n * Inline text editing on the canvas (EC-158) — framework-free core.\n *\n * The canvas wires DOM events (dblclick/keydown/blur) to this module; the\n * module owns the editing-session state machine and the\n * one-gesture-one-command guarantee. A session finishes exactly once: the\n * first `commit()`/`cancel()` wins and every later call returns `null`, so\n * overlapping DOM signals (Enter immediately followed by blur, blur racing a\n * teardown) can never dispatch a second operations command or stack a second\n * undo step.\n *\n * Locale semantics (EC-176): inline editing targets STATIC graph props only,\n * and static graph props are locale-independent by design — the canonical\n * graph stores one value per prop, not per-locale variants. Bound props are\n * never inline-editable (below), so fallback-shown BOUND text on the canvas\n * cannot be edited in place either: translating localized content happens in\n * the CMS surfaces, where the active-locale variant is created explicitly.\n * Nothing here needs the ambient locale.\n */\n\n/** The single graph mutation a finished session may produce. */\nexport interface InlineEditCommit {\n kind: 'commit'\n nodeId: string\n propName: string\n /** The edited text to write as the prop's static value. */\n value: string\n}\n\n/** A cancelled session: restore this text on the DOM, dispatch nothing. */\nexport interface InlineEditCancel {\n kind: 'cancel'\n /** The original rendered value to restore. */\n value: string\n}\n\nexport interface InlineEditSession {\n readonly nodeId: string\n readonly propName: string\n readonly original: string\n /** False once the session committed or cancelled. */\n readonly active: boolean\n /**\n * Finish by committing `text`. Returns the one commit outcome, or `null`\n * when the session already finished or the text is unchanged (an unchanged\n * commit finishes the session but dispatches nothing — graph untouched).\n */\n commit(text: string): InlineEditCommit | null\n /** Finish by cancelling. Returns the restore outcome once, then `null`. */\n cancel(): InlineEditCancel | null\n}\n\nexport function createInlineEditSession(args: {\n nodeId: string\n propName: string\n original: string\n}): InlineEditSession {\n let active = true\n return {\n nodeId: args.nodeId,\n propName: args.propName,\n original: args.original,\n get active() {\n return active\n },\n commit(text: string): InlineEditCommit | null {\n if (!active) return null\n active = false\n if (text === args.original) return null\n return { kind: 'commit', nodeId: args.nodeId, propName: args.propName, value: text }\n },\n cancel(): InlineEditCancel | null {\n if (!active) return null\n active = false\n return { kind: 'cancel', value: args.original }\n },\n }\n}\n\n/** A node prop that can be edited in place right now. */\nexport interface InlineEditableProp {\n propName: string\n /** The current static string value (or string default when unset). */\n value: string\n}\n\n/**\n * Resolve which prop of `node` an inline-edit gesture should target: the\n * first manifest prop flagged `inlineEditable` whose *current* value is a\n * plain static string (or unset with a string default). Bound props and\n * non-string values are never inline-editable — they stay inspector-only.\n */\nexport function resolveInlineEditableProp(\n manifest: ComponentManifest | undefined,\n node: ComponentNode,\n): InlineEditableProp | null {\n if (!manifest) return null\n for (const propName of inlineEditablePropNames(manifest)) {\n const current = node.props?.[propName]\n if (current === undefined) {\n const fallback = manifest.props[propName]?.default\n if (typeof fallback === 'string') return { propName, value: fallback }\n continue\n }\n if (isStaticProp(current) && typeof current.value === 'string') {\n return { propName, value: current.value }\n }\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Rich-text inline editing (EC-159) — the same session contract over the\n// `@elytracms/core/rich-text` storage format.\n// ---------------------------------------------------------------------------\n\n/** A node prop carrying rich-text content that can be edited in place now. */\nexport interface InlineRichTextProp {\n propName: string\n /** The current stored value (or the manifest default when unset). */\n value: RichTextValue\n}\n\n/**\n * Resolve which rich-text prop of `node` an inline-edit gesture should\n * target: the first manifest prop flagged `inlineEditable` whose inspector\n * control is `richtext` and whose *current* value is a static, schema-valid\n * rich-text value (or unset with a valid rich-text default). Bound props are\n * never inline-editable — translating/bound content stays inspector- and\n * CMS-owned, exactly like EC-158's plain-text rule.\n */\nexport function resolveInlineRichTextProp(\n manifest: ComponentManifest | undefined,\n node: ComponentNode,\n): InlineRichTextProp | null {\n if (!manifest) return null\n for (const propName of inlineEditablePropNames(manifest)) {\n // EC-190: a rich-text prop is the `richText` field-def type (was control.kind 'richtext').\n if (manifest.props[propName]?.type !== 'richText') continue\n const current = node.props?.[propName]\n const raw = current === undefined ? manifest.props[propName]?.default : undefined\n if (current !== undefined && !isStaticProp(current)) continue\n const parsed = parseRichTextValue(isStaticProp(current) ? current.value : raw)\n if (parsed.ok) return { propName, value: parsed.value }\n }\n return null\n}\n\n/** The single graph mutation a finished rich-text session may produce. */\nexport interface InlineRichTextCommit {\n kind: 'commit'\n nodeId: string\n propName: string\n /** The edited content to write as the prop's static value. */\n value: RichTextValue\n}\n\nexport interface InlineRichTextSession {\n readonly nodeId: string\n readonly propName: string\n readonly original: RichTextValue\n /** False once the session committed or cancelled. */\n readonly active: boolean\n /**\n * Finish by committing the parsed-back document. Returns the one commit\n * outcome, or `null` when the session already finished or the document is\n * equivalent to the original. An equivalent commit finishes the session and\n * dispatches nothing — the stored value is never rewritten, so untouched\n * content round-trips byte-identical.\n */\n commit(doc: RichTextDoc): InlineRichTextCommit | null\n /** Finish by cancelling. Returns `true` exactly once (restore the DOM). */\n cancel(): boolean\n}\n\n/**\n * One rich-text editing session (EC-159), with EC-158's one-finish guarantee:\n * the first `commit()`/`cancel()` wins and every later call is inert, so\n * overlapping DOM signals can never dispatch a second operations command or\n * stack a second undo step. The committed value keeps the original's stored\n * schema `version` stamp — only the document tree changes.\n */\nexport function createInlineRichTextSession(args: {\n nodeId: string\n propName: string\n original: RichTextValue\n}): InlineRichTextSession {\n let active = true\n return {\n nodeId: args.nodeId,\n propName: args.propName,\n original: args.original,\n get active() {\n return active\n },\n commit(doc: RichTextDoc): InlineRichTextCommit | null {\n if (!active) return null\n active = false\n if (richTextDocsEquivalent(doc, args.original.doc)) return null\n return {\n kind: 'commit',\n nodeId: args.nodeId,\n propName: args.propName,\n value: { version: args.original.version, doc },\n }\n },\n cancel(): boolean {\n if (!active) return false\n active = false\n return true\n },\n }\n}\n\n/**\n * The structural slice of `Element` the host finder needs — kept minimal so\n * the descent logic is unit-testable without a DOM.\n */\nexport interface InlineHostElement {\n readonly childElementCount: number\n readonly firstElementChild: InlineHostElement | null\n readonly textContent: string | null\n getAttribute(name: string): string | null\n}\n\n/**\n * Find the rendered element inside a node's `.ec-editable` wrapper that hosts\n * the editable text. Descends single-child chains as long as no text is lost,\n * never crossing into another node's wrapper, and only accepts a host whose\n * text content is *exactly* the stored prop value — if the component renders\n * the prop transformed (or alongside other text), inline editing is skipped\n * and the inspector remains the editing surface. That exactness is what makes\n * edit-mode typography pixel-identical to render state: we edit the rendered\n * element itself.\n */\nexport function findInlineTextHost(\n wrapper: InlineHostElement,\n expected: string,\n): InlineHostElement | null {\n let el = wrapper.firstElementChild\n if (!el) return null\n if (el.getAttribute('data-ec-node-id') !== null) return null\n while (\n el.childElementCount === 1 &&\n el.firstElementChild!.getAttribute('data-ec-node-id') === null &&\n el.firstElementChild!.textContent === el.textContent\n ) {\n el = el.firstElementChild!\n }\n return (el.textContent ?? '') === expected ? el : null\n}\n"]}
|
|
@@ -11,7 +11,7 @@ import { useBuilder } from './builder-store';
|
|
|
11
11
|
* importing the dead shell. No UI here — pure graph/registry resolution.
|
|
12
12
|
*/
|
|
13
13
|
function shortId(componentId) {
|
|
14
|
-
return componentId.replace('base.
|
|
14
|
+
return componentId.replace('base.', '').replace('project.', '');
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Resolve where an insert lands: into the node's child slot, else as a sibling.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insert-context.js","sourceRoot":"","sources":["../../../src/components/builder/insert-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAEtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAE/C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAE5C;;;;;;;GAOG;AACH,SAAS,OAAO,CAAC,WAAmB;IAClC,OAAO,WAAW,CAAC,OAAO,CAAC,
|
|
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 { intersectAllow } from './blocks-model'\nimport type { InsertContext } from './insert-menu'\nimport { useBuilder } from './builder-store'\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 */\nfunction shortId(componentId: string): string {\n return componentId.replace('base.', '').replace('project.', '')\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: ReturnType<typeof useBuilder>,\n nodeId: string,\n fieldAllow?: readonly string[],\n): InsertContext | null {\n const studio = getStudioRegistry()\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: ReturnType<typeof getStudioRegistry>,\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,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { CONTAINER_ALIGNMENTS, GRID_MAX_COLUMNS, SPACING_TOKEN_NAMES, STACK_COMPONENT_ID, STACK_ON_MOBILE_PROP, gridStacksOnMobile, isLayoutContainerNode, isSpacingTokenName, spacingTokenFromLegacyGap, staticProp, } from '@elytracms/core/project-graph';
|
|
2
|
+
import { CONTAINER_ALIGNMENTS, GRID_MAX_COLUMNS, SPACING_TOKEN_NAMES, STACK_COMPONENT_ID, STACK_ON_MOBILE_PROP, gridStacksOnMobile, isLayoutContainerNode, isSpacingTokenName, isStaticProp, spacingTokenFromLegacyGap, staticProp, } from '@elytracms/core/project-graph';
|
|
3
3
|
import { Field, InspectorSection, Select, Stack } from '@elytracms/studio/ui';
|
|
4
4
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
5
5
|
import { useBuilder } from './builder-store';
|
|
@@ -41,7 +41,7 @@ const ALIGN_LABEL_KEYS = {
|
|
|
41
41
|
};
|
|
42
42
|
function staticValue(node, name) {
|
|
43
43
|
const prop = node.props?.[name];
|
|
44
|
-
return prop
|
|
44
|
+
return isStaticProp(prop) ? prop.value : undefined;
|
|
45
45
|
}
|
|
46
46
|
/** The node's gap as a token step, or `'none'` when absent/off-scale (renders gapless/default). */
|
|
47
47
|
function currentGapToken(node) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout-inspector.js","sourceRoot":"","sources":["../../../src/components/builder/layout-inspector.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n CONTAINER_ALIGNMENTS,\n GRID_MAX_COLUMNS,\n SPACING_TOKEN_NAMES,\n STACK_COMPONENT_ID,\n STACK_ON_MOBILE_PROP,\n gridStacksOnMobile,\n isLayoutContainerNode,\n isSpacingTokenName,\n spacingTokenFromLegacyGap,\n staticProp,\n} from '@elytracms/core/project-graph'\nimport type {\n ComponentNode,\n ContainerAlignment,\n JsonValue,\n SpacingTokenName,\n StackDirection,\n} from '@elytracms/core/project-graph'\nimport { Field, InspectorSection, Select, Stack } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useBuilder } from './builder-store'\n\n/**\n * Contextual layout inspector (EC-162): when a `Stack`/`Grid` container is\n * selected, the Settings tab shows its canonical layout values — direction,\n * gap, alignment, columns — as plain-language controls bound to the EC-161\n * token scale. Gesture creates, this refines: every change is one ordinary\n * `setProp`/`removeProp` builder dispatch (one operations command, one undo\n * step). No CSS vocabulary leaks into the labels.\n */\n\n/** The props this section owns for a container (hidden from the generic Props list). */\nconst LAYOUT_PROPS = ['direction', 'gap', 'align', 'columns', STACK_ON_MOBILE_PROP] as const\n\nexport function layoutManagedPropNames(node: ComponentNode): readonly string[] {\n return isLayoutContainerNode(node) ? LAYOUT_PROPS : []\n}\n\n/**\n * i18n message key per spacing token step (EC-161 scale) — no CSS lengths.\n * Shared with the canvas gap handles (EC-163): the live drag label and this\n * select resolve the same catalog message for the same token.\n */\nexport const GAP_LABEL_KEYS: Record<SpacingTokenName, string> = {\n px: 'builder.layout.gapStep.hairline',\n '1': 'builder.layout.gapStep.extraSmall',\n '2': 'builder.layout.gapStep.small',\n '3': 'builder.layout.gapStep.medium',\n '4': 'builder.layout.gapStep.large',\n '6': 'builder.layout.gapStep.extraLarge',\n '8': 'builder.layout.gapStep.huge',\n}\n\n/** i18n message key for the \"no gap\" step (gap prop absent). */\nexport const GAP_NONE_LABEL_KEY = 'common.none'\n\nconst GAP_NONE = 'none'\n\nconst ALIGN_LABEL_KEYS: Record<ContainerAlignment, string> = {\n start: 'builder.layout.align.start',\n center: 'builder.layout.align.center',\n end: 'builder.layout.align.end',\n stretch: 'builder.layout.align.stretch',\n}\n\nfunction staticValue(node: ComponentNode, name: string): unknown {\n const prop = node.props?.[name]\n return prop?.kind === 'static' ? prop.value : undefined\n}\n\n/** The node's gap as a token step, or `'none'` when absent/off-scale (renders gapless/default). */\nfunction currentGapToken(node: ComponentNode): SpacingTokenName | typeof GAP_NONE {\n const value = staticValue(node, 'gap')\n if (typeof value === 'string' && isSpacingTokenName(value)) return value\n if (typeof value === 'number') {\n const token = spacingTokenFromLegacyGap(value)\n if (token) return token\n }\n return GAP_NONE\n}\n\nfunction currentDirection(node: ComponentNode): StackDirection {\n return staticValue(node, 'direction') === 'horizontal' ? 'horizontal' : 'vertical'\n}\n\nfunction currentAlignment(node: ComponentNode): ContainerAlignment {\n const value = staticValue(node, 'align')\n return typeof value === 'string' && value in ALIGN_LABEL_KEYS ? (value as ContainerAlignment) : 'stretch'\n}\n\n/** The rendered column count: a valid static value, else 1 (the renderer's fallback). */\nfunction currentColumns(node: ComponentNode): number {\n const value = staticValue(node, 'columns')\n return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= GRID_MAX_COLUMNS\n ? value\n : 1\n}\n\nconst COLUMN_OPTIONS = Array.from({ length: GRID_MAX_COLUMNS }, (_, i) => i + 1)\n\n/** The contextual layout section. Renders nothing for non-container nodes. */\nexport function LayoutSection({ node }: { node: ComponentNode }) {\n const builder = useBuilder()\n if (!isLayoutContainerNode(node)) return null\n const isStack = node.componentId === STACK_COMPONENT_ID\n\n const set = (name: string, value: JsonValue) =>\n builder.dispatch({ type: 'setProp', id: node.id, name, value: staticProp(value) })\n const clear = (name: string) => builder.dispatch({ type: 'removeProp', id: node.id, name })\n\n return (\n <InspectorSection title={translate('builder.layout.title')}>\n <Stack gap=\"3\">\n {isStack ? (\n <Field label={translate('builder.layout.direction')} htmlFor={`layout-${node.id}-direction`}>\n <Select\n id={`layout-${node.id}-direction`}\n value={currentDirection(node)}\n onChange={(e) => set('direction', e.target.value)}\n >\n <option value=\"vertical\">{translate('builder.layout.vertical')}</option>\n <option value=\"horizontal\">{translate('builder.layout.horizontal')}</option>\n </Select>\n </Field>\n ) : (\n <Field label={translate('builder.layout.columns')} htmlFor={`layout-${node.id}-columns`}>\n <Select\n id={`layout-${node.id}-columns`}\n value={String(currentColumns(node))}\n onChange={(e) => set('columns', Number(e.target.value))}\n >\n {COLUMN_OPTIONS.map((n) => (\n <option key={n} value={String(n)}>\n {translate(\n n === 1 ? 'builder.layout.columnCount.one' : 'builder.layout.columnCount.other',\n { count: n },\n )}\n </option>\n ))}\n </Select>\n </Field>\n )}\n\n <Field label={translate('builder.layout.gap')} htmlFor={`layout-${node.id}-gap`}>\n <Select\n id={`layout-${node.id}-gap`}\n value={currentGapToken(node)}\n onChange={(e) => (e.target.value === GAP_NONE ? clear('gap') : set('gap', e.target.value))}\n >\n <option value={GAP_NONE}>{translate(GAP_NONE_LABEL_KEY)}</option>\n {SPACING_TOKEN_NAMES.map((name) => (\n <option key={name} value={name}>\n {translate(GAP_LABEL_KEYS[name])}\n </option>\n ))}\n </Select>\n </Field>\n\n <Field label={translate('builder.layout.alignment')} htmlFor={`layout-${node.id}-align`}>\n <Select\n id={`layout-${node.id}-align`}\n value={currentAlignment(node)}\n onChange={(e) => set('align', e.target.value)}\n >\n {CONTAINER_ALIGNMENTS.map((name) => (\n <option key={name} value={name}>\n {translate(ALIGN_LABEL_KEYS[name])}\n </option>\n ))}\n </Select>\n </Field>\n\n {/* Responsive default (EC-163): grids stack on phones unless the editor\n explicitly opts out. \"Stack\" is the default (prop absent); \"Keep\n columns\" writes the explicit `stackOnMobile: false` override. */}\n {!isStack ? (\n <Field label={translate('builder.layout.onPhones')} htmlFor={`layout-${node.id}-mobile`}>\n <Select\n id={`layout-${node.id}-mobile`}\n value={gridStacksOnMobile(node) ? 'stack' : 'keep'}\n onChange={(e) =>\n e.target.value === 'keep'\n ? set(STACK_ON_MOBILE_PROP, false)\n : clear(STACK_ON_MOBILE_PROP)\n }\n >\n <option value=\"stack\">{translate('builder.layout.stack')}</option>\n <option value=\"keep\">{translate('builder.layout.keepColumns')}</option>\n </Select>\n </Field>\n ) : null}\n </Stack>\n </InspectorSection>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"layout-inspector.js","sourceRoot":"","sources":["../../../src/components/builder/layout-inspector.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,YAAY,EACZ,yBAAyB,EACzB,UAAU,GACX,MAAM,+BAA+B,CAAA;AAQtC,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAE5C;;;;;;;GAOG;AAEH,wFAAwF;AACxF,MAAM,YAAY,GAAG,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,oBAAoB,CAAU,CAAA;AAE5F,MAAM,UAAU,sBAAsB,CAAC,IAAmB;IACxD,OAAO,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA;AACxD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAqC;IAC9D,EAAE,EAAE,iCAAiC;IACrC,GAAG,EAAE,mCAAmC;IACxC,GAAG,EAAE,8BAA8B;IACnC,GAAG,EAAE,+BAA+B;IACpC,GAAG,EAAE,8BAA8B;IACnC,GAAG,EAAE,mCAAmC;IACxC,GAAG,EAAE,6BAA6B;CACnC,CAAA;AAED,gEAAgE;AAChE,MAAM,CAAC,MAAM,kBAAkB,GAAG,aAAa,CAAA;AAE/C,MAAM,QAAQ,GAAG,MAAM,CAAA;AAEvB,MAAM,gBAAgB,GAAuC;IAC3D,KAAK,EAAE,4BAA4B;IACnC,MAAM,EAAE,6BAA6B;IACrC,GAAG,EAAE,0BAA0B;IAC/B,OAAO,EAAE,8BAA8B;CACxC,CAAA;AAED,SAAS,WAAW,CAAC,IAAmB,EAAE,IAAY;IACpD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAA;IAC/B,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;AACpD,CAAC;AAED,mGAAmG;AACnG,SAAS,eAAe,CAAC,IAAmB;IAC1C,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACtC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,kBAAkB,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACxE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,yBAAyB,CAAC,KAAK,CAAC,CAAA;QAC9C,IAAI,KAAK;YAAE,OAAO,KAAK,CAAA;IACzB,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAmB;IAC3C,OAAO,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,CAAA;AACpF,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAmB;IAC3C,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IACxC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,IAAI,gBAAgB,CAAC,CAAC,CAAE,KAA4B,CAAC,CAAC,CAAC,SAAS,CAAA;AAC3G,CAAC;AAED,yFAAyF;AACzF,SAAS,cAAc,CAAC,IAAmB;IACzC,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IAC1C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,gBAAgB;QACpG,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,CAAC,CAAA;AACP,CAAC;AAED,MAAM,cAAc,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;AAEhF,8EAA8E;AAC9E,MAAM,UAAU,aAAa,CAAC,EAAE,IAAI,EAA2B;IAC7D,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,KAAK,kBAAkB,CAAA;IAEvD,MAAM,GAAG,GAAG,CAAC,IAAY,EAAE,KAAgB,EAAE,EAAE,CAC7C,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IACpF,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;IAE3F,OAAO,CACL,KAAC,gBAAgB,IAAC,KAAK,EAAE,SAAS,CAAC,sBAAsB,CAAC,YACxD,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACX,OAAO,CAAC,CAAC,CAAC,CACT,KAAC,KAAK,IAAC,KAAK,EAAE,SAAS,CAAC,0BAA0B,CAAC,EAAE,OAAO,EAAE,UAAU,IAAI,CAAC,EAAE,YAAY,YACzF,MAAC,MAAM,IACL,EAAE,EAAE,UAAU,IAAI,CAAC,EAAE,YAAY,EACjC,KAAK,EAAE,gBAAgB,CAAC,IAAI,CAAC,EAC7B,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,aAEjD,iBAAQ,KAAK,EAAC,UAAU,YAAE,SAAS,CAAC,yBAAyB,CAAC,GAAU,EACxE,iBAAQ,KAAK,EAAC,YAAY,YAAE,SAAS,CAAC,2BAA2B,CAAC,GAAU,IACrE,GACH,CACT,CAAC,CAAC,CAAC,CACF,KAAC,KAAK,IAAC,KAAK,EAAE,SAAS,CAAC,wBAAwB,CAAC,EAAE,OAAO,EAAE,UAAU,IAAI,CAAC,EAAE,UAAU,YACrF,KAAC,MAAM,IACL,EAAE,EAAE,UAAU,IAAI,CAAC,EAAE,UAAU,EAC/B,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,EACnC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,YAEtD,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CACzB,iBAAgB,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,YAC7B,SAAS,CACR,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,gCAAgC,CAAC,CAAC,CAAC,kCAAkC,EAC/E,EAAE,KAAK,EAAE,CAAC,EAAE,CACb,IAJU,CAAC,CAKL,CACV,CAAC,GACK,GACH,CACT,EAED,KAAC,KAAK,IAAC,KAAK,EAAE,SAAS,CAAC,oBAAoB,CAAC,EAAE,OAAO,EAAE,UAAU,IAAI,CAAC,EAAE,MAAM,YAC7E,MAAC,MAAM,IACL,EAAE,EAAE,UAAU,IAAI,CAAC,EAAE,MAAM,EAC3B,KAAK,EAAE,eAAe,CAAC,IAAI,CAAC,EAC5B,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,aAE1F,iBAAQ,KAAK,EAAE,QAAQ,YAAG,SAAS,CAAC,kBAAkB,CAAC,GAAU,EAChE,mBAAmB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACjC,iBAAmB,KAAK,EAAE,IAAI,YAC3B,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,IADrB,IAAI,CAER,CACV,CAAC,IACK,GACH,EAER,KAAC,KAAK,IAAC,KAAK,EAAE,SAAS,CAAC,0BAA0B,CAAC,EAAE,OAAO,EAAE,UAAU,IAAI,CAAC,EAAE,QAAQ,YACrF,KAAC,MAAM,IACL,EAAE,EAAE,UAAU,IAAI,CAAC,EAAE,QAAQ,EAC7B,KAAK,EAAE,gBAAgB,CAAC,IAAI,CAAC,EAC7B,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,YAE5C,oBAAoB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAClC,iBAAmB,KAAK,EAAE,IAAI,YAC3B,SAAS,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,IADvB,IAAI,CAER,CACV,CAAC,GACK,GACH,EAKP,CAAC,OAAO,CAAC,CAAC,CAAC,CACV,KAAC,KAAK,IAAC,KAAK,EAAE,SAAS,CAAC,yBAAyB,CAAC,EAAE,OAAO,EAAE,UAAU,IAAI,CAAC,EAAE,SAAS,YACrF,MAAC,MAAM,IACL,EAAE,EAAE,UAAU,IAAI,CAAC,EAAE,SAAS,EAC9B,KAAK,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAClD,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CACd,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,MAAM;4BACvB,CAAC,CAAC,GAAG,CAAC,oBAAoB,EAAE,KAAK,CAAC;4BAClC,CAAC,CAAC,KAAK,CAAC,oBAAoB,CAAC,aAGjC,iBAAQ,KAAK,EAAC,OAAO,YAAE,SAAS,CAAC,sBAAsB,CAAC,GAAU,EAClE,iBAAQ,KAAK,EAAC,MAAM,YAAE,SAAS,CAAC,4BAA4B,CAAC,GAAU,IAChE,GACH,CACT,CAAC,CAAC,CAAC,IAAI,IACF,GACS,CACpB,CAAA;AACH,CAAC","sourcesContent":["import {\n CONTAINER_ALIGNMENTS,\n GRID_MAX_COLUMNS,\n SPACING_TOKEN_NAMES,\n STACK_COMPONENT_ID,\n STACK_ON_MOBILE_PROP,\n gridStacksOnMobile,\n isLayoutContainerNode,\n isSpacingTokenName,\n isStaticProp,\n spacingTokenFromLegacyGap,\n staticProp,\n} from '@elytracms/core/project-graph'\nimport type {\n ComponentNode,\n ContainerAlignment,\n JsonValue,\n SpacingTokenName,\n StackDirection,\n} from '@elytracms/core/project-graph'\nimport { Field, InspectorSection, Select, Stack } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useBuilder } from './builder-store'\n\n/**\n * Contextual layout inspector (EC-162): when a `Stack`/`Grid` container is\n * selected, the Settings tab shows its canonical layout values — direction,\n * gap, alignment, columns — as plain-language controls bound to the EC-161\n * token scale. Gesture creates, this refines: every change is one ordinary\n * `setProp`/`removeProp` builder dispatch (one operations command, one undo\n * step). No CSS vocabulary leaks into the labels.\n */\n\n/** The props this section owns for a container (hidden from the generic Props list). */\nconst LAYOUT_PROPS = ['direction', 'gap', 'align', 'columns', STACK_ON_MOBILE_PROP] as const\n\nexport function layoutManagedPropNames(node: ComponentNode): readonly string[] {\n return isLayoutContainerNode(node) ? LAYOUT_PROPS : []\n}\n\n/**\n * i18n message key per spacing token step (EC-161 scale) — no CSS lengths.\n * Shared with the canvas gap handles (EC-163): the live drag label and this\n * select resolve the same catalog message for the same token.\n */\nexport const GAP_LABEL_KEYS: Record<SpacingTokenName, string> = {\n px: 'builder.layout.gapStep.hairline',\n '1': 'builder.layout.gapStep.extraSmall',\n '2': 'builder.layout.gapStep.small',\n '3': 'builder.layout.gapStep.medium',\n '4': 'builder.layout.gapStep.large',\n '6': 'builder.layout.gapStep.extraLarge',\n '8': 'builder.layout.gapStep.huge',\n}\n\n/** i18n message key for the \"no gap\" step (gap prop absent). */\nexport const GAP_NONE_LABEL_KEY = 'common.none'\n\nconst GAP_NONE = 'none'\n\nconst ALIGN_LABEL_KEYS: Record<ContainerAlignment, string> = {\n start: 'builder.layout.align.start',\n center: 'builder.layout.align.center',\n end: 'builder.layout.align.end',\n stretch: 'builder.layout.align.stretch',\n}\n\nfunction staticValue(node: ComponentNode, name: string): unknown {\n const prop = node.props?.[name]\n return isStaticProp(prop) ? prop.value : undefined\n}\n\n/** The node's gap as a token step, or `'none'` when absent/off-scale (renders gapless/default). */\nfunction currentGapToken(node: ComponentNode): SpacingTokenName | typeof GAP_NONE {\n const value = staticValue(node, 'gap')\n if (typeof value === 'string' && isSpacingTokenName(value)) return value\n if (typeof value === 'number') {\n const token = spacingTokenFromLegacyGap(value)\n if (token) return token\n }\n return GAP_NONE\n}\n\nfunction currentDirection(node: ComponentNode): StackDirection {\n return staticValue(node, 'direction') === 'horizontal' ? 'horizontal' : 'vertical'\n}\n\nfunction currentAlignment(node: ComponentNode): ContainerAlignment {\n const value = staticValue(node, 'align')\n return typeof value === 'string' && value in ALIGN_LABEL_KEYS ? (value as ContainerAlignment) : 'stretch'\n}\n\n/** The rendered column count: a valid static value, else 1 (the renderer's fallback). */\nfunction currentColumns(node: ComponentNode): number {\n const value = staticValue(node, 'columns')\n return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= GRID_MAX_COLUMNS\n ? value\n : 1\n}\n\nconst COLUMN_OPTIONS = Array.from({ length: GRID_MAX_COLUMNS }, (_, i) => i + 1)\n\n/** The contextual layout section. Renders nothing for non-container nodes. */\nexport function LayoutSection({ node }: { node: ComponentNode }) {\n const builder = useBuilder()\n if (!isLayoutContainerNode(node)) return null\n const isStack = node.componentId === STACK_COMPONENT_ID\n\n const set = (name: string, value: JsonValue) =>\n builder.dispatch({ type: 'setProp', id: node.id, name, value: staticProp(value) })\n const clear = (name: string) => builder.dispatch({ type: 'removeProp', id: node.id, name })\n\n return (\n <InspectorSection title={translate('builder.layout.title')}>\n <Stack gap=\"3\">\n {isStack ? (\n <Field label={translate('builder.layout.direction')} htmlFor={`layout-${node.id}-direction`}>\n <Select\n id={`layout-${node.id}-direction`}\n value={currentDirection(node)}\n onChange={(e) => set('direction', e.target.value)}\n >\n <option value=\"vertical\">{translate('builder.layout.vertical')}</option>\n <option value=\"horizontal\">{translate('builder.layout.horizontal')}</option>\n </Select>\n </Field>\n ) : (\n <Field label={translate('builder.layout.columns')} htmlFor={`layout-${node.id}-columns`}>\n <Select\n id={`layout-${node.id}-columns`}\n value={String(currentColumns(node))}\n onChange={(e) => set('columns', Number(e.target.value))}\n >\n {COLUMN_OPTIONS.map((n) => (\n <option key={n} value={String(n)}>\n {translate(\n n === 1 ? 'builder.layout.columnCount.one' : 'builder.layout.columnCount.other',\n { count: n },\n )}\n </option>\n ))}\n </Select>\n </Field>\n )}\n\n <Field label={translate('builder.layout.gap')} htmlFor={`layout-${node.id}-gap`}>\n <Select\n id={`layout-${node.id}-gap`}\n value={currentGapToken(node)}\n onChange={(e) => (e.target.value === GAP_NONE ? clear('gap') : set('gap', e.target.value))}\n >\n <option value={GAP_NONE}>{translate(GAP_NONE_LABEL_KEY)}</option>\n {SPACING_TOKEN_NAMES.map((name) => (\n <option key={name} value={name}>\n {translate(GAP_LABEL_KEYS[name])}\n </option>\n ))}\n </Select>\n </Field>\n\n <Field label={translate('builder.layout.alignment')} htmlFor={`layout-${node.id}-align`}>\n <Select\n id={`layout-${node.id}-align`}\n value={currentAlignment(node)}\n onChange={(e) => set('align', e.target.value)}\n >\n {CONTAINER_ALIGNMENTS.map((name) => (\n <option key={name} value={name}>\n {translate(ALIGN_LABEL_KEYS[name])}\n </option>\n ))}\n </Select>\n </Field>\n\n {/* Responsive default (EC-163): grids stack on phones unless the editor\n explicitly opts out. \"Stack\" is the default (prop absent); \"Keep\n columns\" writes the explicit `stackOnMobile: false` override. */}\n {!isStack ? (\n <Field label={translate('builder.layout.onPhones')} htmlFor={`layout-${node.id}-mobile`}>\n <Select\n id={`layout-${node.id}-mobile`}\n value={gridStacksOnMobile(node) ? 'stack' : 'keep'}\n onChange={(e) =>\n e.target.value === 'keep'\n ? set(STACK_ON_MOBILE_PROP, false)\n : clear(STACK_ON_MOBILE_PROP)\n }\n >\n <option value=\"stack\">{translate('builder.layout.stack')}</option>\n <option value=\"keep\">{translate('builder.layout.keepColumns')}</option>\n </Select>\n </Field>\n ) : null}\n </Stack>\n </InspectorSection>\n )\n}\n"]}
|