@elytracms/studio 0.0.38 → 0.0.39
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/convex/cms.ts +8 -2
- package/dist/components/app-views/default-modules.d.ts +60 -5
- package/dist/components/app-views/default-modules.js +53 -15
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +2 -0
- package/dist/components/app-views/module.js +8 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/assets/asset-detail.d.ts +24 -0
- package/dist/components/assets/asset-detail.js +163 -0
- package/dist/components/assets/asset-detail.js.map +1 -0
- package/dist/components/assets/asset-grid.d.ts +28 -0
- package/dist/components/assets/asset-grid.js +68 -0
- package/dist/components/assets/asset-grid.js.map +1 -0
- package/dist/components/assets/asset-metadata.d.ts +19 -0
- package/dist/components/assets/asset-metadata.js +44 -0
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +54 -0
- package/dist/components/assets/assets-collection-view-source.js +186 -0
- package/dist/components/assets/assets-collection-view-source.js.map +1 -0
- package/dist/components/assets/assets-module.d.ts +18 -0
- package/dist/components/assets/assets-module.js +94 -0
- package/dist/components/assets/assets-module.js.map +1 -0
- package/dist/components/assets/assets-store.js +2 -7
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/assets-view-preference.d.ts +15 -0
- package/dist/components/assets/assets-view-preference.js +22 -0
- package/dist/components/assets/assets-view-preference.js.map +1 -0
- package/dist/components/builder/builder-store.d.ts +12 -0
- package/dist/components/builder/builder-store.js +18 -16
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +5 -14
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.d.ts +19 -0
- package/dist/components/builder/composition-field-editor-lazy.js +29 -0
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -0
- package/dist/components/builder/composition-field-editor.js +1 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +8 -0
- package/dist/components/builder/insert-context.js +1 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.js +1 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +7 -134
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +15 -4
- package/dist/components/cms/cms-store.js +42 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.d.ts +8 -1
- package/dist/components/cms/collection-table.js +62 -10
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/column-width.d.ts +25 -0
- package/dist/components/cms/column-width.js +47 -0
- package/dist/components/cms/column-width.js.map +1 -0
- package/dist/components/cms/detail-editor.js +101 -68
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/discard-draft.d.ts +32 -0
- package/dist/components/cms/discard-draft.js +13 -0
- package/dist/components/cms/discard-draft.js.map +1 -0
- package/dist/components/cms/document-staging.d.ts +7 -0
- package/dist/components/cms/document-staging.js +44 -0
- package/dist/components/cms/document-staging.js.map +1 -0
- package/dist/components/cms/field-connection.d.ts +22 -0
- package/dist/components/cms/field-connection.js +37 -0
- package/dist/components/cms/field-connection.js.map +1 -0
- package/dist/components/cms/field-control.js +158 -34
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +43 -0
- package/dist/components/cms/prop-fields.js +130 -0
- package/dist/components/cms/prop-fields.js.map +1 -0
- package/dist/components/cms/rich-text/component-embed.d.ts +17 -3
- package/dist/components/cms/rich-text/component-embed.js +29 -7
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.d.ts +49 -0
- package/dist/components/cms/rich-text/embed-props.js +106 -0
- package/dist/components/cms/rich-text/embed-props.js.map +1 -0
- package/dist/components/cms/rich-text/index.d.ts +6 -1
- package/dist/components/cms/rich-text/index.js +6 -1
- package/dist/components/cms/rich-text/index.js.map +1 -1
- package/dist/components/cms/rich-text/lazy.d.ts +19 -0
- package/dist/components/cms/rich-text/lazy.js +26 -0
- package/dist/components/cms/rich-text/lazy.js.map +1 -0
- package/dist/components/cms/row-actions.d.ts +28 -0
- package/dist/components/cms/row-actions.js +20 -0
- package/dist/components/cms/row-actions.js.map +1 -0
- package/dist/components/cms/use-document-versions.d.ts +8 -1
- package/dist/components/cms/use-document-versions.js +31 -4
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.d.ts +1 -0
- package/dist/components/cms/version-compare-lazy.js +23 -0
- package/dist/components/cms/version-compare-lazy.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +46 -0
- package/dist/components/cms/version-compare-text.js +78 -0
- package/dist/components/cms/version-compare-text.js.map +1 -1
- package/dist/components/cms/version-compare.d.ts +9 -5
- package/dist/components/cms/version-compare.js +184 -59
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +10 -4
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/components/component-detail.d.ts +5 -3
- package/dist/components/components/component-detail.js +41 -8
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +61 -0
- package/dist/components/components/component-preview.js +103 -0
- package/dist/components/components/component-preview.js.map +1 -0
- package/dist/components/components/components-index.d.ts +5 -3
- package/dist/components/components/components-index.js +19 -24
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/registry-browser.d.ts +7 -0
- package/dist/components/components/registry-browser.js +20 -36
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.d.ts +51 -0
- package/dist/components/components/taxonomy.js +73 -0
- package/dist/components/components/taxonomy.js.map +1 -0
- package/dist/components/components/transfer-dialog.d.ts +20 -0
- package/dist/components/components/transfer-dialog.js +64 -0
- package/dist/components/components/transfer-dialog.js.map +1 -0
- package/dist/components/components/transfer.d.ts +43 -0
- package/dist/components/components/transfer.js +31 -0
- package/dist/components/components/transfer.js.map +1 -0
- package/dist/components/project-list.js +3 -4
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.js +3 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-store-status.d.ts +21 -0
- package/dist/components/rail-store-status.js +21 -0
- package/dist/components/rail-store-status.js.map +1 -0
- package/dist/components/redirects/redirects-module.js +4 -8
- package/dist/components/redirects/redirects-module.js.map +1 -1
- package/dist/components/redirects/redirects-store.js +2 -7
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/sign-in-needed-badge.d.ts +6 -0
- package/dist/components/sign-in-needed-badge.js +13 -0
- package/dist/components/sign-in-needed-badge.js.map +1 -0
- package/dist/components/studio-project-screen.js +1 -4
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +1 -4
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/users/users-module.js +5 -7
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/components/users/users-store.js +2 -7
- package/dist/components/users/users-store.js.map +1 -1
- package/dist/lib/auth/index.js +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +1 -3
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +0 -2
- package/dist/lib/cms-views/view-model.js +0 -7
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/diff-view-preference.d.ts +7 -0
- package/dist/lib/diff-view-preference.js +21 -0
- package/dist/lib/diff-view-preference.js.map +1 -0
- package/dist/lib/i18n/messages.en.js +79 -30
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/local-preference.d.ts +11 -0
- package/dist/lib/local-preference.js +26 -0
- package/dist/lib/local-preference.js.map +1 -0
- package/dist/lib/locale/fallback.d.ts +45 -0
- package/dist/lib/locale/fallback.js +85 -0
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +6 -0
- package/dist/lib/persona/labels.js +8 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/index.d.ts +1 -1
- package/dist/lib/query/index.js +1 -1
- package/dist/lib/query/index.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +14 -2
- package/dist/lib/query/operations-query.js +11 -6
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/rail.d.ts +0 -6
- package/dist/lib/rail.js +6 -25
- package/dist/lib/rail.js.map +1 -1
- package/dist/lib/theme.d.ts +0 -6
- package/dist/lib/theme.js +10 -13
- package/dist/lib/theme.js.map +1 -1
- package/dist/router/match.d.ts +1 -0
- package/dist/router/match.js +4 -0
- package/dist/router/match.js.map +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/package.json +22 -25
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { JsonValue, PropValue } from '@elytracms/core/project-graph';
|
|
2
|
+
import type { PropField } from '@elytracms/core/component-registry';
|
|
3
|
+
/**
|
|
4
|
+
* The EC-190 "props-as-fields" value control — the single renderer for a
|
|
5
|
+
* manifest-declared prop field, shared by the canvas property inspector
|
|
6
|
+
* (`builder/property-inspector.tsx`) and the rich-text component-embed prop editor
|
|
7
|
+
* (`rich-text/embed-props.tsx`). Extracted here so both surfaces render prop fields
|
|
8
|
+
* through *one* control set (asset → library picker, relation → document picker,
|
|
9
|
+
* object → nested form, …) rather than two parallel form systems.
|
|
10
|
+
*
|
|
11
|
+
* It is deliberately store-agnostic: it reads a prop's current value and reports
|
|
12
|
+
* edits through `onSet(rawValue)` / `onClear()`. The caller owns *where* the value
|
|
13
|
+
* lands — the inspector dispatches a builder command; the embed editor rewrites a
|
|
14
|
+
* ProseMirror node attr — and is responsible for wrapping the raw value in a
|
|
15
|
+
* `staticProp` envelope.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The inspector control to render for a prop field-def. The `control.kind`
|
|
19
|
+
* vocabulary derives from the field `type` plus the optional `form.control`
|
|
20
|
+
* presentation hint on `text` fields.
|
|
21
|
+
*/
|
|
22
|
+
export type ControlKind = 'text' | 'textarea' | 'number' | 'boolean' | 'select' | 'json' | 'color' | 'richtext' | 'asset' | 'object' | 'relation';
|
|
23
|
+
export declare function controlKind(spec: PropField): ControlKind;
|
|
24
|
+
export interface PropValueControlProps {
|
|
25
|
+
/** DOM id for the control's primary input (label `htmlFor` target). */
|
|
26
|
+
id: string;
|
|
27
|
+
/** The prop's field name — needed to synthesize the `FieldDef` for reused controls. */
|
|
28
|
+
name: string;
|
|
29
|
+
/** The manifest prop field-def driving the control kind + validation. */
|
|
30
|
+
spec: PropField;
|
|
31
|
+
/** The prop's current value envelope (or `undefined` when unset). */
|
|
32
|
+
value: PropValue | undefined;
|
|
33
|
+
/** Commit a new raw value (the caller wraps it in a `staticProp`). */
|
|
34
|
+
onSet: (value: JsonValue) => void;
|
|
35
|
+
/** Remove the prop (revert to the manifest default). */
|
|
36
|
+
onClear: () => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Render + edit a single manifest prop value. Store-agnostic: emits through
|
|
40
|
+
* `onSet`/`onClear`. Shows a validation issue when the current value fails the
|
|
41
|
+
* prop's field-def validator.
|
|
42
|
+
*/
|
|
43
|
+
export declare function PropValueControl({ id, name, spec, value, onSet, onClear }: PropValueControlProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { isStaticProp } from '@elytracms/core/project-graph';
|
|
4
|
+
import { fieldValueValidator } from '@elytracms/core/cms-core';
|
|
5
|
+
import { Input, Row, Select, Stack, Text } from '@elytracms/studio/ui';
|
|
6
|
+
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
7
|
+
import { FieldControl } from './field-control';
|
|
8
|
+
import { FieldDensityProvider } from './field-density';
|
|
9
|
+
export function controlKind(spec) {
|
|
10
|
+
switch (spec.type) {
|
|
11
|
+
case 'boolean':
|
|
12
|
+
return 'boolean';
|
|
13
|
+
case 'number':
|
|
14
|
+
return 'number';
|
|
15
|
+
case 'select':
|
|
16
|
+
return 'select';
|
|
17
|
+
case 'richText':
|
|
18
|
+
return 'richtext';
|
|
19
|
+
// EC-195: an `asset`-typed prop (e.g. the Image `src`) picks from the asset
|
|
20
|
+
// library — never a raw asset id typed into a text box (the EC-192 persona).
|
|
21
|
+
case 'asset':
|
|
22
|
+
return 'asset';
|
|
23
|
+
// EC-253: an `object`/repeater prop reuses the CMS repeater control below.
|
|
24
|
+
case 'object':
|
|
25
|
+
return 'object';
|
|
26
|
+
// EC-262: a `relation`-typed prop (e.g. a ProductGrid's `category`) reuses the
|
|
27
|
+
// CMS relation picker — never a text box that stringifies the ref to "[object Object]".
|
|
28
|
+
case 'relation':
|
|
29
|
+
return 'relation';
|
|
30
|
+
case 'text': {
|
|
31
|
+
const c = spec.form?.control;
|
|
32
|
+
if (c === 'textarea')
|
|
33
|
+
return 'textarea';
|
|
34
|
+
if (c === 'color')
|
|
35
|
+
return 'color';
|
|
36
|
+
if (c === 'json')
|
|
37
|
+
return 'json';
|
|
38
|
+
return 'text';
|
|
39
|
+
}
|
|
40
|
+
default:
|
|
41
|
+
return 'text';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Render + edit a single manifest prop value. Store-agnostic: emits through
|
|
46
|
+
* `onSet`/`onClear`. Shows a validation issue when the current value fails the
|
|
47
|
+
* prop's field-def validator.
|
|
48
|
+
*/
|
|
49
|
+
export function PropValueControl({ id, name, spec, value, onSet, onClear }) {
|
|
50
|
+
const current = isStaticProp(value) ? value.value : spec.default;
|
|
51
|
+
// EC-190: validate with the prop's field-def value validator.
|
|
52
|
+
const valid = current === undefined || fieldValueValidator({ ...spec, name })(current);
|
|
53
|
+
// EC-190: derive the editor control from the field `type` + optional `form.control`.
|
|
54
|
+
const kind = controlKind(spec);
|
|
55
|
+
// Select options live on the `select` field-def.
|
|
56
|
+
const options = spec.type === 'select' ? spec.options : [];
|
|
57
|
+
let control;
|
|
58
|
+
if (kind === 'boolean') {
|
|
59
|
+
control = (_jsxs("label", { className: "flex h-8 items-center gap-2 text-sm", children: [_jsx("input", { id: id, type: "checkbox", checked: current === true, onChange: (e) => onSet(e.target.checked), className: "size-4 accent-primary" }), _jsx(Text, { size: "sm", tone: "muted", children: current === true ? translate('common.true') : translate('common.false') })] }));
|
|
60
|
+
}
|
|
61
|
+
else if (kind === 'select') {
|
|
62
|
+
control = (_jsx(Select, { id: id, value: String(current ?? ''), onChange: (e) => {
|
|
63
|
+
// EC-190: select option values are strings (field-def `options`).
|
|
64
|
+
const opt = options.find((o) => o.value === e.target.value);
|
|
65
|
+
onSet(opt ? opt.value : e.target.value);
|
|
66
|
+
}, children: options.map((opt) => (_jsx("option", { value: opt.value, children: localizedLabel(opt.label, opt.value) }, opt.value))) }));
|
|
67
|
+
}
|
|
68
|
+
else if (kind === 'number') {
|
|
69
|
+
control = _jsx(NumberControl, { id: id, current: current, onSet: onSet, onClear: onClear });
|
|
70
|
+
}
|
|
71
|
+
else if (kind === 'json') {
|
|
72
|
+
control = _jsx(JsonControl, { id: id, current: current, onSet: onSet });
|
|
73
|
+
}
|
|
74
|
+
else if (kind === 'color') {
|
|
75
|
+
control = (_jsxs(Row, { gap: "2", children: [_jsx("input", { id: id, type: "color", value: typeof current === 'string' && /^#[0-9a-f]{6}$/i.test(current) ? current : '#000000', onChange: (e) => onSet(e.target.value), className: "h-8 w-10 shrink-0 rounded-md border border-input bg-background" }), _jsx(Input, { value: typeof current === 'string' ? current : '', onChange: (e) => onSet(e.target.value) })] }));
|
|
76
|
+
}
|
|
77
|
+
else if (kind === 'textarea' || kind === 'richtext') {
|
|
78
|
+
control = (_jsx("textarea", { id: id, rows: 2, value: typeof current === 'string' ? current : '', onChange: (e) => onSet(e.target.value), className: "flex w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm text-foreground shadow-sm focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 outline-none" }));
|
|
79
|
+
}
|
|
80
|
+
else if (kind === 'asset' || kind === 'object' || kind === 'relation') {
|
|
81
|
+
// EC-195/EC-253/EC-262: reuse the CMS field control. `asset` = library picker +
|
|
82
|
+
// preview; `object` = the nested/repeater form; `relation` = the document picker
|
|
83
|
+
// (resolves the ref to its title, instead of stringifying it to "[object Object]").
|
|
84
|
+
// All edit a structured static prop value through the same shared control documents use.
|
|
85
|
+
control = (_jsx(FieldDensityProvider, { density: "compact", children: _jsx(FieldControl, { id: id, field: { ...spec, name }, value: current, onChange: (v) => (v === undefined || v === null ? onClear() : onSet(v)), readOnly: false }) }));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
control = (_jsx(Input, { id: id, value: typeof current === 'string' ? current : String(current ?? ''), onChange: (e) => onSet(e.target.value) }));
|
|
89
|
+
}
|
|
90
|
+
return (_jsxs(Stack, { gap: "1", children: [control, !valid ? (_jsx(Text, { size: "xs", className: "text-destructive", children: translate('builder.inspector.invalidValue') })) : null] }));
|
|
91
|
+
}
|
|
92
|
+
/** Number input with a local buffer: commits only finite values, never NaN; empty clears the prop. */
|
|
93
|
+
function NumberControl({ id, current, onSet, onClear, }) {
|
|
94
|
+
const [raw, setRaw] = useState(typeof current === 'number' ? String(current) : '');
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
setRaw(typeof current === 'number' ? String(current) : '');
|
|
97
|
+
}, [current]);
|
|
98
|
+
return (_jsx(Input, { id: id, type: "number", value: raw, onChange: (e) => {
|
|
99
|
+
const v = e.target.value;
|
|
100
|
+
setRaw(v);
|
|
101
|
+
if (v.trim() === '') {
|
|
102
|
+
onClear();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const n = Number(v);
|
|
106
|
+
if (Number.isFinite(n))
|
|
107
|
+
onSet(n);
|
|
108
|
+
} }));
|
|
109
|
+
}
|
|
110
|
+
/** JSON textarea with a local buffer: commits only when the text parses; flags parse errors. */
|
|
111
|
+
function JsonControl({ id, current, onSet, }) {
|
|
112
|
+
const [raw, setRaw] = useState(() => JSON.stringify(current ?? null));
|
|
113
|
+
const [error, setError] = useState(null);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
setRaw(JSON.stringify(current ?? null));
|
|
116
|
+
setError(null);
|
|
117
|
+
}, [current]);
|
|
118
|
+
return (_jsxs(Stack, { gap: "1", children: [_jsx("textarea", { id: id, rows: 3, value: raw, onChange: (e) => {
|
|
119
|
+
const v = e.target.value;
|
|
120
|
+
setRaw(v);
|
|
121
|
+
try {
|
|
122
|
+
onSet(JSON.parse(v));
|
|
123
|
+
setError(null);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
setError(translate('builder.inspector.invalidJson'));
|
|
127
|
+
}
|
|
128
|
+
}, className: "flex w-full rounded-md border border-input bg-background px-2.5 py-1.5 font-mono text-xs text-foreground shadow-sm focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 outline-none" }), error ? (_jsx(Text, { size: "xs", className: "text-destructive", children: error })) : null] }));
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=prop-fields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prop-fields.js","sourceRoot":"","sources":["../../../src/components/cms/prop-fields.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAG5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACtE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAmCtD,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,SAAS;YACZ,OAAO,SAAS,CAAA;QAClB,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAA;QACjB,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAA;QACjB,KAAK,UAAU;YACb,OAAO,UAAU,CAAA;QACnB,4EAA4E;QAC5E,6EAA6E;QAC7E,KAAK,OAAO;YACV,OAAO,OAAO,CAAA;QAChB,2EAA2E;QAC3E,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAA;QACjB,+EAA+E;QAC/E,wFAAwF;QACxF,KAAK,UAAU;YACb,OAAO,UAAU,CAAA;QACnB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,OAAO,CAAA;YAC5B,IAAI,CAAC,KAAK,UAAU;gBAAE,OAAO,UAAU,CAAA;YACvC,IAAI,CAAC,KAAK,OAAO;gBAAE,OAAO,OAAO,CAAA;YACjC,IAAI,CAAC,KAAK,MAAM;gBAAE,OAAO,MAAM,CAAA;YAC/B,OAAO,MAAM,CAAA;QACf,CAAC;QACD;YACE,OAAO,MAAM,CAAA;IACjB,CAAC;AACH,CAAC;AAiBD;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAyB;IAC/F,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAE,IAAI,CAAC,OAAiC,CAAA;IAE3F,8DAA8D;IAC9D,MAAM,KAAK,GAAG,OAAO,KAAK,SAAS,IAAI,mBAAmB,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAc,CAAC,CAAC,OAAO,CAAC,CAAA;IAClG,qFAAqF;IACrF,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAA;IAC9B,iDAAiD;IACjD,MAAM,OAAO,GAA4B,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAA;IAEnF,IAAI,OAAO,CAAA;IACX,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,GAAG,CACR,iBAAO,SAAS,EAAC,qCAAqC,aACpD,gBACE,EAAE,EAAE,EAAE,EACN,IAAI,EAAC,UAAU,EACf,OAAO,EAAE,OAAO,KAAK,IAAI,EACzB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EACxC,SAAS,EAAC,uBAAuB,GACjC,EACF,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,GACnE,IACD,CACT,CAAA;IACH,CAAC;SAAM,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,GAAG,CACR,KAAC,MAAM,IACL,EAAE,EAAE,EAAE,EACN,KAAK,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,EAC5B,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;gBACd,kEAAkE;gBAClE,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBAC3D,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACzC,CAAC,YAEA,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACpB,iBAAwB,KAAK,EAAE,GAAG,CAAC,KAAK,YACrC,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,IAD1B,GAAG,CAAC,KAAK,CAEb,CACV,CAAC,GACK,CACV,CAAA;IACH,CAAC;SAAM,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,GAAG,KAAC,aAAa,IAAC,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,GAAI,CAAA;IACvF,CAAC;SAAM,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QAC3B,OAAO,GAAG,KAAC,WAAW,IAAC,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAI,CAAA;IACnE,CAAC;SAAM,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QAC5B,OAAO,GAAG,CACR,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,aACV,gBACE,EAAE,EAAE,EAAE,EACN,IAAI,EAAC,OAAO,EACZ,KAAK,EAAE,OAAO,OAAO,KAAK,QAAQ,IAAI,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,EAC3F,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACtC,SAAS,EAAC,gEAAgE,GAC1E,EACF,KAAC,KAAK,IAAC,KAAK,EAAE,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAI,IAChG,CACP,CAAA;IACH,CAAC;SAAM,IAAI,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACtD,OAAO,GAAG,CACR,mBACE,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,CAAC,EACP,KAAK,EAAE,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EACjD,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACtC,SAAS,EAAC,iMAAiM,GAC3M,CACH,CAAA;IACH,CAAC;SAAM,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxE,gFAAgF;QAChF,iFAAiF;QACjF,oFAAoF;QACpF,yFAAyF;QACzF,OAAO,GAAG,CACR,KAAC,oBAAoB,IAAC,OAAO,EAAC,SAAS,YACrC,KAAC,YAAY,IACX,EAAE,EAAE,EAAE,EACN,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,IAAI,EAAc,EACpC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAc,CAAC,CAAC,EACpF,QAAQ,EAAE,KAAK,GACf,GACmB,CACxB,CAAA;IACH,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,CACR,KAAC,KAAK,IACJ,EAAE,EAAE,EAAE,EACN,KAAK,EAAE,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,EACpE,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GACtC,CACH,CAAA;IACH,CAAC;IAED,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACX,OAAO,EACP,CAAC,KAAK,CAAC,CAAC,CAAC,CACR,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,kBAAkB,YACzC,SAAS,CAAC,gCAAgC,CAAC,GACvC,CACR,CAAC,CAAC,CAAC,IAAI,IACF,CACT,CAAA;AACH,CAAC;AAED,sGAAsG;AACtG,SAAS,aAAa,CAAC,EACrB,EAAE,EACF,OAAO,EACP,KAAK,EACL,OAAO,GAMR;IACC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAClF,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC5D,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IACb,OAAO,CACL,KAAC,KAAK,IACJ,EAAE,EAAE,EAAE,EACN,IAAI,EAAC,QAAQ,EACb,KAAK,EAAE,GAAG,EACV,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;YACd,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAA;YACxB,MAAM,CAAC,CAAC,CAAC,CAAA;YACT,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBACpB,OAAO,EAAE,CAAA;gBACT,OAAM;YACR,CAAC;YACD,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;YACnB,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAAE,KAAK,CAAC,CAAC,CAAC,CAAA;QAClC,CAAC,GACD,CACH,CAAA;AACH,CAAC;AAED,gGAAgG;AAChG,SAAS,WAAW,CAAC,EACnB,EAAE,EACF,OAAO,EACP,KAAK,GAKN;IACC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAA;IACrE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IACvD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAA;QACvC,QAAQ,CAAC,IAAI,CAAC,CAAA;IAChB,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IACb,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,mBACE,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,CAAC,EACP,KAAK,EAAE,GAAG,EACV,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;oBACd,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAA;oBACxB,MAAM,CAAC,CAAC,CAAC,CAAA;oBACT,IAAI,CAAC;wBACH,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAc,CAAC,CAAA;wBACjC,QAAQ,CAAC,IAAI,CAAC,CAAA;oBAChB,CAAC;oBAAC,MAAM,CAAC;wBACP,QAAQ,CAAC,SAAS,CAAC,+BAA+B,CAAC,CAAC,CAAA;oBACtD,CAAC;gBACH,CAAC,EACD,SAAS,EAAC,2MAA2M,GACrN,EACD,KAAK,CAAC,CAAC,CAAC,CACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,kBAAkB,YACzC,KAAK,GACD,CACR,CAAC,CAAC,CAAC,IAAI,IACF,CACT,CAAA;AACH,CAAC","sourcesContent":["import { useEffect, useState } from 'react'\nimport { isStaticProp } from '@elytracms/core/project-graph'\nimport type { JsonValue, PropValue } from '@elytracms/core/project-graph'\nimport type { PropField } from '@elytracms/core/component-registry'\nimport { fieldValueValidator } from '@elytracms/core/cms-core'\nimport type { FieldDef, SelectOption } from '@elytracms/core/cms-core'\nimport { Input, Row, Select, Stack, Text } from '@elytracms/studio/ui'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { FieldControl } from './field-control'\nimport { FieldDensityProvider } from './field-density'\n\n/**\n * The EC-190 \"props-as-fields\" value control — the single renderer for a\n * manifest-declared prop field, shared by the canvas property inspector\n * (`builder/property-inspector.tsx`) and the rich-text component-embed prop editor\n * (`rich-text/embed-props.tsx`). Extracted here so both surfaces render prop fields\n * through *one* control set (asset → library picker, relation → document picker,\n * object → nested form, …) rather than two parallel form systems.\n *\n * It is deliberately store-agnostic: it reads a prop's current value and reports\n * edits through `onSet(rawValue)` / `onClear()`. The caller owns *where* the value\n * lands — the inspector dispatches a builder command; the embed editor rewrites a\n * ProseMirror node attr — and is responsible for wrapping the raw value in a\n * `staticProp` envelope.\n */\n\n/**\n * The inspector control to render for a prop field-def. The `control.kind`\n * vocabulary derives from the field `type` plus the optional `form.control`\n * presentation hint on `text` fields.\n */\nexport type ControlKind =\n | 'text'\n | 'textarea'\n | 'number'\n | 'boolean'\n | 'select'\n | 'json'\n | 'color'\n | 'richtext'\n | 'asset'\n | 'object'\n | 'relation'\n\nexport function controlKind(spec: PropField): ControlKind {\n switch (spec.type) {\n case 'boolean':\n return 'boolean'\n case 'number':\n return 'number'\n case 'select':\n return 'select'\n case 'richText':\n return 'richtext'\n // EC-195: an `asset`-typed prop (e.g. the Image `src`) picks from the asset\n // library — never a raw asset id typed into a text box (the EC-192 persona).\n case 'asset':\n return 'asset'\n // EC-253: an `object`/repeater prop reuses the CMS repeater control below.\n case 'object':\n return 'object'\n // EC-262: a `relation`-typed prop (e.g. a ProductGrid's `category`) reuses the\n // CMS relation picker — never a text box that stringifies the ref to \"[object Object]\".\n case 'relation':\n return 'relation'\n case 'text': {\n const c = spec.form?.control\n if (c === 'textarea') return 'textarea'\n if (c === 'color') return 'color'\n if (c === 'json') return 'json'\n return 'text'\n }\n default:\n return 'text'\n }\n}\n\nexport interface PropValueControlProps {\n /** DOM id for the control's primary input (label `htmlFor` target). */\n id: string\n /** The prop's field name — needed to synthesize the `FieldDef` for reused controls. */\n name: string\n /** The manifest prop field-def driving the control kind + validation. */\n spec: PropField\n /** The prop's current value envelope (or `undefined` when unset). */\n value: PropValue | undefined\n /** Commit a new raw value (the caller wraps it in a `staticProp`). */\n onSet: (value: JsonValue) => void\n /** Remove the prop (revert to the manifest default). */\n onClear: () => void\n}\n\n/**\n * Render + edit a single manifest prop value. Store-agnostic: emits through\n * `onSet`/`onClear`. Shows a validation issue when the current value fails the\n * prop's field-def validator.\n */\nexport function PropValueControl({ id, name, spec, value, onSet, onClear }: PropValueControlProps) {\n const current = isStaticProp(value) ? value.value : (spec.default as JsonValue | undefined)\n\n // EC-190: validate with the prop's field-def value validator.\n const valid = current === undefined || fieldValueValidator({ ...spec, name } as FieldDef)(current)\n // EC-190: derive the editor control from the field `type` + optional `form.control`.\n const kind = controlKind(spec)\n // Select options live on the `select` field-def.\n const options: readonly SelectOption[] = spec.type === 'select' ? spec.options : []\n\n let control\n if (kind === 'boolean') {\n control = (\n <label className=\"flex h-8 items-center gap-2 text-sm\">\n <input\n id={id}\n type=\"checkbox\"\n checked={current === true}\n onChange={(e) => onSet(e.target.checked)}\n className=\"size-4 accent-primary\"\n />\n <Text size=\"sm\" tone=\"muted\">\n {current === true ? translate('common.true') : translate('common.false')}\n </Text>\n </label>\n )\n } else if (kind === 'select') {\n control = (\n <Select\n id={id}\n value={String(current ?? '')}\n onChange={(e) => {\n // EC-190: select option values are strings (field-def `options`).\n const opt = options.find((o) => o.value === e.target.value)\n onSet(opt ? opt.value : e.target.value)\n }}\n >\n {options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {localizedLabel(opt.label, opt.value)}\n </option>\n ))}\n </Select>\n )\n } else if (kind === 'number') {\n control = <NumberControl id={id} current={current} onSet={onSet} onClear={onClear} />\n } else if (kind === 'json') {\n control = <JsonControl id={id} current={current} onSet={onSet} />\n } else if (kind === 'color') {\n control = (\n <Row gap=\"2\">\n <input\n id={id}\n type=\"color\"\n value={typeof current === 'string' && /^#[0-9a-f]{6}$/i.test(current) ? current : '#000000'}\n onChange={(e) => onSet(e.target.value)}\n className=\"h-8 w-10 shrink-0 rounded-md border border-input bg-background\"\n />\n <Input value={typeof current === 'string' ? current : ''} onChange={(e) => onSet(e.target.value)} />\n </Row>\n )\n } else if (kind === 'textarea' || kind === 'richtext') {\n control = (\n <textarea\n id={id}\n rows={2}\n value={typeof current === 'string' ? current : ''}\n onChange={(e) => onSet(e.target.value)}\n className=\"flex w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm text-foreground shadow-sm focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 outline-none\"\n />\n )\n } else if (kind === 'asset' || kind === 'object' || kind === 'relation') {\n // EC-195/EC-253/EC-262: reuse the CMS field control. `asset` = library picker +\n // preview; `object` = the nested/repeater form; `relation` = the document picker\n // (resolves the ref to its title, instead of stringifying it to \"[object Object]\").\n // All edit a structured static prop value through the same shared control documents use.\n control = (\n <FieldDensityProvider density=\"compact\">\n <FieldControl\n id={id}\n field={{ ...spec, name } as FieldDef}\n value={current}\n onChange={(v) => (v === undefined || v === null ? onClear() : onSet(v as JsonValue))}\n readOnly={false}\n />\n </FieldDensityProvider>\n )\n } else {\n control = (\n <Input\n id={id}\n value={typeof current === 'string' ? current : String(current ?? '')}\n onChange={(e) => onSet(e.target.value)}\n />\n )\n }\n\n return (\n <Stack gap=\"1\">\n {control}\n {!valid ? (\n <Text size=\"xs\" className=\"text-destructive\">\n {translate('builder.inspector.invalidValue')}\n </Text>\n ) : null}\n </Stack>\n )\n}\n\n/** Number input with a local buffer: commits only finite values, never NaN; empty clears the prop. */\nfunction NumberControl({\n id,\n current,\n onSet,\n onClear,\n}: {\n id: string\n current: JsonValue | undefined\n onSet: (v: number) => void\n onClear: () => void\n}) {\n const [raw, setRaw] = useState(typeof current === 'number' ? String(current) : '')\n useEffect(() => {\n setRaw(typeof current === 'number' ? String(current) : '')\n }, [current])\n return (\n <Input\n id={id}\n type=\"number\"\n value={raw}\n onChange={(e) => {\n const v = e.target.value\n setRaw(v)\n if (v.trim() === '') {\n onClear()\n return\n }\n const n = Number(v)\n if (Number.isFinite(n)) onSet(n)\n }}\n />\n )\n}\n\n/** JSON textarea with a local buffer: commits only when the text parses; flags parse errors. */\nfunction JsonControl({\n id,\n current,\n onSet,\n}: {\n id: string\n current: JsonValue | undefined\n onSet: (v: JsonValue) => void\n}) {\n const [raw, setRaw] = useState(() => JSON.stringify(current ?? null))\n const [error, setError] = useState<string | null>(null)\n useEffect(() => {\n setRaw(JSON.stringify(current ?? null))\n setError(null)\n }, [current])\n return (\n <Stack gap=\"1\">\n <textarea\n id={id}\n rows={3}\n value={raw}\n onChange={(e) => {\n const v = e.target.value\n setRaw(v)\n try {\n onSet(JSON.parse(v) as JsonValue)\n setError(null)\n } catch {\n setError(translate('builder.inspector.invalidJson'))\n }\n }}\n className=\"flex w-full rounded-md border border-input bg-background px-2.5 py-1.5 font-mono text-xs text-foreground shadow-sm focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 outline-none\"\n />\n {error ? (\n <Text size=\"xs\" className=\"text-destructive\">\n {error}\n </Text>\n ) : null}\n </Stack>\n )\n}\n"]}
|
|
@@ -8,8 +8,22 @@ import { Node } from '@tiptap/react';
|
|
|
8
8
|
* so the extractor still indexes the embedded component's references, and the delivery
|
|
9
9
|
* renderer renders it exactly as a `blocks` value.
|
|
10
10
|
*
|
|
11
|
-
* The editor shows a
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The editor shows a placeholder card naming the embedded component (a live in-editor
|
|
12
|
+
* preview would need the runtime renderer + resolvers; deferred). Insertion is wired
|
|
13
|
+
* from the toolbar / slash menu; the card's Edit affordance opens the EC-314 phase-4
|
|
14
|
+
* prop editor ({@link EmbedPropsDialog}), which rewrites `attrs.node` through a proper
|
|
15
|
+
* ProseMirror transaction (undoable). `readOnly` renders an inert card (no edit/delete).
|
|
14
16
|
*/
|
|
15
17
|
export declare const ComponentEmbed: Node<any, any>;
|
|
18
|
+
/**
|
|
19
|
+
* The presentational embed card — component identity, an optional one-line prop
|
|
20
|
+
* summary, and (only when a handler is supplied) edit / delete affordances. It is
|
|
21
|
+
* kept free of TipTap and registry context so it renders + asserts standalone: an
|
|
22
|
+
* inert (read-only) card is simply the card with no `onEdit`/`onDelete`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function EmbedCard({ displayName, summary, onEdit, onDelete, }: {
|
|
25
|
+
displayName: string;
|
|
26
|
+
summary?: string | undefined;
|
|
27
|
+
onEdit?: (() => void) | undefined;
|
|
28
|
+
onDelete?: (() => void) | undefined;
|
|
29
|
+
}): import("react").JSX.Element;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { Node, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes } from '@tiptap/react';
|
|
3
|
-
import { Blocks, Trash2 } from 'lucide-react';
|
|
4
|
+
import { Blocks, Pencil, Trash2 } from 'lucide-react';
|
|
4
5
|
import { COMPONENT_EMBED_NODE_TYPE, embeddedComponentNode } from '@elytracms/core/rich-text';
|
|
6
|
+
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
5
7
|
import { Button } from '@elytracms/studio/ui';
|
|
6
8
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
9
|
+
import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
|
|
7
10
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
11
|
+
import { EmbedPropsDialog, embedPropsSummary } from './embed-props';
|
|
8
12
|
/**
|
|
9
13
|
* The rich-text component-embed node (EC-189/EC-314): a block atom holding a single
|
|
10
14
|
* composition value at `attrs.node`. It is the third place constrained composition
|
|
@@ -14,9 +18,11 @@ import { cn } from '@elytracms/studio/lib/utils';
|
|
|
14
18
|
* so the extractor still indexes the embedded component's references, and the delivery
|
|
15
19
|
* renderer renders it exactly as a `blocks` value.
|
|
16
20
|
*
|
|
17
|
-
* The editor shows a
|
|
18
|
-
*
|
|
19
|
-
*
|
|
21
|
+
* The editor shows a placeholder card naming the embedded component (a live in-editor
|
|
22
|
+
* preview would need the runtime renderer + resolvers; deferred). Insertion is wired
|
|
23
|
+
* from the toolbar / slash menu; the card's Edit affordance opens the EC-314 phase-4
|
|
24
|
+
* prop editor ({@link EmbedPropsDialog}), which rewrites `attrs.node` through a proper
|
|
25
|
+
* ProseMirror transaction (undoable). `readOnly` renders an inert card (no edit/delete).
|
|
20
26
|
*/
|
|
21
27
|
export const ComponentEmbed = Node.create({
|
|
22
28
|
name: COMPONENT_EMBED_NODE_TYPE,
|
|
@@ -45,10 +51,26 @@ function readEmbedded(attrNode) {
|
|
|
45
51
|
attrs: { node: attrNode },
|
|
46
52
|
});
|
|
47
53
|
}
|
|
48
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The presentational embed card — component identity, an optional one-line prop
|
|
56
|
+
* summary, and (only when a handler is supplied) edit / delete affordances. It is
|
|
57
|
+
* kept free of TipTap and registry context so it renders + asserts standalone: an
|
|
58
|
+
* inert (read-only) card is simply the card with no `onEdit`/`onDelete`.
|
|
59
|
+
*/
|
|
60
|
+
export function EmbedCard({ displayName, summary, onEdit, onDelete, }) {
|
|
61
|
+
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
|
+
}
|
|
63
|
+
function ComponentEmbedView({ node, updateAttributes, deleteNode, selected, editor }) {
|
|
49
64
|
const embedded = readEmbedded(node.attrs.node);
|
|
50
|
-
const
|
|
65
|
+
const manifest = embedded ? getStudioRegistry().registry.getManifest(embedded.componentId) : undefined;
|
|
51
66
|
const editable = editor.isEditable;
|
|
52
|
-
|
|
67
|
+
const [open, setOpen] = useState(false);
|
|
68
|
+
const displayName = embedded
|
|
69
|
+
? componentDisplayName(embedded.componentId, manifest)
|
|
70
|
+
: translate('richText.embed.unknown');
|
|
71
|
+
const summary = embedded ? embedPropsSummary(embedded, manifest) : undefined;
|
|
72
|
+
// Editing needs a manifest to know which prop fields to render.
|
|
73
|
+
const canEdit = editable && embedded != null && manifest != null;
|
|
74
|
+
return (_jsxs(NodeViewWrapper, { "data-drag-handle": true, className: cn('my-2', selected && 'rounded-md ring-2 ring-ring/50'), children: [_jsx(EmbedCard, { displayName: displayName, summary: summary, onEdit: canEdit ? () => setOpen(true) : undefined, onDelete: editable ? () => deleteNode() : undefined }), canEdit && embedded && manifest ? (_jsx(EmbedPropsDialog, { open: open, onOpenChange: setOpen, node: embedded, manifest: manifest, onSave: (next) => updateAttributes({ node: next }) })) : null] }));
|
|
53
75
|
}
|
|
54
76
|
//# 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":";AAAA,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAE7F,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;
|
|
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"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ComponentNode, JsonValue } from '@elytracms/core/project-graph';
|
|
2
|
+
import type { ComponentManifest, PropField } from '@elytracms/core/component-registry';
|
|
3
|
+
/**
|
|
4
|
+
* Prop editing for rich-text component embeds (EC-314 phase 4).
|
|
5
|
+
*
|
|
6
|
+
* An embedded component is a full `ComponentNode` stored verbatim at
|
|
7
|
+
* `componentEmbed.attrs.node`. This dialog edits that node's *props* through the
|
|
8
|
+
* SAME EC-190 "props-as-fields" control stack the canvas inspector and `blocks`
|
|
9
|
+
* field use ({@link PropValueControl}) — asset props open the library picker,
|
|
10
|
+
* relation props the document picker, etc. — never a parallel form system.
|
|
11
|
+
*
|
|
12
|
+
* Fidelity contract: a working `draft` is seeded from the stored node when the
|
|
13
|
+
* dialog opens; the dialog only writes back on Apply, and only when the draft is
|
|
14
|
+
* *structurally different* ({@link componentNodesEqual}) — so opening and closing
|
|
15
|
+
* without a real change never dirties the field. The upstream write goes through a
|
|
16
|
+
* single ProseMirror `updateAttributes` transaction (undoable), never a storage
|
|
17
|
+
* rewrite; the embedded node's `id`/`componentId`/`slots` pass through untouched.
|
|
18
|
+
*
|
|
19
|
+
* `blocks`-typed props (nested composition) are NOT edited here — that needs the
|
|
20
|
+
* canvas, not a popover. They are excluded from the form and surfaced as a note.
|
|
21
|
+
*/
|
|
22
|
+
/** Set a prop to a fresh static value, preserving identity, slots, and sibling props. */
|
|
23
|
+
export declare function setComponentNodeProp(node: ComponentNode, name: string, value: JsonValue): ComponentNode;
|
|
24
|
+
/** Remove a prop (revert to the manifest default), preserving everything else. */
|
|
25
|
+
export declare function clearComponentNodeProp(node: ComponentNode, name: string): ComponentNode;
|
|
26
|
+
/** Key-order-insensitive structural equality — a redundant "save" must not write. */
|
|
27
|
+
export declare function componentNodesEqual(a: ComponentNode, b: ComponentNode): boolean;
|
|
28
|
+
/** The manifest props editable inline: every non-`blocks` prop (composition needs the canvas). */
|
|
29
|
+
export declare function editableEmbedProps(manifest: ComponentManifest): Array<[string, PropField]>;
|
|
30
|
+
/** The manifest's `blocks` (composition) props — excluded from the form, shown as a note. */
|
|
31
|
+
export declare function blocksEmbedProps(manifest: ComponentManifest): Array<[string, PropField]>;
|
|
32
|
+
/**
|
|
33
|
+
* A lean one-line summary of an embed's static scalar props (`Label: value`,
|
|
34
|
+
* up to three), for the card. Skips unset props and structured values (refs /
|
|
35
|
+
* objects / arrays) — those are edited in the dialog, not summarised.
|
|
36
|
+
*/
|
|
37
|
+
export declare function embedPropsSummary(node: ComponentNode, manifest?: ComponentManifest): string | undefined;
|
|
38
|
+
export interface EmbedPropsDialogProps {
|
|
39
|
+
open: boolean;
|
|
40
|
+
onOpenChange: (open: boolean) => void;
|
|
41
|
+
/** The embedded component node whose props are being edited. */
|
|
42
|
+
node: ComponentNode;
|
|
43
|
+
/** Its manifest — drives which prop fields render. */
|
|
44
|
+
manifest: ComponentManifest;
|
|
45
|
+
/** Commit the edited node (upstream writes it through one ProseMirror transaction). */
|
|
46
|
+
onSave: (node: ComponentNode) => void;
|
|
47
|
+
}
|
|
48
|
+
/** The edit-props dialog opened from an embed card. */
|
|
49
|
+
export declare function EmbedPropsDialog({ open, onOpenChange, node, manifest, onSave }: EmbedPropsDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { isStaticProp, staticProp } from '@elytracms/core/project-graph';
|
|
4
|
+
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Field, Stack, Text, } from '@elytracms/studio/ui';
|
|
5
|
+
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
6
|
+
import { humanizeName } from '@elytracms/studio/lib/utils';
|
|
7
|
+
import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
|
|
8
|
+
import { PropValueControl } from '../prop-fields';
|
|
9
|
+
/**
|
|
10
|
+
* Prop editing for rich-text component embeds (EC-314 phase 4).
|
|
11
|
+
*
|
|
12
|
+
* 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.
|
|
17
|
+
*
|
|
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.
|
|
24
|
+
*
|
|
25
|
+
* `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.
|
|
27
|
+
*/
|
|
28
|
+
/** Set a prop to a fresh static value, preserving identity, slots, and sibling props. */
|
|
29
|
+
export function setComponentNodeProp(node, name, value) {
|
|
30
|
+
return { ...node, props: { ...node.props, [name]: staticProp(value) } };
|
|
31
|
+
}
|
|
32
|
+
/** Remove a prop (revert to the manifest default), preserving everything else. */
|
|
33
|
+
export function clearComponentNodeProp(node, name) {
|
|
34
|
+
const props = { ...node.props };
|
|
35
|
+
delete props[name];
|
|
36
|
+
return { ...node, props };
|
|
37
|
+
}
|
|
38
|
+
/** Key-order-insensitive structural equality — a redundant "save" must not write. */
|
|
39
|
+
export function componentNodesEqual(a, b) {
|
|
40
|
+
return stableStringify(a) === stableStringify(b);
|
|
41
|
+
}
|
|
42
|
+
function stableStringify(value) {
|
|
43
|
+
if (value === null || typeof value !== 'object')
|
|
44
|
+
return JSON.stringify(value) ?? 'null';
|
|
45
|
+
if (Array.isArray(value))
|
|
46
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
47
|
+
const entries = Object.entries(value)
|
|
48
|
+
.filter(([, v]) => v !== undefined)
|
|
49
|
+
.sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0))
|
|
50
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`);
|
|
51
|
+
return `{${entries.join(',')}}`;
|
|
52
|
+
}
|
|
53
|
+
/** The manifest props editable inline: every non-`blocks` prop (composition needs the canvas). */
|
|
54
|
+
export function editableEmbedProps(manifest) {
|
|
55
|
+
return Object.entries(manifest.props).filter(([, spec]) => spec.type !== 'blocks');
|
|
56
|
+
}
|
|
57
|
+
/** The manifest's `blocks` (composition) props — excluded from the form, shown as a note. */
|
|
58
|
+
export function blocksEmbedProps(manifest) {
|
|
59
|
+
return Object.entries(manifest.props).filter(([, spec]) => spec.type === 'blocks');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A lean one-line summary of an embed's static scalar props (`Label: value`,
|
|
63
|
+
* up to three), for the card. Skips unset props and structured values (refs /
|
|
64
|
+
* objects / arrays) — those are edited in the dialog, not summarised.
|
|
65
|
+
*/
|
|
66
|
+
export function embedPropsSummary(node, manifest) {
|
|
67
|
+
const parts = [];
|
|
68
|
+
for (const [name, value] of Object.entries(node.props ?? {})) {
|
|
69
|
+
if (!isStaticProp(value))
|
|
70
|
+
continue;
|
|
71
|
+
const raw = value.value;
|
|
72
|
+
if (raw === null || raw === undefined || typeof raw === 'object')
|
|
73
|
+
continue;
|
|
74
|
+
const label = manifest?.props[name]?.form?.label;
|
|
75
|
+
const labelText = label ? localizedLabel(label, name) : humanizeName(name);
|
|
76
|
+
parts.push(`${labelText}: ${String(raw)}`);
|
|
77
|
+
if (parts.length >= 3)
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
return parts.length > 0 ? parts.join(' · ') : undefined;
|
|
81
|
+
}
|
|
82
|
+
/** The edit-props dialog opened from an embed card. */
|
|
83
|
+
export function EmbedPropsDialog({ open, onOpenChange, node, manifest, onSave }) {
|
|
84
|
+
const [draft, setDraft] = useState(node);
|
|
85
|
+
// Re-seed the working draft from the stored node whenever the dialog opens, so a
|
|
86
|
+
// cancelled edit is discarded and a reopen starts from the committed value.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (open)
|
|
89
|
+
setDraft(node);
|
|
90
|
+
}, [open, node]);
|
|
91
|
+
const editable = editableEmbedProps(manifest);
|
|
92
|
+
const blocks = blocksEmbedProps(manifest);
|
|
93
|
+
function apply() {
|
|
94
|
+
// No structural change ⇒ never write (opening + closing must not dirty the field).
|
|
95
|
+
if (!componentNodesEqual(draft, node))
|
|
96
|
+
onSave(draft);
|
|
97
|
+
onOpenChange(false);
|
|
98
|
+
}
|
|
99
|
+
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', {
|
|
100
|
+
name: componentDisplayName(node.componentId, manifest),
|
|
101
|
+
}) }) }), _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]) => {
|
|
102
|
+
const id = `embed-${node.id}-${name}`;
|
|
103
|
+
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));
|
|
104
|
+
})), 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') })] })] }) }));
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=embed-props.js.map
|
|
@@ -0,0 +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\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"]}
|
|
@@ -3,5 +3,10 @@
|
|
|
3
3
|
* `@elytracms/core/rich-text` storage. Public surface is the {@link RichTextEditor}
|
|
4
4
|
* component wired into the `richText` field control; everything else (extensions,
|
|
5
5
|
* toolbar, value bridging) is an implementation detail.
|
|
6
|
+
*
|
|
7
|
+
* EC-322: the public export is the {@link ./lazy lazy-loaded} wrapper — TipTap only
|
|
8
|
+
* downloads when a `richText` field actually renders, keeping it out of the studio's
|
|
9
|
+
* main route chunk. The eager editor stays available as `./editor` for the unit tests
|
|
10
|
+
* and the extension-level modules that import it directly.
|
|
6
11
|
*/
|
|
7
|
-
export { RichTextEditor } from './
|
|
12
|
+
export { RichTextEditor } from './lazy';
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* `@elytracms/core/rich-text` storage. Public surface is the {@link RichTextEditor}
|
|
4
4
|
* component wired into the `richText` field control; everything else (extensions,
|
|
5
5
|
* toolbar, value bridging) is an implementation detail.
|
|
6
|
+
*
|
|
7
|
+
* EC-322: the public export is the {@link ./lazy lazy-loaded} wrapper — TipTap only
|
|
8
|
+
* downloads when a `richText` field actually renders, keeping it out of the studio's
|
|
9
|
+
* main route chunk. The eager editor stays available as `./editor` for the unit tests
|
|
10
|
+
* and the extension-level modules that import it directly.
|
|
6
11
|
*/
|
|
7
|
-
export { RichTextEditor } from './
|
|
12
|
+
export { RichTextEditor } from './lazy';
|
|
8
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA","sourcesContent":["/**\n * The CMS rich-text field editor (EC-314) — a TipTap editor over the shared\n * `@elytracms/core/rich-text` storage. Public surface is the {@link RichTextEditor}\n * component wired into the `richText` field control; everything else (extensions,\n * toolbar, value bridging) is an implementation detail.\n *\n * EC-322: the public export is the {@link ./lazy lazy-loaded} wrapper — TipTap only\n * downloads when a `richText` field actually renders, keeping it out of the studio's\n * main route chunk. The eager editor stays available as `./editor` for the unit tests\n * and the extension-level modules that import it directly.\n */\nexport { RichTextEditor } from './lazy'\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Lazy boundary for the CMS rich-text editor (EC-322 load-time pass).
|
|
4
|
+
*
|
|
5
|
+
* The TipTap editor + its ProseMirror runtime and extension set is a large
|
|
6
|
+
* dependency, and `field-control` sits on the critical path of *every* CMS view.
|
|
7
|
+
* Importing the editor statically pulled all of TipTap into the studio's main
|
|
8
|
+
* route chunk even for documents with no `richText` field on screen. Splitting it
|
|
9
|
+
* behind {@link lazy} moves the whole editor subtree into an on-demand chunk that
|
|
10
|
+
* only downloads when a `richText` field actually renders — the field control's
|
|
11
|
+
* seam is unchanged (it still imports `RichTextEditor` from `./rich-text`).
|
|
12
|
+
*
|
|
13
|
+
* The fallback mirrors the loaded editor's outer frame (same border/rounding + a
|
|
14
|
+
* toolbar-and-body min-height) so the swap-in is layout-jank-free, and carries an
|
|
15
|
+
* accessible loading label from the message catalog.
|
|
16
|
+
*/
|
|
17
|
+
declare const RichTextEditorImpl: import("react").LazyExoticComponent<typeof import("./editor").RichTextEditor>;
|
|
18
|
+
export declare function RichTextEditor(props: ComponentProps<typeof RichTextEditorImpl>): import("react").JSX.Element;
|
|
19
|
+
export {};
|