@elytracms/studio 0.0.66 → 0.0.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/components/builder/property-inspector.d.ts +0 -21
  2. package/dist/components/builder/property-inspector.js +5 -22
  3. package/dist/components/builder/property-inspector.js.map +1 -1
  4. package/dist/components/cms/asset-picker-core.d.ts +52 -0
  5. package/dist/components/cms/asset-picker-core.js +69 -0
  6. package/dist/components/cms/asset-picker-core.js.map +1 -0
  7. package/dist/components/cms/asset-picker.d.ts +16 -4
  8. package/dist/components/cms/asset-picker.js +34 -50
  9. package/dist/components/cms/asset-picker.js.map +1 -1
  10. package/dist/components/cms/detail-editor.js +45 -54
  11. package/dist/components/cms/detail-editor.js.map +1 -1
  12. package/dist/components/cms/prop-visibility.d.ts +3 -0
  13. package/dist/components/cms/prop-visibility.js +31 -0
  14. package/dist/components/cms/prop-visibility.js.map +1 -0
  15. package/dist/components/cms/rich-text/component-embed.d.ts +8 -6
  16. package/dist/components/cms/rich-text/component-embed.js +21 -62
  17. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  18. package/dist/components/cms/rich-text/editor.d.ts +13 -3
  19. package/dist/components/cms/rich-text/editor.js +36 -21
  20. package/dist/components/cms/rich-text/editor.js.map +1 -1
  21. package/dist/components/cms/rich-text/embed-props.d.ts +60 -27
  22. package/dist/components/cms/rich-text/embed-props.js +102 -45
  23. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  24. package/dist/lib/i18n/messages.de.js +1 -2
  25. package/dist/lib/i18n/messages.de.js.map +1 -1
  26. package/dist/lib/i18n/messages.en.d.ts +0 -1
  27. package/dist/lib/i18n/messages.en.js +1 -2
  28. package/dist/lib/i18n/messages.en.js.map +1 -1
  29. package/dist/ui/expandable-surface.d.ts +35 -25
  30. package/dist/ui/expandable-surface.js +41 -21
  31. package/dist/ui/expandable-surface.js.map +1 -1
  32. package/dist/ui/index.d.ts +1 -0
  33. package/dist/ui/index.js +1 -0
  34. package/dist/ui/index.js.map +1 -1
  35. package/dist/ui/surface-geometry.d.ts +68 -0
  36. package/dist/ui/surface-geometry.js +61 -0
  37. package/dist/ui/surface-geometry.js.map +1 -0
  38. package/package.json +2 -2
  39. package/src/styles/studio.css +30 -0
  40. package/dist/components/cms/rich-text/embed-inspector-context.d.ts +0 -50
  41. package/dist/components/cms/rich-text/embed-inspector-context.js +0 -12
  42. package/dist/components/cms/rich-text/embed-inspector-context.js.map +0 -1
  43. package/dist/components/cms/rich-text/embed-inspector.d.ts +0 -25
  44. package/dist/components/cms/rich-text/embed-inspector.js +0 -48
  45. package/dist/components/cms/rich-text/embed-inspector.js.map +0 -1
  46. package/dist/components/cms/use-embed-inspector-host.d.ts +0 -12
  47. package/dist/components/cms/use-embed-inspector-host.js +0 -17
  48. package/dist/components/cms/use-embed-inspector-host.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
2
+ import { useState } from 'react';
3
3
  import { Node, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes } from '@tiptap/react';
4
4
  import { Blocks, Pencil, Trash2 } from 'lucide-react';
5
5
  import { COMPONENT_EMBED_NODE_TYPE, embeddedComponentNode } from '@elytracms/core/rich-text';
@@ -8,8 +8,7 @@ import { Button } from '@elytracms/studio/ui';
8
8
  import { translate } from '@elytracms/studio/lib/i18n';
9
9
  import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
10
10
  import { cn } from '@elytracms/studio/lib/utils';
11
- import { EmbedPropsDialog, embedPropsSummary } from './embed-props.js';
12
- import { useOptionalEmbedInspector } from './embed-inspector-context.js';
11
+ import { EmbedPropsStage, embedPropsSummary } from './embed-props.js';
13
12
  /**
14
13
  * The rich-text component-embed node (EC-189/EC-314): a block atom holding a single
15
14
  * composition value at `attrs.node`. It is the third place constrained composition
@@ -23,12 +22,14 @@ import { useOptionalEmbedInspector } from './embed-inspector-context.js';
23
22
  * preview would need the runtime renderer + resolvers; deferred). Insertion is wired
24
23
  * from the toolbar / slash menu.
25
24
  *
26
- * Prop editing (0.0.41): when a host provides an {@link useOptionalEmbedInspector embed
27
- * inspector} — the CMS detail editor — selecting the card swaps that host's sidebar to
28
- * the embed's prop form and the Edit affordance simply selects the node. Where no host
29
- * exists (the builder property inspector, the asset detail pane) it falls back to the
30
- * {@link EmbedPropsDialog} modal. Either path rewrites `attrs.node` through a proper
31
- * ProseMirror transaction (undoable). `readOnly` renders an inert card (no edit/delete).
25
+ * Prop editing (EC-430/AD-D): "Edit settings" on the card opens the
26
+ * {@link EmbedPropsStage} — the ONE prop form, identical wherever the rich-text
27
+ * editor is mounted (a document field in the form view, a richText PROP in the
28
+ * canvas-tab property inspector, or the expanded rich-text stage). It replaced the
29
+ * 0.0.41 host-sidebar/modal fork, whose sidebar half only rendered in the form view
30
+ * and left the canvas tab's edit affordance dead. Edits rewrite `attrs.node` through
31
+ * a proper ProseMirror transaction (undoable), immediately per prop change.
32
+ * `readOnly` renders an inert card (no edit/delete).
32
33
  */
33
34
  export const ComponentEmbed = Node.create({
34
35
  name: COMPONENT_EMBED_NODE_TYPE,
@@ -64,68 +65,26 @@ function readEmbedded(attrNode) {
64
65
  * inert (read-only) card is simply the card with no `onEdit`/`onDelete`.
65
66
  */
66
67
  export function EmbedCard({ displayName, summary, onEdit, onDelete, }) {
67
- return (_jsxs("div", { className: "flex items-center gap-2.5 rounded-md border border-border bg-muted/40 px-3 py-2.5 text-sm", children: [_jsx(Blocks, { className: "size-4 shrink-0 text-muted-foreground" }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate font-medium text-foreground", children: displayName }), summary ? _jsx("span", { className: "truncate text-xs text-muted-foreground", children: summary }) : null] }), _jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: translate('richText.embed.badge') }), onEdit ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", contentEditable: false, "aria-label": translate('richText.embed.editProps'), onClick: onEdit, children: _jsx(Pencil, { className: "size-3.5" }) })) : null, onDelete ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", contentEditable: false, "aria-label": translate('common.remove'), onClick: onDelete, children: _jsx(Trash2, { className: "size-3.5" }) })) : null] }));
68
+ return (_jsxs("div", { className: "flex items-center gap-2.5 rounded-md border border-border bg-muted/40 px-3 py-2.5 text-sm", children: [_jsx(Blocks, { className: "size-4 shrink-0 text-muted-foreground" }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate font-medium text-foreground", children: displayName }), summary ? _jsx("span", { className: "truncate text-xs text-muted-foreground", children: summary }) : null] }), _jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: translate('richText.embed.badge') }), onEdit ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", contentEditable: false, "aria-label": translate('richText.embed.editProps'),
69
+ // EC-430: opening the stage must not hand this click to ProseMirror's
70
+ // selection/focus handling — the stage takes focus, and the editor
71
+ // re-grabbing it would swallow Escape (prosemirror-view preventDefaults
72
+ // Escape whenever its editable is focused).
73
+ onMouseDown: (e) => e.preventDefault(), onClick: onEdit, children: _jsx(Pencil, { className: "size-3.5" }) })) : null, onDelete ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", contentEditable: false, "aria-label": translate('common.remove'), onClick: onDelete, children: _jsx(Trash2, { className: "size-3.5" }) })) : null] }));
68
74
  }
69
- function ComponentEmbedView({ node, updateAttributes, deleteNode, selected, editor, getPos, }) {
75
+ function ComponentEmbedView({ node, updateAttributes, deleteNode, selected, editor, }) {
70
76
  const embedded = readEmbedded(node.attrs.node);
71
77
  const manifest = embedded ? getStudioRegistry().registry.getManifest(embedded.componentId) : undefined;
72
78
  const editable = editor.isEditable;
73
- // The ambient sidebar sink, if a host provides one (the CMS detail editor). Its
74
- // presence decides sidebar vs. modal — see the node's doc comment.
75
- const inspector = useOptionalEmbedInspector();
76
79
  const [open, setOpen] = useState(false);
77
80
  const displayName = embedded
78
81
  ? componentDisplayName(embedded.componentId, manifest)
79
82
  : translate('richText.embed.unknown');
80
83
  const summary = embedded ? embedPropsSummary(embedded, manifest) : undefined;
81
- // Editing needs a manifest to know which prop fields to render.
84
+ // Editing needs a manifest to know which prop fields to render. A read-only editor
85
+ // never offers the affordance, so the stage is unreachable read-only by
86
+ // construction (and unmounts here if editability flips away while it is open).
82
87
  const canEdit = editable && embedded != null && manifest != null;
83
- // With a host sidebar, props are edited there (selection-driven); the modal is only
84
- // the fallback for surfaces that have no sidebar host.
85
- const useSidebar = canEdit && inspector != null;
86
- // Keep the write-back / position closures fresh without churning the effect: their
87
- // identities change every render, but the trigger is the primitive selection state.
88
- const latest = useRef({ embedded, manifest, updateAttributes, getPos, editor, editable });
89
- latest.current = { embedded, manifest, updateAttributes, getPos, editor, editable };
90
- // A stable signal for "the embedded node meaningfully changed" — its serialization.
91
- // Re-pushing on a prop edit keeps the sidebar showing the just-committed value.
92
- const embeddedKey = embedded ? JSON.stringify(embedded) : '';
93
- // While this embed is the selected node, mirror it into the host sidebar; clear on
94
- // deselect / unmount (caret moves off, node deleted, field unmounts). Keyed on the
95
- // node id so a fast A→B reselect never lets A's cleanup wipe B (selection race).
96
- useEffect(() => {
97
- if (!useSidebar || !selected || !inspector)
98
- return;
99
- const cur = latest.current;
100
- if (!cur.embedded || !cur.manifest)
101
- return;
102
- const nodeId = cur.embedded.id;
103
- inspector.setActiveEmbed({
104
- nodeId,
105
- node: cur.embedded,
106
- manifest: cur.manifest,
107
- readOnly: !cur.editable,
108
- onChange: (next) => latest.current.updateAttributes({ node: next }),
109
- onDeselect: () => {
110
- const pos = latest.current.getPos();
111
- // A TextSelection off the node drops the NodeSelection → deselects → clears.
112
- if (pos != null)
113
- latest.current.editor.commands.setTextSelection(pos);
114
- },
115
- });
116
- return () => inspector.clearActiveEmbed(nodeId);
117
- }, [useSidebar, selected, inspector, embeddedKey]);
118
- // Edit affordance: with a sidebar host, just select the node (which opens the
119
- // inspector via the effect above); otherwise open the modal fallback.
120
- const onEdit = !canEdit
121
- ? undefined
122
- : useSidebar
123
- ? () => {
124
- const pos = getPos();
125
- if (pos != null)
126
- editor.commands.setNodeSelection(pos);
127
- }
128
- : () => setOpen(true);
129
- return (_jsxs(NodeViewWrapper, { "data-drag-handle": true, className: cn('my-2', selected && 'rounded-md ring-2 ring-ring/50'), children: [_jsx(EmbedCard, { displayName: displayName, summary: summary, onEdit: onEdit, onDelete: editable ? () => deleteNode() : undefined }), !useSidebar && canEdit && embedded && manifest ? (_jsx(EmbedPropsDialog, { open: open, onOpenChange: setOpen, node: embedded, manifest: manifest, onSave: (next) => updateAttributes({ node: next }) })) : null] }));
88
+ return (_jsxs(NodeViewWrapper, { "data-drag-handle": true, className: cn('my-2', selected && 'rounded-md ring-2 ring-ring/50'), children: [_jsx(EmbedCard, { displayName: displayName, summary: summary, onEdit: canEdit ? () => setOpen(true) : undefined, onDelete: editable ? () => deleteNode() : undefined }), open && canEdit && embedded && manifest ? (_jsx(EmbedPropsStage, { node: embedded, manifest: manifest, onChange: (next) => updateAttributes({ node: next }), onDismiss: () => setOpen(false) })) : null] }));
130
89
  }
131
90
  //# sourceMappingURL=component-embed.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"component-embed.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/component-embed.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState } from 'react'\nimport { Node, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes } from '@tiptap/react'\nimport type { ReactNodeViewProps } from '@tiptap/react'\nimport { Blocks, Pencil, Trash2 } from 'lucide-react'\nimport { COMPONENT_EMBED_NODE_TYPE, embeddedComponentNode } from '@elytracms/core/rich-text'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { Button } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { EmbedPropsDialog, embedPropsSummary } from './embed-props'\nimport { useOptionalEmbedInspector } from './embed-inspector-context'\n\n/**\n * The rich-text component-embed node (EC-189/EC-314): a block atom holding a single\n * composition value at `attrs.node`. It is the third place constrained composition\n * lives (after the page collection and `blocks` fields) — a registry component\n * embedded in prose. The node carries no ProseMirror content (the embedded component\n * owns its own children through composition slots); `attrs.node` round-trips verbatim\n * so the extractor still indexes the embedded component's references, and the delivery\n * renderer renders it exactly as a `blocks` value.\n *\n * The editor shows a placeholder card naming the embedded component (a live in-editor\n * preview would need the runtime renderer + resolvers; deferred). Insertion is wired\n * from the toolbar / slash menu.\n *\n * Prop editing (0.0.41): when a host provides an {@link useOptionalEmbedInspector embed\n * inspector} — the CMS detail editor — selecting the card swaps that host's sidebar to\n * the embed's prop form and the Edit affordance simply selects the node. Where no host\n * exists (the builder property inspector, the asset detail pane) it falls back to the\n * {@link EmbedPropsDialog} modal. Either path rewrites `attrs.node` through a proper\n * ProseMirror transaction (undoable). `readOnly` renders an inert card (no edit/delete).\n */\nexport const ComponentEmbed = Node.create({\n name: COMPONENT_EMBED_NODE_TYPE,\n group: 'block',\n atom: true,\n selectable: true,\n draggable: true,\n\n addAttributes() {\n // `node` holds the embedded ComponentNode JSON; storage/JSON only, not markup.\n return { node: { default: null, rendered: false } }\n },\n\n parseHTML() {\n return [{ tag: `div[data-${COMPONENT_EMBED_NODE_TYPE}]` }]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['div', mergeAttributes(HTMLAttributes, { [`data-${COMPONENT_EMBED_NODE_TYPE}`]: '' })]\n },\n\n addNodeView() {\n return ReactNodeViewRenderer(ComponentEmbedView)\n },\n})\n\n/** Read the embedded ComponentNode from a ProseMirror node's attrs, if valid. */\nfunction readEmbedded(attrNode: unknown): ComponentNode | undefined {\n return embeddedComponentNode({\n type: COMPONENT_EMBED_NODE_TYPE,\n attrs: { node: attrNode as JsonValue },\n })\n}\n\n/**\n * The presentational embed card — component identity, an optional one-line prop\n * summary, and (only when a handler is supplied) edit / delete affordances. It is\n * kept free of TipTap and registry context so it renders + asserts standalone: an\n * inert (read-only) card is simply the card with no `onEdit`/`onDelete`.\n */\nexport function EmbedCard({\n displayName,\n summary,\n onEdit,\n onDelete,\n}: {\n displayName: string\n summary?: string | undefined\n onEdit?: (() => void) | undefined\n onDelete?: (() => void) | undefined\n}) {\n return (\n <div className=\"flex items-center gap-2.5 rounded-md border border-border bg-muted/40 px-3 py-2.5 text-sm\">\n <Blocks className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate font-medium text-foreground\">{displayName}</span>\n {summary ? <span className=\"truncate text-xs text-muted-foreground\">{summary}</span> : null}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {translate('richText.embed.badge')}\n </span>\n {onEdit ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n contentEditable={false}\n aria-label={translate('richText.embed.editProps')}\n onClick={onEdit}\n >\n <Pencil className=\"size-3.5\" />\n </Button>\n ) : null}\n {onDelete ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n contentEditable={false}\n aria-label={translate('common.remove')}\n onClick={onDelete}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n ) : null}\n </div>\n )\n}\n\nfunction ComponentEmbedView({\n node,\n updateAttributes,\n deleteNode,\n selected,\n editor,\n getPos,\n}: ReactNodeViewProps) {\n const embedded = readEmbedded(node.attrs.node)\n const manifest = embedded ? getStudioRegistry().registry.getManifest(embedded.componentId) : undefined\n const editable = editor.isEditable\n // The ambient sidebar sink, if a host provides one (the CMS detail editor). Its\n // presence decides sidebar vs. modal — see the node's doc comment.\n const inspector = useOptionalEmbedInspector()\n const [open, setOpen] = useState(false)\n\n const displayName = embedded\n ? componentDisplayName(embedded.componentId, manifest)\n : translate('richText.embed.unknown')\n const summary = embedded ? embedPropsSummary(embedded, manifest) : undefined\n // Editing needs a manifest to know which prop fields to render.\n const canEdit = editable && embedded != null && manifest != null\n // With a host sidebar, props are edited there (selection-driven); the modal is only\n // the fallback for surfaces that have no sidebar host.\n const useSidebar = canEdit && inspector != null\n\n // Keep the write-back / position closures fresh without churning the effect: their\n // identities change every render, but the trigger is the primitive selection state.\n const latest = useRef({ embedded, manifest, updateAttributes, getPos, editor, editable })\n latest.current = { embedded, manifest, updateAttributes, getPos, editor, editable }\n\n // A stable signal for \"the embedded node meaningfully changed\" — its serialization.\n // Re-pushing on a prop edit keeps the sidebar showing the just-committed value.\n const embeddedKey = embedded ? JSON.stringify(embedded) : ''\n\n // While this embed is the selected node, mirror it into the host sidebar; clear on\n // deselect / unmount (caret moves off, node deleted, field unmounts). Keyed on the\n // node id so a fast A→B reselect never lets A's cleanup wipe B (selection race).\n useEffect(() => {\n if (!useSidebar || !selected || !inspector) return\n const cur = latest.current\n if (!cur.embedded || !cur.manifest) return\n const nodeId = cur.embedded.id\n inspector.setActiveEmbed({\n nodeId,\n node: cur.embedded,\n manifest: cur.manifest,\n readOnly: !cur.editable,\n onChange: (next) => latest.current.updateAttributes({ node: next }),\n onDeselect: () => {\n const pos = latest.current.getPos()\n // A TextSelection off the node drops the NodeSelection → deselects → clears.\n if (pos != null) latest.current.editor.commands.setTextSelection(pos)\n },\n })\n return () => inspector.clearActiveEmbed(nodeId)\n }, [useSidebar, selected, inspector, embeddedKey])\n\n // Edit affordance: with a sidebar host, just select the node (which opens the\n // inspector via the effect above); otherwise open the modal fallback.\n const onEdit = !canEdit\n ? undefined\n : useSidebar\n ? () => {\n const pos = getPos()\n if (pos != null) editor.commands.setNodeSelection(pos)\n }\n : () => setOpen(true)\n\n return (\n <NodeViewWrapper data-drag-handle className={cn('my-2', selected && 'rounded-md ring-2 ring-ring/50')}>\n <EmbedCard\n displayName={displayName}\n summary={summary}\n onEdit={onEdit}\n onDelete={editable ? () => deleteNode() : undefined}\n />\n {!useSidebar && canEdit && embedded && manifest ? (\n <EmbedPropsDialog\n open={open}\n onOpenChange={setOpen}\n node={embedded}\n manifest={manifest}\n onSave={(next) => updateAttributes({ node: next })}\n />\n ) : null}\n </NodeViewWrapper>\n )\n}\n"]}
1
+ {"version":3,"file":"component-embed.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/component-embed.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport { Node, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes } from '@tiptap/react'\nimport type { ReactNodeViewProps } from '@tiptap/react'\nimport { Blocks, Pencil, Trash2 } from 'lucide-react'\nimport { COMPONENT_EMBED_NODE_TYPE, embeddedComponentNode } from '@elytracms/core/rich-text'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { Button } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { EmbedPropsStage, embedPropsSummary } from './embed-props'\n\n/**\n * The rich-text component-embed node (EC-189/EC-314): a block atom holding a single\n * composition value at `attrs.node`. It is the third place constrained composition\n * lives (after the page collection and `blocks` fields) — a registry component\n * embedded in prose. The node carries no ProseMirror content (the embedded component\n * owns its own children through composition slots); `attrs.node` round-trips verbatim\n * so the extractor still indexes the embedded component's references, and the delivery\n * renderer renders it exactly as a `blocks` value.\n *\n * The editor shows a placeholder card naming the embedded component (a live in-editor\n * preview would need the runtime renderer + resolvers; deferred). Insertion is wired\n * from the toolbar / slash menu.\n *\n * Prop editing (EC-430/AD-D): \"Edit settings\" on the card opens the\n * {@link EmbedPropsStage} — the ONE prop form, identical wherever the rich-text\n * editor is mounted (a document field in the form view, a richText PROP in the\n * canvas-tab property inspector, or the expanded rich-text stage). It replaced the\n * 0.0.41 host-sidebar/modal fork, whose sidebar half only rendered in the form view\n * and left the canvas tab's edit affordance dead. Edits rewrite `attrs.node` through\n * a proper ProseMirror transaction (undoable), immediately per prop change.\n * `readOnly` renders an inert card (no edit/delete).\n */\nexport const ComponentEmbed = Node.create({\n name: COMPONENT_EMBED_NODE_TYPE,\n group: 'block',\n atom: true,\n selectable: true,\n draggable: true,\n\n addAttributes() {\n // `node` holds the embedded ComponentNode JSON; storage/JSON only, not markup.\n return { node: { default: null, rendered: false } }\n },\n\n parseHTML() {\n return [{ tag: `div[data-${COMPONENT_EMBED_NODE_TYPE}]` }]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['div', mergeAttributes(HTMLAttributes, { [`data-${COMPONENT_EMBED_NODE_TYPE}`]: '' })]\n },\n\n addNodeView() {\n return ReactNodeViewRenderer(ComponentEmbedView)\n },\n})\n\n/** Read the embedded ComponentNode from a ProseMirror node's attrs, if valid. */\nfunction readEmbedded(attrNode: unknown): ComponentNode | undefined {\n return embeddedComponentNode({\n type: COMPONENT_EMBED_NODE_TYPE,\n attrs: { node: attrNode as JsonValue },\n })\n}\n\n/**\n * The presentational embed card — component identity, an optional one-line prop\n * summary, and (only when a handler is supplied) edit / delete affordances. It is\n * kept free of TipTap and registry context so it renders + asserts standalone: an\n * inert (read-only) card is simply the card with no `onEdit`/`onDelete`.\n */\nexport function EmbedCard({\n displayName,\n summary,\n onEdit,\n onDelete,\n}: {\n displayName: string\n summary?: string | undefined\n onEdit?: (() => void) | undefined\n onDelete?: (() => void) | undefined\n}) {\n return (\n <div className=\"flex items-center gap-2.5 rounded-md border border-border bg-muted/40 px-3 py-2.5 text-sm\">\n <Blocks className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate font-medium text-foreground\">{displayName}</span>\n {summary ? <span className=\"truncate text-xs text-muted-foreground\">{summary}</span> : null}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {translate('richText.embed.badge')}\n </span>\n {onEdit ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n contentEditable={false}\n aria-label={translate('richText.embed.editProps')}\n // EC-430: opening the stage must not hand this click to ProseMirror's\n // selection/focus handling — the stage takes focus, and the editor\n // re-grabbing it would swallow Escape (prosemirror-view preventDefaults\n // Escape whenever its editable is focused).\n onMouseDown={(e) => e.preventDefault()}\n onClick={onEdit}\n >\n <Pencil className=\"size-3.5\" />\n </Button>\n ) : null}\n {onDelete ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n contentEditable={false}\n aria-label={translate('common.remove')}\n onClick={onDelete}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n ) : null}\n </div>\n )\n}\n\nfunction ComponentEmbedView({\n node,\n updateAttributes,\n deleteNode,\n selected,\n editor,\n}: ReactNodeViewProps) {\n const embedded = readEmbedded(node.attrs.node)\n const manifest = embedded ? getStudioRegistry().registry.getManifest(embedded.componentId) : undefined\n const editable = editor.isEditable\n const [open, setOpen] = useState(false)\n\n const displayName = embedded\n ? componentDisplayName(embedded.componentId, manifest)\n : translate('richText.embed.unknown')\n const summary = embedded ? embedPropsSummary(embedded, manifest) : undefined\n // Editing needs a manifest to know which prop fields to render. A read-only editor\n // never offers the affordance, so the stage is unreachable read-only by\n // construction (and unmounts here if editability flips away while it is open).\n const canEdit = editable && embedded != null && manifest != null\n\n return (\n <NodeViewWrapper data-drag-handle className={cn('my-2', selected && 'rounded-md ring-2 ring-ring/50')}>\n <EmbedCard\n displayName={displayName}\n summary={summary}\n onEdit={canEdit ? () => setOpen(true) : undefined}\n onDelete={editable ? () => deleteNode() : undefined}\n />\n {/* EC-430/AD-D: the ONE embed prop form, on the centered stage. It portals to\n the studio body, so mounting it from inside the node view works identically\n on every rich-text surface (form view, canvas-tab prop inspector, expanded\n rich-text stage — where it rides the layer stack ABOVE the prose stage).\n Each prop edit commits immediately through one updateAttributes transaction;\n the node view re-renders with the fresh attrs, so the open form always\n reflects the committed node. */}\n {open && canEdit && embedded && manifest ? (\n <EmbedPropsStage\n node={embedded}\n manifest={manifest}\n onChange={(next) => updateAttributes({ node: next })}\n onDismiss={() => setOpen(false)}\n />\n ) : null}\n </NodeViewWrapper>\n )\n}\n"]}
@@ -25,9 +25,19 @@ interface RichTextEditorProps {
25
25
  */
26
26
  export declare function RichTextEditor({ value, onChange, readOnly, id, allow }: RichTextEditorProps): import("react").JSX.Element | null;
27
27
  /**
28
- * The inline strip shown in the field row while the editor is expanded into its
29
- * sheet (the real editing surface lives in the sheet then). Clicking it returns —
30
- * so the collapsed field never appears as a broken empty box.
28
+ * EC-431 (AD-B) — the expanded editing surface's scroll body: the stage may be
29
+ * wider than the text (that is the point — distraction-free writing, not more
30
+ * pixels), so the PROSE COLUMN inside is constrained to the classic readable
31
+ * measure (~70ch) and centered with `mx-auto`, which keeps the gutters
32
+ * symmetric at every stage width. Applied to the `EditorContent` wrapper only
33
+ * while expanded; it is also the scroll area, so the toolbar above it stays
34
+ * put. Pinned by `expand.test.tsx` (the EC-391 class-contract pattern).
35
+ */
36
+ export declare const RICH_TEXT_STAGE_CONTENT_CLASS = "min-h-0 flex-1 overflow-y-auto px-6 py-6 sm:px-10 sm:py-8 [&>.ec-rte-content]:mx-auto [&>.ec-rte-content]:max-w-[70ch]";
37
+ /**
38
+ * The inline strip shown in the field row while the editor is expanded onto its
39
+ * stage (the real editing surface lives there then). Clicking it returns — so
40
+ * the collapsed field never appears as a broken empty box.
31
41
  */
32
42
  export declare function RichTextExpandPlaceholder({ onCollapse }: {
33
43
  onCollapse: () => void;
@@ -37,7 +37,7 @@ export function RichTextEditor({ value, onChange, readOnly = false, id, allow })
37
37
  const assets = useOptionalAssets();
38
38
  const [imageOpen, setImageOpen] = useState(false);
39
39
  const [embedOpen, setEmbedOpen] = useState(false);
40
- // EC-388: whether the editor is promoted to its roomy expand sheet.
40
+ // EC-388/EC-431: whether the editor is promoted onto its centered expand stage.
41
41
  const [expanded, setExpanded] = useState(false);
42
42
  const canImage = assets != null;
43
43
  // The `allow`-gated components this field may embed — drives both the toolbar
@@ -126,40 +126,55 @@ export function RichTextEditor({ value, onChange, readOnly = false, id, allow })
126
126
  if (!editor)
127
127
  return null;
128
128
  // The editing surface — the toolbar (which carries the expand toggle) + the
129
- // editable content. Mounted inline OR inside the expand sheet, never both. TipTap
129
+ // editable content. Mounted inline OR inside the expand stage, never both. TipTap
130
130
  // preserves the editor instance across `EditorContent` remounts (its view DOM is
131
131
  // carried onto the new element), so relocating this surface keeps ONE editor
132
132
  // document and ONE value flow — the same instance, grown, not a second form.
133
- const surface = (_jsxs(_Fragment, { children: [!readOnly ? (_jsx(RichTextToolbar, { editor: editor, targets: linkTargets, onOpenImage: openImage, onOpenEmbed: openEmbed, expanded: expanded, onToggleExpand: () => setExpanded((v) => !v) })) : null, _jsx(EditorContent, { editor: editor, className: cn(expanded && 'min-h-0 flex-1 overflow-y-auto') })] }));
134
- return (_jsxs("div", { className: cn('ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm', 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40', readOnly && 'opacity-70'), children: [expanded ? (_jsxs(_Fragment, { children: [_jsx(RichTextExpandPlaceholder, { onCollapse: () => setExpanded(false) }), _jsx(RichTextExpandSheet, { onCollapse: () => setExpanded(false), children: surface })] })) : (surface), canImage ? (_jsx(AssetPicker, { open: imageOpen, onOpenChange: setImageOpen, value: null, accept: "image/*", onChange: (assetId) => {
133
+ // While expanded, the toolbar stays put above the scrolling prose column
134
+ // (the scroll area is the EditorContent wrapper).
135
+ const surface = (_jsxs(_Fragment, { children: [!readOnly ? (_jsx(RichTextToolbar, { editor: editor, targets: linkTargets, onOpenImage: openImage, onOpenEmbed: openEmbed, expanded: expanded, onToggleExpand: () => setExpanded((v) => !v) })) : null, _jsx(EditorContent, { editor: editor, className: cn(expanded && RICH_TEXT_STAGE_CONTENT_CLASS) })] }));
136
+ return (_jsxs("div", { className: cn('ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm', 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40', readOnly && 'opacity-70'), children: [expanded ? (_jsxs(_Fragment, { children: [_jsx(RichTextExpandPlaceholder, { onCollapse: () => setExpanded(false) }), _jsx(RichTextExpandStage, { onCollapse: () => setExpanded(false), children: surface })] })) : (surface), canImage ? (_jsx(AssetPicker, { open: imageOpen, onOpenChange: setImageOpen, value: null, accept: "image/*", onChange: (assetId) => {
135
137
  setImageOpen(false);
136
138
  if (assetId)
137
139
  insertAssetImage(editor, assetId, assets);
138
140
  } })) : null, canEmbed ? (_jsx(EmbedPicker, { open: embedOpen, onOpenChange: setEmbedOpen, allow: allow, onSelect: (manifest) => insertComponentEmbed(editor, manifest, { select: true }) })) : null] }));
139
141
  }
140
142
  /**
141
- * The inline strip shown in the field row while the editor is expanded into its
142
- * sheet (the real editing surface lives in the sheet then). Clicking it returns —
143
- * so the collapsed field never appears as a broken empty box.
143
+ * EC-431 (AD-B) — the expanded editing surface's scroll body: the stage may be
144
+ * wider than the text (that is the point — distraction-free writing, not more
145
+ * pixels), so the PROSE COLUMN inside is constrained to the classic readable
146
+ * measure (~70ch) and centered with `mx-auto`, which keeps the gutters
147
+ * symmetric at every stage width. Applied to the `EditorContent` wrapper only
148
+ * while expanded; it is also the scroll area, so the toolbar above it stays
149
+ * put. Pinned by `expand.test.tsx` (the EC-391 class-contract pattern).
150
+ */
151
+ export const RICH_TEXT_STAGE_CONTENT_CLASS = 'min-h-0 flex-1 overflow-y-auto px-6 py-6 sm:px-10 sm:py-8 [&>.ec-rte-content]:mx-auto [&>.ec-rte-content]:max-w-[70ch]';
152
+ /**
153
+ * The inline strip shown in the field row while the editor is expanded onto its
154
+ * stage (the real editing surface lives there then). Clicking it returns — so
155
+ * the collapsed field never appears as a broken empty box.
144
156
  */
145
157
  export function RichTextExpandPlaceholder({ onCollapse }) {
146
158
  return (_jsxs("button", { type: "button", onClick: onCollapse, className: "flex min-h-[8rem] w-full items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground transition-colors hover:bg-muted/40", children: [_jsx(Minimize2, { className: "size-4" }), _jsx(Text, { size: "sm", tone: "muted", children: translate('richText.expand.editingInSheet') })] }));
147
159
  }
148
160
  /**
149
- * EC-388 — the expanded editing surface: the SAME editor, grown into a roomy,
150
- * full-height right-anchored sheet where long prose (a privacy statement, a
151
- * Freeform block) is comfortable to edit. Since EC-392 it renders through the
152
- * shared {@link ExpandableSurface} primitive (portal-to-body, backdrop, header
153
- * close, layer-stacked Escape) at the shipped 820px width. It is the same edit
154
- * surface, not a second form: no save/cancel of its own — edits flow through the
155
- * editor's existing value contract, and it is lightly dismissible (Escape /
156
- * backdrop / the toolbar's collapse toggle / the header close), the
157
- * forms-never-modal spirit applied. The `children` are the live toolbar +
158
- * `EditorContent`; unmounting them here returns the editor inline. The asset
159
- * picker opened from the sheet's toolbar is a Radix dialog ABOVE the sheet — it
160
- * consumes Escape first (the sheet stays), per the EC-392 stacking rules.
161
+ * EC-388/EC-431 — the expanded editing surface: the SAME editor, grown onto the
162
+ * centered STAGE (ratified AD-B — writing is a primary activity, so it takes
163
+ * the `stage` prose preset, never the peripheral side sheet) where long prose
164
+ * (a privacy statement, a Freeform block) is comfortable to edit. It renders
165
+ * through the shared {@link ExpandableSurface} primitive (portal-to-body,
166
+ * backdrop, header close, layer-stacked Escape); the prose column inside is
167
+ * constrained to the distraction-free measure
168
+ * ({@link RICH_TEXT_STAGE_CONTENT_CLASS}). It is the same edit surface, not a
169
+ * second form: no save/cancel of its own — edits flow through the editor's
170
+ * existing value contract, and it is lightly dismissible (Escape / backdrop /
171
+ * the toolbar's collapse toggle / the header close), the forms-never-modal
172
+ * spirit applied. The `children` are the live toolbar + `EditorContent`;
173
+ * unmounting them here returns the editor inline. The asset picker opened from
174
+ * the stage's toolbar is a Radix dialog ABOVE the stage — it consumes Escape
175
+ * first (the stage stays), per the EC-392 stacking rules.
161
176
  */
162
- function RichTextExpandSheet({ children, onCollapse, }) {
163
- return (_jsx(ExpandableSurface, { title: translate('richText.expand.title'), closeLabel: translate('richText.expand.collapse'), width: "min(820px, 92vw)", onDismiss: onCollapse, children: _jsx("div", { className: "ec-rte flex min-h-0 flex-1 flex-col overflow-hidden bg-background", children: children }) }));
177
+ function RichTextExpandStage({ children, onCollapse, }) {
178
+ return (_jsx(ExpandableSurface, { title: translate('richText.expand.title'), closeLabel: translate('richText.expand.collapse'), variant: "stage", preset: "prose", onDismiss: onCollapse, children: _jsx("div", { className: "ec-rte flex min-h-0 flex-1 flex-col overflow-hidden bg-background", children: children }) }));
164
179
  }
165
180
  //# sourceMappingURL=editor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"editor.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { Minimize2 } from 'lucide-react'\nimport type { RichTextDoc } from '@elytracms/core/rich-text'\nimport { ExpandableSurface, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { useOptionalCollectionView } from '../../collection-view/collection-view-context'\nimport { useOptionalAssets } from '../../assets/assets-store'\nimport { AssetPicker } from '../asset-picker'\nimport { buildRichTextExtensions } from './extensions'\nimport {\n isEffectivelyEmpty,\n isEquivalentToStored,\n normalizeEditorDoc,\n richTextValueFromDoc,\n toEditorDoc,\n} from './value'\nimport { insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert'\nimport { EmbedPicker } from './embed-picker'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { RichTextToolbar } from './toolbar'\nimport type { LinkTarget } from './link-control'\n\ninterface RichTextEditorProps {\n /** The stored `richText` value (`{ version, doc }`, legacy string, or empty). */\n value: unknown\n onChange: (value: unknown) => void\n readOnly?: boolean\n id?: string\n /** The field's embed `allow` list (component ids editors may embed). */\n allow?: string[]\n}\n\n/**\n * The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared\n * `@elytracms/core/rich-text` storage, replacing the plaintext textarea stub.\n *\n * It stays *controlled without churn*: the editor is created once from the initial\n * value; the stored value is only re-pushed into ProseMirror when it changes\n * externally (locale switch, reset, compare) and is not already equivalent to what\n * is on screen. On edit, output is pruned to the lean storage convention\n * ({@link normalizeEditorDoc}) and — crucially — when an edit lands the document back\n * on its stored meaning, the *exact stored value* is re-emitted so the field never\n * shows a spurious dirty state (the same \"commit-nothing-when-equivalent\" contract\n * the canvas inline editor uses).\n *\n * The image and component-embed pickers are owned here (not the toolbar) so both the\n * toolbar buttons and the `/` slash menu open the same modal — one insertion path.\n */\nexport function RichTextEditor({ value, onChange, readOnly = false, id, allow }: RichTextEditorProps) {\n // Ambient context for reference-aware insertion (null outside a collection view /\n // asset backend — the editor then degrades to external links and no image insert).\n const source = useOptionalCollectionView()\n const assets = useOptionalAssets()\n\n const [imageOpen, setImageOpen] = useState(false)\n const [embedOpen, setEmbedOpen] = useState(false)\n // EC-388: whether the editor is promoted to its roomy expand sheet.\n const [expanded, setExpanded] = useState(false)\n\n const canImage = assets != null\n // The `allow`-gated components this field may embed — drives both the toolbar\n // picker and the slash menu's direct per-component items (0.0.41).\n const embeddable = useMemo(\n () => listEmbeddable(allow, getStudioRegistry().registry.list()),\n [allow],\n )\n const canEmbed = embeddable.length > 0\n\n // Every linkable document across the workspace (excluding the asset rail — you link\n // to content, not to raw files). Recomputed only when the source changes.\n const linkTargets = useMemo<LinkTarget[]>(() => {\n if (!source) return []\n const out: LinkTarget[] = []\n for (const collection of source.collections) {\n if (collection.id === source.assetCollectionId) continue\n const collectionLabel = localizedLabel(collection.form?.labelSingular, collection.id)\n for (const doc of source.referenceCandidates(collection.id)) {\n const ref = { collection: collection.id, id: doc.id }\n out.push({ ref, label: source.titleOf(ref), collectionLabel })\n }\n }\n return out\n }, [source])\n\n // Latest value/onChange, read inside the stable onUpdate closure.\n const valueRef = useRef(value)\n valueRef.current = value\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n // What we last emitted upward — lets the sync effect ignore our own echoes.\n const lastEmitted = useRef<unknown>(value)\n\n // Media openers for the toolbar + slash menu. setState fns are stable, so these\n // stay valid for the life of the editor even though it's built once.\n const openImage = canImage ? () => setImageOpen(true) : undefined\n const openEmbed = canEmbed ? () => setEmbedOpen(true) : undefined\n\n const editor = useEditor({\n extensions: buildRichTextExtensions({\n slash: {\n onImage: openImage,\n // Direct per-component slash items (0.0.41) — insert with manifest defaults\n // and select, so the embed inspector opens onto the new node's props.\n embed: canEmbed\n ? {\n components: embeddable,\n insert: (ed, range, manifest) => {\n ed.chain().focus().deleteRange(range).run()\n insertComponentEmbed(ed, manifest, { select: true })\n },\n }\n : undefined,\n },\n }),\n content: toEditorDoc(value),\n editable: !readOnly,\n editorProps: {\n attributes: { class: 'ec-rte-content', ...(id ? { id } : {}) },\n },\n onUpdate: ({ editor }) => {\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n const stored = valueRef.current\n // Three outcomes: unchanged → re-emit the stored value verbatim (no dirty);\n // emptied → clear the field (undefined); otherwise → the new stamped value.\n const next = isEquivalentToStored(current, stored)\n ? stored\n : isEffectivelyEmpty(current)\n ? undefined\n : richTextValueFromDoc(current)\n lastEmitted.current = next\n onChangeRef.current(next)\n },\n })\n\n // Reflect external value changes into the editor, never echoing our own emits.\n useEffect(() => {\n if (!editor) return\n if (value === lastEmitted.current) return\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n if (isEquivalentToStored(current, value)) return\n editor.commands.setContent(toEditorDoc(value), { emitUpdate: false })\n }, [editor, value])\n\n // Toggle editability without rebuilding the editor (preserves history/selection).\n useEffect(() => {\n editor?.setEditable(!readOnly)\n }, [editor, readOnly])\n\n if (!editor) return null\n\n // The editing surface — the toolbar (which carries the expand toggle) + the\n // editable content. Mounted inline OR inside the expand sheet, never both. TipTap\n // preserves the editor instance across `EditorContent` remounts (its view DOM is\n // carried onto the new element), so relocating this surface keeps ONE editor\n // document and ONE value flow — the same instance, grown, not a second form.\n const surface = (\n <>\n {!readOnly ? (\n <RichTextToolbar\n editor={editor}\n targets={linkTargets}\n onOpenImage={openImage}\n onOpenEmbed={openEmbed}\n expanded={expanded}\n onToggleExpand={() => setExpanded((v) => !v)}\n />\n ) : null}\n <EditorContent editor={editor} className={cn(expanded && 'min-h-0 flex-1 overflow-y-auto')} />\n </>\n )\n\n return (\n <div\n className={cn(\n 'ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm',\n 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40',\n readOnly && 'opacity-70',\n )}\n >\n {expanded ? (\n <>\n <RichTextExpandPlaceholder onCollapse={() => setExpanded(false)} />\n <RichTextExpandSheet onCollapse={() => setExpanded(false)}>{surface}</RichTextExpandSheet>\n </>\n ) : (\n surface\n )}\n {canImage ? (\n <AssetPicker\n open={imageOpen}\n onOpenChange={setImageOpen}\n value={null}\n accept=\"image/*\"\n onChange={(assetId) => {\n setImageOpen(false)\n if (assetId) insertAssetImage(editor, assetId, assets)\n }}\n />\n ) : null}\n {canEmbed ? (\n <EmbedPicker\n open={embedOpen}\n onOpenChange={setEmbedOpen}\n allow={allow}\n onSelect={(manifest) => insertComponentEmbed(editor, manifest, { select: true })}\n />\n ) : null}\n </div>\n )\n}\n\n/**\n * The inline strip shown in the field row while the editor is expanded into its\n * sheet (the real editing surface lives in the sheet then). Clicking it returns —\n * so the collapsed field never appears as a broken empty box.\n */\nexport function RichTextExpandPlaceholder({ onCollapse }: { onCollapse: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onCollapse}\n className=\"flex min-h-[8rem] w-full items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground transition-colors hover:bg-muted/40\"\n >\n <Minimize2 className=\"size-4\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('richText.expand.editingInSheet')}\n </Text>\n </button>\n )\n}\n\n/**\n * EC-388 — the expanded editing surface: the SAME editor, grown into a roomy,\n * full-height right-anchored sheet where long prose (a privacy statement, a\n * Freeform block) is comfortable to edit. Since EC-392 it renders through the\n * shared {@link ExpandableSurface} primitive (portal-to-body, backdrop, header\n * close, layer-stacked Escape) at the shipped 820px width. It is the same edit\n * surface, not a second form: no save/cancel of its own — edits flow through the\n * editor's existing value contract, and it is lightly dismissible (Escape /\n * backdrop / the toolbar's collapse toggle / the header close), the\n * forms-never-modal spirit applied. The `children` are the live toolbar +\n * `EditorContent`; unmounting them here returns the editor inline. The asset\n * picker opened from the sheet's toolbar is a Radix dialog ABOVE the sheet — it\n * consumes Escape first (the sheet stays), per the EC-392 stacking rules.\n */\nfunction RichTextExpandSheet({\n children,\n onCollapse,\n}: {\n children: ReactNode\n onCollapse: () => void\n}) {\n return (\n <ExpandableSurface\n title={translate('richText.expand.title')}\n closeLabel={translate('richText.expand.collapse')}\n width=\"min(820px, 92vw)\"\n onDismiss={onCollapse}\n >\n <div className=\"ec-rte flex min-h-0 flex-1 flex-col overflow-hidden bg-background\">\n {children}\n </div>\n </ExpandableSurface>\n )\n}\n"]}
1
+ {"version":3,"file":"editor.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { Minimize2 } from 'lucide-react'\nimport type { RichTextDoc } from '@elytracms/core/rich-text'\nimport { ExpandableSurface, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { useOptionalCollectionView } from '../../collection-view/collection-view-context'\nimport { useOptionalAssets } from '../../assets/assets-store'\nimport { AssetPicker } from '../asset-picker'\nimport { buildRichTextExtensions } from './extensions'\nimport {\n isEffectivelyEmpty,\n isEquivalentToStored,\n normalizeEditorDoc,\n richTextValueFromDoc,\n toEditorDoc,\n} from './value'\nimport { insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert'\nimport { EmbedPicker } from './embed-picker'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { RichTextToolbar } from './toolbar'\nimport type { LinkTarget } from './link-control'\n\ninterface RichTextEditorProps {\n /** The stored `richText` value (`{ version, doc }`, legacy string, or empty). */\n value: unknown\n onChange: (value: unknown) => void\n readOnly?: boolean\n id?: string\n /** The field's embed `allow` list (component ids editors may embed). */\n allow?: string[]\n}\n\n/**\n * The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared\n * `@elytracms/core/rich-text` storage, replacing the plaintext textarea stub.\n *\n * It stays *controlled without churn*: the editor is created once from the initial\n * value; the stored value is only re-pushed into ProseMirror when it changes\n * externally (locale switch, reset, compare) and is not already equivalent to what\n * is on screen. On edit, output is pruned to the lean storage convention\n * ({@link normalizeEditorDoc}) and — crucially — when an edit lands the document back\n * on its stored meaning, the *exact stored value* is re-emitted so the field never\n * shows a spurious dirty state (the same \"commit-nothing-when-equivalent\" contract\n * the canvas inline editor uses).\n *\n * The image and component-embed pickers are owned here (not the toolbar) so both the\n * toolbar buttons and the `/` slash menu open the same modal — one insertion path.\n */\nexport function RichTextEditor({ value, onChange, readOnly = false, id, allow }: RichTextEditorProps) {\n // Ambient context for reference-aware insertion (null outside a collection view /\n // asset backend — the editor then degrades to external links and no image insert).\n const source = useOptionalCollectionView()\n const assets = useOptionalAssets()\n\n const [imageOpen, setImageOpen] = useState(false)\n const [embedOpen, setEmbedOpen] = useState(false)\n // EC-388/EC-431: whether the editor is promoted onto its centered expand stage.\n const [expanded, setExpanded] = useState(false)\n\n const canImage = assets != null\n // The `allow`-gated components this field may embed — drives both the toolbar\n // picker and the slash menu's direct per-component items (0.0.41).\n const embeddable = useMemo(\n () => listEmbeddable(allow, getStudioRegistry().registry.list()),\n [allow],\n )\n const canEmbed = embeddable.length > 0\n\n // Every linkable document across the workspace (excluding the asset rail — you link\n // to content, not to raw files). Recomputed only when the source changes.\n const linkTargets = useMemo<LinkTarget[]>(() => {\n if (!source) return []\n const out: LinkTarget[] = []\n for (const collection of source.collections) {\n if (collection.id === source.assetCollectionId) continue\n const collectionLabel = localizedLabel(collection.form?.labelSingular, collection.id)\n for (const doc of source.referenceCandidates(collection.id)) {\n const ref = { collection: collection.id, id: doc.id }\n out.push({ ref, label: source.titleOf(ref), collectionLabel })\n }\n }\n return out\n }, [source])\n\n // Latest value/onChange, read inside the stable onUpdate closure.\n const valueRef = useRef(value)\n valueRef.current = value\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n // What we last emitted upward — lets the sync effect ignore our own echoes.\n const lastEmitted = useRef<unknown>(value)\n\n // Media openers for the toolbar + slash menu. setState fns are stable, so these\n // stay valid for the life of the editor even though it's built once.\n const openImage = canImage ? () => setImageOpen(true) : undefined\n const openEmbed = canEmbed ? () => setEmbedOpen(true) : undefined\n\n const editor = useEditor({\n extensions: buildRichTextExtensions({\n slash: {\n onImage: openImage,\n // Direct per-component slash items (0.0.41) — insert with manifest defaults\n // and select, so the embed inspector opens onto the new node's props.\n embed: canEmbed\n ? {\n components: embeddable,\n insert: (ed, range, manifest) => {\n ed.chain().focus().deleteRange(range).run()\n insertComponentEmbed(ed, manifest, { select: true })\n },\n }\n : undefined,\n },\n }),\n content: toEditorDoc(value),\n editable: !readOnly,\n editorProps: {\n attributes: { class: 'ec-rte-content', ...(id ? { id } : {}) },\n },\n onUpdate: ({ editor }) => {\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n const stored = valueRef.current\n // Three outcomes: unchanged → re-emit the stored value verbatim (no dirty);\n // emptied → clear the field (undefined); otherwise → the new stamped value.\n const next = isEquivalentToStored(current, stored)\n ? stored\n : isEffectivelyEmpty(current)\n ? undefined\n : richTextValueFromDoc(current)\n lastEmitted.current = next\n onChangeRef.current(next)\n },\n })\n\n // Reflect external value changes into the editor, never echoing our own emits.\n useEffect(() => {\n if (!editor) return\n if (value === lastEmitted.current) return\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n if (isEquivalentToStored(current, value)) return\n editor.commands.setContent(toEditorDoc(value), { emitUpdate: false })\n }, [editor, value])\n\n // Toggle editability without rebuilding the editor (preserves history/selection).\n useEffect(() => {\n editor?.setEditable(!readOnly)\n }, [editor, readOnly])\n\n if (!editor) return null\n\n // The editing surface — the toolbar (which carries the expand toggle) + the\n // editable content. Mounted inline OR inside the expand stage, never both. TipTap\n // preserves the editor instance across `EditorContent` remounts (its view DOM is\n // carried onto the new element), so relocating this surface keeps ONE editor\n // document and ONE value flow — the same instance, grown, not a second form.\n // While expanded, the toolbar stays put above the scrolling prose column\n // (the scroll area is the EditorContent wrapper).\n const surface = (\n <>\n {!readOnly ? (\n <RichTextToolbar\n editor={editor}\n targets={linkTargets}\n onOpenImage={openImage}\n onOpenEmbed={openEmbed}\n expanded={expanded}\n onToggleExpand={() => setExpanded((v) => !v)}\n />\n ) : null}\n <EditorContent editor={editor} className={cn(expanded && RICH_TEXT_STAGE_CONTENT_CLASS)} />\n </>\n )\n\n return (\n <div\n className={cn(\n 'ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm',\n 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40',\n readOnly && 'opacity-70',\n )}\n >\n {expanded ? (\n <>\n <RichTextExpandPlaceholder onCollapse={() => setExpanded(false)} />\n <RichTextExpandStage onCollapse={() => setExpanded(false)}>{surface}</RichTextExpandStage>\n </>\n ) : (\n surface\n )}\n {canImage ? (\n <AssetPicker\n open={imageOpen}\n onOpenChange={setImageOpen}\n value={null}\n accept=\"image/*\"\n onChange={(assetId) => {\n setImageOpen(false)\n if (assetId) insertAssetImage(editor, assetId, assets)\n }}\n />\n ) : null}\n {canEmbed ? (\n <EmbedPicker\n open={embedOpen}\n onOpenChange={setEmbedOpen}\n allow={allow}\n onSelect={(manifest) => insertComponentEmbed(editor, manifest, { select: true })}\n />\n ) : null}\n </div>\n )\n}\n\n/**\n * EC-431 (AD-B) — the expanded editing surface's scroll body: the stage may be\n * wider than the text (that is the point — distraction-free writing, not more\n * pixels), so the PROSE COLUMN inside is constrained to the classic readable\n * measure (~70ch) and centered with `mx-auto`, which keeps the gutters\n * symmetric at every stage width. Applied to the `EditorContent` wrapper only\n * while expanded; it is also the scroll area, so the toolbar above it stays\n * put. Pinned by `expand.test.tsx` (the EC-391 class-contract pattern).\n */\nexport const RICH_TEXT_STAGE_CONTENT_CLASS =\n 'min-h-0 flex-1 overflow-y-auto px-6 py-6 sm:px-10 sm:py-8 [&>.ec-rte-content]:mx-auto [&>.ec-rte-content]:max-w-[70ch]'\n\n/**\n * The inline strip shown in the field row while the editor is expanded onto its\n * stage (the real editing surface lives there then). Clicking it returns — so\n * the collapsed field never appears as a broken empty box.\n */\nexport function RichTextExpandPlaceholder({ onCollapse }: { onCollapse: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onCollapse}\n className=\"flex min-h-[8rem] w-full items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground transition-colors hover:bg-muted/40\"\n >\n <Minimize2 className=\"size-4\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('richText.expand.editingInSheet')}\n </Text>\n </button>\n )\n}\n\n/**\n * EC-388/EC-431 — the expanded editing surface: the SAME editor, grown onto the\n * centered STAGE (ratified AD-B — writing is a primary activity, so it takes\n * the `stage` prose preset, never the peripheral side sheet) where long prose\n * (a privacy statement, a Freeform block) is comfortable to edit. It renders\n * through the shared {@link ExpandableSurface} primitive (portal-to-body,\n * backdrop, header close, layer-stacked Escape); the prose column inside is\n * constrained to the distraction-free measure\n * ({@link RICH_TEXT_STAGE_CONTENT_CLASS}). It is the same edit surface, not a\n * second form: no save/cancel of its own — edits flow through the editor's\n * existing value contract, and it is lightly dismissible (Escape / backdrop /\n * the toolbar's collapse toggle / the header close), the forms-never-modal\n * spirit applied. The `children` are the live toolbar + `EditorContent`;\n * unmounting them here returns the editor inline. The asset picker opened from\n * the stage's toolbar is a Radix dialog ABOVE the stage — it consumes Escape\n * first (the stage stays), per the EC-392 stacking rules.\n */\nfunction RichTextExpandStage({\n children,\n onCollapse,\n}: {\n children: ReactNode\n onCollapse: () => void\n}) {\n return (\n <ExpandableSurface\n title={translate('richText.expand.title')}\n closeLabel={translate('richText.expand.collapse')}\n variant=\"stage\"\n preset=\"prose\"\n onDismiss={onCollapse}\n >\n <div className=\"ec-rte flex min-h-0 flex-1 flex-col overflow-hidden bg-background\">\n {children}\n </div>\n </ExpandableSurface>\n )\n}\n"]}
@@ -1,23 +1,32 @@
1
1
  import type { ComponentNode, JsonValue } from '@elytracms/core/project-graph';
2
2
  import type { ComponentManifest, PropField } from '@elytracms/core/component-registry';
3
3
  /**
4
- * Prop editing for rich-text component embeds (EC-314 phase 4).
4
+ * Prop editing for rich-text component embeds (EC-314 phase 4; stage surface
5
+ * EC-430/AD-D).
5
6
  *
6
7
  * An embedded component is a full `ComponentNode` stored verbatim at
7
- * `componentEmbed.attrs.node`. This dialog edits that node's *props* through the
8
- * SAME EC-190 "props-as-fields" control stack the canvas inspector and `blocks`
9
- * field use ({@link PropValueControl}) — asset props open the library picker,
10
- * relation props the document picker, etc. — never a parallel form system.
8
+ * `componentEmbed.attrs.node`. The {@link EmbedPropsStage} edits that node's *props*
9
+ * through the SAME EC-190 "props-as-fields" control stack the canvas inspector and
10
+ * `blocks` field use ({@link PropValueControl}) — asset props open the library
11
+ * picker, relation props the document picker, etc. — never a parallel form system.
11
12
  *
12
- * Fidelity contract: a working `draft` is seeded from the stored node when the
13
- * dialog opens; the dialog only writes back on Apply, and only when the draft is
14
- * *structurally different* ({@link componentNodesEqual}) — so opening and closing
15
- * without a real change never dirties the field. The upstream write goes through a
16
- * single ProseMirror `updateAttributes` transaction (undoable), never a storage
17
- * rewrite; the embedded node's `id`/`componentId`/`slots` pass through untouched.
13
+ * One surface, reachable everywhere (ratified AD-D, 2026-07-16): "Edit settings" on
14
+ * an embed card opens the centered stage (`ExpandableSurface variant="stage"
15
+ * preset="form"`) identically from the CANVAS tab and the form view. It replaced
16
+ * BOTH halves of the 0.0.41 fork — the selection-driven sidebar swap (which only
17
+ * rendered in the form view, leaving the canvas tab's "Edit settings" dead) and the
18
+ * modal fallback.
19
+ *
20
+ * Value contract (unchanged from the 0.0.41 sidebar inspector): write-back is
21
+ * **immediate per prop change** — each edit commits through `onChange` (one
22
+ * ProseMirror `updateAttributes` transaction upstream, undoable), guarded by
23
+ * {@link resolveEmbedWrite} so an unchanged write never dirties the field. There is
24
+ * no draft+Apply and no save/cancel of its own: the stage is the same value flow
25
+ * relocated, lightly dismissible (Escape / backdrop / header close) per the
26
+ * forms-never-modal spirit. The dialog fork's draft+Apply semantics died with it.
18
27
  *
19
28
  * `blocks`-typed props (nested composition) are NOT edited here — that needs the
20
- * canvas, not a popover. They are excluded from the form and surfaced as a note.
29
+ * canvas, not a form. They are excluded from the form and surfaced as a note.
21
30
  */
22
31
  /** Set a prop to a fresh static value, preserving identity, slots, and sibling props. */
23
32
  export declare function setComponentNodeProp(node: ComponentNode, name: string, value: JsonValue): ComponentNode;
@@ -26,31 +35,55 @@ export declare function clearComponentNodeProp(node: ComponentNode, name: string
26
35
  /** Key-order-insensitive structural equality — a redundant "save" must not write. */
27
36
  export declare function componentNodesEqual(a: ComponentNode, b: ComponentNode): boolean;
28
37
  /**
29
- * The no-dirty guard for immediate per-prop write-back (0.0.41 sidebar inspector):
30
- * the node to commit for an edit, or `undefined` when it is a structural no-op so an
31
- * unchanged write never dirties the field. Same contract the old draft+Apply dialog
32
- * enforced ({@link componentNodesEqual}), applied per keystroke instead of on Apply.
38
+ * The no-dirty guard for immediate per-prop write-back (0.0.41 sidebar inspector,
39
+ * inherited verbatim by the EC-430 stage form): the node to commit for an edit, or
40
+ * `undefined` when it is a structural no-op so an unchanged write never dirties the
41
+ * field ({@link componentNodesEqual}), applied per keystroke.
33
42
  */
34
43
  export declare function resolveEmbedWrite(current: ComponentNode, next: ComponentNode): ComponentNode | undefined;
35
- /** The manifest props editable inline: every non-`blocks` prop (composition needs the canvas). */
36
- export declare function editableEmbedProps(manifest: ComponentManifest): Array<[string, PropField]>;
37
44
  /** The manifest's `blocks` (composition) props — excluded from the form, shown as a note. */
38
45
  export declare function blocksEmbedProps(manifest: ComponentManifest): Array<[string, PropField]>;
39
46
  /**
40
47
  * A lean one-line summary of an embed's static scalar props (`Label: value`,
41
48
  * up to three), for the card. Skips unset props and structured values (refs /
42
- * objects / arrays) — those are edited in the dialog, not summarised.
49
+ * objects / arrays) — those are edited in the stage form, not summarised.
43
50
  */
44
51
  export declare function embedPropsSummary(node: ComponentNode, manifest?: ComponentManifest): string | undefined;
45
- export interface EmbedPropsDialogProps {
46
- open: boolean;
47
- onOpenChange: (open: boolean) => void;
48
- /** The embedded component node whose props are being edited. */
52
+ export interface EmbedStageFormProps {
53
+ /** The embedded component node whose props are being edited (the STORED node). */
49
54
  node: ComponentNode;
50
55
  /** Its manifest — drives which prop fields render. */
51
56
  manifest: ComponentManifest;
52
- /** Commit the edited node (upstream writes it through one ProseMirror transaction). */
53
- onSave: (node: ComponentNode) => void;
57
+ /**
58
+ * Commit an edited node. Upstream writes it through one ProseMirror
59
+ * `updateAttributes` transaction (undoable); the no-dirty guard has already been
60
+ * applied here, so this only fires on a real structural change.
61
+ */
62
+ onChange: (node: ComponentNode) => void;
54
63
  }
55
- /** The edit-props dialog opened from an embed card. */
56
- export declare function EmbedPropsDialog({ open, onOpenChange, node, manifest, onSave }: EmbedPropsDialogProps): import("react").JSX.Element;
64
+ /**
65
+ * The embed prop form body — pure of surface chrome so it smoke-renders in the node
66
+ * test env (the stage itself portals and renders `null` without a DOM).
67
+ *
68
+ * EC-393 parity: rows filter through the SAME {@link visiblePropFields} core the
69
+ * canvas property inspector uses — a predicate-hidden row (`form.hidden` over the
70
+ * sibling prop values) unmounts/remounts live as its driver prop flips, because the
71
+ * committed node re-renders this form. EC-395 parity: `form.readOnly` renders the
72
+ * document form's Lock badge next to the label and disables the delegated control
73
+ * (`PropValueControl` reads the same flag); `form.control` and `form.description`
74
+ * route through the same delegation.
75
+ */
76
+ export declare function EmbedStageForm({ node, manifest, onChange }: EmbedStageFormProps): import("react").JSX.Element;
77
+ export interface EmbedPropsStageProps extends EmbedStageFormProps {
78
+ /** Dismiss request (Escape / backdrop / header close). The caller owns the open state. */
79
+ onDismiss: () => void;
80
+ }
81
+ /**
82
+ * The embed prop inspector on the centered stage (EC-430/AD-D) — mounted by the
83
+ * embed card's "Edit settings" from ANY rich-text surface (form view, canvas-tab
84
+ * prop inspector, or inside the expanded rich-text stage — a later mount rides the
85
+ * EC-392 layer stack above it, and pickers opened from these controls stack above
86
+ * this stage in turn). Hook-free by construction so the geometry contract is
87
+ * pinnable by direct invocation in the node test env.
88
+ */
89
+ export declare function EmbedPropsStage({ node, manifest, onChange, onDismiss }: EmbedPropsStageProps): import("react").JSX.Element;