@elytracms/studio 0.0.37 → 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/editor.d.ts +3 -0
- package/dist/components/cms/rich-text/editor.js +21 -3
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/embed-picker.d.ts +17 -0
- package/dist/components/cms/rich-text/embed-picker.js +34 -0
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -0
- 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/extensions.d.ts +7 -1
- package/dist/components/cms/rich-text/extensions.js +6 -1
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- 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/insert.d.ts +21 -0
- package/dist/components/cms/rich-text/insert.js +55 -0
- package/dist/components/cms/rich-text/insert.js.map +1 -0
- 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/rich-text/slash-command.d.ts +27 -0
- package/dist/components/cms/rich-text/slash-command.js +158 -0
- package/dist/components/cms/rich-text/slash-command.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar.d.ts +5 -6
- package/dist/components/cms/rich-text/toolbar.js +3 -5
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- 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 +81 -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 -24
- package/src/styles/studio.css +17 -0
|
@@ -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"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Extensions } from '@tiptap/react';
|
|
2
|
+
import type { SlashCommandOptions } from './slash-command';
|
|
2
3
|
/**
|
|
3
4
|
* The TipTap extension set for the CMS rich-text editor (EC-314).
|
|
4
5
|
*
|
|
@@ -19,5 +20,10 @@ import type { Extensions } from '@tiptap/react';
|
|
|
19
20
|
*
|
|
20
21
|
* No `allow` gating lives here — that governs *insertion* (the embed picker) and
|
|
21
22
|
* *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.
|
|
23
|
+
*
|
|
24
|
+
* `slash` wires the Notion-style `/` menu to the editor's shared image/embed pickers
|
|
25
|
+
* (omit an action to hide that slash item).
|
|
22
26
|
*/
|
|
23
|
-
export declare function buildRichTextExtensions(
|
|
27
|
+
export declare function buildRichTextExtensions(options?: {
|
|
28
|
+
slash?: SlashCommandOptions;
|
|
29
|
+
}): Extensions;
|
|
@@ -4,6 +4,7 @@ import { translate } from '@elytracms/studio/lib/i18n';
|
|
|
4
4
|
import { RefLink } from './ref-link';
|
|
5
5
|
import { AssetImage } from './asset-image';
|
|
6
6
|
import { ComponentEmbed } from './component-embed';
|
|
7
|
+
import { createSlashCommand } from './slash-command';
|
|
7
8
|
/**
|
|
8
9
|
* The TipTap extension set for the CMS rich-text editor (EC-314).
|
|
9
10
|
*
|
|
@@ -24,8 +25,11 @@ import { ComponentEmbed } from './component-embed';
|
|
|
24
25
|
*
|
|
25
26
|
* No `allow` gating lives here — that governs *insertion* (the embed picker) and
|
|
26
27
|
* *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.
|
|
28
|
+
*
|
|
29
|
+
* `slash` wires the Notion-style `/` menu to the editor's shared image/embed pickers
|
|
30
|
+
* (omit an action to hide that slash item).
|
|
27
31
|
*/
|
|
28
|
-
export function buildRichTextExtensions() {
|
|
32
|
+
export function buildRichTextExtensions(options = {}) {
|
|
29
33
|
return [
|
|
30
34
|
StarterKit.configure({
|
|
31
35
|
link: false,
|
|
@@ -35,6 +39,7 @@ export function buildRichTextExtensions() {
|
|
|
35
39
|
AssetImage,
|
|
36
40
|
ComponentEmbed,
|
|
37
41
|
Placeholder.configure({ placeholder: translate('richText.placeholder') }),
|
|
42
|
+
createSlashCommand(options.slash ?? {}),
|
|
38
43
|
];
|
|
39
44
|
}
|
|
40
45
|
//# sourceMappingURL=extensions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extensions.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/extensions.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,qBAAqB,CAAA;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"extensions.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/extensions.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,qBAAqB,CAAA;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAGpD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,uBAAuB,CAAC,UAA2C,EAAE;IACnF,OAAO;QACL,UAAU,CAAC,SAAS,CAAC;YACnB,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE;SACxC,CAAC;QACF,OAAO,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAC5E,UAAU;QACV,cAAc;QACd,WAAW,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,SAAS,CAAC,sBAAsB,CAAC,EAAE,CAAC;QACzE,kBAAkB,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;KACxC,CAAA;AACH,CAAC","sourcesContent":["import StarterKit from '@tiptap/starter-kit'\nimport { Placeholder } from '@tiptap/extensions'\nimport type { Extensions } from '@tiptap/react'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { RefLink } from './ref-link'\nimport { AssetImage } from './asset-image'\nimport { ComponentEmbed } from './component-embed'\nimport { createSlashCommand } from './slash-command'\nimport type { SlashCommandOptions } from './slash-command'\n\n/**\n * The TipTap extension set for the CMS rich-text editor (EC-314).\n *\n * Deliberately matches the storage vocabulary (`@elytracms/core/rich-text` schema)\n * one-for-one so every editor document round-trips through `parseRichTextValue` /\n * the delivery renderer with no format change:\n *\n * - **StarterKit** supplies `doc/paragraph/text/heading/bulletList/orderedList/\n * listItem/blockquote/codeBlock/hardBreak/horizontalRule` and the marks\n * `bold/italic/code/strike/underline` — all ProseMirror-standard names identical\n * to storage. Its bundled `link` is disabled and replaced by {@link RefLink} so\n * internal-document links can carry a structured `ref` (EC-290). Headings allow\n * all six levels so existing `h4`–`h6` content is preserved even though the\n * toolbar surfaces only 1–3.\n * - {@link AssetImage} / {@link ComponentEmbed} declare the two reference-carrying\n * atoms (asset image, embedded registry component) so existing content of those\n * kinds survives a round-trip verbatim rather than being dropped by the schema.\n *\n * No `allow` gating lives here — that governs *insertion* (the embed picker) and\n * *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.\n *\n * `slash` wires the Notion-style `/` menu to the editor's shared image/embed pickers\n * (omit an action to hide that slash item).\n */\nexport function buildRichTextExtensions(options: { slash?: SlashCommandOptions } = {}): Extensions {\n return [\n StarterKit.configure({\n link: false,\n heading: { levels: [1, 2, 3, 4, 5, 6] },\n }),\n RefLink.configure({ openOnClick: false, autolink: true, linkOnPaste: true }),\n AssetImage,\n ComponentEmbed,\n Placeholder.configure({ placeholder: translate('richText.placeholder') }),\n createSlashCommand(options.slash ?? {}),\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,21 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/react';
|
|
2
|
+
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
3
|
+
import type { ComponentManifest } from '@elytracms/core/component-registry';
|
|
4
|
+
import type { AssetsStore } from '../../assets/assets-store';
|
|
5
|
+
/**
|
|
6
|
+
* Reference-carrying node insertions shared by the toolbar controls and the slash
|
|
7
|
+
* menu (EC-314). Centralised so both entry points produce byte-identical envelopes
|
|
8
|
+
* (the shapes `reference-extraction` indexes).
|
|
9
|
+
*/
|
|
10
|
+
/** Insert an `image` node for a project asset (canonical `attrs.ref`, snapshot `src`). */
|
|
11
|
+
export declare function insertAssetImage(editor: Editor, assetId: string, assets: AssetsStore | null): void;
|
|
12
|
+
/** Insert a `componentEmbed` wrapping a fresh ComponentNode built from manifest defaults. */
|
|
13
|
+
export declare function insertComponentEmbed(editor: Editor, manifest: ComponentManifest): void;
|
|
14
|
+
/** A fresh ComponentNode from a manifest's prop defaults (mirrors builder node-factory). */
|
|
15
|
+
export declare function embedNodeFromManifest(manifest: ComponentManifest): ComponentNode;
|
|
16
|
+
/**
|
|
17
|
+
* The components embeddable in a rich-text field: the field's `allow` list, or every
|
|
18
|
+
* non-primitive component when open — matching the `blocks` field and the
|
|
19
|
+
* `validateRichTextEmbeds` leash.
|
|
20
|
+
*/
|
|
21
|
+
export declare function listEmbeddable(allow: string[] | undefined, registered: ComponentManifest[]): ComponentManifest[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { assetNode } from '@elytracms/core/cms-core';
|
|
2
|
+
import { COMPONENT_EMBED_NODE_TYPE } from '@elytracms/core/rich-text';
|
|
3
|
+
import { staticProp } from '@elytracms/core/project-graph';
|
|
4
|
+
import { isPrimitive } from '@elytracms/core/component-registry';
|
|
5
|
+
/**
|
|
6
|
+
* Reference-carrying node insertions shared by the toolbar controls and the slash
|
|
7
|
+
* menu (EC-314). Centralised so both entry points produce byte-identical envelopes
|
|
8
|
+
* (the shapes `reference-extraction` indexes).
|
|
9
|
+
*/
|
|
10
|
+
/** Insert an `image` node for a project asset (canonical `attrs.ref`, snapshot `src`). */
|
|
11
|
+
export function insertAssetImage(editor, assetId, assets) {
|
|
12
|
+
const record = assets?.assets.find((a) => a.id === assetId);
|
|
13
|
+
editor
|
|
14
|
+
.chain()
|
|
15
|
+
.focus()
|
|
16
|
+
.insertContent({
|
|
17
|
+
type: 'image',
|
|
18
|
+
attrs: { src: record?.url ?? '', alt: record?.alt ?? '', ref: assetNode(assetId) },
|
|
19
|
+
})
|
|
20
|
+
.run();
|
|
21
|
+
}
|
|
22
|
+
/** Insert a `componentEmbed` wrapping a fresh ComponentNode built from manifest defaults. */
|
|
23
|
+
export function insertComponentEmbed(editor, manifest) {
|
|
24
|
+
editor
|
|
25
|
+
.chain()
|
|
26
|
+
.focus()
|
|
27
|
+
.insertContent({ type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: embedNodeFromManifest(manifest) } })
|
|
28
|
+
.run();
|
|
29
|
+
}
|
|
30
|
+
/** A fresh ComponentNode from a manifest's prop defaults (mirrors builder node-factory). */
|
|
31
|
+
export function embedNodeFromManifest(manifest) {
|
|
32
|
+
const props = {};
|
|
33
|
+
for (const [name, spec] of Object.entries(manifest.props)) {
|
|
34
|
+
if (spec.default !== undefined)
|
|
35
|
+
props[name] = staticProp(spec.default);
|
|
36
|
+
}
|
|
37
|
+
return { id: newEmbedId(), componentId: manifest.id, props };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The components embeddable in a rich-text field: the field's `allow` list, or every
|
|
41
|
+
* non-primitive component when open — matching the `blocks` field and the
|
|
42
|
+
* `validateRichTextEmbeds` leash.
|
|
43
|
+
*/
|
|
44
|
+
export function listEmbeddable(allow, registered) {
|
|
45
|
+
const permitted = allow && allow.length > 0
|
|
46
|
+
? registered.filter((m) => allow.includes(m.id))
|
|
47
|
+
: registered.filter((m) => !isPrimitive(m.id));
|
|
48
|
+
return permitted.filter((m) => m.clientRenderable !== false);
|
|
49
|
+
}
|
|
50
|
+
/** A unique id for an embedded node (studio runs in the browser; crypto is present). */
|
|
51
|
+
function newEmbedId() {
|
|
52
|
+
const uuid = globalThis.crypto?.randomUUID?.();
|
|
53
|
+
return uuid ? `embed-${uuid}` : `embed-${Math.random().toString(36).slice(2)}`;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=insert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"insert.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/insert.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAA;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAE1D,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAA;AAIhE;;;;GAIG;AAEH,0FAA0F;AAC1F,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,OAAe,EAAE,MAA0B;IAC1F,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,CAAA;IAC3D,MAAM;SACH,KAAK,EAAE;SACP,KAAK,EAAE;SACP,aAAa,CAAC;QACb,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE;KACnF,CAAC;SACD,GAAG,EAAE,CAAA;AACV,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,oBAAoB,CAAC,MAAc,EAAE,QAA2B;IAC9E,MAAM;SACH,KAAK,EAAE;SACP,KAAK,EAAE;SACP,aAAa,CAAC,EAAE,IAAI,EAAE,yBAAyB,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,qBAAqB,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;SACpG,GAAG,EAAE,CAAA;AACV,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,qBAAqB,CAAC,QAA2B;IAC/D,MAAM,KAAK,GAA2B,EAAE,CAAA;IACxC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1D,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,OAAoB,CAAC,CAAA;IACrF,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,WAAW,EAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,CAAA;AAC9D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,KAA2B,EAC3B,UAA+B;IAE/B,MAAM,SAAS,GACb,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QACvB,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAChD,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAClD,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,gBAAgB,KAAK,KAAK,CAAC,CAAA;AAC9D,CAAC;AAED,wFAAwF;AACxF,SAAS,UAAU;IACjB,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,CAAA;IAC9C,OAAO,IAAI,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;AAChF,CAAC","sourcesContent":["import type { Editor } from '@tiptap/react'\nimport { assetNode } from '@elytracms/core/cms-core'\nimport { COMPONENT_EMBED_NODE_TYPE } from '@elytracms/core/rich-text'\nimport { staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { isPrimitive } from '@elytracms/core/component-registry'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport type { AssetsStore } from '../../assets/assets-store'\n\n/**\n * Reference-carrying node insertions shared by the toolbar controls and the slash\n * menu (EC-314). Centralised so both entry points produce byte-identical envelopes\n * (the shapes `reference-extraction` indexes).\n */\n\n/** Insert an `image` node for a project asset (canonical `attrs.ref`, snapshot `src`). */\nexport function insertAssetImage(editor: Editor, assetId: string, assets: AssetsStore | null): void {\n const record = assets?.assets.find((a) => a.id === assetId)\n editor\n .chain()\n .focus()\n .insertContent({\n type: 'image',\n attrs: { src: record?.url ?? '', alt: record?.alt ?? '', ref: assetNode(assetId) },\n })\n .run()\n}\n\n/** Insert a `componentEmbed` wrapping a fresh ComponentNode built from manifest defaults. */\nexport function insertComponentEmbed(editor: Editor, manifest: ComponentManifest): void {\n editor\n .chain()\n .focus()\n .insertContent({ type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: embedNodeFromManifest(manifest) } })\n .run()\n}\n\n/** A fresh ComponentNode from a manifest's prop defaults (mirrors builder node-factory). */\nexport function embedNodeFromManifest(manifest: ComponentManifest): ComponentNode {\n const props: ComponentNode['props'] = {}\n for (const [name, spec] of Object.entries(manifest.props)) {\n if (spec.default !== undefined) props[name] = staticProp(spec.default as JsonValue)\n }\n return { id: newEmbedId(), componentId: manifest.id, props }\n}\n\n/**\n * The components embeddable in a rich-text field: the field's `allow` list, or every\n * non-primitive component when open — matching the `blocks` field and the\n * `validateRichTextEmbeds` leash.\n */\nexport function listEmbeddable(\n allow: string[] | undefined,\n registered: ComponentManifest[],\n): ComponentManifest[] {\n const permitted =\n allow && allow.length > 0\n ? registered.filter((m) => allow.includes(m.id))\n : registered.filter((m) => !isPrimitive(m.id))\n return permitted.filter((m) => m.clientRenderable !== false)\n}\n\n/** A unique id for an embedded node (studio runs in the browser; crypto is present). */\nfunction newEmbedId(): string {\n const uuid = globalThis.crypto?.randomUUID?.()\n return uuid ? `embed-${uuid}` : `embed-${Math.random().toString(36).slice(2)}`\n}\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 {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, lazy } from 'react';
|
|
3
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
4
|
+
/**
|
|
5
|
+
* Lazy boundary for the CMS rich-text editor (EC-322 load-time pass).
|
|
6
|
+
*
|
|
7
|
+
* The TipTap editor + its ProseMirror runtime and extension set is a large
|
|
8
|
+
* dependency, and `field-control` sits on the critical path of *every* CMS view.
|
|
9
|
+
* Importing the editor statically pulled all of TipTap into the studio's main
|
|
10
|
+
* route chunk even for documents with no `richText` field on screen. Splitting it
|
|
11
|
+
* behind {@link lazy} moves the whole editor subtree into an on-demand chunk that
|
|
12
|
+
* only downloads when a `richText` field actually renders — the field control's
|
|
13
|
+
* seam is unchanged (it still imports `RichTextEditor` from `./rich-text`).
|
|
14
|
+
*
|
|
15
|
+
* The fallback mirrors the loaded editor's outer frame (same border/rounding + a
|
|
16
|
+
* toolbar-and-body min-height) so the swap-in is layout-jank-free, and carries an
|
|
17
|
+
* accessible loading label from the message catalog.
|
|
18
|
+
*/
|
|
19
|
+
const RichTextEditorImpl = lazy(() => import('./editor').then((module) => ({ default: module.RichTextEditor })));
|
|
20
|
+
function RichTextEditorFallback() {
|
|
21
|
+
return (_jsx("div", { className: "ec-rte min-h-[7.5rem] animate-pulse rounded-md border border-input bg-muted/20 shadow-sm", role: "status", "aria-busy": "true", children: _jsx("span", { className: "sr-only", children: translate('richText.loading') }) }));
|
|
22
|
+
}
|
|
23
|
+
export function RichTextEditor(props) {
|
|
24
|
+
return (_jsx(Suspense, { fallback: _jsx(RichTextEditorFallback, {}), children: _jsx(RichTextEditorImpl, { ...props }) }));
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=lazy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lazy.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/lazy.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,EAAE,CACnC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC,CAC1E,CAAA;AAED,SAAS,sBAAsB;IAC7B,OAAO,CACL,cACE,SAAS,EAAC,0FAA0F,EACpG,IAAI,EAAC,QAAQ,eACH,MAAM,YAEhB,eAAM,SAAS,EAAC,SAAS,YAAE,SAAS,CAAC,kBAAkB,CAAC,GAAQ,GAC5D,CACP,CAAA;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAgD;IAC7E,OAAO,CACL,KAAC,QAAQ,IAAC,QAAQ,EAAE,KAAC,sBAAsB,KAAG,YAC5C,KAAC,kBAAkB,OAAK,KAAK,GAAI,GACxB,CACZ,CAAA;AACH,CAAC","sourcesContent":["import { Suspense, lazy } from 'react'\nimport type { ComponentProps } from 'react'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Lazy boundary for the CMS rich-text editor (EC-322 load-time pass).\n *\n * The TipTap editor + its ProseMirror runtime and extension set is a large\n * dependency, and `field-control` sits on the critical path of *every* CMS view.\n * Importing the editor statically pulled all of TipTap into the studio's main\n * route chunk even for documents with no `richText` field on screen. Splitting it\n * behind {@link lazy} moves the whole editor subtree into an on-demand chunk that\n * only downloads when a `richText` field actually renders — the field control's\n * seam is unchanged (it still imports `RichTextEditor` from `./rich-text`).\n *\n * The fallback mirrors the loaded editor's outer frame (same border/rounding + a\n * toolbar-and-body min-height) so the swap-in is layout-jank-free, and carries an\n * accessible loading label from the message catalog.\n */\nconst RichTextEditorImpl = lazy(() =>\n import('./editor').then((module) => ({ default: module.RichTextEditor })),\n)\n\nfunction RichTextEditorFallback() {\n return (\n <div\n className=\"ec-rte min-h-[7.5rem] animate-pulse rounded-md border border-input bg-muted/20 shadow-sm\"\n role=\"status\"\n aria-busy=\"true\"\n >\n <span className=\"sr-only\">{translate('richText.loading')}</span>\n </div>\n )\n}\n\nexport function RichTextEditor(props: ComponentProps<typeof RichTextEditorImpl>) {\n return (\n <Suspense fallback={<RichTextEditorFallback />}>\n <RichTextEditorImpl {...props} />\n </Suspense>\n )\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/react';
|
|
2
|
+
import type { Editor, Range } from '@tiptap/react';
|
|
3
|
+
import type { LucideIcon } from 'lucide-react';
|
|
4
|
+
/**
|
|
5
|
+
* Notion-style slash menu (EC-314): typing `/` at the caret opens a filterable
|
|
6
|
+
* command palette that inserts block-level content — headings, lists, quote, code,
|
|
7
|
+
* divider — and, when available, an image or a component embed (delegated to the
|
|
8
|
+
* editor's shared pickers via {@link SlashCommandOptions}). Built on TipTap's
|
|
9
|
+
* `Suggestion` utility; the menu is a React overlay positioned at the caret.
|
|
10
|
+
*/
|
|
11
|
+
export interface SlashCommandOptions {
|
|
12
|
+
/** Open the asset picker (omitted ⇒ no `/image` item). */
|
|
13
|
+
onImage?: () => void;
|
|
14
|
+
/** Open the component-embed picker (omitted ⇒ no `/embed` item). */
|
|
15
|
+
onEmbed?: () => void;
|
|
16
|
+
}
|
|
17
|
+
export interface SlashItem {
|
|
18
|
+
key: string;
|
|
19
|
+
title: string;
|
|
20
|
+
keywords: string;
|
|
21
|
+
icon: LucideIcon;
|
|
22
|
+
run: (editor: Editor, range: Range) => void;
|
|
23
|
+
}
|
|
24
|
+
/** The command list, gated by which media actions the editor provided. */
|
|
25
|
+
export declare function buildSlashItems(options: SlashCommandOptions): SlashItem[];
|
|
26
|
+
/** Build the slash-command extension bound to the editor's shared media pickers. */
|
|
27
|
+
export declare function createSlashCommand(options: SlashCommandOptions): Extension<SlashCommandOptions, any>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
|
3
|
+
import { Extension, ReactRenderer } from '@tiptap/react';
|
|
4
|
+
import Suggestion from '@tiptap/suggestion';
|
|
5
|
+
import { Blocks, Heading1, Heading2, Heading3, ImagePlus, List, ListOrdered, Minus, Pilcrow, Quote, SquareCode, } from 'lucide-react';
|
|
6
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
7
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
8
|
+
/** The command list, gated by which media actions the editor provided. */
|
|
9
|
+
export function buildSlashItems(options) {
|
|
10
|
+
const block = (run) => (editor, range) => run(editor.chain().focus().deleteRange(range)).run();
|
|
11
|
+
const items = [
|
|
12
|
+
{ key: 'text', title: translate('richText.slash.text'), keywords: 'paragraph text plain', icon: Pilcrow, run: block((c) => c.setParagraph()) },
|
|
13
|
+
{ key: 'h1', title: translate('richText.heading', { level: 1 }), keywords: 'heading title h1', icon: Heading1, run: block((c) => c.toggleHeading({ level: 1 })) },
|
|
14
|
+
{ key: 'h2', title: translate('richText.heading', { level: 2 }), keywords: 'heading subtitle h2', icon: Heading2, run: block((c) => c.toggleHeading({ level: 2 })) },
|
|
15
|
+
{ key: 'h3', title: translate('richText.heading', { level: 3 }), keywords: 'heading h3', icon: Heading3, run: block((c) => c.toggleHeading({ level: 3 })) },
|
|
16
|
+
{ key: 'bulletList', title: translate('richText.bulletList'), keywords: 'bullet unordered list ul', icon: List, run: block((c) => c.toggleBulletList()) },
|
|
17
|
+
{ key: 'orderedList', title: translate('richText.orderedList'), keywords: 'ordered numbered list ol', icon: ListOrdered, run: block((c) => c.toggleOrderedList()) },
|
|
18
|
+
{ key: 'blockquote', title: translate('richText.blockquote'), keywords: 'quote blockquote', icon: Quote, run: block((c) => c.toggleBlockquote()) },
|
|
19
|
+
{ key: 'codeBlock', title: translate('richText.codeBlock'), keywords: 'code block pre', icon: SquareCode, run: block((c) => c.toggleCodeBlock()) },
|
|
20
|
+
{ key: 'divider', title: translate('richText.horizontalRule'), keywords: 'divider rule hr separator', icon: Minus, run: block((c) => c.setHorizontalRule()) },
|
|
21
|
+
];
|
|
22
|
+
if (options.onImage) {
|
|
23
|
+
items.push({
|
|
24
|
+
key: 'image',
|
|
25
|
+
title: translate('richText.image.add'),
|
|
26
|
+
keywords: 'image picture photo media',
|
|
27
|
+
icon: ImagePlus,
|
|
28
|
+
run: (editor, range) => {
|
|
29
|
+
editor.chain().focus().deleteRange(range).run();
|
|
30
|
+
options.onImage?.();
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (options.onEmbed) {
|
|
35
|
+
items.push({
|
|
36
|
+
key: 'embed',
|
|
37
|
+
title: translate('richText.embed.add'),
|
|
38
|
+
keywords: 'embed component block',
|
|
39
|
+
icon: Blocks,
|
|
40
|
+
run: (editor, range) => {
|
|
41
|
+
editor.chain().focus().deleteRange(range).run();
|
|
42
|
+
options.onEmbed?.();
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return items;
|
|
47
|
+
}
|
|
48
|
+
const SlashMenu = forwardRef((props, ref) => {
|
|
49
|
+
const [selected, setSelected] = useState(0);
|
|
50
|
+
useEffect(() => setSelected(0), [props.items]);
|
|
51
|
+
useImperativeHandle(ref, () => ({
|
|
52
|
+
onKeyDown: ({ event }) => {
|
|
53
|
+
const count = props.items.length;
|
|
54
|
+
if (count === 0)
|
|
55
|
+
return false;
|
|
56
|
+
if (event.key === 'ArrowUp') {
|
|
57
|
+
setSelected((s) => (s + count - 1) % count);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (event.key === 'ArrowDown') {
|
|
61
|
+
setSelected((s) => (s + 1) % count);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (event.key === 'Enter') {
|
|
65
|
+
const item = props.items[selected];
|
|
66
|
+
if (item)
|
|
67
|
+
props.command(item);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
},
|
|
72
|
+
}));
|
|
73
|
+
if (props.items.length === 0) {
|
|
74
|
+
return (_jsx("div", { className: "ec-rte-slash", children: _jsx("span", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: translate('richText.slash.empty') }) }));
|
|
75
|
+
}
|
|
76
|
+
return (_jsx("div", { className: "ec-rte-slash", role: "menu", children: props.items.map((item, i) => {
|
|
77
|
+
const Icon = item.icon;
|
|
78
|
+
return (_jsxs("button", { type: "button", role: "menuitem", onMouseEnter: () => setSelected(i), onMouseDown: (e) => {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
props.command(item);
|
|
81
|
+
}, className: cn('flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left text-sm outline-none', i === selected ? 'bg-muted' : 'hover:bg-muted/60'), children: [_jsx(Icon, { className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: item.title })] }, item.key));
|
|
82
|
+
}) }));
|
|
83
|
+
});
|
|
84
|
+
SlashMenu.displayName = 'SlashMenu';
|
|
85
|
+
/** Position a floating element under the caret, flipping above near the viewport edge. */
|
|
86
|
+
function place(el, rect) {
|
|
87
|
+
if (!rect)
|
|
88
|
+
return;
|
|
89
|
+
const margin = 8;
|
|
90
|
+
const height = el.offsetHeight || 280;
|
|
91
|
+
const below = rect.bottom + 4;
|
|
92
|
+
const flip = below + height > window.innerHeight - margin && rect.top - height > margin;
|
|
93
|
+
el.style.position = 'fixed';
|
|
94
|
+
el.style.left = `${Math.min(rect.left, window.innerWidth - 260)}px`;
|
|
95
|
+
el.style.top = flip ? `${rect.top - height - 4}px` : `${below}px`;
|
|
96
|
+
el.style.zIndex = '60';
|
|
97
|
+
}
|
|
98
|
+
/** The suggestion config (everything but the `editor`, which the extension injects). */
|
|
99
|
+
function slashSuggestion(options) {
|
|
100
|
+
return {
|
|
101
|
+
char: '/',
|
|
102
|
+
startOfLine: false,
|
|
103
|
+
items: ({ query }) => {
|
|
104
|
+
const q = query.trim().toLowerCase();
|
|
105
|
+
const all = buildSlashItems(options);
|
|
106
|
+
if (!q)
|
|
107
|
+
return all;
|
|
108
|
+
return all.filter((item) => item.title.toLowerCase().includes(q) || item.keywords.includes(q));
|
|
109
|
+
},
|
|
110
|
+
command: ({ editor, range, props }) => props.run(editor, range),
|
|
111
|
+
render: () => {
|
|
112
|
+
let renderer = null;
|
|
113
|
+
let wrapper = null;
|
|
114
|
+
return {
|
|
115
|
+
onStart: (props) => {
|
|
116
|
+
renderer = new ReactRenderer(SlashMenu, {
|
|
117
|
+
props,
|
|
118
|
+
editor: props.editor,
|
|
119
|
+
});
|
|
120
|
+
wrapper = document.createElement('div');
|
|
121
|
+
wrapper.appendChild(renderer.element);
|
|
122
|
+
document.body.appendChild(wrapper);
|
|
123
|
+
place(wrapper, props.clientRect?.());
|
|
124
|
+
},
|
|
125
|
+
onUpdate: (props) => {
|
|
126
|
+
renderer?.updateProps(props);
|
|
127
|
+
if (wrapper)
|
|
128
|
+
place(wrapper, props.clientRect?.());
|
|
129
|
+
},
|
|
130
|
+
onKeyDown: (props) => {
|
|
131
|
+
if (props.event.key === 'Escape') {
|
|
132
|
+
wrapper?.remove();
|
|
133
|
+
wrapper = null;
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return renderer?.ref?.onKeyDown(props) ?? false;
|
|
137
|
+
},
|
|
138
|
+
onExit: () => {
|
|
139
|
+
wrapper?.remove();
|
|
140
|
+
wrapper = null;
|
|
141
|
+
renderer?.destroy();
|
|
142
|
+
renderer = null;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** Build the slash-command extension bound to the editor's shared media pickers. */
|
|
149
|
+
export function createSlashCommand(options) {
|
|
150
|
+
return Extension.create({
|
|
151
|
+
name: 'slashCommand',
|
|
152
|
+
addOptions: () => options,
|
|
153
|
+
addProseMirrorPlugins() {
|
|
154
|
+
return [Suggestion({ editor: this.editor, ...slashSuggestion(this.options) })];
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=slash-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slash-command.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/slash-command.tsx"],"names":[],"mappings":"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","sourcesContent":["import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'\nimport type { ComponentType } from 'react'\nimport { Extension, ReactRenderer } from '@tiptap/react'\nimport type { Editor, Range } from '@tiptap/react'\nimport Suggestion from '@tiptap/suggestion'\nimport type { SuggestionKeyDownProps, SuggestionOptions, SuggestionProps } from '@tiptap/suggestion'\nimport {\n Blocks,\n Heading1,\n Heading2,\n Heading3,\n ImagePlus,\n List,\n ListOrdered,\n Minus,\n Pilcrow,\n Quote,\n SquareCode,\n} from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\n\n/**\n * Notion-style slash menu (EC-314): typing `/` at the caret opens a filterable\n * command palette that inserts block-level content — headings, lists, quote, code,\n * divider — and, when available, an image or a component embed (delegated to the\n * editor's shared pickers via {@link SlashCommandOptions}). Built on TipTap's\n * `Suggestion` utility; the menu is a React overlay positioned at the caret.\n */\n\nexport interface SlashCommandOptions {\n /** Open the asset picker (omitted ⇒ no `/image` item). */\n onImage?: () => void\n /** Open the component-embed picker (omitted ⇒ no `/embed` item). */\n onEmbed?: () => void\n}\n\nexport interface SlashItem {\n key: string\n title: string\n keywords: string\n icon: LucideIcon\n run: (editor: Editor, range: Range) => void\n}\n\n/** The command list, gated by which media actions the editor provided. */\nexport function buildSlashItems(options: SlashCommandOptions): SlashItem[] {\n const block = (run: (chain: ReturnType<Editor['chain']>) => ReturnType<Editor['chain']>) =>\n (editor: Editor, range: Range) => run(editor.chain().focus().deleteRange(range)).run()\n\n const items: SlashItem[] = [\n { key: 'text', title: translate('richText.slash.text'), keywords: 'paragraph text plain', icon: Pilcrow, run: block((c) => c.setParagraph()) },\n { key: 'h1', title: translate('richText.heading', { level: 1 }), keywords: 'heading title h1', icon: Heading1, run: block((c) => c.toggleHeading({ level: 1 })) },\n { key: 'h2', title: translate('richText.heading', { level: 2 }), keywords: 'heading subtitle h2', icon: Heading2, run: block((c) => c.toggleHeading({ level: 2 })) },\n { key: 'h3', title: translate('richText.heading', { level: 3 }), keywords: 'heading h3', icon: Heading3, run: block((c) => c.toggleHeading({ level: 3 })) },\n { key: 'bulletList', title: translate('richText.bulletList'), keywords: 'bullet unordered list ul', icon: List, run: block((c) => c.toggleBulletList()) },\n { key: 'orderedList', title: translate('richText.orderedList'), keywords: 'ordered numbered list ol', icon: ListOrdered, run: block((c) => c.toggleOrderedList()) },\n { key: 'blockquote', title: translate('richText.blockquote'), keywords: 'quote blockquote', icon: Quote, run: block((c) => c.toggleBlockquote()) },\n { key: 'codeBlock', title: translate('richText.codeBlock'), keywords: 'code block pre', icon: SquareCode, run: block((c) => c.toggleCodeBlock()) },\n { key: 'divider', title: translate('richText.horizontalRule'), keywords: 'divider rule hr separator', icon: Minus, run: block((c) => c.setHorizontalRule()) },\n ]\n if (options.onImage) {\n items.push({\n key: 'image',\n title: translate('richText.image.add'),\n keywords: 'image picture photo media',\n icon: ImagePlus,\n run: (editor, range) => {\n editor.chain().focus().deleteRange(range).run()\n options.onImage?.()\n },\n })\n }\n if (options.onEmbed) {\n items.push({\n key: 'embed',\n title: translate('richText.embed.add'),\n keywords: 'embed component block',\n icon: Blocks,\n run: (editor, range) => {\n editor.chain().focus().deleteRange(range).run()\n options.onEmbed?.()\n },\n })\n }\n return items\n}\n\n/** Imperative handle the suggestion render loop calls for keyboard navigation. */\ninterface SlashMenuHandle {\n onKeyDown: (props: SuggestionKeyDownProps) => boolean\n}\n\nconst SlashMenu = forwardRef<SlashMenuHandle, SuggestionProps<SlashItem, SlashItem>>((props, ref) => {\n const [selected, setSelected] = useState(0)\n useEffect(() => setSelected(0), [props.items])\n\n useImperativeHandle(ref, () => ({\n onKeyDown: ({ event }) => {\n const count = props.items.length\n if (count === 0) return false\n if (event.key === 'ArrowUp') {\n setSelected((s) => (s + count - 1) % count)\n return true\n }\n if (event.key === 'ArrowDown') {\n setSelected((s) => (s + 1) % count)\n return true\n }\n if (event.key === 'Enter') {\n const item = props.items[selected]\n if (item) props.command(item)\n return true\n }\n return false\n },\n }))\n\n if (props.items.length === 0) {\n return (\n <div className=\"ec-rte-slash\">\n <span className=\"px-2 py-1.5 text-xs text-muted-foreground\">\n {translate('richText.slash.empty')}\n </span>\n </div>\n )\n }\n\n return (\n <div className=\"ec-rte-slash\" role=\"menu\">\n {props.items.map((item, i) => {\n const Icon = item.icon\n return (\n <button\n key={item.key}\n type=\"button\"\n role=\"menuitem\"\n onMouseEnter={() => setSelected(i)}\n onMouseDown={(e) => {\n e.preventDefault()\n props.command(item)\n }}\n className={cn(\n 'flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left text-sm outline-none',\n i === selected ? 'bg-muted' : 'hover:bg-muted/60',\n )}\n >\n <Icon className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"truncate\">{item.title}</span>\n </button>\n )\n })}\n </div>\n )\n})\nSlashMenu.displayName = 'SlashMenu'\n\n/** Position a floating element under the caret, flipping above near the viewport edge. */\nfunction place(el: HTMLElement, rect: DOMRect | null | undefined): void {\n if (!rect) return\n const margin = 8\n const height = el.offsetHeight || 280\n const below = rect.bottom + 4\n const flip = below + height > window.innerHeight - margin && rect.top - height > margin\n el.style.position = 'fixed'\n el.style.left = `${Math.min(rect.left, window.innerWidth - 260)}px`\n el.style.top = flip ? `${rect.top - height - 4}px` : `${below}px`\n el.style.zIndex = '60'\n}\n\n/** The suggestion config (everything but the `editor`, which the extension injects). */\nfunction slashSuggestion(options: SlashCommandOptions): Omit<SuggestionOptions<SlashItem, SlashItem>, 'editor'> {\n return {\n char: '/',\n startOfLine: false,\n items: ({ query }) => {\n const q = query.trim().toLowerCase()\n const all = buildSlashItems(options)\n if (!q) return all\n return all.filter((item) => item.title.toLowerCase().includes(q) || item.keywords.includes(q))\n },\n command: ({ editor, range, props }) => props.run(editor, range),\n render: () => {\n let renderer: ReactRenderer<SlashMenuHandle, SuggestionProps<SlashItem, SlashItem>> | null = null\n let wrapper: HTMLDivElement | null = null\n\n return {\n onStart: (props) => {\n renderer = new ReactRenderer(SlashMenu as ComponentType<SuggestionProps<SlashItem, SlashItem>>, {\n props,\n editor: props.editor,\n })\n wrapper = document.createElement('div')\n wrapper.appendChild(renderer.element)\n document.body.appendChild(wrapper)\n place(wrapper, props.clientRect?.())\n },\n onUpdate: (props) => {\n renderer?.updateProps(props)\n if (wrapper) place(wrapper, props.clientRect?.())\n },\n onKeyDown: (props) => {\n if (props.event.key === 'Escape') {\n wrapper?.remove()\n wrapper = null\n return true\n }\n return renderer?.ref?.onKeyDown(props) ?? false\n },\n onExit: () => {\n wrapper?.remove()\n wrapper = null\n renderer?.destroy()\n renderer = null\n },\n }\n },\n }\n}\n\n/** Build the slash-command extension bound to the editor's shared media pickers. */\nexport function createSlashCommand(options: SlashCommandOptions) {\n return Extension.create<SlashCommandOptions>({\n name: 'slashCommand',\n addOptions: () => options,\n addProseMirrorPlugins() {\n return [Suggestion({ editor: this.editor, ...slashSuggestion(this.options) })]\n },\n })\n}\n"]}
|