@elytracms/studio 0.0.67 → 0.0.69
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-detail.d.ts +4 -2
- package/dist/components/assets/asset-detail.js +4 -2
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +30 -19
- package/dist/components/assets/assets-collection-view-source.js +55 -23
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +6 -1
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/property-inspector.d.ts +0 -21
- package/dist/components/builder/property-inspector.js +5 -22
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/selection-overlay.d.ts +17 -4
- package/dist/components/builder/selection-overlay.js +17 -15
- package/dist/components/builder/selection-overlay.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +26 -1
- package/dist/components/builder/spacing-handles.js +66 -3
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +52 -0
- package/dist/components/cms/asset-picker-core.js +69 -0
- package/dist/components/cms/asset-picker-core.js.map +1 -0
- package/dist/components/cms/asset-picker.d.ts +16 -4
- package/dist/components/cms/asset-picker.js +34 -50
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-bar.d.ts +11 -1
- package/dist/components/cms/bulk-bar.js +10 -3
- package/dist/components/cms/bulk-bar.js.map +1 -1
- package/dist/components/cms/collection-table.js +19 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +45 -54
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/field-control.js +5 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.js +5 -1
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/prop-visibility.d.ts +3 -0
- package/dist/components/cms/prop-visibility.js +31 -0
- package/dist/components/cms/prop-visibility.js.map +1 -0
- package/dist/components/cms/reference-confirm.js +5 -1
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/rich-text/component-embed.d.ts +8 -6
- package/dist/components/cms/rich-text/component-embed.js +21 -62
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/editor.d.ts +33 -4
- package/dist/components/cms/rich-text/editor.js +52 -22
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.d.ts +60 -27
- package/dist/components/cms/rich-text/embed-props.js +102 -45
- package/dist/components/cms/rich-text/embed-props.js.map +1 -1
- package/dist/components/rail-view-source.d.ts +50 -1
- package/dist/components/rail-view-source.js +56 -2
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.d.ts +3 -2
- package/dist/components/redirects/redirects-collection-view-source.js +4 -3
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/users/users-collection-view-source.js +1 -1
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +12 -0
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/dnd/gap-drag.d.ts +8 -1
- package/dist/lib/dnd/gap-drag.js +4 -0
- package/dist/lib/dnd/gap-drag.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +3 -2
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +1 -1
- package/dist/lib/i18n/messages.en.js +4 -2
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/references/references-store.d.ts +8 -1
- package/dist/lib/references/references-store.js +12 -2
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/ui/expandable-surface.d.ts +35 -25
- package/dist/ui/expandable-surface.js +41 -21
- package/dist/ui/expandable-surface.js.map +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/surface-geometry.d.ts +71 -0
- package/dist/ui/surface-geometry.js +64 -0
- package/dist/ui/surface-geometry.js.map +1 -0
- package/package.json +2 -2
- package/src/styles/studio.css +30 -0
- package/dist/components/cms/rich-text/embed-inspector-context.d.ts +0 -50
- package/dist/components/cms/rich-text/embed-inspector-context.js +0 -12
- package/dist/components/cms/rich-text/embed-inspector-context.js.map +0 -1
- package/dist/components/cms/rich-text/embed-inspector.d.ts +0 -25
- package/dist/components/cms/rich-text/embed-inspector.js +0 -48
- package/dist/components/cms/rich-text/embed-inspector.js.map +0 -1
- package/dist/components/cms/use-embed-inspector-host.d.ts +0 -12
- package/dist/components/cms/use-embed-inspector-host.js +0 -17
- 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 {
|
|
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 {
|
|
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 (
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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'),
|
|
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,
|
|
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
|
-
|
|
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"]}
|
|
@@ -6,6 +6,14 @@ interface RichTextEditorProps {
|
|
|
6
6
|
id?: string;
|
|
7
7
|
/** The field's embed `allow` list (component ids editors may embed). */
|
|
8
8
|
allow?: string[];
|
|
9
|
+
/**
|
|
10
|
+
* EC-436 (AD-1) — the field's resolved localized display label, the SAME
|
|
11
|
+
* string the field/prop row renders next to this editor. Used only to title
|
|
12
|
+
* the expand stage ("<label> — Rich text" via the parameterized
|
|
13
|
+
* `richText.expand.titleWithField` message, {@link richTextStageTitle});
|
|
14
|
+
* absent → the generic `richText.expand.title` fallback.
|
|
15
|
+
*/
|
|
16
|
+
label?: string;
|
|
9
17
|
}
|
|
10
18
|
/**
|
|
11
19
|
* The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared
|
|
@@ -23,13 +31,34 @@ interface RichTextEditorProps {
|
|
|
23
31
|
* The image and component-embed pickers are owned here (not the toolbar) so both the
|
|
24
32
|
* toolbar buttons and the `/` slash menu open the same modal — one insertion path.
|
|
25
33
|
*/
|
|
26
|
-
export declare function RichTextEditor({ value, onChange, readOnly, id, allow }: RichTextEditorProps): import("react").JSX.Element | null;
|
|
34
|
+
export declare function RichTextEditor({ value, onChange, readOnly, id, allow, label }: RichTextEditorProps): import("react").JSX.Element | null;
|
|
27
35
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* so the
|
|
36
|
+
* EC-431 (AD-B) — the expanded editing surface's scroll body: the stage may be
|
|
37
|
+
* wider than the text (that is the point — distraction-free writing, not more
|
|
38
|
+
* pixels), so the PROSE COLUMN inside is constrained to a readable measure and
|
|
39
|
+
* centered with `mx-auto`, which keeps the gutters symmetric at every stage
|
|
40
|
+
* width. EC-436 widened the measure from the classic ~70ch to ~78ch in step
|
|
41
|
+
* with the wider `prose` stage panel (owner feedback: "the dialog + text looks
|
|
42
|
+
* a bit narrow on a big screen"). Applied to the `EditorContent` wrapper only
|
|
43
|
+
* while expanded; it is also the scroll area, so the toolbar above it stays
|
|
44
|
+
* put. Pinned by `expand.test.tsx` (the EC-391 class-contract pattern).
|
|
45
|
+
*/
|
|
46
|
+
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-[78ch]";
|
|
47
|
+
/**
|
|
48
|
+
* The inline strip shown in the field row while the editor is expanded onto its
|
|
49
|
+
* stage (the real editing surface lives there then). Clicking it returns — so
|
|
50
|
+
* the collapsed field never appears as a broken empty box.
|
|
31
51
|
*/
|
|
32
52
|
export declare function RichTextExpandPlaceholder({ onCollapse }: {
|
|
33
53
|
onCollapse: () => void;
|
|
34
54
|
}): import("react").JSX.Element;
|
|
55
|
+
/**
|
|
56
|
+
* EC-436 (AD-1) — the expand stage's title. With a field label in reach it
|
|
57
|
+
* composes "<label> — <localized type>" through the PARAMETERIZED
|
|
58
|
+
* `richText.expand.titleWithField` message — never string concatenation in
|
|
59
|
+
* code, because word order is a translation concern; without one (a custom
|
|
60
|
+
* mount that passes no label) it falls back to the generic
|
|
61
|
+
* `richText.expand.title`, unchanged. Exported for the `expand.test.tsx` pins.
|
|
62
|
+
*/
|
|
63
|
+
export declare function richTextStageTitle(label?: string): string;
|
|
35
64
|
export {};
|
|
@@ -30,14 +30,14 @@ import { RichTextToolbar } from './toolbar.js';
|
|
|
30
30
|
* The image and component-embed pickers are owned here (not the toolbar) so both the
|
|
31
31
|
* toolbar buttons and the `/` slash menu open the same modal — one insertion path.
|
|
32
32
|
*/
|
|
33
|
-
export function RichTextEditor({ value, onChange, readOnly = false, id, allow }) {
|
|
33
|
+
export function RichTextEditor({ value, onChange, readOnly = false, id, allow, label }) {
|
|
34
34
|
// Ambient context for reference-aware insertion (null outside a collection view /
|
|
35
35
|
// asset backend — the editor then degrades to external links and no image insert).
|
|
36
36
|
const source = useOptionalCollectionView();
|
|
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
|
|
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,70 @@ 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
|
|
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
|
-
|
|
134
|
-
|
|
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, { label: label, 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
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* so the
|
|
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 a readable measure and
|
|
146
|
+
* centered with `mx-auto`, which keeps the gutters symmetric at every stage
|
|
147
|
+
* width. EC-436 widened the measure from the classic ~70ch to ~78ch in step
|
|
148
|
+
* with the wider `prose` stage panel (owner feedback: "the dialog + text looks
|
|
149
|
+
* a bit narrow on a big screen"). Applied to the `EditorContent` wrapper only
|
|
150
|
+
* while expanded; it is also the scroll area, so the toolbar above it stays
|
|
151
|
+
* put. Pinned by `expand.test.tsx` (the EC-391 class-contract pattern).
|
|
152
|
+
*/
|
|
153
|
+
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-[78ch]';
|
|
154
|
+
/**
|
|
155
|
+
* The inline strip shown in the field row while the editor is expanded onto its
|
|
156
|
+
* stage (the real editing surface lives there then). Clicking it returns — so
|
|
157
|
+
* the collapsed field never appears as a broken empty box.
|
|
144
158
|
*/
|
|
145
159
|
export function RichTextExpandPlaceholder({ onCollapse }) {
|
|
146
160
|
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
161
|
}
|
|
148
162
|
/**
|
|
149
|
-
* EC-
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
* EC-436 (AD-1) — the expand stage's title. With a field label in reach it
|
|
164
|
+
* composes "<label> — <localized type>" through the PARAMETERIZED
|
|
165
|
+
* `richText.expand.titleWithField` message — never string concatenation in
|
|
166
|
+
* code, because word order is a translation concern; without one (a custom
|
|
167
|
+
* mount that passes no label) it falls back to the generic
|
|
168
|
+
* `richText.expand.title`, unchanged. Exported for the `expand.test.tsx` pins.
|
|
169
|
+
*/
|
|
170
|
+
export function richTextStageTitle(label) {
|
|
171
|
+
return label !== undefined && label.trim() !== ''
|
|
172
|
+
? translate('richText.expand.titleWithField', { field: label })
|
|
173
|
+
: translate('richText.expand.title');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* EC-388/EC-431 — the expanded editing surface: the SAME editor, grown onto the
|
|
177
|
+
* centered STAGE (ratified AD-B — writing is a primary activity, so it takes
|
|
178
|
+
* the `stage` prose preset, never the peripheral side sheet) where long prose
|
|
179
|
+
* (a privacy statement, a Freeform block) is comfortable to edit. It renders
|
|
180
|
+
* through the shared {@link ExpandableSurface} primitive (portal-to-body,
|
|
181
|
+
* backdrop, header close, layer-stacked Escape); the prose column inside is
|
|
182
|
+
* constrained to the distraction-free measure
|
|
183
|
+
* ({@link RICH_TEXT_STAGE_CONTENT_CLASS}). It is the same edit surface, not a
|
|
184
|
+
* second form: no save/cancel of its own — edits flow through the editor's
|
|
185
|
+
* existing value contract, and it is lightly dismissible (Escape / backdrop /
|
|
186
|
+
* the toolbar's collapse toggle / the header close), the forms-never-modal
|
|
187
|
+
* spirit applied. The `children` are the live toolbar + `EditorContent`;
|
|
188
|
+
* unmounting them here returns the editor inline. The asset picker opened from
|
|
189
|
+
* the stage's toolbar is a Radix dialog ABOVE the stage — it consumes Escape
|
|
190
|
+
* first (the stage stays), per the EC-392 stacking rules.
|
|
161
191
|
*/
|
|
162
|
-
function
|
|
163
|
-
return (_jsx(ExpandableSurface, { title:
|
|
192
|
+
function RichTextExpandStage({ children, onCollapse, label, }) {
|
|
193
|
+
return (_jsx(ExpandableSurface, { title: richTextStageTitle(label), 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
194
|
}
|
|
165
195
|
//# 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 * EC-436 (AD-1) — the field's resolved localized display label, the SAME\n * string the field/prop row renders next to this editor. Used only to title\n * the expand stage (\"<label> — Rich text\" via the parameterized\n * `richText.expand.titleWithField` message, {@link richTextStageTitle});\n * absent → the generic `richText.expand.title` fallback.\n */\n label?: 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, label }: 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 label={label} onCollapse={() => setExpanded(false)}>\n {surface}\n </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 a readable measure and\n * centered with `mx-auto`, which keeps the gutters symmetric at every stage\n * width. EC-436 widened the measure from the classic ~70ch to ~78ch in step\n * with the wider `prose` stage panel (owner feedback: \"the dialog + text looks\n * a bit narrow on a big screen\"). 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-[78ch]'\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-436 (AD-1) — the expand stage's title. With a field label in reach it\n * composes \"<label> — <localized type>\" through the PARAMETERIZED\n * `richText.expand.titleWithField` message — never string concatenation in\n * code, because word order is a translation concern; without one (a custom\n * mount that passes no label) it falls back to the generic\n * `richText.expand.title`, unchanged. Exported for the `expand.test.tsx` pins.\n */\nexport function richTextStageTitle(label?: string): string {\n return label !== undefined && label.trim() !== ''\n ? translate('richText.expand.titleWithField', { field: label })\n : translate('richText.expand.title')\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 label,\n}: {\n children: ReactNode\n onCollapse: () => void\n /** The field's resolved display label — see {@link richTextStageTitle}. */\n label?: string\n}) {\n return (\n <ExpandableSurface\n title={richTextStageTitle(label)}\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`.
|
|
8
|
-
* SAME EC-190 "props-as-fields" control stack the canvas inspector and
|
|
9
|
-
* field use ({@link PropValueControl}) — asset props open the library
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
|
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
|
|
46
|
-
|
|
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
|
-
/**
|
|
53
|
-
|
|
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
|
-
/**
|
|
56
|
-
|
|
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;
|