@elytracms/studio 0.0.32 → 0.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/app-views/default-modules.js +20 -11
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-library.js +21 -13
- package/dist/components/assets/asset-library.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +3 -2
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/backend-gate.js +4 -3
- package/dist/components/backend-gate.js.map +1 -1
- package/dist/components/builder/blocks-panel.js +11 -2
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/canvas.js +6 -5
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +12 -6
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +7 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/fixture-thumbnail.js +2 -1
- package/dist/components/builder/fixture-thumbnail.js.map +1 -1
- package/dist/components/builder/layout-inspector.d.ts +5 -5
- package/dist/components/builder/layout-inspector.js +21 -20
- package/dist/components/builder/layout-inspector.js.map +1 -1
- package/dist/components/builder/page-tree.js +5 -4
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +10 -10
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +9 -9
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/rich-text-toolbar.js +4 -3
- package/dist/components/builder/rich-text-toolbar.js.map +1 -1
- package/dist/components/builder/spacing-handles.js +4 -3
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-picker.js +8 -7
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-bar.js +8 -5
- package/dist/components/cms/bulk-bar.js.map +1 -1
- package/dist/components/cms/cell.js +9 -5
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +7 -6
- package/dist/components/cms/cms-store.js +21 -12
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +18 -7
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +45 -29
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/details-pane.js +4 -3
- package/dist/components/cms/details-pane.js.map +1 -1
- package/dist/components/cms/editable-cell.js +4 -4
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.js +29 -21
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.js +2 -1
- package/dist/components/cms/field-renderer.js.map +1 -1
- package/dist/components/cms/filter-builder.js +9 -8
- package/dist/components/cms/filter-builder.js.map +1 -1
- package/dist/components/cms/import-export.js +3 -2
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/issues-pane.js +3 -2
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/reference-confirm.js +24 -10
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/relation-picker.js +5 -4
- package/dist/components/cms/relation-picker.js.map +1 -1
- package/dist/components/cms/scalar-value-editor.js +5 -5
- package/dist/components/cms/scalar-value-editor.js.map +1 -1
- package/dist/components/cms/version-compare.js +36 -21
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +8 -5
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/cms/view-bar.js +3 -2
- package/dist/components/cms/view-bar.js.map +1 -1
- package/dist/components/components/component-detail.js +3 -2
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-status.js +8 -13
- package/dist/components/components/component-status.js.map +1 -1
- package/dist/components/components/components-index.js +5 -1
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/field-inspector.js +4 -4
- package/dist/components/components/field-inspector.js.map +1 -1
- package/dist/components/components/fixture-runner.js +4 -1
- package/dist/components/components/fixture-runner.js.map +1 -1
- package/dist/components/components/registry-browser.js +4 -3
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/delivery-health-banner.js +2 -1
- package/dist/components/delivery-health-banner.js.map +1 -1
- package/dist/components/dev-login-badge.js +2 -1
- package/dist/components/dev-login-badge.js.map +1 -1
- package/dist/components/extensions/extensions-view.js +22 -21
- package/dist/components/extensions/extensions-view.js.map +1 -1
- package/dist/components/guards.js +3 -2
- package/dist/components/guards.js.map +1 -1
- package/dist/components/operation-error.js +3 -2
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/playground-badge.js +2 -1
- package/dist/components/playground-badge.js.map +1 -1
- package/dist/components/project-list.js +5 -4
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-session.js +3 -2
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/project-switcher.js +4 -3
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/redirects/redirects-module.js +3 -2
- package/dist/components/redirects/redirects-module.js.map +1 -1
- package/dist/components/references/references-panel.js +7 -6
- package/dist/components/references/references-panel.js.map +1 -1
- package/dist/components/section-nav.js +8 -5
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/section-state.js +13 -32
- package/dist/components/section-state.js.map +1 -1
- package/dist/components/settings-form.js +2 -1
- package/dist/components/settings-form.js.map +1 -1
- package/dist/components/studio-project-screen.js +3 -2
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +3 -2
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/users/users-module.js +5 -4
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/components/workspace-gate.js +2 -1
- package/dist/components/workspace-gate.js.map +1 -1
- package/dist/lib/cms-views/view-model.js +3 -2
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/i18n/i18n.d.ts +19 -1
- package/dist/lib/i18n/i18n.js +45 -6
- package/dist/lib/i18n/i18n.js.map +1 -1
- package/dist/lib/i18n/index.d.ts +1 -1
- package/dist/lib/i18n/index.js +1 -1
- package/dist/lib/i18n/index.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +11 -4
- package/dist/lib/i18n/messages.en.js +660 -6
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/references/reference-model.js +6 -4
- package/dist/lib/references/reference-model.js.map +1 -1
- package/package.json +2 -2
|
@@ -6,7 +6,7 @@ import { fieldValueValidator } from '@elytracms/core/cms-core';
|
|
|
6
6
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
7
7
|
import { Badge, Code, Field, Input, InspectorSection, PanelHeader, Row, Select, Spacer, Stack, Text, } from '@elytracms/studio/ui';
|
|
8
8
|
import { humanizeName } from '@elytracms/studio/lib/utils';
|
|
9
|
-
import { localizedLabel } from '@elytracms/studio/lib/i18n';
|
|
9
|
+
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
10
10
|
import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
|
|
11
11
|
import { FieldControl } from '../cms/field-control';
|
|
12
12
|
import { FieldDensityProvider } from '../cms/field-density';
|
|
@@ -17,10 +17,10 @@ export function PropertyInspector() {
|
|
|
17
17
|
const studio = getStudioRegistry();
|
|
18
18
|
const node = builder.selectedNode;
|
|
19
19
|
if (!node) {
|
|
20
|
-
return (_jsx("div", { className: "grid h-full place-items-center p-6 text-center", children: _jsx(Text, { size: "sm", tone: "muted", children:
|
|
20
|
+
return (_jsx("div", { className: "grid h-full place-items-center p-6 text-center", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('builder.inspector.emptySelection') }) }));
|
|
21
21
|
}
|
|
22
22
|
const manifest = studio.registry.getManifest(node.componentId);
|
|
23
|
-
return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs(PanelHeader, { children: [_jsx(Text, { size: "sm", weight: "semibold", children:
|
|
23
|
+
return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs(PanelHeader, { children: [_jsx(Text, { size: "sm", weight: "semibold", children: translate('builder.inspector.title') }), _jsx(Spacer, {}), _jsx(Code, { tone: "muted", title: node.componentId, children: componentDisplayName(node.componentId, manifest) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: _jsx(SettingsTab, { node: node, manifest: manifest }) })] }));
|
|
24
24
|
}
|
|
25
25
|
/** The full property surface: node identity, static props, condition, slots. */
|
|
26
26
|
function SettingsTab({ node, manifest, }) {
|
|
@@ -34,14 +34,14 @@ function SettingsTab({ node, manifest, }) {
|
|
|
34
34
|
? Object.entries(manifest.props).filter(([name, spec]) => !layoutManaged.has(name) && spec.type !== 'blocks')
|
|
35
35
|
: [];
|
|
36
36
|
const slots = manifest ? manifestSlots(manifest) : [];
|
|
37
|
-
return (_jsxs(_Fragment, { children: [_jsx(LayoutSection, { node: node }), !manifest ? (_jsxs("div", { className: "p-3", children: [_jsx(Badge, { variant: "error", children:
|
|
37
|
+
return (_jsxs(_Fragment, { children: [_jsx(LayoutSection, { node: node }), !manifest ? (_jsxs("div", { className: "p-3", children: [_jsx(Badge, { variant: "error", children: translate('builder.inspector.unknownComponent') }), _jsx(Text, { size: "xs", tone: "muted", className: "mt-2", children: translate('builder.inspector.noManifest') })] })) : (_jsx(InspectorSection, { title: translate('builder.inspector.props'), children: props.length === 0 ? (_jsx(Text, { size: "xs", tone: "subtle", children: translate('builder.inspector.noEditableProps') })) : (_jsx(Stack, { gap: "4", children: props.map(([name, spec]) => (
|
|
38
38
|
// Key by node id so per-node local control state never leaks
|
|
39
39
|
// across a selection change to a same-named prop.
|
|
40
|
-
_jsx(PropField, { node: node, name: name, spec: spec }, `${node.id}:${name}`))) })) })), slots.length > 0 ? (_jsxs(InspectorSection, { title:
|
|
40
|
+
_jsx(PropField, { node: node, name: name, spec: spec }, `${node.id}:${name}`))) })) })), slots.length > 0 ? (_jsxs(InspectorSection, { title: translate('builder.inspector.slots'), children: [_jsx(Stack, { gap: "1", children: slots.map((slot) => {
|
|
41
41
|
const count = node.slots?.[slot.name]?.length ?? 0;
|
|
42
42
|
const missing = slot.required && count === 0;
|
|
43
|
-
return (_jsxs(Row, { gap: "2", children: [_jsx(Text, { size: "xs", title: slot.name, children: humanizeName(slot.name) }), slot.required ? (_jsx(Badge, { variant: missing ? 'error' : 'outline', children:
|
|
44
|
-
}) }), _jsx(Text, { size: "xs", tone: "subtle", className: "mt-2", children:
|
|
43
|
+
return (_jsxs(Row, { gap: "2", children: [_jsx(Text, { size: "xs", title: slot.name, children: humanizeName(slot.name) }), slot.required ? (_jsx(Badge, { variant: missing ? 'error' : 'outline', children: translate('common.required') })) : null, _jsx(Spacer, {}), _jsx(Text, { size: "xs", tone: "subtle", mono: true, children: count })] }, slot.name));
|
|
44
|
+
}) }), _jsx(Text, { size: "xs", tone: "subtle", className: "mt-2", children: translate('builder.inspector.slotsHint') })] })) : null] }));
|
|
45
45
|
}
|
|
46
46
|
function PropField({ node, name, spec }) {
|
|
47
47
|
const value = node.props?.[name];
|
|
@@ -101,7 +101,7 @@ function StaticControl({ node, name, spec, value, }) {
|
|
|
101
101
|
const options = spec.type === 'select' ? spec.options : [];
|
|
102
102
|
let control;
|
|
103
103
|
if (kind === 'boolean') {
|
|
104
|
-
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) => set(e.target.checked), className: "size-4 accent-primary" }), _jsx(Text, { size: "sm", tone: "muted", children: current === true ? 'true' : 'false' })] }));
|
|
104
|
+
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) => set(e.target.checked), className: "size-4 accent-primary" }), _jsx(Text, { size: "sm", tone: "muted", children: current === true ? translate('common.true') : translate('common.false') })] }));
|
|
105
105
|
}
|
|
106
106
|
else if (kind === 'select') {
|
|
107
107
|
control = (_jsx(Select, { id: id, value: String(current ?? ''), onChange: (e) => {
|
|
@@ -132,7 +132,7 @@ function StaticControl({ node, name, spec, value, }) {
|
|
|
132
132
|
else {
|
|
133
133
|
control = (_jsx(Input, { id: id, value: typeof current === 'string' ? current : String(current ?? ''), onChange: (e) => set(e.target.value) }));
|
|
134
134
|
}
|
|
135
|
-
return (_jsxs(Stack, { gap: "1", children: [control, !valid ? (_jsx(Text, { size: "xs", className: "text-destructive", children:
|
|
135
|
+
return (_jsxs(Stack, { gap: "1", children: [control, !valid ? (_jsx(Text, { size: "xs", className: "text-destructive", children: translate('builder.inspector.invalidValue') })) : null] }));
|
|
136
136
|
}
|
|
137
137
|
/** Number input with a local buffer: commits only finite values, never NaN; empty clears the prop. */
|
|
138
138
|
function NumberControl({ id, current, onSet, onClear, }) {
|
|
@@ -168,7 +168,7 @@ function JsonControl({ id, current, onSet, }) {
|
|
|
168
168
|
setError(null);
|
|
169
169
|
}
|
|
170
170
|
catch {
|
|
171
|
-
setError('
|
|
171
|
+
setError(translate('builder.inspector.invalidJson'));
|
|
172
172
|
}
|
|
173
173
|
}, 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] }));
|
|
174
174
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"property-inspector.js","sourceRoot":"","sources":["../../../src/components/builder/property-inspector.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useState } from 'react'\nimport { staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue, PropValue } from '@elytracms/core/project-graph'\nimport type { ComponentManifest, PropField } from '@elytracms/core/component-registry'\nimport { manifestSlots } from '@elytracms/core/component-registry'\nimport { fieldValueValidator } from '@elytracms/core/cms-core'\nimport type { FieldDef, SelectOption } from '@elytracms/core/cms-core'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport {\n Badge,\n Code,\n Field,\n Input,\n InspectorSection,\n PanelHeader,\n Row,\n Select,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { humanizeName } from '@elytracms/studio/lib/utils'\nimport { localizedLabel } from '@elytracms/studio/lib/i18n'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { FieldControl } from '../cms/field-control'\nimport { FieldDensityProvider } from '../cms/field-density'\nimport { useBuilder } from './builder-store'\nimport { LayoutSection, layoutManagedPropNames } from './layout-inspector'\n\nexport function PropertyInspector() {\n const builder = useBuilder()\n const studio = getStudioRegistry()\n const node = builder.selectedNode\n\n if (!node) {\n return (\n <div className=\"grid h-full place-items-center p-6 text-center\">\n <Text size=\"sm\" tone=\"muted\">\n Select a node in the tree or canvas to edit its properties.\n </Text>\n </div>\n )\n }\n\n const manifest = studio.registry.getManifest(node.componentId)\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <PanelHeader>\n <Text size=\"sm\" weight=\"semibold\">\n Inspector\n </Text>\n <Spacer />\n <Code tone=\"muted\" title={node.componentId}>\n {componentDisplayName(node.componentId, manifest)}\n </Code>\n </PanelHeader>\n\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <SettingsTab node={node} manifest={manifest} />\n </div>\n </div>\n )\n}\n\n/** The full property surface: node identity, static props, condition, slots. */\nfunction SettingsTab({\n node,\n manifest,\n}: {\n node: ComponentNode\n manifest: ComponentManifest | undefined\n}) {\n // Container layout props render in the dedicated Layout section (EC-162),\n // not as a second set of generic controls.\n const layoutManaged = new Set(layoutManagedPropNames(node))\n // EC-190: `blocks` props are slots (named child regions), edited via the page\n // tree / composition surface, not as a scalar control — so they show in the\n // Slots section, not Props.\n const props = manifest\n ? Object.entries(manifest.props).filter(\n ([name, spec]) => !layoutManaged.has(name) && spec.type !== 'blocks',\n )\n : []\n const slots = manifest ? manifestSlots(manifest) : []\n\n return (\n <>\n {/* EC-192: the editor-facing inspector no longer renders the raw node id or\n namespaced component id (the persona promise). The component's human name\n is in the panel header (with the manifest id in its `title`); the canvas\n and tree provide selection. */}\n {/* Contextual layout inspector for Stack/Grid containers (EC-162). */}\n <LayoutSection node={node} />\n\n {!manifest ? (\n <div className=\"p-3\">\n <Badge variant=\"error\">unknown component</Badge>\n <Text size=\"xs\" tone=\"muted\" className=\"mt-2\">\n No manifest is registered for this component, so its props can't be edited.\n </Text>\n </div>\n ) : (\n <InspectorSection title=\"Props\">\n {props.length === 0 ? (\n <Text size=\"xs\" tone=\"subtle\">\n No editable props.\n </Text>\n ) : (\n <Stack gap=\"4\">\n {props.map(([name, spec]) => (\n // Key by node id so per-node local control state never leaks\n // across a selection change to a same-named prop.\n <PropField key={`${node.id}:${name}`} node={node} name={name} spec={spec} />\n ))}\n </Stack>\n )}\n </InspectorSection>\n )}\n\n {slots.length > 0 ? (\n <InspectorSection title=\"Slots\">\n <Stack gap=\"1\">\n {slots.map((slot) => {\n const count = node.slots?.[slot.name]?.length ?? 0\n const missing = slot.required && count === 0\n return (\n <Row key={slot.name} gap=\"2\">\n <Text size=\"xs\" title={slot.name}>\n {humanizeName(slot.name)}\n </Text>\n {slot.required ? (\n <Badge variant={missing ? 'error' : 'outline'}>required</Badge>\n ) : null}\n <Spacer />\n <Text size=\"xs\" tone=\"subtle\" mono>\n {count}\n </Text>\n </Row>\n )\n })}\n </Stack>\n <Text size=\"xs\" tone=\"subtle\" className=\"mt-2\">\n Add or reorder slot children from the page tree.\n </Text>\n </InspectorSection>\n ) : null}\n </>\n )\n}\n\nfunction PropField({ node, name, spec }: { node: ComponentNode; name: string; spec: PropField }) {\n const value = node.props?.[name]\n\n return (\n <Field label={localizedLabel(spec.form?.label, name)} htmlFor={`prop-${node.id}-${name}`}>\n <StaticControl node={node} name={name} spec={spec} value={value} />\n </Field>\n )\n}\n\n/**\n * EC-190: the inspector control to render for a prop field-def. The old\n * `control.kind` vocabulary derives from the field `type` plus the optional\n * `form.control` presentation hint on `text` fields. Slice 3 unifies this with\n * the CMS `FieldControl`; for now it keeps the existing per-kind control set.\n */\ntype ControlKind =\n | 'text'\n | 'textarea'\n | 'number'\n | 'boolean'\n | 'select'\n | 'json'\n | 'color'\n | 'richtext'\n | 'asset'\n | 'object'\n | 'relation'\nfunction 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\nfunction StaticControl({\n node,\n name,\n spec,\n value,\n}: {\n node: ComponentNode\n name: string\n spec: PropField\n value: PropValue | undefined\n}) {\n const builder = useBuilder()\n const current = value?.kind === 'static' ? value.value : (spec.default as JsonValue | undefined)\n const id = `prop-${node.id}-${name}`\n const coalesceKey = `${node.id}:${name}`\n\n function set(v: JsonValue) {\n builder.dispatch({ type: 'setProp', id: node.id, name, value: staticProp(v), coalesceKey })\n }\n function clear() {\n builder.dispatch({ type: 'removeProp', id: node.id, name })\n }\n\n // EC-190: validate with the prop's field-def value validator (was `spec.schema.safeParse`).\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 (was `spec.control.options`).\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) => set(e.target.checked)}\n className=\"size-4 accent-primary\"\n />\n <Text size=\"sm\" tone=\"muted\">\n {current === true ? 'true' : '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 set(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={set} onClear={clear} />\n } else if (kind === 'json') {\n control = <JsonControl id={id} current={current} onSet={set} />\n } else if (kind === 'color') {\n control = (\n <Row gap=\"2\">\n <input\n id={id}\n type=\"color\"\n value={\n typeof current === 'string' && /^#[0-9a-f]{6}$/i.test(current) ? current : '#000000'\n }\n onChange={(e) => set(e.target.value)}\n className=\"h-8 w-10 shrink-0 rounded-md border border-input bg-background\"\n />\n <Input\n value={typeof current === 'string' ? current : ''}\n onChange={(e) => set(e.target.value)}\n />\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) => set(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 ? clear() : set(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) => set(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 Value doesn't match the prop schema — the canvas falls back to the default.\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('Invalid JSON')\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\n"]}
|
|
1
|
+
{"version":3,"file":"property-inspector.js","sourceRoot":"","sources":["../../../src/components/builder/property-inspector.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useState } from 'react'\nimport { staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue, PropValue } from '@elytracms/core/project-graph'\nimport type { ComponentManifest, PropField } from '@elytracms/core/component-registry'\nimport { manifestSlots } from '@elytracms/core/component-registry'\nimport { fieldValueValidator } from '@elytracms/core/cms-core'\nimport type { FieldDef, SelectOption } from '@elytracms/core/cms-core'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport {\n Badge,\n Code,\n Field,\n Input,\n InspectorSection,\n PanelHeader,\n Row,\n Select,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { humanizeName } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { FieldControl } from '../cms/field-control'\nimport { FieldDensityProvider } from '../cms/field-density'\nimport { useBuilder } from './builder-store'\nimport { LayoutSection, layoutManagedPropNames } from './layout-inspector'\n\nexport function PropertyInspector() {\n const builder = useBuilder()\n const studio = getStudioRegistry()\n const node = builder.selectedNode\n\n if (!node) {\n return (\n <div className=\"grid h-full place-items-center p-6 text-center\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('builder.inspector.emptySelection')}\n </Text>\n </div>\n )\n }\n\n const manifest = studio.registry.getManifest(node.componentId)\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <PanelHeader>\n <Text size=\"sm\" weight=\"semibold\">\n {translate('builder.inspector.title')}\n </Text>\n <Spacer />\n <Code tone=\"muted\" title={node.componentId}>\n {componentDisplayName(node.componentId, manifest)}\n </Code>\n </PanelHeader>\n\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <SettingsTab node={node} manifest={manifest} />\n </div>\n </div>\n )\n}\n\n/** The full property surface: node identity, static props, condition, slots. */\nfunction SettingsTab({\n node,\n manifest,\n}: {\n node: ComponentNode\n manifest: ComponentManifest | undefined\n}) {\n // Container layout props render in the dedicated Layout section (EC-162),\n // not as a second set of generic controls.\n const layoutManaged = new Set(layoutManagedPropNames(node))\n // EC-190: `blocks` props are slots (named child regions), edited via the page\n // tree / composition surface, not as a scalar control — so they show in the\n // Slots section, not Props.\n const props = manifest\n ? Object.entries(manifest.props).filter(\n ([name, spec]) => !layoutManaged.has(name) && spec.type !== 'blocks',\n )\n : []\n const slots = manifest ? manifestSlots(manifest) : []\n\n return (\n <>\n {/* EC-192: the editor-facing inspector no longer renders the raw node id or\n namespaced component id (the persona promise). The component's human name\n is in the panel header (with the manifest id in its `title`); the canvas\n and tree provide selection. */}\n {/* Contextual layout inspector for Stack/Grid containers (EC-162). */}\n <LayoutSection node={node} />\n\n {!manifest ? (\n <div className=\"p-3\">\n <Badge variant=\"error\">{translate('builder.inspector.unknownComponent')}</Badge>\n <Text size=\"xs\" tone=\"muted\" className=\"mt-2\">\n {translate('builder.inspector.noManifest')}\n </Text>\n </div>\n ) : (\n <InspectorSection title={translate('builder.inspector.props')}>\n {props.length === 0 ? (\n <Text size=\"xs\" tone=\"subtle\">\n {translate('builder.inspector.noEditableProps')}\n </Text>\n ) : (\n <Stack gap=\"4\">\n {props.map(([name, spec]) => (\n // Key by node id so per-node local control state never leaks\n // across a selection change to a same-named prop.\n <PropField key={`${node.id}:${name}`} node={node} name={name} spec={spec} />\n ))}\n </Stack>\n )}\n </InspectorSection>\n )}\n\n {slots.length > 0 ? (\n <InspectorSection title={translate('builder.inspector.slots')}>\n <Stack gap=\"1\">\n {slots.map((slot) => {\n const count = node.slots?.[slot.name]?.length ?? 0\n const missing = slot.required && count === 0\n return (\n <Row key={slot.name} gap=\"2\">\n <Text size=\"xs\" title={slot.name}>\n {humanizeName(slot.name)}\n </Text>\n {slot.required ? (\n <Badge variant={missing ? 'error' : 'outline'}>{translate('common.required')}</Badge>\n ) : null}\n <Spacer />\n <Text size=\"xs\" tone=\"subtle\" mono>\n {count}\n </Text>\n </Row>\n )\n })}\n </Stack>\n <Text size=\"xs\" tone=\"subtle\" className=\"mt-2\">\n {translate('builder.inspector.slotsHint')}\n </Text>\n </InspectorSection>\n ) : null}\n </>\n )\n}\n\nfunction PropField({ node, name, spec }: { node: ComponentNode; name: string; spec: PropField }) {\n const value = node.props?.[name]\n\n return (\n <Field label={localizedLabel(spec.form?.label, name)} htmlFor={`prop-${node.id}-${name}`}>\n <StaticControl node={node} name={name} spec={spec} value={value} />\n </Field>\n )\n}\n\n/**\n * EC-190: the inspector control to render for a prop field-def. The old\n * `control.kind` vocabulary derives from the field `type` plus the optional\n * `form.control` presentation hint on `text` fields. Slice 3 unifies this with\n * the CMS `FieldControl`; for now it keeps the existing per-kind control set.\n */\ntype ControlKind =\n | 'text'\n | 'textarea'\n | 'number'\n | 'boolean'\n | 'select'\n | 'json'\n | 'color'\n | 'richtext'\n | 'asset'\n | 'object'\n | 'relation'\nfunction 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\nfunction StaticControl({\n node,\n name,\n spec,\n value,\n}: {\n node: ComponentNode\n name: string\n spec: PropField\n value: PropValue | undefined\n}) {\n const builder = useBuilder()\n const current = value?.kind === 'static' ? value.value : (spec.default as JsonValue | undefined)\n const id = `prop-${node.id}-${name}`\n const coalesceKey = `${node.id}:${name}`\n\n function set(v: JsonValue) {\n builder.dispatch({ type: 'setProp', id: node.id, name, value: staticProp(v), coalesceKey })\n }\n function clear() {\n builder.dispatch({ type: 'removeProp', id: node.id, name })\n }\n\n // EC-190: validate with the prop's field-def value validator (was `spec.schema.safeParse`).\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 (was `spec.control.options`).\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) => set(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 set(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={set} onClear={clear} />\n } else if (kind === 'json') {\n control = <JsonControl id={id} current={current} onSet={set} />\n } else if (kind === 'color') {\n control = (\n <Row gap=\"2\">\n <input\n id={id}\n type=\"color\"\n value={\n typeof current === 'string' && /^#[0-9a-f]{6}$/i.test(current) ? current : '#000000'\n }\n onChange={(e) => set(e.target.value)}\n className=\"h-8 w-10 shrink-0 rounded-md border border-input bg-background\"\n />\n <Input\n value={typeof current === 'string' ? current : ''}\n onChange={(e) => set(e.target.value)}\n />\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) => set(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 ? clear() : set(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) => set(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\n"]}
|
|
@@ -4,7 +4,7 @@ import { blockRootsOf, getNode } from '@elytracms/core/project-graph';
|
|
|
4
4
|
import { readDocumentRef } from '@elytracms/core/cms-core';
|
|
5
5
|
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Field, Input, Stack, toast, } from '@elytracms/studio/ui';
|
|
6
6
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
7
|
-
import { localizedLabel } from '@elytracms/studio/lib/i18n';
|
|
7
|
+
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
8
8
|
import { useCms } from '../cms/cms-store';
|
|
9
9
|
import { useBuilder } from './builder-store';
|
|
10
10
|
import { buildReusableValues, makeEmbedNode, resolveReuseCapability } from './reuse';
|
|
@@ -40,7 +40,7 @@ export function ReuseProvider({ children }) {
|
|
|
40
40
|
return;
|
|
41
41
|
const manifestTitle = manifests.find((m) => m.id === node.componentId)?.title;
|
|
42
42
|
const collection = capability ? cms.getCollection(capability.collectionId) : undefined;
|
|
43
|
-
const defaultName = manifestTitle ?? (localizedLabel(collection?.form?.labelSingular) || localizedLabel(collection?.form?.label) || '
|
|
43
|
+
const defaultName = manifestTitle ?? (localizedLabel(collection?.form?.labelSingular) || localizedLabel(collection?.form?.label) || translate('builder.reuse.defaultElementName'));
|
|
44
44
|
setPending({ nodeId, defaultName });
|
|
45
45
|
}, [canMakeReusable, builder, manifests, capability, cms]);
|
|
46
46
|
const confirmMakeReusable = useCallback((name, publish) => {
|
|
@@ -69,10 +69,10 @@ export function ReuseProvider({ children }) {
|
|
|
69
69
|
// SAVE DRAFT keeps it editable, visible in the draft canvas, surfaced as a
|
|
70
70
|
// pending reference until published. No silent auto-publish (AD-16).
|
|
71
71
|
if (publish) {
|
|
72
|
-
void cms.publishDocument(ref).then(() => toast.success(
|
|
72
|
+
void cms.publishDocument(ref).then(() => toast.success(translate('builder.reuse.madeReusablePublished', { name })), () => toast.error(translate('builder.reuse.madeReusablePublishFailed', { name })));
|
|
73
73
|
}
|
|
74
74
|
else {
|
|
75
|
-
toast.success(
|
|
75
|
+
toast.success(translate('builder.reuse.madeReusableDraft', { name }));
|
|
76
76
|
}
|
|
77
77
|
}, [capability, pending, builder, cms]);
|
|
78
78
|
const detach = useCallback((nodeId) => {
|
|
@@ -85,17 +85,17 @@ export function ReuseProvider({ children }) {
|
|
|
85
85
|
const prop = node.props?.[cap.relationProp];
|
|
86
86
|
const ref = prop?.kind === 'static' ? readDocumentRef(prop.value) : undefined;
|
|
87
87
|
if (!ref) {
|
|
88
|
-
toast.error('
|
|
88
|
+
toast.error(translate('builder.reuse.noTargetToDetach'));
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
const target = cms.getDocument(ref);
|
|
92
92
|
const blocks = blockRootsOf(target?.values?.[cap.contentField]);
|
|
93
93
|
if (blocks.length === 0) {
|
|
94
|
-
toast.error('
|
|
94
|
+
toast.error(translate('builder.reuse.noContentToDetach'));
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
builder.dispatch({ type: 'replace', id: nodeId, nodes: blocks });
|
|
98
|
-
toast.success('
|
|
98
|
+
toast.success(translate('builder.reuse.detachedToCopy'));
|
|
99
99
|
}, [capability, builder, cms]);
|
|
100
100
|
const value = useMemo(() => ({ capability, canMakeReusable, canDetach, startMakeReusable, detach }), [capability, canMakeReusable, canDetach, startMakeReusable, detach]);
|
|
101
101
|
return (_jsxs(ReuseContext.Provider, { value: value, children: [children, _jsx(MakeReusableDialog, { open: pending !== null, defaultName: pending?.defaultName ?? '', onConfirm: confirmMakeReusable, onCancel: () => setPending(null) })] }));
|
|
@@ -121,9 +121,9 @@ function MakeReusableDialog({ open, defaultName, onConfirm, onCancel, }) {
|
|
|
121
121
|
onCancel();
|
|
122
122
|
setSeededFor(null);
|
|
123
123
|
}
|
|
124
|
-
}, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children:
|
|
124
|
+
}, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('builder.reuse.makeReusable') }), _jsx(DialogDescription, { children: translate('builder.reuse.makeReusableDescription') })] }), _jsxs("form", { onSubmit: (event) => {
|
|
125
125
|
event.preventDefault();
|
|
126
126
|
commit(true);
|
|
127
|
-
}, children: [_jsx(Stack, { gap: "3", children: _jsx(Field, { label:
|
|
127
|
+
}, children: [_jsx(Stack, { gap: "3", children: _jsx(Field, { label: translate('builder.reuse.nameLabel'), htmlFor: "make-reusable-name", required: true, children: _jsx(Input, { id: "make-reusable-name", value: name, onChange: (event) => setName(event.target.value), placeholder: translate('builder.reuse.namePlaceholder'), autoFocus: true, required: true }) }) }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "mr-auto", onClick: onCancel, children: translate('common.cancel') }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", disabled: empty, onClick: () => commit(false), "data-testid": "make-reusable-draft", children: translate('builder.reuse.saveDraft') }), _jsx(Button, { type: "submit", size: "sm", disabled: empty, "data-testid": "make-reusable-publish", children: translate('builder.reuse.publish') })] })] })] }) }));
|
|
128
128
|
}
|
|
129
129
|
//# sourceMappingURL=reuse-context.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reuse-context.js","sourceRoot":"","sources":["../../../src/components/builder/reuse-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEjF,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAA;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EACL,MAAM,EACN,MAAM,EACN,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,KAAK,EACL,KAAK,EACL,KAAK,EACL,KAAK,GACN,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAC3D,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAA;AA0BpF,MAAM,YAAY,GAAG,aAAa,CAAuB,IAAI,CAAC,CAAA;AAE9D,MAAM,UAAU,QAAQ;IACtB,OAAO,UAAU,CAAC,YAAY,CAAC,CAAA;AACjC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAE,QAAQ,EAA2B;IACjE,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;IACpB,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAiD,IAAI,CAAC,CAAA;IAE5F,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,iBAAiB,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;IACxE,MAAM,UAAU,GAAG,OAAO,CACxB,GAAG,EAAE,CAAC,sBAAsB,CAAC,SAAS,EAAE,GAAG,CAAC,WAAW,CAAC,EACxD,CAAC,SAAS,EAAE,GAAG,CAAC,WAAW,CAAC,CAC7B,CAAA;IAED,MAAM,eAAe,GAAG,WAAW,CACjC,CAAC,MAAc,EAAW,EAAE;QAC1B,IAAI,CAAC,UAAU;YAAE,OAAO,KAAK,CAAA;QAC7B,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAC7C,yEAAyE;QACzE,wEAAwE;QACxE,4DAA4D;QAC5D,OAAO,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,EAAE,CAAA;IAC7D,CAAC,EACD,CAAC,UAAU,EAAE,OAAO,CAAC,CACtB,CAAA;IAED,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,MAAc,EAAW,EAAE;QAC1B,IAAI,CAAC,UAAU;YAAE,OAAO,KAAK,CAAA;QAC7B,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,UAAU,CAAC,gBAAgB,CAAA;IACtF,CAAC,EACD,CAAC,UAAU,EAAE,OAAO,CAAC,CACtB,CAAA;IAED,MAAM,iBAAiB,GAAG,WAAW,CACnC,CAAC,MAAc,EAAE,EAAE;QACjB,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC;YAAE,OAAM;QACpC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAC7C,IAAI,CAAC,IAAI;YAAE,OAAM;QACjB,MAAM,aAAa,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,WAAW,CAAC,EAAE,KAAK,CAAA;QAC7E,MAAM,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QACtF,MAAM,WAAW,GACf,aAAa,IAAI,CAAC,cAAc,CAAC,UAAU,EAAE,IAAI,EAAE,aAAa,CAAC,IAAI,cAAc,CAAC,UAAU,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,kBAAkB,CAAC,CAAA;QACrI,UAAU,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAA;IACrC,CAAC,EACD,CAAC,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,CAAC,CACvD,CAAA;IAED,MAAM,mBAAmB,GAAG,WAAW,CACrC,CAAC,IAAY,EAAE,OAAgB,EAAE,EAAE;QACjC,MAAM,GAAG,GAAG,UAAU,CAAA;QACtB,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO;YAAE,OAAM;QAC5B,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;QACrD,MAAM,UAAU,GAAG,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;QACtD,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACzB,UAAU,CAAC,IAAI,CAAC,CAAA;YAChB,OAAM;QACR,CAAC;QACD,6EAA6E;QAC7E,qEAAqE;QACrE,MAAM,GAAG,GAAG,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;QAChD,MAAM,MAAM,GAAG,mBAAmB,CAAC,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAA;QAClG,GAAG,CAAC,YAAY,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,YAAY,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;QACtE,uEAAuE;QACvE,2EAA2E;QAC3E,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;QAC9F,UAAU,CAAC,IAAI,CAAC,CAAA;QAChB,4EAA4E;QAC5E,+EAA+E;QAC/E,iFAAiF;QACjF,kFAAkF;QAClF,2EAA2E;QAC3E,qEAAqE;QACrE,IAAI,OAAO,EAAE,CAAC;YACZ,KAAK,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,IAAI,CAChC,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,IAAI,8BAA8B,CAAC,EAChE,GAAG,EAAE,CACH,KAAK,CAAC,KAAK,CACT,SAAS,IAAI,mEAAmE,CACjF,CACJ,CAAA;QACH,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,OAAO,CAAC,SAAS,IAAI,wDAAwD,CAAC,CAAA;QACtF,CAAC;IACH,CAAC,EACD,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,CACpC,CAAA;IAED,MAAM,MAAM,GAAG,WAAW,CACxB,CAAC,MAAc,EAAE,EAAE;QACjB,MAAM,GAAG,GAAG,UAAU,CAAA;QACtB,IAAI,CAAC,GAAG;YAAE,OAAM;QAChB,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAC7C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,KAAK,GAAG,CAAC,gBAAgB;YAAE,OAAM;QAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;QAC3C,MAAM,GAAG,GAAG,IAAI,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAC7E,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,KAAK,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAA;YACpD,OAAM;QACR,CAAC;QACD,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;QACnC,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAA;QAC/D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,KAAK,CAAC,KAAK,CAAC,kDAAkD,CAAC,CAAA;YAC/D,OAAM;QACR,CAAC;QACD,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;QAChE,KAAK,CAAC,OAAO,CAAC,yDAAyD,CAAC,CAAA;IAC1E,CAAC,EACD,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,CAAC,CAC3B,CAAA;IAED,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,EAAE,CAAC,EAC7E,CAAC,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,CAAC,CACpE,CAAA;IAED,OAAO,CACL,MAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,aAChC,QAAQ,EACT,KAAC,kBAAkB,IACjB,IAAI,EAAE,OAAO,KAAK,IAAI,EACtB,WAAW,EAAE,OAAO,EAAE,WAAW,IAAI,EAAE,EACvC,SAAS,EAAE,mBAAmB,EAC9B,QAAQ,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,GAChC,IACoB,CACzB,CAAA;AACH,CAAC;AAED,8DAA8D;AAC9D,SAAS,kBAAkB,CAAC,EAC1B,IAAI,EACJ,WAAW,EACX,SAAS,EACT,QAAQ,GAOT;IACC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAA;IAC7C,oEAAoE;IACpE,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAC/D,IAAI,IAAI,IAAI,SAAS,KAAK,WAAW,EAAE,CAAC;QACtC,OAAO,CAAC,WAAW,CAAC,CAAA;QACpB,YAAY,CAAC,WAAW,CAAC,CAAA;IAC3B,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAA;IAChC,SAAS,MAAM,CAAC,OAAgB;QAC9B,IAAI,KAAK;YAAE,OAAM;QACjB,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAA;QAC/B,YAAY,CAAC,IAAI,CAAC,CAAA;IACpB,CAAC;IAED,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE;YACrB,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,QAAQ,EAAE,CAAA;gBACV,YAAY,CAAC,IAAI,CAAC,CAAA;YACpB,CAAC;QACH,CAAC,YAED,MAAC,aAAa,eACZ,MAAC,YAAY,eACX,KAAC,WAAW,gCAA4B,EACxC,KAAC,iBAAiB,kPAIE,IACP,EAEf,gBACE,QAAQ,EAAE,CAAC,KAAgB,EAAE,EAAE;wBAC7B,KAAK,CAAC,cAAc,EAAE,CAAA;wBACtB,MAAM,CAAC,IAAI,CAAC,CAAA;oBACd,CAAC,aAED,KAAC,KAAK,IAAC,GAAG,EAAC,GAAG,YACZ,KAAC,KAAK,IAAC,KAAK,EAAC,MAAM,EAAC,OAAO,EAAC,oBAAoB,EAAC,QAAQ,kBACvD,KAAC,KAAK,IACJ,EAAE,EAAC,oBAAoB,EACvB,KAAK,EAAE,IAAI,EACX,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAChD,WAAW,EAAC,iBAAiB,EAC7B,SAAS,QACT,QAAQ,SACR,GACI,GACF,EACR,MAAC,YAAY,IAAC,SAAS,EAAC,MAAM,aAE5B,KAAC,MAAM,IAAC,IAAI,EAAC,QAAQ,EAAC,OAAO,EAAC,OAAO,EAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,SAAS,EAAC,OAAO,EAAE,QAAQ,uBAE5E,EACT,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,IAAI,EACT,QAAQ,EAAE,KAAK,EACf,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,iBAChB,qBAAqB,2BAG1B,EACT,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,IAAI,EAAC,IAAI,EACT,QAAQ,EAAE,KAAK,iBACH,uBAAuB,wBAG5B,IACI,IACV,IACO,GACT,CACV,CAAA;AACH,CAAC","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useState } from 'react'\nimport type { FormEvent, ReactNode } from 'react'\nimport { blockRootsOf, getNode } from '@elytracms/core/project-graph'\nimport { readDocumentRef } from '@elytracms/core/cms-core'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Field,\n Input,\n Stack,\n toast,\n} from '@elytracms/studio/ui'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { localizedLabel } from '@elytracms/studio/lib/i18n'\nimport { useCms } from '../cms/cms-store'\nimport { useBuilder } from './builder-store'\nimport { buildReusableValues, makeEmbedNode, resolveReuseCapability } from './reuse'\nimport type { ReuseCapability } from './reuse'\n\n/**\n * In-canvas reuse gestures (EC-282): \"Make reusable\" promotes a selected block to\n * a new reusable document and replaces it in place with a transcluding embed;\n * \"Detach to local copy\" inlines an embed's referenced blocks so a page can\n * diverge. Both round-trip through the existing persistence + in-Convex reference\n * index (EC-277), so where-used is correct immediately.\n *\n * The provider needs BOTH the builder store (the edited composition) and the CMS\n * store (to create/read the reusable document), so it mounts inside the\n * composition-field editor's `BuilderProvider`, itself inside the CMS document\n * editor. Where no provider is mounted (or the capability is absent), the menu\n * reads `null` and simply omits the gestures.\n */\nexport interface ReuseGestures {\n capability: ReuseCapability | null\n canMakeReusable: (nodeId: string) => boolean\n canDetach: (nodeId: string) => boolean\n /** Open the name dialog for promoting `nodeId`. */\n startMakeReusable: (nodeId: string) => void\n /** Replace a `Reusable` embed with an editable inline copy of its target's blocks. */\n detach: (nodeId: string) => void\n}\n\nconst ReuseContext = createContext<ReuseGestures | null>(null)\n\nexport function useReuse(): ReuseGestures | null {\n return useContext(ReuseContext)\n}\n\nexport function ReuseProvider({ children }: { children: ReactNode }) {\n const builder = useBuilder()\n const cms = useCms()\n const [pending, setPending] = useState<{ nodeId: string; defaultName: string } | null>(null)\n\n const manifests = useMemo(() => getStudioRegistry().registry.list(), [])\n const capability = useMemo(\n () => resolveReuseCapability(manifests, cms.collections),\n [manifests, cms.collections],\n )\n\n const canMakeReusable = useCallback(\n (nodeId: string): boolean => {\n if (!capability) return false\n const node = getNode(builder.present, nodeId)\n // Anything but the synthetic composition root can be promoted; nesting a\n // reusable inside a reusable is allowed (the renderer + bake are cycle-\n // guarded, EC-281), so an embed itself is still promotable.\n return node !== undefined && node.id !== builder.present.id\n },\n [capability, builder],\n )\n\n const canDetach = useCallback(\n (nodeId: string): boolean => {\n if (!capability) return false\n return getNode(builder.present, nodeId)?.componentId === capability.embedComponentId\n },\n [capability, builder],\n )\n\n const startMakeReusable = useCallback(\n (nodeId: string) => {\n if (!canMakeReusable(nodeId)) return\n const node = getNode(builder.present, nodeId)\n if (!node) return\n const manifestTitle = manifests.find((m) => m.id === node.componentId)?.title\n const collection = capability ? cms.getCollection(capability.collectionId) : undefined\n const defaultName =\n manifestTitle ?? (localizedLabel(collection?.form?.labelSingular) || localizedLabel(collection?.form?.label) || 'Reusable element')\n setPending({ nodeId, defaultName })\n },\n [canMakeReusable, builder, manifests, capability, cms],\n )\n\n const confirmMakeReusable = useCallback(\n (name: string, publish: boolean) => {\n const cap = capability\n if (!cap || !pending) return\n const node = getNode(builder.present, pending.nodeId)\n const collection = cms.getCollection(cap.collectionId)\n if (!node || !collection) {\n setPending(null)\n return\n }\n // Mint the reusable document, then write its title + content (createDocument\n // only seeds defaults). The new id is referenced by the embed below.\n const ref = cms.createDocument(cap.collectionId)\n const values = buildReusableValues(collection, cap, name, node, cms.documentsOf(cap.collectionId))\n cms.saveDocument({ collection: cap.collectionId, id: ref.id, values })\n // Replace the selection in place with a transcluding embed (the action\n // clones to a fresh id; the source `node` stays untouched in the new doc).\n builder.dispatch({ type: 'replace', id: pending.nodeId, nodes: [makeEmbedNode(cap, ref.id)] })\n setPending(null)\n // EC-300: the new reusable is a separate document with its own lifecycle. A\n // draft-only target resolves to nothing in the published perspective (delivery\n // + the published canvas view), so the editor chooses at creation: PUBLISH keeps\n // the section visible everywhere immediately (the continuity-preserving default);\n // SAVE DRAFT keeps it editable, visible in the draft canvas, surfaced as a\n // pending reference until published. No silent auto-publish (AD-16).\n if (publish) {\n void cms.publishDocument(ref).then(\n () => toast.success(`Made “${name}” reusable and published it.`),\n () =>\n toast.error(\n `Made “${name}” reusable, but publishing failed — publish it from its document.`,\n ),\n )\n } else {\n toast.success(`Made “${name}” reusable (draft) — publish it to show on live pages.`)\n }\n },\n [capability, pending, builder, cms],\n )\n\n const detach = useCallback(\n (nodeId: string) => {\n const cap = capability\n if (!cap) return\n const node = getNode(builder.present, nodeId)\n if (!node || node.componentId !== cap.embedComponentId) return\n const prop = node.props?.[cap.relationProp]\n const ref = prop?.kind === 'static' ? readDocumentRef(prop.value) : undefined\n if (!ref) {\n toast.error('This element has no target to detach.')\n return\n }\n const target = cms.getDocument(ref)\n const blocks = blockRootsOf(target?.values?.[cap.contentField])\n if (blocks.length === 0) {\n toast.error('The referenced element has no content to detach.')\n return\n }\n builder.dispatch({ type: 'replace', id: nodeId, nodes: blocks })\n toast.success('Detached to an editable copy — the live link is broken.')\n },\n [capability, builder, cms],\n )\n\n const value = useMemo<ReuseGestures>(\n () => ({ capability, canMakeReusable, canDetach, startMakeReusable, detach }),\n [capability, canMakeReusable, canDetach, startMakeReusable, detach],\n )\n\n return (\n <ReuseContext.Provider value={value}>\n {children}\n <MakeReusableDialog\n open={pending !== null}\n defaultName={pending?.defaultName ?? ''}\n onConfirm={confirmMakeReusable}\n onCancel={() => setPending(null)}\n />\n </ReuseContext.Provider>\n )\n}\n\n/** Name-and-create dialog for the \"Make reusable\" gesture. */\nfunction MakeReusableDialog({\n open,\n defaultName,\n onConfirm,\n onCancel,\n}: {\n open: boolean\n defaultName: string\n /** `publish` chooses the new reusable's initial state (EC-300/AD-16). */\n onConfirm: (name: string, publish: boolean) => void\n onCancel: () => void\n}) {\n const [name, setName] = useState(defaultName)\n // Re-seed the field each time the dialog opens for a new selection.\n const [seededFor, setSeededFor] = useState<string | null>(null)\n if (open && seededFor !== defaultName) {\n setName(defaultName)\n setSeededFor(defaultName)\n }\n\n const empty = name.trim() === ''\n function commit(publish: boolean) {\n if (empty) return\n onConfirm(name.trim(), publish)\n setSeededFor(null)\n }\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!next) {\n onCancel()\n setSeededFor(null)\n }\n }}\n >\n <DialogContent>\n <DialogHeader>\n <DialogTitle>Make reusable</DialogTitle>\n <DialogDescription>\n Moves the selected block into a new reusable element and embeds it here. The same element\n can then be embedded on any page. Publish it now to keep the section visible everywhere,\n or save it as a draft to keep editing first.\n </DialogDescription>\n </DialogHeader>\n {/* Enter submits the primary action (Publish) — the continuity-preserving default. */}\n <form\n onSubmit={(event: FormEvent) => {\n event.preventDefault()\n commit(true)\n }}\n >\n <Stack gap=\"3\">\n <Field label=\"Name\" htmlFor=\"make-reusable-name\" required>\n <Input\n id=\"make-reusable-name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n placeholder=\"Reservation CTA\"\n autoFocus\n required\n />\n </Field>\n </Stack>\n <DialogFooter className=\"mt-4\">\n {/* Cancel pinned left; the two commit actions group right. */}\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"mr-auto\" onClick={onCancel}>\n Cancel\n </Button>\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={empty}\n onClick={() => commit(false)}\n data-testid=\"make-reusable-draft\"\n >\n Save Draft\n </Button>\n <Button\n type=\"submit\"\n size=\"sm\"\n disabled={empty}\n data-testid=\"make-reusable-publish\"\n >\n Publish\n </Button>\n </DialogFooter>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reuse-context.js","sourceRoot":"","sources":["../../../src/components/builder/reuse-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useState } from 'react'\nimport type { FormEvent, ReactNode } from 'react'\nimport { blockRootsOf, getNode } from '@elytracms/core/project-graph'\nimport { readDocumentRef } from '@elytracms/core/cms-core'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Field,\n Input,\n Stack,\n toast,\n} from '@elytracms/studio/ui'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { useCms } from '../cms/cms-store'\nimport { useBuilder } from './builder-store'\nimport { buildReusableValues, makeEmbedNode, resolveReuseCapability } from './reuse'\nimport type { ReuseCapability } from './reuse'\n\n/**\n * In-canvas reuse gestures (EC-282): \"Make reusable\" promotes a selected block to\n * a new reusable document and replaces it in place with a transcluding embed;\n * \"Detach to local copy\" inlines an embed's referenced blocks so a page can\n * diverge. Both round-trip through the existing persistence + in-Convex reference\n * index (EC-277), so where-used is correct immediately.\n *\n * The provider needs BOTH the builder store (the edited composition) and the CMS\n * store (to create/read the reusable document), so it mounts inside the\n * composition-field editor's `BuilderProvider`, itself inside the CMS document\n * editor. Where no provider is mounted (or the capability is absent), the menu\n * reads `null` and simply omits the gestures.\n */\nexport interface ReuseGestures {\n capability: ReuseCapability | null\n canMakeReusable: (nodeId: string) => boolean\n canDetach: (nodeId: string) => boolean\n /** Open the name dialog for promoting `nodeId`. */\n startMakeReusable: (nodeId: string) => void\n /** Replace a `Reusable` embed with an editable inline copy of its target's blocks. */\n detach: (nodeId: string) => void\n}\n\nconst ReuseContext = createContext<ReuseGestures | null>(null)\n\nexport function useReuse(): ReuseGestures | null {\n return useContext(ReuseContext)\n}\n\nexport function ReuseProvider({ children }: { children: ReactNode }) {\n const builder = useBuilder()\n const cms = useCms()\n const [pending, setPending] = useState<{ nodeId: string; defaultName: string } | null>(null)\n\n const manifests = useMemo(() => getStudioRegistry().registry.list(), [])\n const capability = useMemo(\n () => resolveReuseCapability(manifests, cms.collections),\n [manifests, cms.collections],\n )\n\n const canMakeReusable = useCallback(\n (nodeId: string): boolean => {\n if (!capability) return false\n const node = getNode(builder.present, nodeId)\n // Anything but the synthetic composition root can be promoted; nesting a\n // reusable inside a reusable is allowed (the renderer + bake are cycle-\n // guarded, EC-281), so an embed itself is still promotable.\n return node !== undefined && node.id !== builder.present.id\n },\n [capability, builder],\n )\n\n const canDetach = useCallback(\n (nodeId: string): boolean => {\n if (!capability) return false\n return getNode(builder.present, nodeId)?.componentId === capability.embedComponentId\n },\n [capability, builder],\n )\n\n const startMakeReusable = useCallback(\n (nodeId: string) => {\n if (!canMakeReusable(nodeId)) return\n const node = getNode(builder.present, nodeId)\n if (!node) return\n const manifestTitle = manifests.find((m) => m.id === node.componentId)?.title\n const collection = capability ? cms.getCollection(capability.collectionId) : undefined\n const defaultName =\n manifestTitle ?? (localizedLabel(collection?.form?.labelSingular) || localizedLabel(collection?.form?.label) || translate('builder.reuse.defaultElementName'))\n setPending({ nodeId, defaultName })\n },\n [canMakeReusable, builder, manifests, capability, cms],\n )\n\n const confirmMakeReusable = useCallback(\n (name: string, publish: boolean) => {\n const cap = capability\n if (!cap || !pending) return\n const node = getNode(builder.present, pending.nodeId)\n const collection = cms.getCollection(cap.collectionId)\n if (!node || !collection) {\n setPending(null)\n return\n }\n // Mint the reusable document, then write its title + content (createDocument\n // only seeds defaults). The new id is referenced by the embed below.\n const ref = cms.createDocument(cap.collectionId)\n const values = buildReusableValues(collection, cap, name, node, cms.documentsOf(cap.collectionId))\n cms.saveDocument({ collection: cap.collectionId, id: ref.id, values })\n // Replace the selection in place with a transcluding embed (the action\n // clones to a fresh id; the source `node` stays untouched in the new doc).\n builder.dispatch({ type: 'replace', id: pending.nodeId, nodes: [makeEmbedNode(cap, ref.id)] })\n setPending(null)\n // EC-300: the new reusable is a separate document with its own lifecycle. A\n // draft-only target resolves to nothing in the published perspective (delivery\n // + the published canvas view), so the editor chooses at creation: PUBLISH keeps\n // the section visible everywhere immediately (the continuity-preserving default);\n // SAVE DRAFT keeps it editable, visible in the draft canvas, surfaced as a\n // pending reference until published. No silent auto-publish (AD-16).\n if (publish) {\n void cms.publishDocument(ref).then(\n () => toast.success(translate('builder.reuse.madeReusablePublished', { name })),\n () => toast.error(translate('builder.reuse.madeReusablePublishFailed', { name })),\n )\n } else {\n toast.success(translate('builder.reuse.madeReusableDraft', { name }))\n }\n },\n [capability, pending, builder, cms],\n )\n\n const detach = useCallback(\n (nodeId: string) => {\n const cap = capability\n if (!cap) return\n const node = getNode(builder.present, nodeId)\n if (!node || node.componentId !== cap.embedComponentId) return\n const prop = node.props?.[cap.relationProp]\n const ref = prop?.kind === 'static' ? readDocumentRef(prop.value) : undefined\n if (!ref) {\n toast.error(translate('builder.reuse.noTargetToDetach'))\n return\n }\n const target = cms.getDocument(ref)\n const blocks = blockRootsOf(target?.values?.[cap.contentField])\n if (blocks.length === 0) {\n toast.error(translate('builder.reuse.noContentToDetach'))\n return\n }\n builder.dispatch({ type: 'replace', id: nodeId, nodes: blocks })\n toast.success(translate('builder.reuse.detachedToCopy'))\n },\n [capability, builder, cms],\n )\n\n const value = useMemo<ReuseGestures>(\n () => ({ capability, canMakeReusable, canDetach, startMakeReusable, detach }),\n [capability, canMakeReusable, canDetach, startMakeReusable, detach],\n )\n\n return (\n <ReuseContext.Provider value={value}>\n {children}\n <MakeReusableDialog\n open={pending !== null}\n defaultName={pending?.defaultName ?? ''}\n onConfirm={confirmMakeReusable}\n onCancel={() => setPending(null)}\n />\n </ReuseContext.Provider>\n )\n}\n\n/** Name-and-create dialog for the \"Make reusable\" gesture. */\nfunction MakeReusableDialog({\n open,\n defaultName,\n onConfirm,\n onCancel,\n}: {\n open: boolean\n defaultName: string\n /** `publish` chooses the new reusable's initial state (EC-300/AD-16). */\n onConfirm: (name: string, publish: boolean) => void\n onCancel: () => void\n}) {\n const [name, setName] = useState(defaultName)\n // Re-seed the field each time the dialog opens for a new selection.\n const [seededFor, setSeededFor] = useState<string | null>(null)\n if (open && seededFor !== defaultName) {\n setName(defaultName)\n setSeededFor(defaultName)\n }\n\n const empty = name.trim() === ''\n function commit(publish: boolean) {\n if (empty) return\n onConfirm(name.trim(), publish)\n setSeededFor(null)\n }\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!next) {\n onCancel()\n setSeededFor(null)\n }\n }}\n >\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{translate('builder.reuse.makeReusable')}</DialogTitle>\n <DialogDescription>\n {translate('builder.reuse.makeReusableDescription')}\n </DialogDescription>\n </DialogHeader>\n {/* Enter submits the primary action (Publish) — the continuity-preserving default. */}\n <form\n onSubmit={(event: FormEvent) => {\n event.preventDefault()\n commit(true)\n }}\n >\n <Stack gap=\"3\">\n <Field label={translate('builder.reuse.nameLabel')} htmlFor=\"make-reusable-name\" required>\n <Input\n id=\"make-reusable-name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n placeholder={translate('builder.reuse.namePlaceholder')}\n autoFocus\n required\n />\n </Field>\n </Stack>\n <DialogFooter className=\"mt-4\">\n {/* Cancel pinned left; the two commit actions group right. */}\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"mr-auto\" onClick={onCancel}>\n {translate('common.cancel')}\n </Button>\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={empty}\n onClick={() => commit(false)}\n data-testid=\"make-reusable-draft\"\n >\n {translate('builder.reuse.saveDraft')}\n </Button>\n <Button\n type=\"submit\"\n size=\"sm\"\n disabled={empty}\n data-testid=\"make-reusable-publish\"\n >\n {translate('builder.reuse.publish')}\n </Button>\n </DialogFooter>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
4
5
|
const TOOLBAR_OFFSET_PX = 8;
|
|
5
6
|
function selectionState(doc, host) {
|
|
6
7
|
const selection = doc.getSelection();
|
|
@@ -77,7 +78,7 @@ export function RichTextToolbar({ doc, host }) {
|
|
|
77
78
|
return null;
|
|
78
79
|
const left = state?.left ?? savedRangeRef.current?.getBoundingClientRect().left ?? 0;
|
|
79
80
|
const top = state?.top ?? savedRangeRef.current?.getBoundingClientRect().top ?? 0;
|
|
80
|
-
return createPortal(_jsx("div", { "data-ec-rt-toolbar": "", role: "toolbar", "aria-label":
|
|
81
|
+
return createPortal(_jsx("div", { "data-ec-rt-toolbar": "", role: "toolbar", "aria-label": translate('builder.richText.toolbarLabel'), className: "ec-rt-toolbar", style: {
|
|
81
82
|
position: 'fixed',
|
|
82
83
|
left: Math.max(8, left),
|
|
83
84
|
top: Math.max(8, top - TOOLBAR_OFFSET_PX),
|
|
@@ -90,13 +91,13 @@ export function RichTextToolbar({ doc, host }) {
|
|
|
90
91
|
}, children: linkOpen ? (_jsxs("form", { className: "ec-rt-toolbar-row", onSubmit: (e) => {
|
|
91
92
|
e.preventDefault();
|
|
92
93
|
applyLink(linkDraft);
|
|
93
|
-
}, children: [_jsx("input", { autoFocus: true, type: "text", value: linkDraft, placeholder:
|
|
94
|
+
}, children: [_jsx("input", { autoFocus: true, type: "text", value: linkDraft, placeholder: translate('builder.richText.linkPlaceholder'), "aria-label": translate('builder.richText.linkUrl'), onChange: (e) => setLinkDraft(e.target.value), onKeyDown: (e) => {
|
|
94
95
|
if (e.key === 'Escape') {
|
|
95
96
|
e.preventDefault();
|
|
96
97
|
e.stopPropagation();
|
|
97
98
|
closeLinkEditor();
|
|
98
99
|
}
|
|
99
|
-
}, className: "ec-rt-toolbar-input" }), _jsx("button", { type: "submit", className: "ec-rt-toolbar-btn", "aria-label":
|
|
100
|
+
}, className: "ec-rt-toolbar-input" }), _jsx("button", { type: "submit", className: "ec-rt-toolbar-btn", "aria-label": translate('builder.richText.applyLink'), children: translate('builder.richText.set') })] })) : (_jsxs("div", { className: "ec-rt-toolbar-row", children: [_jsx(MarkButton, { label: translate('builder.richText.bold'), active: state?.bold ?? false, onClick: () => exec('bold'), children: "B" }), _jsx(MarkButton, { label: translate('builder.richText.italic'), active: state?.italic ?? false, onClick: () => exec('italic'), children: "I" }), _jsx(MarkButton, { label: translate('builder.richText.link'), active: state !== null && state.linkHref !== null, onClick: openLinkEditor, children: translate('builder.richText.link') }), state !== null && state.linkHref !== null ? (_jsx(MarkButton, { label: translate('builder.richText.removeLink'), active: false, onClick: () => exec('unlink'), children: translate('builder.richText.unlink') })) : null] })) }), doc.body);
|
|
100
101
|
}
|
|
101
102
|
function MarkButton({ label, active, onClick, children, }) {
|
|
102
103
|
return (_jsx("button", { type: "button", "aria-label": label, "aria-pressed": active, title: label, onClick: onClick, className: "ec-rt-toolbar-btn", "data-active": active || undefined, children: children }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rich-text-toolbar.js","sourceRoot":"","sources":["../../../src/components/builder/rich-text-toolbar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\n\n/**\n * Floating mark toolbar for inline rich-text editing (EC-159): anchored to the\n * live text selection inside the canvas iframe, offering bold, italic, and\n * link (set + remove). Portaled into the *iframe* document — selection\n * rectangles are iframe-viewport coordinates, so the toolbar positions\n * `fixed` in that viewport and never needs cross-frame math.\n *\n * Mark application uses the document's native editing commands on the live\n * selection (`bold`/`italic`/`createLink`/`unlink`), which produce exactly the\n * tags the `@elytracms/core/rich-text` DOM parser inverts (`b/strong`, `i/em`,\n * `a[href]`). Pointer-down on the toolbar is swallowed so the host keeps its\n * selection; the link input is the one exception (it needs focus), and the\n * canvas blur handler ignores blurs into `[data-ec-rt-toolbar]`.\n */\n\ninterface ToolbarState {\n left: number\n top: number\n bold: boolean\n italic: boolean\n /** The href of the link the selection sits inside, if any. */\n linkHref: string | null\n}\n\nconst TOOLBAR_OFFSET_PX = 8\n\nfunction selectionState(doc: Document, host: HTMLElement): ToolbarState | null {\n const selection = doc.getSelection()\n if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null\n if (!host.contains(selection.anchorNode) || !host.contains(selection.focusNode)) return null\n const rect = selection.getRangeAt(0).getBoundingClientRect()\n if (rect.width === 0 && rect.height === 0) return null\n const anchor = selection.anchorNode\n const anchorElement = anchor instanceof Element ? anchor : anchor?.parentElement\n const link = anchorElement?.closest('a') ?? null\n return {\n left: rect.left + rect.width / 2,\n top: rect.top,\n bold: doc.queryCommandState('bold'),\n italic: doc.queryCommandState('italic'),\n linkHref: link?.getAttribute('href') ?? null,\n }\n}\n\nexport function RichTextToolbar({ doc, host }: { doc: Document; host: HTMLElement }) {\n const [state, setState] = useState<ToolbarState | null>(null)\n const [linkOpen, setLinkOpen] = useState(false)\n const [linkDraft, setLinkDraft] = useState('')\n const savedRangeRef = useRef<Range | null>(null)\n\n const refresh = useCallback(() => {\n setState(selectionState(doc, host))\n }, [doc, host])\n\n useEffect(() => {\n refresh()\n doc.addEventListener('selectionchange', refresh)\n return () => doc.removeEventListener('selectionchange', refresh)\n }, [doc, refresh])\n\n // Close a stale link editor when the selection leaves the host entirely.\n useEffect(() => {\n if (!state && !linkOpen) setLinkDraft('')\n }, [state, linkOpen])\n\n const exec = useCallback(\n (command: string, value?: string) => {\n doc.execCommand(command, false, value)\n refresh()\n },\n [doc, refresh],\n )\n\n const openLinkEditor = useCallback(() => {\n const selection = doc.getSelection()\n if (selection && selection.rangeCount > 0) {\n savedRangeRef.current = selection.getRangeAt(0).cloneRange()\n }\n setLinkDraft(state?.linkHref ?? '')\n setLinkOpen(true)\n }, [doc, state])\n\n const applyLink = useCallback(\n (href: string) => {\n const selection = doc.getSelection()\n const saved = savedRangeRef.current\n if (saved && selection) {\n selection.removeAllRanges()\n selection.addRange(saved)\n }\n const trimmed = href.trim()\n if (trimmed.length > 0) exec('createLink', trimmed)\n else exec('unlink')\n setLinkOpen(false)\n savedRangeRef.current = null\n host.focus({ preventScroll: true })\n },\n [doc, exec, host],\n )\n\n const closeLinkEditor = useCallback(() => {\n setLinkOpen(false)\n savedRangeRef.current = null\n host.focus({ preventScroll: true })\n }, [host])\n\n if (!state && !linkOpen) return null\n const left = state?.left ?? savedRangeRef.current?.getBoundingClientRect().left ?? 0\n const top = state?.top ?? savedRangeRef.current?.getBoundingClientRect().top ?? 0\n\n return createPortal(\n <div\n data-ec-rt-toolbar=\"\"\n role=\"toolbar\"\n aria-label=\"Text formatting\"\n className=\"ec-rt-toolbar\"\n style={{\n position: 'fixed',\n left: Math.max(8, left),\n top: Math.max(8, top - TOOLBAR_OFFSET_PX),\n transform: 'translate(-50%, -100%)',\n zIndex: 50,\n }}\n onMouseDown={(e) => {\n // Keep the host's selection/focus — except for the link input.\n if (!(e.target as HTMLElement).closest('input')) e.preventDefault()\n }}\n >\n {linkOpen ? (\n <form\n className=\"ec-rt-toolbar-row\"\n onSubmit={(e) => {\n e.preventDefault()\n applyLink(linkDraft)\n }}\n >\n <input\n autoFocus\n type=\"text\"\n value={linkDraft}\n placeholder=\"https://… (empty removes)\"\n aria-label=\"Link URL\"\n onChange={(e) => setLinkDraft(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n closeLinkEditor()\n }\n }}\n className=\"ec-rt-toolbar-input\"\n />\n <button type=\"submit\" className=\"ec-rt-toolbar-btn\" aria-label=\"Apply link\">\n Set\n </button>\n </form>\n ) : (\n <div className=\"ec-rt-toolbar-row\">\n <MarkButton label=\"Bold\" active={state?.bold ?? false} onClick={() => exec('bold')}>\n B\n </MarkButton>\n <MarkButton label=\"Italic\" active={state?.italic ?? false} onClick={() => exec('italic')}>\n I\n </MarkButton>\n <MarkButton\n label=\"Link\"\n active={state !== null && state.linkHref !== null}\n onClick={openLinkEditor}\n >\n Link\n </MarkButton>\n {state !== null && state.linkHref !== null ? (\n <MarkButton label=\"Remove link\" active={false} onClick={() => exec('unlink')}>\n Unlink\n </MarkButton>\n ) : null}\n </div>\n )}\n </div>,\n doc.body,\n )\n}\n\nfunction MarkButton({\n label,\n active,\n onClick,\n children,\n}: {\n label: string\n active: boolean\n onClick: () => void\n children: React.ReactNode\n}) {\n return (\n <button\n type=\"button\"\n aria-label={label}\n aria-pressed={active}\n title={label}\n onClick={onClick}\n className=\"ec-rt-toolbar-btn\"\n data-active={active || undefined}\n >\n {children}\n </button>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rich-text-toolbar.js","sourceRoot":"","sources":["../../../src/components/builder/rich-text-toolbar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Floating mark toolbar for inline rich-text editing (EC-159): anchored to the\n * live text selection inside the canvas iframe, offering bold, italic, and\n * link (set + remove). Portaled into the *iframe* document — selection\n * rectangles are iframe-viewport coordinates, so the toolbar positions\n * `fixed` in that viewport and never needs cross-frame math.\n *\n * Mark application uses the document's native editing commands on the live\n * selection (`bold`/`italic`/`createLink`/`unlink`), which produce exactly the\n * tags the `@elytracms/core/rich-text` DOM parser inverts (`b/strong`, `i/em`,\n * `a[href]`). Pointer-down on the toolbar is swallowed so the host keeps its\n * selection; the link input is the one exception (it needs focus), and the\n * canvas blur handler ignores blurs into `[data-ec-rt-toolbar]`.\n */\n\ninterface ToolbarState {\n left: number\n top: number\n bold: boolean\n italic: boolean\n /** The href of the link the selection sits inside, if any. */\n linkHref: string | null\n}\n\nconst TOOLBAR_OFFSET_PX = 8\n\nfunction selectionState(doc: Document, host: HTMLElement): ToolbarState | null {\n const selection = doc.getSelection()\n if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null\n if (!host.contains(selection.anchorNode) || !host.contains(selection.focusNode)) return null\n const rect = selection.getRangeAt(0).getBoundingClientRect()\n if (rect.width === 0 && rect.height === 0) return null\n const anchor = selection.anchorNode\n const anchorElement = anchor instanceof Element ? anchor : anchor?.parentElement\n const link = anchorElement?.closest('a') ?? null\n return {\n left: rect.left + rect.width / 2,\n top: rect.top,\n bold: doc.queryCommandState('bold'),\n italic: doc.queryCommandState('italic'),\n linkHref: link?.getAttribute('href') ?? null,\n }\n}\n\nexport function RichTextToolbar({ doc, host }: { doc: Document; host: HTMLElement }) {\n const [state, setState] = useState<ToolbarState | null>(null)\n const [linkOpen, setLinkOpen] = useState(false)\n const [linkDraft, setLinkDraft] = useState('')\n const savedRangeRef = useRef<Range | null>(null)\n\n const refresh = useCallback(() => {\n setState(selectionState(doc, host))\n }, [doc, host])\n\n useEffect(() => {\n refresh()\n doc.addEventListener('selectionchange', refresh)\n return () => doc.removeEventListener('selectionchange', refresh)\n }, [doc, refresh])\n\n // Close a stale link editor when the selection leaves the host entirely.\n useEffect(() => {\n if (!state && !linkOpen) setLinkDraft('')\n }, [state, linkOpen])\n\n const exec = useCallback(\n (command: string, value?: string) => {\n doc.execCommand(command, false, value)\n refresh()\n },\n [doc, refresh],\n )\n\n const openLinkEditor = useCallback(() => {\n const selection = doc.getSelection()\n if (selection && selection.rangeCount > 0) {\n savedRangeRef.current = selection.getRangeAt(0).cloneRange()\n }\n setLinkDraft(state?.linkHref ?? '')\n setLinkOpen(true)\n }, [doc, state])\n\n const applyLink = useCallback(\n (href: string) => {\n const selection = doc.getSelection()\n const saved = savedRangeRef.current\n if (saved && selection) {\n selection.removeAllRanges()\n selection.addRange(saved)\n }\n const trimmed = href.trim()\n if (trimmed.length > 0) exec('createLink', trimmed)\n else exec('unlink')\n setLinkOpen(false)\n savedRangeRef.current = null\n host.focus({ preventScroll: true })\n },\n [doc, exec, host],\n )\n\n const closeLinkEditor = useCallback(() => {\n setLinkOpen(false)\n savedRangeRef.current = null\n host.focus({ preventScroll: true })\n }, [host])\n\n if (!state && !linkOpen) return null\n const left = state?.left ?? savedRangeRef.current?.getBoundingClientRect().left ?? 0\n const top = state?.top ?? savedRangeRef.current?.getBoundingClientRect().top ?? 0\n\n return createPortal(\n <div\n data-ec-rt-toolbar=\"\"\n role=\"toolbar\"\n aria-label={translate('builder.richText.toolbarLabel')}\n className=\"ec-rt-toolbar\"\n style={{\n position: 'fixed',\n left: Math.max(8, left),\n top: Math.max(8, top - TOOLBAR_OFFSET_PX),\n transform: 'translate(-50%, -100%)',\n zIndex: 50,\n }}\n onMouseDown={(e) => {\n // Keep the host's selection/focus — except for the link input.\n if (!(e.target as HTMLElement).closest('input')) e.preventDefault()\n }}\n >\n {linkOpen ? (\n <form\n className=\"ec-rt-toolbar-row\"\n onSubmit={(e) => {\n e.preventDefault()\n applyLink(linkDraft)\n }}\n >\n <input\n autoFocus\n type=\"text\"\n value={linkDraft}\n placeholder={translate('builder.richText.linkPlaceholder')}\n aria-label={translate('builder.richText.linkUrl')}\n onChange={(e) => setLinkDraft(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n closeLinkEditor()\n }\n }}\n className=\"ec-rt-toolbar-input\"\n />\n <button type=\"submit\" className=\"ec-rt-toolbar-btn\" aria-label={translate('builder.richText.applyLink')}>\n {translate('builder.richText.set')}\n </button>\n </form>\n ) : (\n <div className=\"ec-rt-toolbar-row\">\n <MarkButton label={translate('builder.richText.bold')} active={state?.bold ?? false} onClick={() => exec('bold')}>\n B\n </MarkButton>\n <MarkButton label={translate('builder.richText.italic')} active={state?.italic ?? false} onClick={() => exec('italic')}>\n I\n </MarkButton>\n <MarkButton\n label={translate('builder.richText.link')}\n active={state !== null && state.linkHref !== null}\n onClick={openLinkEditor}\n >\n {translate('builder.richText.link')}\n </MarkButton>\n {state !== null && state.linkHref !== null ? (\n <MarkButton label={translate('builder.richText.removeLink')} active={false} onClick={() => exec('unlink')}>\n {translate('builder.richText.unlink')}\n </MarkButton>\n ) : null}\n </div>\n )}\n </div>,\n doc.body,\n )\n}\n\nfunction MarkButton({\n label,\n active,\n onClick,\n children,\n}: {\n label: string\n active: boolean\n onClick: () => void\n children: React.ReactNode\n}) {\n return (\n <button\n type=\"button\"\n aria-label={label}\n aria-pressed={active}\n title={label}\n onClick={onClick}\n className=\"ec-rt-toolbar-btn\"\n data-active={active || undefined}\n >\n {children}\n </button>\n )\n}\n"]}
|
|
@@ -4,8 +4,9 @@ import { createPortal } from 'react-dom';
|
|
|
4
4
|
import { DEFAULT_SLOT, isLayoutContainerNode, staticProp } from '@elytracms/core/project-graph';
|
|
5
5
|
import { containerFlowAxis } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
6
6
|
import { containerGapStep, gapHandleSpots, gapStepCss, gapStepFromDrag, } from '@elytracms/studio/lib/dnd/gap-drag';
|
|
7
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
7
8
|
import { useBuilder } from './builder-store';
|
|
8
|
-
import {
|
|
9
|
+
import { GAP_LABEL_KEYS, GAP_NONE_LABEL_KEY } from './layout-inspector';
|
|
9
10
|
/**
|
|
10
11
|
* Tokenized spacing handles (EC-163, vision §3.1 / AD-10): when a Stack/Grid
|
|
11
12
|
* container is selected on the canvas, draggable gap handles appear between
|
|
@@ -20,7 +21,7 @@ import { GAP_LABELS, GAP_NONE_LABEL } from './layout-inspector';
|
|
|
20
21
|
*/
|
|
21
22
|
/** Plain-language label for a gap step — the inspector's vocabulary. */
|
|
22
23
|
export function gapStepLabel(step) {
|
|
23
|
-
return step === null ?
|
|
24
|
+
return step === null ? translate(GAP_NONE_LABEL_KEY) : translate(GAP_LABEL_KEYS[step]);
|
|
24
25
|
}
|
|
25
26
|
/** The container's rendered element (the Stack/Grid div carrying the gap style). */
|
|
26
27
|
function containerElementFor(doc, nodeId) {
|
|
@@ -175,7 +176,7 @@ export function SpacingHandles({ doc, node }) {
|
|
|
175
176
|
return createPortal(_jsxs("div", { "data-ec-spacing-handles": "", children: [spots.map((spot) => {
|
|
176
177
|
const length = Math.min(spot.crossLength, HANDLE_MAX_LENGTH);
|
|
177
178
|
const active = drag?.index === spot.index;
|
|
178
|
-
return (_jsx("div", { "data-ec-gap-handle": spot.index, role: "separator", "aria-label":
|
|
179
|
+
return (_jsx("div", { "data-ec-gap-handle": spot.index, role: "separator", "aria-label": translate('builder.spacing.gapAfterChild', { index: spot.index + 1 }), "aria-orientation": horizontal ? 'vertical' : 'horizontal', onPointerDown: onPointerDown(spot), onPointerMove: onPointerMove, onPointerUp: () => finish('commit'), onPointerCancel: () => finish('cancel'), style: {
|
|
179
180
|
position: 'absolute',
|
|
180
181
|
left: spot.x - (horizontal ? HANDLE_THICKNESS / 2 : length / 2),
|
|
181
182
|
top: spot.y - (horizontal ? length / 2 : HANDLE_THICKNESS / 2),
|