@elytracms/studio 0.0.40 → 0.0.42
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/builder/canvas.js +16 -2
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/cms/detail-editor.js +95 -73
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/details-pane.d.ts +11 -21
- package/dist/components/cms/details-pane.js +14 -31
- package/dist/components/cms/details-pane.js.map +1 -1
- package/dist/components/cms/document-url-path.d.ts +39 -0
- package/dist/components/cms/document-url-path.js +60 -0
- package/dist/components/cms/document-url-path.js.map +1 -0
- package/dist/components/cms/field-control.js +18 -12
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/rich-text/component-embed.d.ts +7 -2
- package/dist/components/cms/rich-text/component-embed.js +60 -5
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/editor.js +24 -5
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/embed-inspector-context.d.ts +50 -0
- package/dist/components/cms/rich-text/embed-inspector-context.js +12 -0
- package/dist/components/cms/rich-text/embed-inspector-context.js.map +1 -0
- package/dist/components/cms/rich-text/embed-inspector.d.ts +25 -0
- package/dist/components/cms/rich-text/embed-inspector.js +48 -0
- package/dist/components/cms/rich-text/embed-inspector.js.map +1 -0
- package/dist/components/cms/rich-text/embed-picker.d.ts +0 -2
- package/dist/components/cms/rich-text/embed-picker.js +0 -4
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.d.ts +7 -0
- package/dist/components/cms/rich-text/embed-props.js +9 -0
- package/dist/components/cms/rich-text/embed-props.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +8 -2
- package/dist/components/cms/rich-text/insert.js +32 -3
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/rich-text/slash-command.d.ts +23 -6
- package/dist/components/cms/rich-text/slash-command.js +31 -23
- package/dist/components/cms/rich-text/slash-command.js.map +1 -1
- package/dist/components/cms/version-compare.js +4 -3
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/components/component-preview.js +62 -9
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/inspector/inspector-sidebar.js +20 -3
- package/dist/components/inspector/inspector-sidebar.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +9 -5
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/package.json +2 -2
|
@@ -10,8 +10,13 @@ import { Node } from '@tiptap/react';
|
|
|
10
10
|
*
|
|
11
11
|
* The editor shows a placeholder card naming the embedded component (a live in-editor
|
|
12
12
|
* preview would need the runtime renderer + resolvers; deferred). Insertion is wired
|
|
13
|
-
* from the toolbar / slash menu
|
|
14
|
-
*
|
|
13
|
+
* from the toolbar / slash menu.
|
|
14
|
+
*
|
|
15
|
+
* Prop editing (0.0.41): when a host provides an {@link useOptionalEmbedInspector embed
|
|
16
|
+
* inspector} — the CMS detail editor — selecting the card swaps that host's sidebar to
|
|
17
|
+
* the embed's prop form and the Edit affordance simply selects the node. Where no host
|
|
18
|
+
* exists (the builder property inspector, the asset detail pane) it falls back to the
|
|
19
|
+
* {@link EmbedPropsDialog} modal. Either path rewrites `attrs.node` through a proper
|
|
15
20
|
* ProseMirror transaction (undoable). `readOnly` renders an inert card (no edit/delete).
|
|
16
21
|
*/
|
|
17
22
|
export declare const ComponentEmbed: Node<any, any>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, 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';
|
|
@@ -9,6 +9,7 @@ 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
11
|
import { EmbedPropsDialog, embedPropsSummary } from './embed-props';
|
|
12
|
+
import { useOptionalEmbedInspector } from './embed-inspector-context';
|
|
12
13
|
/**
|
|
13
14
|
* The rich-text component-embed node (EC-189/EC-314): a block atom holding a single
|
|
14
15
|
* composition value at `attrs.node`. It is the third place constrained composition
|
|
@@ -20,8 +21,13 @@ import { EmbedPropsDialog, embedPropsSummary } from './embed-props';
|
|
|
20
21
|
*
|
|
21
22
|
* The editor shows a placeholder card naming the embedded component (a live in-editor
|
|
22
23
|
* preview would need the runtime renderer + resolvers; deferred). Insertion is wired
|
|
23
|
-
* from the toolbar / slash menu
|
|
24
|
-
*
|
|
24
|
+
* from the toolbar / slash menu.
|
|
25
|
+
*
|
|
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
|
|
25
31
|
* ProseMirror transaction (undoable). `readOnly` renders an inert card (no edit/delete).
|
|
26
32
|
*/
|
|
27
33
|
export const ComponentEmbed = Node.create({
|
|
@@ -60,10 +66,13 @@ function readEmbedded(attrNode) {
|
|
|
60
66
|
export function EmbedCard({ displayName, summary, onEdit, onDelete, }) {
|
|
61
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] }));
|
|
62
68
|
}
|
|
63
|
-
function ComponentEmbedView({ node, updateAttributes, deleteNode, selected, editor }) {
|
|
69
|
+
function ComponentEmbedView({ node, updateAttributes, deleteNode, selected, editor, getPos, }) {
|
|
64
70
|
const embedded = readEmbedded(node.attrs.node);
|
|
65
71
|
const manifest = embedded ? getStudioRegistry().registry.getManifest(embedded.componentId) : undefined;
|
|
66
72
|
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();
|
|
67
76
|
const [open, setOpen] = useState(false);
|
|
68
77
|
const displayName = embedded
|
|
69
78
|
? componentDisplayName(embedded.componentId, manifest)
|
|
@@ -71,6 +80,52 @@ function ComponentEmbedView({ node, updateAttributes, deleteNode, selected, edit
|
|
|
71
80
|
const summary = embedded ? embedPropsSummary(embedded, manifest) : undefined;
|
|
72
81
|
// Editing needs a manifest to know which prop fields to render.
|
|
73
82
|
const canEdit = editable && embedded != null && manifest != null;
|
|
74
|
-
|
|
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] }));
|
|
75
130
|
}
|
|
76
131
|
//# 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 { 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'\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; the card's Edit affordance opens the EC-314 phase-4\n * prop editor ({@link EmbedPropsDialog}), which 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({ node, updateAttributes, deleteNode, selected, editor }: 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.\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 {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 { 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"]}
|
|
@@ -8,8 +8,9 @@ import { useOptionalAssets } from '../../assets/assets-store';
|
|
|
8
8
|
import { AssetPicker } from '../asset-picker';
|
|
9
9
|
import { buildRichTextExtensions } from './extensions';
|
|
10
10
|
import { isEffectivelyEmpty, isEquivalentToStored, normalizeEditorDoc, richTextValueFromDoc, toEditorDoc, } from './value';
|
|
11
|
-
import { insertAssetImage, insertComponentEmbed } from './insert';
|
|
12
|
-
import { EmbedPicker
|
|
11
|
+
import { insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert';
|
|
12
|
+
import { EmbedPicker } from './embed-picker';
|
|
13
|
+
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
13
14
|
import { RichTextToolbar } from './toolbar';
|
|
14
15
|
/**
|
|
15
16
|
* The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared
|
|
@@ -35,7 +36,10 @@ export function RichTextEditor({ value, onChange, readOnly = false, id, allow })
|
|
|
35
36
|
const [imageOpen, setImageOpen] = useState(false);
|
|
36
37
|
const [embedOpen, setEmbedOpen] = useState(false);
|
|
37
38
|
const canImage = assets != null;
|
|
38
|
-
|
|
39
|
+
// The `allow`-gated components this field may embed — drives both the toolbar
|
|
40
|
+
// picker and the slash menu's direct per-component items (0.0.41).
|
|
41
|
+
const embeddable = useMemo(() => listEmbeddable(allow, getStudioRegistry().registry.list()), [allow]);
|
|
42
|
+
const canEmbed = embeddable.length > 0;
|
|
39
43
|
// Every linkable document across the workspace (excluding the asset rail — you link
|
|
40
44
|
// to content, not to raw files). Recomputed only when the source changes.
|
|
41
45
|
const linkTargets = useMemo(() => {
|
|
@@ -65,7 +69,22 @@ export function RichTextEditor({ value, onChange, readOnly = false, id, allow })
|
|
|
65
69
|
const openImage = canImage ? () => setImageOpen(true) : undefined;
|
|
66
70
|
const openEmbed = canEmbed ? () => setEmbedOpen(true) : undefined;
|
|
67
71
|
const editor = useEditor({
|
|
68
|
-
extensions: buildRichTextExtensions({
|
|
72
|
+
extensions: buildRichTextExtensions({
|
|
73
|
+
slash: {
|
|
74
|
+
onImage: openImage,
|
|
75
|
+
// Direct per-component slash items (0.0.41) — insert with manifest defaults
|
|
76
|
+
// and select, so the embed inspector opens onto the new node's props.
|
|
77
|
+
embed: canEmbed
|
|
78
|
+
? {
|
|
79
|
+
components: embeddable,
|
|
80
|
+
insert: (ed, range, manifest) => {
|
|
81
|
+
ed.chain().focus().deleteRange(range).run();
|
|
82
|
+
insertComponentEmbed(ed, manifest, { select: true });
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
: undefined,
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
69
88
|
content: toEditorDoc(value),
|
|
70
89
|
editable: !readOnly,
|
|
71
90
|
editorProps: {
|
|
@@ -106,6 +125,6 @@ export function RichTextEditor({ value, onChange, readOnly = false, id, allow })
|
|
|
106
125
|
setImageOpen(false);
|
|
107
126
|
if (assetId)
|
|
108
127
|
insertAssetImage(editor, assetId, assets);
|
|
109
|
-
} })) : null, canEmbed ? (_jsx(EmbedPicker, { open: embedOpen, onOpenChange: setEmbedOpen, allow: allow, onSelect: (manifest) => insertComponentEmbed(editor, manifest) })) : null] }));
|
|
128
|
+
} })) : null, canEmbed ? (_jsx(EmbedPicker, { open: embedOpen, onOpenChange: setEmbedOpen, allow: allow, onSelect: (manifest) => insertComponentEmbed(editor, manifest, { select: true }) })) : null] }));
|
|
110
129
|
}
|
|
111
130
|
//# 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 { EditorContent, useEditor } from '@tiptap/react'\nimport type { RichTextDoc } from '@elytracms/core/rich-text'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel } 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 } from './insert'\nimport { EmbedPicker, hasEmbeddable } from './embed-picker'\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\n const canImage = assets != null\n const canEmbed = useMemo(() => hasEmbeddable(allow), [allow])\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({ slash: { onImage: openImage, onEmbed: openEmbed } }),\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 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 {!readOnly ? (\n <RichTextToolbar\n editor={editor}\n targets={linkTargets}\n onOpenImage={openImage}\n onOpenEmbed={openEmbed}\n />\n ) : null}\n <EditorContent editor={editor} />\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)}\n />\n ) : null}\n </div>\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 { EditorContent, useEditor } from '@tiptap/react'\nimport type { RichTextDoc } from '@elytracms/core/rich-text'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel } 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\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 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 {!readOnly ? (\n <RichTextToolbar\n editor={editor}\n targets={linkTargets}\n onOpenImage={openImage}\n onOpenEmbed={openEmbed}\n />\n ) : null}\n <EditorContent editor={editor} />\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"]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
2
|
+
import type { ComponentManifest } from '@elytracms/core/component-registry';
|
|
3
|
+
/**
|
|
4
|
+
* Selection-driven embed inspector plumbing (0.0.41) — a deliberately **TipTap-free**
|
|
5
|
+
* seam between the rich-text editor and the CMS detail editor's right sidebar.
|
|
6
|
+
*
|
|
7
|
+
* When a `componentEmbed` node is selected inside a rich-text field, the field pushes
|
|
8
|
+
* an {@link EmbedInspectorPayload} through this context; the detail editor swaps its
|
|
9
|
+
* inspector sidebar for the embed's prop form (the same EC-190 controls the old modal
|
|
10
|
+
* used). Deselecting clears it and the normal sidebar returns.
|
|
11
|
+
*
|
|
12
|
+
* It carries only plain data + callbacks (no `@tiptap/*` imports) so the detail
|
|
13
|
+
* editor — which sits on the critical path and must NOT statically pull TipTap
|
|
14
|
+
* (EC-322) — can consume it while the editor subtree stays behind `rich-text/lazy`.
|
|
15
|
+
*/
|
|
16
|
+
export interface EmbedInspectorPayload {
|
|
17
|
+
/**
|
|
18
|
+
* Stable identity of the embedded node (its `ComponentNode.id`). Dedupes selection
|
|
19
|
+
* races: when the selection moves from embed A to embed B, B's "open" and A's
|
|
20
|
+
* "close" can interleave — clearing keys on this id so A never wipes B.
|
|
21
|
+
*/
|
|
22
|
+
nodeId: string;
|
|
23
|
+
/** The embedded component node whose props the inspector edits. */
|
|
24
|
+
node: ComponentNode;
|
|
25
|
+
/** Its manifest — drives which prop fields render. */
|
|
26
|
+
manifest: ComponentManifest;
|
|
27
|
+
/** Render the inspector read-only (mirrors the field's `readOnly`). */
|
|
28
|
+
readOnly: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Commit an edited node. The field writes it back through one ProseMirror
|
|
31
|
+
* `updateAttributes` transaction (undoable); the caller has already applied the
|
|
32
|
+
* no-dirty guard, so this only fires on a real structural change.
|
|
33
|
+
*/
|
|
34
|
+
onChange: (node: ComponentNode) => void;
|
|
35
|
+
/** Move the editor selection off this node — the inspector's back affordance. */
|
|
36
|
+
onDeselect: () => void;
|
|
37
|
+
}
|
|
38
|
+
export interface EmbedInspectorApi {
|
|
39
|
+
/** Show (or refocus) the inspector for the currently-selected embed. */
|
|
40
|
+
setActiveEmbed: (payload: EmbedInspectorPayload) => void;
|
|
41
|
+
/** Hide the inspector, but only when `nodeId` is the one currently shown. */
|
|
42
|
+
clearActiveEmbed: (nodeId: string) => void;
|
|
43
|
+
}
|
|
44
|
+
export declare const EmbedInspectorProvider: import("react").Provider<EmbedInspectorApi | null>;
|
|
45
|
+
/**
|
|
46
|
+
* The ambient embed-inspector sink, or `null` when the rich-text field is mounted
|
|
47
|
+
* outside a host that provides one (the builder property inspector, the asset detail
|
|
48
|
+
* pane). A `null` return is the signal to fall back to the modal prop editor.
|
|
49
|
+
*/
|
|
50
|
+
export declare function useOptionalEmbedInspector(): EmbedInspectorApi | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
const EmbedInspectorContext = createContext(null);
|
|
3
|
+
export const EmbedInspectorProvider = EmbedInspectorContext.Provider;
|
|
4
|
+
/**
|
|
5
|
+
* The ambient embed-inspector sink, or `null` when the rich-text field is mounted
|
|
6
|
+
* outside a host that provides one (the builder property inspector, the asset detail
|
|
7
|
+
* pane). A `null` return is the signal to fall back to the modal prop editor.
|
|
8
|
+
*/
|
|
9
|
+
export function useOptionalEmbedInspector() {
|
|
10
|
+
return useContext(EmbedInspectorContext);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=embed-inspector-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed-inspector-context.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-inspector-context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AA+CjD,MAAM,qBAAqB,GAAG,aAAa,CAA2B,IAAI,CAAC,CAAA;AAE3E,MAAM,CAAC,MAAM,sBAAsB,GAAG,qBAAqB,CAAC,QAAQ,CAAA;AAEpE;;;;GAIG;AACH,MAAM,UAAU,yBAAyB;IACvC,OAAO,UAAU,CAAC,qBAAqB,CAAC,CAAA;AAC1C,CAAC","sourcesContent":["import { createContext, useContext } from 'react'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\n\n/**\n * Selection-driven embed inspector plumbing (0.0.41) — a deliberately **TipTap-free**\n * seam between the rich-text editor and the CMS detail editor's right sidebar.\n *\n * When a `componentEmbed` node is selected inside a rich-text field, the field pushes\n * an {@link EmbedInspectorPayload} through this context; the detail editor swaps its\n * inspector sidebar for the embed's prop form (the same EC-190 controls the old modal\n * used). Deselecting clears it and the normal sidebar returns.\n *\n * It carries only plain data + callbacks (no `@tiptap/*` imports) so the detail\n * editor — which sits on the critical path and must NOT statically pull TipTap\n * (EC-322) — can consume it while the editor subtree stays behind `rich-text/lazy`.\n */\nexport interface EmbedInspectorPayload {\n /**\n * Stable identity of the embedded node (its `ComponentNode.id`). Dedupes selection\n * races: when the selection moves from embed A to embed B, B's \"open\" and A's\n * \"close\" can interleave — clearing keys on this id so A never wipes B.\n */\n nodeId: string\n /** The embedded component node whose props the inspector edits. */\n node: ComponentNode\n /** Its manifest — drives which prop fields render. */\n manifest: ComponentManifest\n /** Render the inspector read-only (mirrors the field's `readOnly`). */\n readOnly: boolean\n /**\n * Commit an edited node. The field writes it back through one ProseMirror\n * `updateAttributes` transaction (undoable); the caller has already applied the\n * no-dirty guard, so this only fires on a real structural change.\n */\n onChange: (node: ComponentNode) => void\n /** Move the editor selection off this node — the inspector's back affordance. */\n onDeselect: () => void\n}\n\nexport interface EmbedInspectorApi {\n /** Show (or refocus) the inspector for the currently-selected embed. */\n setActiveEmbed: (payload: EmbedInspectorPayload) => void\n /** Hide the inspector, but only when `nodeId` is the one currently shown. */\n clearActiveEmbed: (nodeId: string) => void\n}\n\nconst EmbedInspectorContext = createContext<EmbedInspectorApi | null>(null)\n\nexport const EmbedInspectorProvider = EmbedInspectorContext.Provider\n\n/**\n * The ambient embed-inspector sink, or `null` when the rich-text field is mounted\n * outside a host that provides one (the builder property inspector, the asset detail\n * pane). A `null` return is the signal to fall back to the modal prop editor.\n */\nexport function useOptionalEmbedInspector(): EmbedInspectorApi | null {\n return useContext(EmbedInspectorContext)\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EmbedInspectorPayload } from './embed-inspector-context';
|
|
2
|
+
/**
|
|
3
|
+
* The selection-driven embed prop inspector (0.0.41) — the detail editor's right
|
|
4
|
+
* sidebar swaps to this when a `componentEmbed` node is selected in a rich-text
|
|
5
|
+
* field. It replaces the retired-for-this-surface {@link EmbedPropsDialog} modal so
|
|
6
|
+
* embed props are edited in the SAME place document fields are, not a third form
|
|
7
|
+
* surface (the product-owner feedback that motivated the change).
|
|
8
|
+
*
|
|
9
|
+
* It reuses the exact modal machinery — the EC-190 {@link PropValueControl} stack and
|
|
10
|
+
* the {@link editableEmbedProps}/{@link blocksEmbedProps} manifest partitioning — so
|
|
11
|
+
* `blocks`-typed composition props are excluded (edited on the canvas) with a visible
|
|
12
|
+
* note, and asset/relation/object props open their real pickers.
|
|
13
|
+
*
|
|
14
|
+
* Write-back is **immediate per prop change** (like the builder inspector): each edit
|
|
15
|
+
* commits through the payload's `onChange` (one ProseMirror transaction upstream),
|
|
16
|
+
* guarded by {@link resolveEmbedWrite} so an unchanged write never dirties the field.
|
|
17
|
+
* There is no draft+Apply — the sidebar always reflects the committed node.
|
|
18
|
+
*
|
|
19
|
+
* TipTap-free by construction (see {@link EmbedInspectorPayload}) so the detail editor
|
|
20
|
+
* can import it without pulling the editor runtime onto the critical path (EC-322).
|
|
21
|
+
*/
|
|
22
|
+
export declare function EmbedInspector({ payload, className, }: {
|
|
23
|
+
payload: EmbedInspectorPayload;
|
|
24
|
+
className?: string;
|
|
25
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronLeft } from 'lucide-react';
|
|
3
|
+
import { Button, Field, Row, Separator, Stack, Text } from '@elytracms/studio/ui';
|
|
4
|
+
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
5
|
+
import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
|
|
6
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
7
|
+
import { PropValueControl } from '../prop-fields';
|
|
8
|
+
import { blocksEmbedProps, clearComponentNodeProp, editableEmbedProps, resolveEmbedWrite, setComponentNodeProp, } from './embed-props';
|
|
9
|
+
/**
|
|
10
|
+
* The selection-driven embed prop inspector (0.0.41) — the detail editor's right
|
|
11
|
+
* sidebar swaps to this when a `componentEmbed` node is selected in a rich-text
|
|
12
|
+
* field. It replaces the retired-for-this-surface {@link EmbedPropsDialog} modal so
|
|
13
|
+
* embed props are edited in the SAME place document fields are, not a third form
|
|
14
|
+
* surface (the product-owner feedback that motivated the change).
|
|
15
|
+
*
|
|
16
|
+
* It reuses the exact modal machinery — the EC-190 {@link PropValueControl} stack and
|
|
17
|
+
* the {@link editableEmbedProps}/{@link blocksEmbedProps} manifest partitioning — so
|
|
18
|
+
* `blocks`-typed composition props are excluded (edited on the canvas) with a visible
|
|
19
|
+
* note, and asset/relation/object props open their real pickers.
|
|
20
|
+
*
|
|
21
|
+
* Write-back is **immediate per prop change** (like the builder inspector): each edit
|
|
22
|
+
* commits through the payload's `onChange` (one ProseMirror transaction upstream),
|
|
23
|
+
* guarded by {@link resolveEmbedWrite} so an unchanged write never dirties the field.
|
|
24
|
+
* There is no draft+Apply — the sidebar always reflects the committed node.
|
|
25
|
+
*
|
|
26
|
+
* TipTap-free by construction (see {@link EmbedInspectorPayload}) so the detail editor
|
|
27
|
+
* can import it without pulling the editor runtime onto the critical path (EC-322).
|
|
28
|
+
*/
|
|
29
|
+
export function EmbedInspector({ payload, className, }) {
|
|
30
|
+
const { node, manifest, readOnly, onChange, onDeselect } = payload;
|
|
31
|
+
const editable = editableEmbedProps(manifest);
|
|
32
|
+
const blocks = blocksEmbedProps(manifest);
|
|
33
|
+
function set(name, value) {
|
|
34
|
+
const next = resolveEmbedWrite(node, setComponentNodeProp(node, name, value));
|
|
35
|
+
if (next)
|
|
36
|
+
onChange(next);
|
|
37
|
+
}
|
|
38
|
+
function clear(name) {
|
|
39
|
+
const next = resolveEmbedWrite(node, clearComponentNodeProp(node, name));
|
|
40
|
+
if (next)
|
|
41
|
+
onChange(next);
|
|
42
|
+
}
|
|
43
|
+
return (_jsxs("div", { className: cn('flex h-full min-h-0 flex-col bg-card', className), children: [_jsxs(Row, { gap: "1", align: "center", className: "shrink-0 border-b border-border p-1.5", children: [_jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": translate('richText.embed.inspectorBack'), onClick: onDeselect, children: _jsx(ChevronLeft, { className: "size-4" }) }), _jsx(Text, { as: "span", size: "xs", weight: "semibold", className: "min-w-0 flex-1 truncate", children: componentDisplayName(node.componentId, manifest) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-3", children: _jsxs(Stack, { gap: "4", "aria-disabled": readOnly || undefined, className: cn(readOnly && 'pointer-events-none opacity-70'), children: [editable.length === 0 ? (_jsx(Text, { size: "sm", tone: "muted", children: translate('richText.embed.noProps') })) : (editable.map(([name, spec]) => {
|
|
44
|
+
const id = `embed-${node.id}-${name}`;
|
|
45
|
+
return (_jsx(Field, { label: localizedLabel(spec.form?.label, name), htmlFor: id, children: _jsx(PropValueControl, { id: id, name: name, spec: spec, value: node.props?.[name], onSet: (v) => set(name, v), onClear: () => clear(name) }) }, name));
|
|
46
|
+
})), blocks.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx(Text, { size: "xs", tone: "subtle", children: translate('richText.embed.blocksNote') })] })) : null] }) })] }));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=embed-inspector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed-inspector.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-inspector.tsx"],"names":[],"mappings":"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","sourcesContent":["import { ChevronLeft } from 'lucide-react'\nimport type { JsonValue } from '@elytracms/core/project-graph'\nimport { Button, Field, Row, Separator, Stack, Text } from '@elytracms/studio/ui'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { PropValueControl } from '../prop-fields'\nimport {\n blocksEmbedProps,\n clearComponentNodeProp,\n editableEmbedProps,\n resolveEmbedWrite,\n setComponentNodeProp,\n} from './embed-props'\nimport type { EmbedInspectorPayload } from './embed-inspector-context'\n\n/**\n * The selection-driven embed prop inspector (0.0.41) — the detail editor's right\n * sidebar swaps to this when a `componentEmbed` node is selected in a rich-text\n * field. It replaces the retired-for-this-surface {@link EmbedPropsDialog} modal so\n * embed props are edited in the SAME place document fields are, not a third form\n * surface (the product-owner feedback that motivated the change).\n *\n * It reuses the exact modal machinery — the EC-190 {@link PropValueControl} stack and\n * the {@link editableEmbedProps}/{@link blocksEmbedProps} manifest partitioning — so\n * `blocks`-typed composition props are excluded (edited on the canvas) with a visible\n * note, and asset/relation/object props open their real pickers.\n *\n * Write-back is **immediate per prop change** (like the builder inspector): each edit\n * commits through the payload's `onChange` (one ProseMirror transaction upstream),\n * guarded by {@link resolveEmbedWrite} so an unchanged write never dirties the field.\n * There is no draft+Apply — the sidebar always reflects the committed node.\n *\n * TipTap-free by construction (see {@link EmbedInspectorPayload}) so the detail editor\n * can import it without pulling the editor runtime onto the critical path (EC-322).\n */\nexport function EmbedInspector({\n payload,\n className,\n}: {\n payload: EmbedInspectorPayload\n className?: string\n}) {\n const { node, manifest, readOnly, onChange, onDeselect } = payload\n const editable = editableEmbedProps(manifest)\n const blocks = blocksEmbedProps(manifest)\n\n function set(name: string, value: JsonValue) {\n const next = resolveEmbedWrite(node, setComponentNodeProp(node, name, value))\n if (next) onChange(next)\n }\n function clear(name: string) {\n const next = resolveEmbedWrite(node, clearComponentNodeProp(node, name))\n if (next) onChange(next)\n }\n\n return (\n <div className={cn('flex h-full min-h-0 flex-col bg-card', className)}>\n <Row gap=\"1\" align=\"center\" className=\"shrink-0 border-b border-border p-1.5\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={translate('richText.embed.inspectorBack')}\n onClick={onDeselect}\n >\n <ChevronLeft className=\"size-4\" />\n </Button>\n <Text as=\"span\" size=\"xs\" weight=\"semibold\" className=\"min-w-0 flex-1 truncate\">\n {componentDisplayName(node.componentId, manifest)}\n </Text>\n </Row>\n <div className=\"min-h-0 flex-1 overflow-auto p-3\">\n <Stack\n gap=\"4\"\n aria-disabled={readOnly || undefined}\n className={cn(readOnly && 'pointer-events-none opacity-70')}\n >\n {editable.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\">\n {translate('richText.embed.noProps')}\n </Text>\n ) : (\n editable.map(([name, spec]) => {\n const id = `embed-${node.id}-${name}`\n return (\n <Field key={name} label={localizedLabel(spec.form?.label, name)} htmlFor={id}>\n <PropValueControl\n id={id}\n name={name}\n spec={spec}\n value={node.props?.[name]}\n onSet={(v) => set(name, v)}\n onClear={() => clear(name)}\n />\n </Field>\n )\n })\n )}\n {blocks.length > 0 ? (\n <>\n <Separator />\n <Text size=\"xs\" tone=\"subtle\">\n {translate('richText.embed.blocksNote')}\n </Text>\n </>\n ) : null}\n </Stack>\n </div>\n </div>\n )\n}\n"]}
|
|
@@ -12,6 +12,4 @@ interface EmbedPickerProps {
|
|
|
12
12
|
* opened by the toolbar button or the `/embed` slash command.
|
|
13
13
|
*/
|
|
14
14
|
export declare function EmbedPicker({ open, onOpenChange, allow, onSelect }: EmbedPickerProps): import("react").JSX.Element;
|
|
15
|
-
/** Whether any component is embeddable — gates showing the embed affordances. */
|
|
16
|
-
export declare function hasEmbeddable(allow: string[] | undefined): boolean;
|
|
17
15
|
export {};
|
|
@@ -27,8 +27,4 @@ export function EmbedPicker({ open, onOpenChange, allow, onSelect }) {
|
|
|
27
27
|
}
|
|
28
28
|
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: translate('richText.embed.add') }) }), _jsxs(Stack, { gap: "2", children: [_jsx(Input, { autoFocus: true, value: query, placeholder: translate('richText.link.documentSearch'), onChange: (e) => setQuery(e.target.value) }), _jsx(Stack, { gap: "none", className: "max-h-72 overflow-y-auto", children: results.length === 0 ? (_jsx(Text, { as: "span", size: "xs", tone: "muted", className: "px-1 py-2", children: translate('richText.embed.empty') })) : (results.map((manifest) => (_jsxs("button", { type: "button", onClick: () => choose(manifest), className: cn('flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-left', 'hover:bg-muted focus-visible:bg-muted outline-none'), 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 text-sm font-medium", children: manifest.title ?? manifest.id }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: manifest.id })] })] }, manifest.id)))) })] })] }) }));
|
|
29
29
|
}
|
|
30
|
-
/** Whether any component is embeddable — gates showing the embed affordances. */
|
|
31
|
-
export function hasEmbeddable(allow) {
|
|
32
|
-
return listEmbeddable(allow, getStudioRegistry().registry.list()).length > 0;
|
|
33
|
-
}
|
|
34
30
|
//# sourceMappingURL=embed-picker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embed-picker.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-picker.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"embed-picker.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { Blocks } from 'lucide-react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n Input,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { listEmbeddable } from './insert'\n\ninterface EmbedPickerProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n /** The field's embed `allow` list (component ids). Empty/absent ⇒ any non-primitive. */\n allow?: string[]\n onSelect: (manifest: ComponentManifest) => void\n}\n\n/**\n * The component-embed picker dialog (EC-314/EC-189): a searchable list of the\n * components permitted for inline embedding. Rendered once at the editor level and\n * opened by the toolbar button or the `/embed` slash command.\n */\nexport function EmbedPicker({ open, onOpenChange, allow, onSelect }: EmbedPickerProps) {\n const [query, setQuery] = useState('')\n\n const embeddable = useMemo(\n () => listEmbeddable(allow, getStudioRegistry().registry.list()),\n [allow],\n )\n\n const results = useMemo(() => {\n const q = query.trim().toLowerCase()\n if (!q) return embeddable\n return embeddable.filter(\n (m) => (m.title ?? m.id).toLowerCase().includes(q) || m.id.toLowerCase().includes(q),\n )\n }, [embeddable, query])\n\n function choose(manifest: ComponentManifest) {\n onSelect(manifest)\n onOpenChange(false)\n setQuery('')\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{translate('richText.embed.add')}</DialogTitle>\n </DialogHeader>\n <Stack gap=\"2\">\n <Input\n autoFocus\n value={query}\n placeholder={translate('richText.link.documentSearch')}\n onChange={(e) => setQuery(e.target.value)}\n />\n <Stack gap=\"none\" className=\"max-h-72 overflow-y-auto\">\n {results.length === 0 ? (\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"px-1 py-2\">\n {translate('richText.embed.empty')}\n </Text>\n ) : (\n results.map((manifest) => (\n <button\n key={manifest.id}\n type=\"button\"\n onClick={() => choose(manifest)}\n className={cn(\n 'flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-left',\n 'hover:bg-muted focus-visible:bg-muted outline-none',\n )}\n >\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 text-sm font-medium\">{manifest.title ?? manifest.id}</span>\n <span className=\"truncate text-xs text-muted-foreground\">{manifest.id}</span>\n </span>\n </button>\n ))\n )}\n </Stack>\n </Stack>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
@@ -25,6 +25,13 @@ export declare function setComponentNodeProp(node: ComponentNode, name: string,
|
|
|
25
25
|
export declare function clearComponentNodeProp(node: ComponentNode, name: string): ComponentNode;
|
|
26
26
|
/** Key-order-insensitive structural equality — a redundant "save" must not write. */
|
|
27
27
|
export declare function componentNodesEqual(a: ComponentNode, b: ComponentNode): boolean;
|
|
28
|
+
/**
|
|
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.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveEmbedWrite(current: ComponentNode, next: ComponentNode): ComponentNode | undefined;
|
|
28
35
|
/** The manifest props editable inline: every non-`blocks` prop (composition needs the canvas). */
|
|
29
36
|
export declare function editableEmbedProps(manifest: ComponentManifest): Array<[string, PropField]>;
|
|
30
37
|
/** The manifest's `blocks` (composition) props — excluded from the form, shown as a note. */
|
|
@@ -39,6 +39,15 @@ export function clearComponentNodeProp(node, name) {
|
|
|
39
39
|
export function componentNodesEqual(a, b) {
|
|
40
40
|
return stableStringify(a) === stableStringify(b);
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The no-dirty guard for immediate per-prop write-back (0.0.41 sidebar inspector):
|
|
44
|
+
* the node to commit for an edit, or `undefined` when it is a structural no-op so an
|
|
45
|
+
* unchanged write never dirties the field. Same contract the old draft+Apply dialog
|
|
46
|
+
* enforced ({@link componentNodesEqual}), applied per keystroke instead of on Apply.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveEmbedWrite(current, next) {
|
|
49
|
+
return componentNodesEqual(next, current) ? undefined : next;
|
|
50
|
+
}
|
|
42
51
|
function stableStringify(value) {
|
|
43
52
|
if (value === null || typeof value !== 'object')
|
|
44
53
|
return JSON.stringify(value) ?? 'null';
|