@elytracms/studio 0.0.67 → 0.0.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/components/builder/property-inspector.d.ts +0 -21
  2. package/dist/components/builder/property-inspector.js +5 -22
  3. package/dist/components/builder/property-inspector.js.map +1 -1
  4. package/dist/components/cms/asset-picker-core.d.ts +52 -0
  5. package/dist/components/cms/asset-picker-core.js +69 -0
  6. package/dist/components/cms/asset-picker-core.js.map +1 -0
  7. package/dist/components/cms/asset-picker.d.ts +16 -4
  8. package/dist/components/cms/asset-picker.js +34 -50
  9. package/dist/components/cms/asset-picker.js.map +1 -1
  10. package/dist/components/cms/detail-editor.js +45 -54
  11. package/dist/components/cms/detail-editor.js.map +1 -1
  12. package/dist/components/cms/prop-visibility.d.ts +3 -0
  13. package/dist/components/cms/prop-visibility.js +31 -0
  14. package/dist/components/cms/prop-visibility.js.map +1 -0
  15. package/dist/components/cms/rich-text/component-embed.d.ts +8 -6
  16. package/dist/components/cms/rich-text/component-embed.js +21 -62
  17. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  18. package/dist/components/cms/rich-text/editor.d.ts +13 -3
  19. package/dist/components/cms/rich-text/editor.js +36 -21
  20. package/dist/components/cms/rich-text/editor.js.map +1 -1
  21. package/dist/components/cms/rich-text/embed-props.d.ts +60 -27
  22. package/dist/components/cms/rich-text/embed-props.js +102 -45
  23. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  24. package/dist/lib/i18n/messages.de.js +1 -2
  25. package/dist/lib/i18n/messages.de.js.map +1 -1
  26. package/dist/lib/i18n/messages.en.d.ts +0 -1
  27. package/dist/lib/i18n/messages.en.js +1 -2
  28. package/dist/lib/i18n/messages.en.js.map +1 -1
  29. package/dist/ui/expandable-surface.d.ts +35 -25
  30. package/dist/ui/expandable-surface.js +41 -21
  31. package/dist/ui/expandable-surface.js.map +1 -1
  32. package/dist/ui/index.d.ts +1 -0
  33. package/dist/ui/index.js +1 -0
  34. package/dist/ui/index.js.map +1 -1
  35. package/dist/ui/surface-geometry.d.ts +68 -0
  36. package/dist/ui/surface-geometry.js +61 -0
  37. package/dist/ui/surface-geometry.js.map +1 -0
  38. package/package.json +2 -2
  39. package/src/styles/studio.css +30 -0
  40. package/dist/components/cms/rich-text/embed-inspector-context.d.ts +0 -50
  41. package/dist/components/cms/rich-text/embed-inspector-context.js +0 -12
  42. package/dist/components/cms/rich-text/embed-inspector-context.js.map +0 -1
  43. package/dist/components/cms/rich-text/embed-inspector.d.ts +0 -25
  44. package/dist/components/cms/rich-text/embed-inspector.js +0 -48
  45. package/dist/components/cms/rich-text/embed-inspector.js.map +0 -1
  46. package/dist/components/cms/use-embed-inspector-host.d.ts +0 -12
  47. package/dist/components/cms/use-embed-inspector-host.js +0 -17
  48. package/dist/components/cms/use-embed-inspector-host.js.map +0 -1
@@ -1,29 +1,39 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Lock } from 'lucide-react';
3
3
  import { isStaticProp, staticProp } from '@elytracms/core/project-graph';
4
- import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Field, Stack, Text, } from '@elytracms/studio/ui';
4
+ import { Badge, ExpandableSurface, Field, Row, Separator, Stack, Text, } from '@elytracms/studio/ui';
5
5
  import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
6
6
  import { humanizeName } from '@elytracms/studio/lib/utils';
7
7
  import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
8
8
  import { PropValueControl } from '../prop-fields.js';
9
+ import { visiblePropFields } from '../prop-visibility.js';
9
10
  /**
10
- * Prop editing for rich-text component embeds (EC-314 phase 4).
11
+ * Prop editing for rich-text component embeds (EC-314 phase 4; stage surface
12
+ * EC-430/AD-D).
11
13
  *
12
14
  * An embedded component is a full `ComponentNode` stored verbatim at
13
- * `componentEmbed.attrs.node`. This dialog edits that node's *props* through the
14
- * SAME EC-190 "props-as-fields" control stack the canvas inspector and `blocks`
15
- * field use ({@link PropValueControl}) — asset props open the library picker,
16
- * relation props the document picker, etc. — never a parallel form system.
15
+ * `componentEmbed.attrs.node`. The {@link EmbedPropsStage} edits that node's *props*
16
+ * through the SAME EC-190 "props-as-fields" control stack the canvas inspector and
17
+ * `blocks` field use ({@link PropValueControl}) — asset props open the library
18
+ * picker, relation props the document picker, etc. — never a parallel form system.
17
19
  *
18
- * Fidelity contract: a working `draft` is seeded from the stored node when the
19
- * dialog opens; the dialog only writes back on Apply, and only when the draft is
20
- * *structurally different* ({@link componentNodesEqual}) — so opening and closing
21
- * without a real change never dirties the field. The upstream write goes through a
22
- * single ProseMirror `updateAttributes` transaction (undoable), never a storage
23
- * rewrite; the embedded node's `id`/`componentId`/`slots` pass through untouched.
20
+ * One surface, reachable everywhere (ratified AD-D, 2026-07-16): "Edit settings" on
21
+ * an embed card opens the centered stage (`ExpandableSurface variant="stage"
22
+ * preset="form"`) identically from the CANVAS tab and the form view. It replaced
23
+ * BOTH halves of the 0.0.41 fork — the selection-driven sidebar swap (which only
24
+ * rendered in the form view, leaving the canvas tab's "Edit settings" dead) and the
25
+ * modal fallback.
26
+ *
27
+ * Value contract (unchanged from the 0.0.41 sidebar inspector): write-back is
28
+ * **immediate per prop change** — each edit commits through `onChange` (one
29
+ * ProseMirror `updateAttributes` transaction upstream, undoable), guarded by
30
+ * {@link resolveEmbedWrite} so an unchanged write never dirties the field. There is
31
+ * no draft+Apply and no save/cancel of its own: the stage is the same value flow
32
+ * relocated, lightly dismissible (Escape / backdrop / header close) per the
33
+ * forms-never-modal spirit. The dialog fork's draft+Apply semantics died with it.
24
34
  *
25
35
  * `blocks`-typed props (nested composition) are NOT edited here — that needs the
26
- * canvas, not a popover. They are excluded from the form and surfaced as a note.
36
+ * canvas, not a form. They are excluded from the form and surfaced as a note.
27
37
  */
28
38
  /** Set a prop to a fresh static value, preserving identity, slots, and sibling props. */
29
39
  export function setComponentNodeProp(node, name, value) {
@@ -40,10 +50,10 @@ export function componentNodesEqual(a, b) {
40
50
  return stableStringify(a) === stableStringify(b);
41
51
  }
42
52
  /**
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.
53
+ * The no-dirty guard for immediate per-prop write-back (0.0.41 sidebar inspector,
54
+ * inherited verbatim by the EC-430 stage form): the node to commit for an edit, or
55
+ * `undefined` when it is a structural no-op so an unchanged write never dirties the
56
+ * field ({@link componentNodesEqual}), applied per keystroke.
47
57
  */
48
58
  export function resolveEmbedWrite(current, next) {
49
59
  return componentNodesEqual(next, current) ? undefined : next;
@@ -59,10 +69,6 @@ function stableStringify(value) {
59
69
  .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`);
60
70
  return `{${entries.join(',')}}`;
61
71
  }
62
- /** The manifest props editable inline: every non-`blocks` prop (composition needs the canvas). */
63
- export function editableEmbedProps(manifest) {
64
- return Object.entries(manifest.props).filter(([, spec]) => spec.type !== 'blocks');
65
- }
66
72
  /** The manifest's `blocks` (composition) props — excluded from the form, shown as a note. */
67
73
  export function blocksEmbedProps(manifest) {
68
74
  return Object.entries(manifest.props).filter(([, spec]) => spec.type === 'blocks');
@@ -70,7 +76,7 @@ export function blocksEmbedProps(manifest) {
70
76
  /**
71
77
  * A lean one-line summary of an embed's static scalar props (`Label: value`,
72
78
  * up to three), for the card. Skips unset props and structured values (refs /
73
- * objects / arrays) — those are edited in the dialog, not summarised.
79
+ * objects / arrays) — those are edited in the stage form, not summarised.
74
80
  */
75
81
  export function embedPropsSummary(node, manifest) {
76
82
  const parts = [];
@@ -88,28 +94,79 @@ export function embedPropsSummary(node, manifest) {
88
94
  }
89
95
  return parts.length > 0 ? parts.join(' · ') : undefined;
90
96
  }
91
- /** The edit-props dialog opened from an embed card. */
92
- export function EmbedPropsDialog({ open, onOpenChange, node, manifest, onSave }) {
93
- const [draft, setDraft] = useState(node);
94
- // Re-seed the working draft from the stored node whenever the dialog opens, so a
95
- // cancelled edit is discarded and a reopen starts from the committed value.
96
- useEffect(() => {
97
- if (open)
98
- setDraft(node);
99
- }, [open, node]);
100
- const editable = editableEmbedProps(manifest);
97
+ /**
98
+ * The embed prop form body — pure of surface chrome so it smoke-renders in the node
99
+ * test env (the stage itself portals and renders `null` without a DOM).
100
+ *
101
+ * EC-393 parity: rows filter through the SAME {@link visiblePropFields} core the
102
+ * canvas property inspector uses — a predicate-hidden row (`form.hidden` over the
103
+ * sibling prop values) unmounts/remounts live as its driver prop flips, because the
104
+ * committed node re-renders this form. EC-395 parity: `form.readOnly` renders the
105
+ * document form's Lock badge next to the label and disables the delegated control
106
+ * (`PropValueControl` reads the same flag); `form.control` and `form.description`
107
+ * route through the same delegation.
108
+ */
109
+ export function EmbedStageForm({ node, manifest, onChange }) {
110
+ const visible = visiblePropFields(manifest, node);
101
111
  const blocks = blocksEmbedProps(manifest);
102
- function apply() {
103
- // No structural change ⇒ never write (opening + closing must not dirty the field).
104
- if (!componentNodesEqual(draft, node))
105
- onSave(draft);
106
- onOpenChange(false);
112
+ function set(name, value) {
113
+ const next = resolveEmbedWrite(node, setComponentNodeProp(node, name, value));
114
+ if (next)
115
+ onChange(next);
116
+ }
117
+ function clear(name) {
118
+ const next = resolveEmbedWrite(node, clearComponentNodeProp(node, name));
119
+ if (next)
120
+ onChange(next);
107
121
  }
108
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: translate('richText.embed.editTitle', {
109
- name: componentDisplayName(node.componentId, manifest),
110
- }) }) }), _jsxs(Stack, { gap: "4", className: "max-h-[60vh] overflow-y-auto", children: [editable.length === 0 ? (_jsx(Text, { size: "sm", tone: "muted", children: translate('richText.embed.noProps') })) : (editable.map(([name, spec]) => {
111
- const id = `embed-${node.id}-${name}`;
112
- return (_jsx(Field, { label: localizedLabel(spec.form?.label, name), htmlFor: id, children: _jsx(PropValueControl, { id: id, name: name, spec: spec, value: draft.props?.[name], onSet: (v) => setDraft((d) => setComponentNodeProp(d, name, v)), onClear: () => setDraft((d) => clearComponentNodeProp(d, name)) }) }, name));
113
- })), blocks.length > 0 ? (_jsx(Text, { size: "xs", tone: "subtle", children: translate('richText.embed.blocksNote') })) : null] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: translate('common.cancel') }), _jsx(Button, { type: "button", onClick: apply, children: translate('common.apply') })] })] }) }));
122
+ return (_jsxs(Stack, { gap: "4", children: [visible.length === 0 ? (_jsx(Text, { size: "sm", tone: "muted", children: translate('richText.embed.noProps') })) : (visible.map(([name, spec]) => {
123
+ const id = `embed-${node.id}-${name}`;
124
+ return (_jsx(Field, { label: spec.form?.readOnly === true ? (_jsxs(Row, { gap: "2", children: [_jsx("span", { children: localizedLabel(spec.form?.label, name) }), _jsxs(Badge, { variant: "outline", children: [_jsx(Lock, { className: "size-3" }), " ", translate('cms.editor.readOnlyBadge')] })] })) : (localizedLabel(spec.form?.label, name)), htmlFor: id, hint: localizedLabel(spec.form?.description) || undefined, children: _jsx(PropValueControl, { id: id, name: name, spec: spec, value: node.props?.[name], onSet: (v) => set(name, v), onClear: () => clear(name) }) }, name));
125
+ })), blocks.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx(Text, { size: "xs", tone: "subtle", children: translate('richText.embed.blocksNote') })] })) : null] }));
126
+ }
127
+ /**
128
+ * The stage-body focus handoff (a module-level constant so React never re-invokes it
129
+ * on re-renders — a per-keystroke re-invocation would churn the guard). The stage
130
+ * opens from a card INSIDE a focused TipTap editor, and prosemirror-view
131
+ * preventDefaults every Escape while its contenteditable holds focus
132
+ * (`capturekeys.ts` — its "dangerous default" backdrop map), which the surface's
133
+ * EC-392 bridge reads as "handled by a layer above" — so Escape would never dismiss,
134
+ * and stray typing would land in the covered document. Moving focus into the stage
135
+ * on mount hands Escape to the layer stack; the `focusin` guard keeps it there,
136
+ * because ProseMirror re-asserts focus on its editable off deferred click handling
137
+ * and window-refocus events. The guard is NOT a focus trap: it only reclaims from a
138
+ * ProseMirror editable OUTSIDE the stage (unreachable to the user behind the
139
+ * backdrop — only programmatic steals hit this), never from the stage's own controls
140
+ * (incl. nested rich-text prop editors, `el.contains`) or a picker dialog above.
141
+ * Per-caller policy, deliberately NOT in ExpandableSurface: the rich-text prose
142
+ * stage RELOCATES the editor, so there the editor keeping focus is the point.
143
+ * React 19 ref-cleanup unregisters the guard when the stage unmounts.
144
+ */
145
+ const focusStageBody = (el) => {
146
+ if (!el)
147
+ return undefined;
148
+ el.focus();
149
+ const guard = () => {
150
+ const active = document.activeElement;
151
+ if (el.contains(active))
152
+ return;
153
+ if (active instanceof HTMLElement && active.closest('.ProseMirror'))
154
+ el.focus();
155
+ };
156
+ document.addEventListener('focusin', guard);
157
+ return () => document.removeEventListener('focusin', guard);
158
+ };
159
+ /**
160
+ * The embed prop inspector on the centered stage (EC-430/AD-D) — mounted by the
161
+ * embed card's "Edit settings" from ANY rich-text surface (form view, canvas-tab
162
+ * prop inspector, or inside the expanded rich-text stage — a later mount rides the
163
+ * EC-392 layer stack above it, and pickers opened from these controls stack above
164
+ * this stage in turn). Hook-free by construction so the geometry contract is
165
+ * pinnable by direct invocation in the node test env.
166
+ */
167
+ export function EmbedPropsStage({ node, manifest, onChange, onDismiss }) {
168
+ return (_jsx(ExpandableSurface, { variant: "stage", preset: "form", title: translate('richText.embed.editTitle', {
169
+ name: componentDisplayName(node.componentId, manifest),
170
+ }), closeLabel: translate('common.close'), onDismiss: onDismiss, children: _jsx("div", { ref: focusStageBody, tabIndex: -1, className: "min-h-0 flex-1 overflow-y-auto p-6 outline-none", children: _jsx(EmbedStageForm, { node: node, manifest: manifest, onChange: onChange }) }) }));
114
171
  }
115
172
  //# sourceMappingURL=embed-props.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"embed-props.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-props.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useState } from 'react'\nimport { isStaticProp, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport type { ComponentManifest, PropField } from '@elytracms/core/component-registry'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Field,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { humanizeName } from '@elytracms/studio/lib/utils'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { PropValueControl } from '../prop-fields'\n\n/**\n * Prop editing for rich-text component embeds (EC-314 phase 4).\n *\n * An embedded component is a full `ComponentNode` stored verbatim at\n * `componentEmbed.attrs.node`. This dialog edits that node's *props* through the\n * SAME EC-190 \"props-as-fields\" control stack the canvas inspector and `blocks`\n * field use ({@link PropValueControl}) — asset props open the library picker,\n * relation props the document picker, etc. — never a parallel form system.\n *\n * Fidelity contract: a working `draft` is seeded from the stored node when the\n * dialog opens; the dialog only writes back on Apply, and only when the draft is\n * *structurally different* ({@link componentNodesEqual}) — so opening and closing\n * without a real change never dirties the field. The upstream write goes through a\n * single ProseMirror `updateAttributes` transaction (undoable), never a storage\n * rewrite; the embedded node's `id`/`componentId`/`slots` pass through untouched.\n *\n * `blocks`-typed props (nested composition) are NOT edited here — that needs the\n * canvas, not a popover. They are excluded from the form and surfaced as a note.\n */\n\n/** Set a prop to a fresh static value, preserving identity, slots, and sibling props. */\nexport function setComponentNodeProp(\n node: ComponentNode,\n name: string,\n value: JsonValue,\n): ComponentNode {\n return { ...node, props: { ...node.props, [name]: staticProp(value) } }\n}\n\n/** Remove a prop (revert to the manifest default), preserving everything else. */\nexport function clearComponentNodeProp(node: ComponentNode, name: string): ComponentNode {\n const props = { ...node.props }\n delete props[name]\n return { ...node, props }\n}\n\n/** Key-order-insensitive structural equality — a redundant \"save\" must not write. */\nexport function componentNodesEqual(a: ComponentNode, b: ComponentNode): boolean {\n return stableStringify(a) === stableStringify(b)\n}\n\n/**\n * The no-dirty guard for immediate per-prop write-back (0.0.41 sidebar inspector):\n * the node to commit for an edit, or `undefined` when it is a structural no-op so an\n * unchanged write never dirties the field. Same contract the old draft+Apply dialog\n * enforced ({@link componentNodesEqual}), applied per keystroke instead of on Apply.\n */\nexport function resolveEmbedWrite(current: ComponentNode, next: ComponentNode): ComponentNode | undefined {\n return componentNodesEqual(next, current) ? undefined : next\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0))\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`)\n return `{${entries.join(',')}}`\n}\n\n/** The manifest props editable inline: every non-`blocks` prop (composition needs the canvas). */\nexport function editableEmbedProps(manifest: ComponentManifest): Array<[string, PropField]> {\n return Object.entries(manifest.props).filter(([, spec]) => spec.type !== 'blocks')\n}\n\n/** The manifest's `blocks` (composition) props — excluded from the form, shown as a note. */\nexport function blocksEmbedProps(manifest: ComponentManifest): Array<[string, PropField]> {\n return Object.entries(manifest.props).filter(([, spec]) => spec.type === 'blocks')\n}\n\n/**\n * A lean one-line summary of an embed's static scalar props (`Label: value`,\n * up to three), for the card. Skips unset props and structured values (refs /\n * objects / arrays) — those are edited in the dialog, not summarised.\n */\nexport function embedPropsSummary(\n node: ComponentNode,\n manifest?: ComponentManifest,\n): string | undefined {\n const parts: string[] = []\n for (const [name, value] of Object.entries(node.props ?? {})) {\n if (!isStaticProp(value)) continue\n const raw = value.value\n if (raw === null || raw === undefined || typeof raw === 'object') continue\n const label = manifest?.props[name]?.form?.label\n const labelText = label ? localizedLabel(label, name) : humanizeName(name)\n parts.push(`${labelText}: ${String(raw)}`)\n if (parts.length >= 3) break\n }\n return parts.length > 0 ? parts.join(' · ') : undefined\n}\n\nexport interface EmbedPropsDialogProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n /** The embedded component node whose props are being edited. */\n node: ComponentNode\n /** Its manifest — drives which prop fields render. */\n manifest: ComponentManifest\n /** Commit the edited node (upstream writes it through one ProseMirror transaction). */\n onSave: (node: ComponentNode) => void\n}\n\n/** The edit-props dialog opened from an embed card. */\nexport function EmbedPropsDialog({ open, onOpenChange, node, manifest, onSave }: EmbedPropsDialogProps) {\n const [draft, setDraft] = useState<ComponentNode>(node)\n\n // Re-seed the working draft from the stored node whenever the dialog opens, so a\n // cancelled edit is discarded and a reopen starts from the committed value.\n useEffect(() => {\n if (open) setDraft(node)\n }, [open, node])\n\n const editable = editableEmbedProps(manifest)\n const blocks = blocksEmbedProps(manifest)\n\n function apply() {\n // No structural change ⇒ never write (opening + closing must not dirty the field).\n if (!componentNodesEqual(draft, node)) onSave(draft)\n onOpenChange(false)\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {translate('richText.embed.editTitle', {\n name: componentDisplayName(node.componentId, manifest),\n })}\n </DialogTitle>\n </DialogHeader>\n <Stack gap=\"4\" className=\"max-h-[60vh] overflow-y-auto\">\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={draft.props?.[name]}\n onSet={(v) => setDraft((d) => setComponentNodeProp(d, name, v))}\n onClear={() => setDraft((d) => clearComponentNodeProp(d, name))}\n />\n </Field>\n )\n })\n )}\n {blocks.length > 0 ? (\n <Text size=\"xs\" tone=\"subtle\">\n {translate('richText.embed.blocksNote')}\n </Text>\n ) : null}\n </Stack>\n <DialogFooter>\n <Button type=\"button\" variant=\"ghost\" onClick={() => onOpenChange(false)}>\n {translate('common.cancel')}\n </Button>\n <Button type=\"button\" onClick={apply}>\n {translate('common.apply')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
1
+ {"version":3,"file":"embed-props.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-props.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Lock } from 'lucide-react'\nimport { isStaticProp, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport type { ComponentManifest, PropField } from '@elytracms/core/component-registry'\nimport {\n Badge,\n ExpandableSurface,\n Field,\n Row,\n Separator,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { humanizeName } from '@elytracms/studio/lib/utils'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { PropValueControl } from '../prop-fields'\nimport { visiblePropFields } from '../prop-visibility'\n\n/**\n * Prop editing for rich-text component embeds (EC-314 phase 4; stage surface\n * EC-430/AD-D).\n *\n * An embedded component is a full `ComponentNode` stored verbatim at\n * `componentEmbed.attrs.node`. The {@link EmbedPropsStage} edits that node's *props*\n * through the SAME EC-190 \"props-as-fields\" control stack the canvas inspector and\n * `blocks` field use ({@link PropValueControl}) — asset props open the library\n * picker, relation props the document picker, etc. — never a parallel form system.\n *\n * One surface, reachable everywhere (ratified AD-D, 2026-07-16): \"Edit settings\" on\n * an embed card opens the centered stage (`ExpandableSurface variant=\"stage\"\n * preset=\"form\"`) identically from the CANVAS tab and the form view. It replaced\n * BOTH halves of the 0.0.41 fork — the selection-driven sidebar swap (which only\n * rendered in the form view, leaving the canvas tab's \"Edit settings\" dead) and the\n * modal fallback.\n *\n * Value contract (unchanged from the 0.0.41 sidebar inspector): write-back is\n * **immediate per prop change** — each edit commits through `onChange` (one\n * ProseMirror `updateAttributes` transaction upstream, undoable), guarded by\n * {@link resolveEmbedWrite} so an unchanged write never dirties the field. There is\n * no draft+Apply and no save/cancel of its own: the stage is the same value flow\n * relocated, lightly dismissible (Escape / backdrop / header close) per the\n * forms-never-modal spirit. The dialog fork's draft+Apply semantics died with it.\n *\n * `blocks`-typed props (nested composition) are NOT edited here — that needs the\n * canvas, not a form. They are excluded from the form and surfaced as a note.\n */\n\n/** Set a prop to a fresh static value, preserving identity, slots, and sibling props. */\nexport function setComponentNodeProp(\n node: ComponentNode,\n name: string,\n value: JsonValue,\n): ComponentNode {\n return { ...node, props: { ...node.props, [name]: staticProp(value) } }\n}\n\n/** Remove a prop (revert to the manifest default), preserving everything else. */\nexport function clearComponentNodeProp(node: ComponentNode, name: string): ComponentNode {\n const props = { ...node.props }\n delete props[name]\n return { ...node, props }\n}\n\n/** Key-order-insensitive structural equality — a redundant \"save\" must not write. */\nexport function componentNodesEqual(a: ComponentNode, b: ComponentNode): boolean {\n return stableStringify(a) === stableStringify(b)\n}\n\n/**\n * The no-dirty guard for immediate per-prop write-back (0.0.41 sidebar inspector,\n * inherited verbatim by the EC-430 stage form): the node to commit for an edit, or\n * `undefined` when it is a structural no-op so an unchanged write never dirties the\n * field ({@link componentNodesEqual}), applied per keystroke.\n */\nexport function resolveEmbedWrite(current: ComponentNode, next: ComponentNode): ComponentNode | undefined {\n return componentNodesEqual(next, current) ? undefined : next\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0))\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`)\n return `{${entries.join(',')}}`\n}\n\n/** The manifest's `blocks` (composition) props — excluded from the form, shown as a note. */\nexport function blocksEmbedProps(manifest: ComponentManifest): Array<[string, PropField]> {\n return Object.entries(manifest.props).filter(([, spec]) => spec.type === 'blocks')\n}\n\n/**\n * A lean one-line summary of an embed's static scalar props (`Label: value`,\n * up to three), for the card. Skips unset props and structured values (refs /\n * objects / arrays) — those are edited in the stage form, not summarised.\n */\nexport function embedPropsSummary(\n node: ComponentNode,\n manifest?: ComponentManifest,\n): string | undefined {\n const parts: string[] = []\n for (const [name, value] of Object.entries(node.props ?? {})) {\n if (!isStaticProp(value)) continue\n const raw = value.value\n if (raw === null || raw === undefined || typeof raw === 'object') continue\n const label = manifest?.props[name]?.form?.label\n const labelText = label ? localizedLabel(label, name) : humanizeName(name)\n parts.push(`${labelText}: ${String(raw)}`)\n if (parts.length >= 3) break\n }\n return parts.length > 0 ? parts.join(' · ') : undefined\n}\n\nexport interface EmbedStageFormProps {\n /** The embedded component node whose props are being edited (the STORED node). */\n node: ComponentNode\n /** Its manifest — drives which prop fields render. */\n manifest: ComponentManifest\n /**\n * Commit an edited node. Upstream writes it through one ProseMirror\n * `updateAttributes` transaction (undoable); the no-dirty guard has already been\n * applied here, so this only fires on a real structural change.\n */\n onChange: (node: ComponentNode) => void\n}\n\n/**\n * The embed prop form body — pure of surface chrome so it smoke-renders in the node\n * test env (the stage itself portals and renders `null` without a DOM).\n *\n * EC-393 parity: rows filter through the SAME {@link visiblePropFields} core the\n * canvas property inspector uses — a predicate-hidden row (`form.hidden` over the\n * sibling prop values) unmounts/remounts live as its driver prop flips, because the\n * committed node re-renders this form. EC-395 parity: `form.readOnly` renders the\n * document form's Lock badge next to the label and disables the delegated control\n * (`PropValueControl` reads the same flag); `form.control` and `form.description`\n * route through the same delegation.\n */\nexport function EmbedStageForm({ node, manifest, onChange }: EmbedStageFormProps) {\n const visible = visiblePropFields(manifest, node)\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 <Stack gap=\"4\">\n {visible.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\">\n {translate('richText.embed.noProps')}\n </Text>\n ) : (\n visible.map(([name, spec]) => {\n const id = `embed-${node.id}-${name}`\n return (\n <Field\n key={name}\n label={\n spec.form?.readOnly === true ? (\n // EC-395: the SAME read-only badge the document form and the canvas\n // prop inspector render next to a locked label — identical icon,\n // i18n key, and `Badge` variant, never a bespoke treatment.\n <Row gap=\"2\">\n <span>{localizedLabel(spec.form?.label, name)}</span>\n <Badge variant=\"outline\">\n <Lock className=\"size-3\" /> {translate('cms.editor.readOnlyBadge')}\n </Badge>\n </Row>\n ) : (\n localizedLabel(spec.form?.label, name)\n )\n }\n htmlFor={id}\n hint={localizedLabel(spec.form?.description) || undefined}\n >\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 )\n}\n\nexport interface EmbedPropsStageProps extends EmbedStageFormProps {\n /** Dismiss request (Escape / backdrop / header close). The caller owns the open state. */\n onDismiss: () => void\n}\n\n/**\n * The stage-body focus handoff (a module-level constant so React never re-invokes it\n * on re-renders — a per-keystroke re-invocation would churn the guard). The stage\n * opens from a card INSIDE a focused TipTap editor, and prosemirror-view\n * preventDefaults every Escape while its contenteditable holds focus\n * (`capturekeys.ts` — its \"dangerous default\" backdrop map), which the surface's\n * EC-392 bridge reads as \"handled by a layer above\" — so Escape would never dismiss,\n * and stray typing would land in the covered document. Moving focus into the stage\n * on mount hands Escape to the layer stack; the `focusin` guard keeps it there,\n * because ProseMirror re-asserts focus on its editable off deferred click handling\n * and window-refocus events. The guard is NOT a focus trap: it only reclaims from a\n * ProseMirror editable OUTSIDE the stage (unreachable to the user behind the\n * backdrop — only programmatic steals hit this), never from the stage's own controls\n * (incl. nested rich-text prop editors, `el.contains`) or a picker dialog above.\n * Per-caller policy, deliberately NOT in ExpandableSurface: the rich-text prose\n * stage RELOCATES the editor, so there the editor keeping focus is the point.\n * React 19 ref-cleanup unregisters the guard when the stage unmounts.\n */\nconst focusStageBody = (el: HTMLDivElement | null) => {\n if (!el) return undefined\n el.focus()\n const guard = () => {\n const active = document.activeElement\n if (el.contains(active)) return\n if (active instanceof HTMLElement && active.closest('.ProseMirror')) el.focus()\n }\n document.addEventListener('focusin', guard)\n return () => document.removeEventListener('focusin', guard)\n}\n\n/**\n * The embed prop inspector on the centered stage (EC-430/AD-D) — mounted by the\n * embed card's \"Edit settings\" from ANY rich-text surface (form view, canvas-tab\n * prop inspector, or inside the expanded rich-text stage — a later mount rides the\n * EC-392 layer stack above it, and pickers opened from these controls stack above\n * this stage in turn). Hook-free by construction so the geometry contract is\n * pinnable by direct invocation in the node test env.\n */\nexport function EmbedPropsStage({ node, manifest, onChange, onDismiss }: EmbedPropsStageProps) {\n return (\n <ExpandableSurface\n variant=\"stage\"\n preset=\"form\"\n title={translate('richText.embed.editTitle', {\n name: componentDisplayName(node.componentId, manifest),\n })}\n closeLabel={translate('common.close')}\n onDismiss={onDismiss}\n >\n <div\n ref={focusStageBody}\n tabIndex={-1}\n className=\"min-h-0 flex-1 overflow-y-auto p-6 outline-none\"\n >\n <EmbedStageForm node={node} manifest={manifest} onChange={onChange} />\n </div>\n </ExpandableSurface>\n )\n}\n"]}
@@ -956,11 +956,10 @@ export const deMessages = {
956
956
  'richText.embed.editTitle': '{name} bearbeiten',
957
957
  'richText.embed.noProps': 'Diese Komponente hat keine bearbeitbaren Einstellungen.',
958
958
  'richText.embed.blocksNote': 'Verschachtelte Inhaltsbereiche werden auf der Arbeitsfläche bearbeitet, nicht hier.',
959
- 'richText.embed.inspectorBack': 'Zurück zum Dokument',
960
959
  'richText.slash.text': 'Text',
961
960
  'richText.slash.empty': 'Keine passenden Blöcke',
962
961
  'richText.slash.componentsGroup': 'Komponenten',
963
- // EC-388 — expand the editing surface (a roomy sheet for long prose).
962
+ // EC-388/EC-431 — expand the editing surface (the centered stage for long prose).
964
963
  'richText.expand.expand': 'Editor vergrößern',
965
964
  'richText.expand.collapse': 'Editor verkleinern',
966
965
  'richText.expand.title': 'Rich Text',