@elytracms/studio 0.0.64 → 0.0.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/convex/assets.ts +11 -0
  2. package/convex/delivery.ts +5 -0
  3. package/convex/schema.ts +23 -0
  4. package/dist/components/assets/asset-thumb.d.ts +4 -2
  5. package/dist/components/assets/asset-thumb.js +26 -4
  6. package/dist/components/assets/asset-thumb.js.map +1 -1
  7. package/dist/components/assets/assets-module.js +48 -2
  8. package/dist/components/assets/assets-module.js.map +1 -1
  9. package/dist/components/assets/assets-store.d.ts +11 -1
  10. package/dist/components/assets/assets-store.js +18 -1
  11. package/dist/components/assets/assets-store.js.map +1 -1
  12. package/dist/components/builder/blocks-model.d.ts +11 -0
  13. package/dist/components/builder/blocks-model.js +26 -8
  14. package/dist/components/builder/blocks-model.js.map +1 -1
  15. package/dist/components/builder/canvas-chrome-css.js +90 -8
  16. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  17. package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
  18. package/dist/components/builder/canvas-inline-editing.js +3 -1
  19. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  20. package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
  21. package/dist/components/builder/canvas-overlay-geometry.js +80 -0
  22. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
  23. package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
  24. package/dist/components/builder/canvas-rect-tracking.js +198 -0
  25. package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
  26. package/dist/components/builder/canvas.js +61 -12
  27. package/dist/components/builder/canvas.js.map +1 -1
  28. package/dist/components/builder/composition-field-editor.js +22 -3
  29. package/dist/components/builder/composition-field-editor.js.map +1 -1
  30. package/dist/components/builder/delete-key.d.ts +38 -0
  31. package/dist/components/builder/delete-key.js +46 -0
  32. package/dist/components/builder/delete-key.js.map +1 -0
  33. package/dist/components/builder/drag-session.d.ts +8 -0
  34. package/dist/components/builder/drag-session.js +15 -0
  35. package/dist/components/builder/drag-session.js.map +1 -1
  36. package/dist/components/builder/insert-affordance-context.d.ts +19 -0
  37. package/dist/components/builder/insert-affordance-context.js +7 -0
  38. package/dist/components/builder/insert-affordance-context.js.map +1 -0
  39. package/dist/components/builder/insert-context.d.ts +40 -0
  40. package/dist/components/builder/insert-context.js +86 -3
  41. package/dist/components/builder/insert-context.js.map +1 -1
  42. package/dist/components/builder/node-title.d.ts +60 -0
  43. package/dist/components/builder/node-title.js +135 -0
  44. package/dist/components/builder/node-title.js.map +1 -0
  45. package/dist/components/builder/page-tree.js +21 -3
  46. package/dist/components/builder/page-tree.js.map +1 -1
  47. package/dist/components/builder/prop-selection.d.ts +52 -0
  48. package/dist/components/builder/prop-selection.js +38 -0
  49. package/dist/components/builder/prop-selection.js.map +1 -0
  50. package/dist/components/builder/prop-sync-context.d.ts +49 -0
  51. package/dist/components/builder/prop-sync-context.js +23 -0
  52. package/dist/components/builder/prop-sync-context.js.map +1 -0
  53. package/dist/components/builder/property-inspector.js +23 -1
  54. package/dist/components/builder/property-inspector.js.map +1 -1
  55. package/dist/components/builder/reuse-context.js +1 -1
  56. package/dist/components/builder/reuse-context.js.map +1 -1
  57. package/dist/components/builder/selection-overlay.d.ts +79 -0
  58. package/dist/components/builder/selection-overlay.js +244 -0
  59. package/dist/components/builder/selection-overlay.js.map +1 -0
  60. package/dist/components/builder/spacing-handles.d.ts +57 -1
  61. package/dist/components/builder/spacing-handles.js +142 -26
  62. package/dist/components/builder/spacing-handles.js.map +1 -1
  63. package/dist/components/cms/asset-picker.js +11 -5
  64. package/dist/components/cms/asset-picker.js.map +1 -1
  65. package/dist/components/cms/rich-text/block-style.d.ts +48 -0
  66. package/dist/components/cms/rich-text/block-style.js +30 -0
  67. package/dist/components/cms/rich-text/block-style.js.map +1 -0
  68. package/dist/components/cms/rich-text/embed-picker.js +4 -3
  69. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  70. package/dist/components/cms/rich-text/extensions.d.ts +2 -0
  71. package/dist/components/cms/rich-text/extensions.js +4 -0
  72. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  73. package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
  74. package/dist/components/cms/rich-text/font-size-control.js +40 -0
  75. package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
  76. package/dist/components/cms/rich-text/font-size.d.ts +16 -0
  77. package/dist/components/cms/rich-text/font-size.js +40 -0
  78. package/dist/components/cms/rich-text/font-size.js.map +1 -0
  79. package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
  80. package/dist/components/cms/rich-text/toolbar-state.js +3 -0
  81. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
  82. package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
  83. package/dist/components/cms/rich-text/toolbar.js +17 -5
  84. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  85. package/dist/components/components/component-detail.js +3 -3
  86. package/dist/components/components/component-detail.js.map +1 -1
  87. package/dist/components/components/component-preview.js +1 -1
  88. package/dist/components/components/component-preview.js.map +1 -1
  89. package/dist/components/components/registry-browser.js +2 -2
  90. package/dist/components/components/registry-browser.js.map +1 -1
  91. package/dist/components/components/taxonomy.js +3 -2
  92. package/dist/components/components/taxonomy.js.map +1 -1
  93. package/dist/components/components/transfer-dialog.js +1 -1
  94. package/dist/components/components/transfer-dialog.js.map +1 -1
  95. package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
  96. package/dist/lib/assets/backfill-thumbs.js +120 -0
  97. package/dist/lib/assets/backfill-thumbs.js.map +1 -0
  98. package/dist/lib/assets/delete-asset.d.ts +2 -1
  99. package/dist/lib/assets/delete-asset.js +12 -0
  100. package/dist/lib/assets/delete-asset.js.map +1 -1
  101. package/dist/lib/assets/index.d.ts +2 -0
  102. package/dist/lib/assets/index.js +2 -0
  103. package/dist/lib/assets/index.js.map +1 -1
  104. package/dist/lib/assets/thumbnail.d.ts +145 -0
  105. package/dist/lib/assets/thumbnail.js +220 -0
  106. package/dist/lib/assets/thumbnail.js.map +1 -0
  107. package/dist/lib/assets/upload.d.ts +9 -0
  108. package/dist/lib/assets/upload.js +31 -0
  109. package/dist/lib/assets/upload.js.map +1 -1
  110. package/dist/lib/i18n/messages.de.js +16 -0
  111. package/dist/lib/i18n/messages.de.js.map +1 -1
  112. package/dist/lib/i18n/messages.en.d.ts +14 -0
  113. package/dist/lib/i18n/messages.en.js +16 -0
  114. package/dist/lib/i18n/messages.en.js.map +1 -1
  115. package/dist/lib/persistence/convex-adapter.js +4 -0
  116. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  117. package/dist/lib/persona/labels.d.ts +9 -5
  118. package/dist/lib/persona/labels.js +13 -8
  119. package/dist/lib/persona/labels.js.map +1 -1
  120. package/dist/lib/references/references-store.js +7 -1
  121. package/dist/lib/references/references-store.js.map +1 -1
  122. package/package.json +2 -2
  123. package/src/styles/studio.css +13 -0
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Pilcrow } from 'lucide-react';
3
+ import { translate } from '@elytracms/studio/lib/i18n';
4
+ /** The block styles the toolbar offers, in menu order (paragraph, then H1–H3). */
5
+ export const BLOCK_STYLES = [
6
+ { headingLevel: null, chain: (c) => c.setParagraph() },
7
+ { headingLevel: 1, chain: (c) => c.toggleHeading({ level: 1 }) },
8
+ { headingLevel: 2, chain: (c) => c.toggleHeading({ level: 2 }) },
9
+ { headingLevel: 3, chain: (c) => c.toggleHeading({ level: 3 }) },
10
+ ];
11
+ export function blockStyleIcon(headingLevel) {
12
+ return headingLevel == null ? { kind: 'paragraph' } : { kind: 'heading', level: headingLevel };
13
+ }
14
+ /** The localized label for a block style — the menu-item text and the trigger's a11y name. */
15
+ export function blockStyleLabel(headingLevel) {
16
+ return headingLevel == null
17
+ ? translate('richText.paragraph')
18
+ : translate('richText.heading', { level: headingLevel });
19
+ }
20
+ /**
21
+ * The one glyph rendered for a block style, shared by the trigger and the menu
22
+ * items (EC-418). Fixed-width by construction: the `Pilcrow` icon is `size-3.5`
23
+ * (14px) and the heading badge is `w-3.5` (14px), so the active-style glyph never
24
+ * changes the trigger's width as the selection moves across styles.
25
+ */
26
+ export function BlockStyleGlyph({ headingLevel }) {
27
+ const icon = blockStyleIcon(headingLevel);
28
+ return icon.kind === 'paragraph' ? (_jsx(Pilcrow, { className: "size-3.5" })) : (_jsxs("span", { className: "w-3.5 text-center text-xs font-semibold", children: ["H", icon.level] }));
29
+ }
30
+ //# sourceMappingURL=block-style.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-style.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/block-style.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AA0BtD,kFAAkF;AAClF,MAAM,CAAC,MAAM,YAAY,GAAoC;IAC3D,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,EAAE,EAAE;IACtD,EAAE,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE;IAChE,EAAE,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE;IAChE,EAAE,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE;CACxD,CAAA;AASV,MAAM,UAAU,cAAc,CAAC,YAA2B;IACxD,OAAO,YAAY,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,CAAA;AAChG,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,eAAe,CAAC,YAA2B;IACzD,OAAO,YAAY,IAAI,IAAI;QACzB,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC;QACjC,CAAC,CAAC,SAAS,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,YAAY,EAAmC;IAC/E,MAAM,IAAI,GAAG,cAAc,CAAC,YAAY,CAAC,CAAA;IACzC,OAAO,IAAI,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,CACjC,KAAC,OAAO,IAAC,SAAS,EAAC,UAAU,GAAG,CACjC,CAAC,CAAC,CAAC,CACF,gBAAM,SAAS,EAAC,yCAAyC,kBAAG,IAAI,CAAC,KAAK,IAAQ,CAC/E,CAAA;AACH,CAAC","sourcesContent":["import { Pilcrow } from 'lucide-react'\nimport type { Editor } from '@tiptap/react'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Shared block-style vocabulary for the rich-text toolbar (EC-418).\n *\n * The block-style dropdown's TRIGGER and its MENU ITEMS must show the SAME\n * icon-per-style so they can never drift: paragraph → the lucide `Pilcrow`, a\n * heading → its compact `H{level}` badge. Declaring the mapping ONCE here —\n * consumed by both — makes that drift structurally impossible, the same pattern\n * `block-commands.ts` (EC-328) uses for the list/quote/code commands.\n *\n * This also underpins the EC-418 width-stability fix: the trigger renders the\n * ACTIVE style's fixed-width glyph instead of its variable-width localized label\n * (\"Absatz\" ≪ \"Überschrift 2\"), so caret movement across styles never reflows the\n * strip. The full localized labels appear only in the opened menu, and the trigger\n * keeps its accessible name via {@link blockStyleLabel} (an `aria-label`).\n */\n\n/** A block-style option: paragraph (`headingLevel: null`) or a heading level. */\nexport interface BlockStyleDescriptor {\n /** Heading level, or `null` for paragraph (the base body style). */\n headingLevel: number | null\n /** Apply this style on an already-`.focus()`ed chain. */\n chain: (chain: ReturnType<Editor['chain']>) => ReturnType<Editor['chain']>\n}\n\n/** The block styles the toolbar offers, in menu order (paragraph, then H1–H3). */\nexport const BLOCK_STYLES: readonly BlockStyleDescriptor[] = [\n { headingLevel: null, chain: (c) => c.setParagraph() },\n { headingLevel: 1, chain: (c) => c.toggleHeading({ level: 1 }) },\n { headingLevel: 2, chain: (c) => c.toggleHeading({ level: 2 }) },\n { headingLevel: 3, chain: (c) => c.toggleHeading({ level: 3 }) },\n] as const\n\n/**\n * The pure icon-per-style decision (EC-418) — the single source both the trigger\n * and the menu items derive their glyph from (via {@link BlockStyleGlyph}). Kept\n * data-only (no JSX) so it is unit-testable in a node env without a DOM.\n */\nexport type BlockStyleIcon = { kind: 'paragraph' } | { kind: 'heading'; level: number }\n\nexport function blockStyleIcon(headingLevel: number | null): BlockStyleIcon {\n return headingLevel == null ? { kind: 'paragraph' } : { kind: 'heading', level: headingLevel }\n}\n\n/** The localized label for a block style — the menu-item text and the trigger's a11y name. */\nexport function blockStyleLabel(headingLevel: number | null): string {\n return headingLevel == null\n ? translate('richText.paragraph')\n : translate('richText.heading', { level: headingLevel })\n}\n\n/**\n * The one glyph rendered for a block style, shared by the trigger and the menu\n * items (EC-418). Fixed-width by construction: the `Pilcrow` icon is `size-3.5`\n * (14px) and the heading badge is `w-3.5` (14px), so the active-style glyph never\n * changes the trigger's width as the selection moves across styles.\n */\nexport function BlockStyleGlyph({ headingLevel }: { headingLevel: number | null }) {\n const icon = blockStyleIcon(headingLevel)\n return icon.kind === 'paragraph' ? (\n <Pilcrow className=\"size-3.5\" />\n ) : (\n <span className=\"w-3.5 text-center text-xs font-semibold\">H{icon.level}</span>\n )\n}\n"]}
@@ -3,7 +3,8 @@ import { useMemo, useState } from 'react';
3
3
  import { Blocks } from 'lucide-react';
4
4
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
5
5
  import { Dialog, DialogContent, DialogHeader, DialogTitle, Input, Stack, Text, } from '@elytracms/studio/ui';
6
- import { translate } from '@elytracms/studio/lib/i18n';
6
+ import { localizedStringVariants } from '@elytracms/core/cms-core';
7
+ import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
7
8
  import { cn } from '@elytracms/studio/lib/utils';
8
9
  import { listEmbeddable } from './insert.js';
9
10
  /**
@@ -18,13 +19,13 @@ export function EmbedPicker({ open, onOpenChange, allow, onSelect }) {
18
19
  const q = query.trim().toLowerCase();
19
20
  if (!q)
20
21
  return embeddable;
21
- return embeddable.filter((m) => (m.title ?? m.id).toLowerCase().includes(q) || m.id.toLowerCase().includes(q));
22
+ return embeddable.filter((m) => [m.id, ...localizedStringVariants(m.title)].some((s) => s.toLowerCase().includes(q)));
22
23
  }, [embeddable, query]);
23
24
  function choose(manifest) {
24
25
  onSelect(manifest);
25
26
  onOpenChange(false);
26
27
  setQuery('');
27
28
  }
28
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: translate('richText.embed.add') }) }), _jsxs(Stack, { gap: "2", children: [_jsx(Input, { autoFocus: true, value: query, placeholder: translate('richText.link.documentSearch'), onChange: (e) => setQuery(e.target.value) }), _jsx(Stack, { gap: "none", className: "max-h-72 overflow-y-auto", children: results.length === 0 ? (_jsx(Text, { as: "span", size: "xs", tone: "muted", className: "px-1 py-2", children: translate('richText.embed.empty') })) : (results.map((manifest) => (_jsxs("button", { type: "button", onClick: () => choose(manifest), className: cn('flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-left', 'hover:bg-muted focus-visible:bg-muted outline-none'), children: [_jsx(Blocks, { className: "size-4 shrink-0 text-muted-foreground" }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate text-sm font-medium", children: manifest.title ?? manifest.id }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: manifest.id })] })] }, manifest.id)))) })] })] }) }));
29
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: translate('richText.embed.add') }) }), _jsxs(Stack, { gap: "2", children: [_jsx(Input, { autoFocus: true, value: query, placeholder: translate('richText.link.documentSearch'), onChange: (e) => setQuery(e.target.value) }), _jsx(Stack, { gap: "none", className: "max-h-72 overflow-y-auto", children: results.length === 0 ? (_jsx(Text, { as: "span", size: "xs", tone: "muted", className: "px-1 py-2", children: translate('richText.embed.empty') })) : (results.map((manifest) => (_jsxs("button", { type: "button", onClick: () => choose(manifest), className: cn('flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-left', 'hover:bg-muted focus-visible:bg-muted outline-none'), children: [_jsx(Blocks, { className: "size-4 shrink-0 text-muted-foreground" }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate text-sm font-medium", children: localizedLabel(manifest.title, manifest.id) }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: manifest.id })] })] }, manifest.id)))) })] })] }) }));
29
30
  }
30
31
  //# sourceMappingURL=embed-picker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"embed-picker.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { Blocks } from 'lucide-react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n Input,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { listEmbeddable } from './insert'\n\ninterface EmbedPickerProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n /** The field's embed `allow` list (component ids). Empty/absent ⇒ any non-primitive. */\n allow?: string[]\n onSelect: (manifest: ComponentManifest) => void\n}\n\n/**\n * The component-embed picker dialog (EC-314/EC-189): a searchable list of the\n * components permitted for inline embedding. Rendered once at the editor level and\n * opened by the toolbar button or the `/embed` slash command.\n */\nexport function EmbedPicker({ open, onOpenChange, allow, onSelect }: EmbedPickerProps) {\n const [query, setQuery] = useState('')\n\n const embeddable = useMemo(\n () => listEmbeddable(allow, getStudioRegistry().registry.list()),\n [allow],\n )\n\n const results = useMemo(() => {\n const q = query.trim().toLowerCase()\n if (!q) return embeddable\n return embeddable.filter(\n (m) => (m.title ?? m.id).toLowerCase().includes(q) || m.id.toLowerCase().includes(q),\n )\n }, [embeddable, query])\n\n function choose(manifest: ComponentManifest) {\n onSelect(manifest)\n onOpenChange(false)\n setQuery('')\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{translate('richText.embed.add')}</DialogTitle>\n </DialogHeader>\n <Stack gap=\"2\">\n <Input\n autoFocus\n value={query}\n placeholder={translate('richText.link.documentSearch')}\n onChange={(e) => setQuery(e.target.value)}\n />\n <Stack gap=\"none\" className=\"max-h-72 overflow-y-auto\">\n {results.length === 0 ? (\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"px-1 py-2\">\n {translate('richText.embed.empty')}\n </Text>\n ) : (\n results.map((manifest) => (\n <button\n key={manifest.id}\n type=\"button\"\n onClick={() => choose(manifest)}\n className={cn(\n 'flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-left',\n 'hover:bg-muted focus-visible:bg-muted outline-none',\n )}\n >\n <Blocks className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-sm font-medium\">{manifest.title ?? manifest.id}</span>\n <span className=\"truncate text-xs text-muted-foreground\">{manifest.id}</span>\n </span>\n </button>\n ))\n )}\n </Stack>\n </Stack>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
1
+ {"version":3,"file":"embed-picker.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { Blocks } from 'lucide-react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n Input,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { localizedStringVariants } from '@elytracms/core/cms-core'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { listEmbeddable } from './insert'\n\ninterface EmbedPickerProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n /** The field's embed `allow` list (component ids). Empty/absent ⇒ any non-primitive. */\n allow?: string[]\n onSelect: (manifest: ComponentManifest) => void\n}\n\n/**\n * The component-embed picker dialog (EC-314/EC-189): a searchable list of the\n * components permitted for inline embedding. Rendered once at the editor level and\n * opened by the toolbar button or the `/embed` slash command.\n */\nexport function EmbedPicker({ open, onOpenChange, allow, onSelect }: EmbedPickerProps) {\n const [query, setQuery] = useState('')\n\n const embeddable = useMemo(\n () => listEmbeddable(allow, getStudioRegistry().registry.list()),\n [allow],\n )\n\n const results = useMemo(() => {\n const q = query.trim().toLowerCase()\n if (!q) return embeddable\n return embeddable.filter((m) =>\n [m.id, ...localizedStringVariants(m.title)].some((s) => s.toLowerCase().includes(q)),\n )\n }, [embeddable, query])\n\n function choose(manifest: ComponentManifest) {\n onSelect(manifest)\n onOpenChange(false)\n setQuery('')\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{translate('richText.embed.add')}</DialogTitle>\n </DialogHeader>\n <Stack gap=\"2\">\n <Input\n autoFocus\n value={query}\n placeholder={translate('richText.link.documentSearch')}\n onChange={(e) => setQuery(e.target.value)}\n />\n <Stack gap=\"none\" className=\"max-h-72 overflow-y-auto\">\n {results.length === 0 ? (\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"px-1 py-2\">\n {translate('richText.embed.empty')}\n </Text>\n ) : (\n results.map((manifest) => (\n <button\n key={manifest.id}\n type=\"button\"\n onClick={() => choose(manifest)}\n className={cn(\n 'flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-left',\n 'hover:bg-muted focus-visible:bg-muted outline-none',\n )}\n >\n <Blocks className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-sm font-medium\">{localizedLabel(manifest.title, manifest.id)}</span>\n <span className=\"truncate text-xs text-muted-foreground\">{manifest.id}</span>\n </span>\n </button>\n ))\n )}\n </Stack>\n </Stack>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
@@ -17,6 +17,8 @@ import type { SlashCommandOptions } from './slash-command.js';
17
17
  * - {@link AssetImage} / {@link ComponentEmbed} declare the two reference-carrying
18
18
  * atoms (asset image, embedded registry component) so existing content of those
19
19
  * kinds survives a round-trip verbatim rather than being dropped by the schema.
20
+ * - {@link FontSize} declares the semantic font-size mark (EC-411) so a sized run
21
+ * round-trips (an undeclared mark would be silently dropped on load).
20
22
  *
21
23
  * No `allow` gating lives here — that governs *insertion* (the embed picker) and
22
24
  * *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.
@@ -2,6 +2,7 @@ import StarterKit from '@tiptap/starter-kit';
2
2
  import { Placeholder } from '@tiptap/extensions';
3
3
  import { translate } from '@elytracms/studio/lib/i18n';
4
4
  import { RefLink } from './ref-link.js';
5
+ import { FontSize } from './font-size.js';
5
6
  import { AssetImage } from './asset-image.js';
6
7
  import { ComponentEmbed } from './component-embed.js';
7
8
  import { createSlashCommand } from './slash-command.js';
@@ -22,6 +23,8 @@ import { createSlashCommand } from './slash-command.js';
22
23
  * - {@link AssetImage} / {@link ComponentEmbed} declare the two reference-carrying
23
24
  * atoms (asset image, embedded registry component) so existing content of those
24
25
  * kinds survives a round-trip verbatim rather than being dropped by the schema.
26
+ * - {@link FontSize} declares the semantic font-size mark (EC-411) so a sized run
27
+ * round-trips (an undeclared mark would be silently dropped on load).
25
28
  *
26
29
  * No `allow` gating lives here — that governs *insertion* (the embed picker) and
27
30
  * *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.
@@ -36,6 +39,7 @@ export function buildRichTextExtensions(options = {}) {
36
39
  heading: { levels: [1, 2, 3, 4, 5, 6] },
37
40
  }),
38
41
  RefLink.configure({ openOnClick: false, autolink: true, linkOnPaste: true }),
42
+ FontSize,
39
43
  AssetImage,
40
44
  ComponentEmbed,
41
45
  Placeholder.configure({ placeholder: translate('richText.placeholder') }),
@@ -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;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"]}
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,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAGpD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;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,QAAQ;QACR,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 { FontSize } from './font-size'\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 * - {@link FontSize} declares the semantic font-size mark (EC-411) so a sized run\n * round-trips (an undeclared mark would be silently dropped on load).\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 FontSize,\n AssetImage,\n ComponentEmbed,\n Placeholder.configure({ placeholder: translate('richText.placeholder') }),\n createSlashCommand(options.slash ?? {}),\n ]\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import type { Editor } from '@tiptap/react';
2
+ import type { RichTextFontSize } from '@elytracms/core/rich-text';
3
+ import type { MessageKey } from '@elytracms/studio/lib/i18n';
4
+ /**
5
+ * A font-size preset in the toolbar's dropdown (EC-411). `size: null` is the
6
+ * markless *normal* step — selecting it REMOVES the mark rather than storing a value.
7
+ */
8
+ export interface FontSizePreset {
9
+ size: RichTextFontSize | null;
10
+ labelKey: MessageKey;
11
+ }
12
+ /** The semantic scale the toolbar offers, ascending: small · normal · large · x-large. */
13
+ export declare const FONT_SIZE_PRESETS: readonly FontSizePreset[];
14
+ /** Whether a preset is the currently-active step (normal ⇔ no mark ⇔ activeSize null). */
15
+ export declare function isPresetActive(preset: FontSizePreset, activeSize: RichTextFontSize | null): boolean;
16
+ /** Apply a preset on a focused chain: set the mark for a step, or unset it for normal. */
17
+ export declare function applyFontSizePreset(editor: Editor, size: RichTextFontSize | null): void;
18
+ interface FontSizeControlProps {
19
+ editor: Editor;
20
+ /** The active semantic step (from the toolbar state), or null for normal. */
21
+ activeSize: RichTextFontSize | null;
22
+ }
23
+ /**
24
+ * The rich-text font-size control (EC-411). Width-stable under the EC-418 contract
25
+ * (AD-4): the trigger shows a fixed-width icon (+ ChevronDown) in the SAME fixed box
26
+ * as the block-style trigger ({@link RICH_TEXT_MENU_TRIGGER_CLASS}) — the size labels
27
+ * live only in the opened menu, never on the strip, so the control never resizes with
28
+ * selection. The trigger keeps an accessible name via an `aria-label`.
29
+ */
30
+ export declare function FontSizeControl({ editor, activeSize }: FontSizeControlProps): import("react").JSX.Element;
31
+ export {};
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ALargeSmall, Check, ChevronDown } from 'lucide-react';
3
+ import { FONT_SIZE_MARK_TYPE } from '@elytracms/core/rich-text';
4
+ import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@elytracms/studio/ui';
5
+ import { cn } from '@elytracms/studio/lib/utils';
6
+ import { translate } from '@elytracms/studio/lib/i18n';
7
+ import { RICH_TEXT_MENU_TRIGGER_CLASS } from './toolbar.js';
8
+ /** The semantic scale the toolbar offers, ascending: small · normal · large · x-large. */
9
+ export const FONT_SIZE_PRESETS = [
10
+ { size: 'sm', labelKey: 'richText.fontSize.small' },
11
+ { size: null, labelKey: 'richText.fontSize.normal' },
12
+ { size: 'lg', labelKey: 'richText.fontSize.large' },
13
+ { size: 'xl', labelKey: 'richText.fontSize.xlarge' },
14
+ ];
15
+ /** Whether a preset is the currently-active step (normal ⇔ no mark ⇔ activeSize null). */
16
+ export function isPresetActive(preset, activeSize) {
17
+ return preset.size === activeSize;
18
+ }
19
+ /** Apply a preset on a focused chain: set the mark for a step, or unset it for normal. */
20
+ export function applyFontSizePreset(editor, size) {
21
+ const chain = editor.chain().focus();
22
+ if (size)
23
+ chain.setMark(FONT_SIZE_MARK_TYPE, { size }).run();
24
+ else
25
+ chain.unsetMark(FONT_SIZE_MARK_TYPE).run();
26
+ }
27
+ /**
28
+ * The rich-text font-size control (EC-411). Width-stable under the EC-418 contract
29
+ * (AD-4): the trigger shows a fixed-width icon (+ ChevronDown) in the SAME fixed box
30
+ * as the block-style trigger ({@link RICH_TEXT_MENU_TRIGGER_CLASS}) — the size labels
31
+ * live only in the opened menu, never on the strip, so the control never resizes with
32
+ * selection. The trigger keeps an accessible name via an `aria-label`.
33
+ */
34
+ export function FontSizeControl({ editor, activeSize }) {
35
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: RICH_TEXT_MENU_TRIGGER_CLASS, "aria-label": translate('richText.fontSize.label'), title: translate('richText.fontSize.label'), onMouseDown: (e) => e.preventDefault(), children: [_jsx(ALargeSmall, { className: "size-3.5" }), _jsx(ChevronDown, { className: "size-3 opacity-60" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "min-w-40", children: FONT_SIZE_PRESETS.map((preset) => {
36
+ const active = isPresetActive(preset, activeSize);
37
+ return (_jsxs(DropdownMenuItem, { onSelect: () => applyFontSizePreset(editor, preset.size), children: [_jsx(Check, { className: cn('size-3.5', !active && 'invisible') }), ' ', translate(preset.labelKey)] }, preset.size ?? 'normal'));
38
+ }) })] }));
39
+ }
40
+ //# sourceMappingURL=font-size-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"font-size-control.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/font-size-control.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAA;AAE/D,OAAO,EACL,MAAM,EACN,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD,OAAO,EAAE,4BAA4B,EAAE,MAAM,WAAW,CAAA;AAWxD,0FAA0F;AAC1F,MAAM,CAAC,MAAM,iBAAiB,GAA8B;IAC1D,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,yBAAyB,EAAE;IACnD,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,0BAA0B,EAAE;IACpD,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,yBAAyB,EAAE;IACnD,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,0BAA0B,EAAE;CACrD,CAAA;AAED,0FAA0F;AAC1F,MAAM,UAAU,cAAc,CAAC,MAAsB,EAAE,UAAmC;IACxF,OAAO,MAAM,CAAC,IAAI,KAAK,UAAU,CAAA;AACnC,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,IAA6B;IAC/E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAA;IACpC,IAAI,IAAI;QAAE,KAAK,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,CAAA;;QACvD,KAAK,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC,GAAG,EAAE,CAAA;AACjD,CAAC;AAQD;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,MAAM,EAAE,UAAU,EAAwB;IAC1E,OAAO,CACL,MAAC,YAAY,eACX,KAAC,mBAAmB,IAAC,OAAO,kBAC1B,MAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,SAAS,EAAE,4BAA4B,gBAC3B,SAAS,CAAC,yBAAyB,CAAC,EAChD,KAAK,EAAE,SAAS,CAAC,yBAAyB,CAAC,EAC3C,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,EAAE,aAEtC,KAAC,WAAW,IAAC,SAAS,EAAC,UAAU,GAAG,EACpC,KAAC,WAAW,IAAC,SAAS,EAAC,mBAAmB,GAAG,IACtC,GACW,EACtB,KAAC,mBAAmB,IAAC,KAAK,EAAC,OAAO,EAAC,SAAS,EAAC,UAAU,YACpD,iBAAiB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;oBAChC,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;oBACjD,OAAO,CACL,MAAC,gBAAgB,IAEf,QAAQ,EAAE,GAAG,EAAE,CAAC,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,aAExD,KAAC,KAAK,IAAC,SAAS,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,MAAM,IAAI,WAAW,CAAC,GAAI,EAAC,GAAG,EAC/D,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,KAJtB,MAAM,CAAC,IAAI,IAAI,QAAQ,CAKX,CACpB,CAAA;gBACH,CAAC,CAAC,GACkB,IACT,CAChB,CAAA;AACH,CAAC","sourcesContent":["import { ALargeSmall, Check, ChevronDown } from 'lucide-react'\nimport type { Editor } from '@tiptap/react'\nimport { FONT_SIZE_MARK_TYPE } from '@elytracms/core/rich-text'\nimport type { RichTextFontSize } from '@elytracms/core/rich-text'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { MessageKey } from '@elytracms/studio/lib/i18n'\nimport { RICH_TEXT_MENU_TRIGGER_CLASS } from './toolbar'\n\n/**\n * A font-size preset in the toolbar's dropdown (EC-411). `size: null` is the\n * markless *normal* step — selecting it REMOVES the mark rather than storing a value.\n */\nexport interface FontSizePreset {\n size: RichTextFontSize | null\n labelKey: MessageKey\n}\n\n/** The semantic scale the toolbar offers, ascending: small · normal · large · x-large. */\nexport const FONT_SIZE_PRESETS: readonly FontSizePreset[] = [\n { size: 'sm', labelKey: 'richText.fontSize.small' },\n { size: null, labelKey: 'richText.fontSize.normal' },\n { size: 'lg', labelKey: 'richText.fontSize.large' },\n { size: 'xl', labelKey: 'richText.fontSize.xlarge' },\n]\n\n/** Whether a preset is the currently-active step (normal ⇔ no mark ⇔ activeSize null). */\nexport function isPresetActive(preset: FontSizePreset, activeSize: RichTextFontSize | null): boolean {\n return preset.size === activeSize\n}\n\n/** Apply a preset on a focused chain: set the mark for a step, or unset it for normal. */\nexport function applyFontSizePreset(editor: Editor, size: RichTextFontSize | null): void {\n const chain = editor.chain().focus()\n if (size) chain.setMark(FONT_SIZE_MARK_TYPE, { size }).run()\n else chain.unsetMark(FONT_SIZE_MARK_TYPE).run()\n}\n\ninterface FontSizeControlProps {\n editor: Editor\n /** The active semantic step (from the toolbar state), or null for normal. */\n activeSize: RichTextFontSize | null\n}\n\n/**\n * The rich-text font-size control (EC-411). Width-stable under the EC-418 contract\n * (AD-4): the trigger shows a fixed-width icon (+ ChevronDown) in the SAME fixed box\n * as the block-style trigger ({@link RICH_TEXT_MENU_TRIGGER_CLASS}) — the size labels\n * live only in the opened menu, never on the strip, so the control never resizes with\n * selection. The trigger keeps an accessible name via an `aria-label`.\n */\nexport function FontSizeControl({ editor, activeSize }: FontSizeControlProps) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={RICH_TEXT_MENU_TRIGGER_CLASS}\n aria-label={translate('richText.fontSize.label')}\n title={translate('richText.fontSize.label')}\n onMouseDown={(e) => e.preventDefault()}\n >\n <ALargeSmall className=\"size-3.5\" />\n <ChevronDown className=\"size-3 opacity-60\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"min-w-40\">\n {FONT_SIZE_PRESETS.map((preset) => {\n const active = isPresetActive(preset, activeSize)\n return (\n <DropdownMenuItem\n key={preset.size ?? 'normal'}\n onSelect={() => applyFontSizePreset(editor, preset.size)}\n >\n <Check className={cn('size-3.5', !active && 'invisible')} />{' '}\n {translate(preset.labelKey)}\n </DropdownMenuItem>\n )\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import { Mark } from '@tiptap/react';
2
+ /**
3
+ * The CMS font-size mark (EC-411) — a project-owned TipTap mark over the semantic
4
+ * size model in `@elytracms/core/rich-text`. It stores exactly `{ type: 'fontSize',
5
+ * attrs: { size } }` where `size ∈ sm/lg/xl`, matching the storage vocabulary
6
+ * one-for-one so the document round-trips through `parseRichTextValue` / the delivery
7
+ * renderer with no format change (the same discipline as the rest of the extension
8
+ * set). *Normal = no mark*: the toolbar removes the mark rather than storing a value.
9
+ *
10
+ * The mark carries no custom commands — the toolbar applies/removes it with the
11
+ * built-in `setMark('fontSize', { size })` / `unsetMark('fontSize')`. Its DOM
12
+ * rendering mirrors the delivery renderer's hook exactly (a `data-rich-text-size`
13
+ * attribute + `rich-text-size-{size}` class), so the editing surface styles the steps
14
+ * with the same CSS hook the delivery output exposes (no inline pixel styles).
15
+ */
16
+ export declare const FontSize: Mark<any, any>;
@@ -0,0 +1,40 @@
1
+ import { Mark, mergeAttributes } from '@tiptap/react';
2
+ import { FONT_SIZE_DATA_ATTR, FONT_SIZE_MARK_TYPE, fontSizeClass, isRichTextFontSize, } from '@elytracms/core/rich-text';
3
+ /**
4
+ * The CMS font-size mark (EC-411) — a project-owned TipTap mark over the semantic
5
+ * size model in `@elytracms/core/rich-text`. It stores exactly `{ type: 'fontSize',
6
+ * attrs: { size } }` where `size ∈ sm/lg/xl`, matching the storage vocabulary
7
+ * one-for-one so the document round-trips through `parseRichTextValue` / the delivery
8
+ * renderer with no format change (the same discipline as the rest of the extension
9
+ * set). *Normal = no mark*: the toolbar removes the mark rather than storing a value.
10
+ *
11
+ * The mark carries no custom commands — the toolbar applies/removes it with the
12
+ * built-in `setMark('fontSize', { size })` / `unsetMark('fontSize')`. Its DOM
13
+ * rendering mirrors the delivery renderer's hook exactly (a `data-rich-text-size`
14
+ * attribute + `rich-text-size-{size}` class), so the editing surface styles the steps
15
+ * with the same CSS hook the delivery output exposes (no inline pixel styles).
16
+ */
17
+ export const FontSize = Mark.create({
18
+ name: FONT_SIZE_MARK_TYPE,
19
+ addAttributes() {
20
+ return {
21
+ size: {
22
+ default: null,
23
+ parseHTML: (element) => element.getAttribute(FONT_SIZE_DATA_ATTR),
24
+ renderHTML: (attributes) => {
25
+ const size = attributes.size;
26
+ if (!isRichTextFontSize(size))
27
+ return {};
28
+ return { [FONT_SIZE_DATA_ATTR]: size, class: fontSizeClass(size) };
29
+ },
30
+ },
31
+ };
32
+ },
33
+ parseHTML() {
34
+ return [{ tag: `span[${FONT_SIZE_DATA_ATTR}]` }];
35
+ },
36
+ renderHTML({ HTMLAttributes }) {
37
+ return ['span', mergeAttributes(HTMLAttributes), 0];
38
+ },
39
+ });
40
+ //# sourceMappingURL=font-size.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"font-size.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/font-size.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,aAAa,EACb,kBAAkB,GACnB,MAAM,2BAA2B,CAAA;AAElC;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,mBAAmB;IAEzB,aAAa;QACX,OAAO;YACL,IAAI,EAAE;gBACJ,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,mBAAmB,CAAC;gBACjE,UAAU,EAAE,CAAC,UAAU,EAAE,EAAE;oBACzB,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAA;oBAC5B,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC;wBAAE,OAAO,EAAE,CAAA;oBACxC,OAAO,EAAE,CAAC,mBAAmB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,CAAA;gBACpE,CAAC;aACF;SACF,CAAA;IACH,CAAC;IAED,SAAS;QACP,OAAO,CAAC,EAAE,GAAG,EAAE,QAAQ,mBAAmB,GAAG,EAAE,CAAC,CAAA;IAClD,CAAC;IAED,UAAU,CAAC,EAAE,cAAc,EAAE;QAC3B,OAAO,CAAC,MAAM,EAAE,eAAe,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAA;IACrD,CAAC;CACF,CAAC,CAAA","sourcesContent":["import { Mark, mergeAttributes } from '@tiptap/react'\nimport {\n FONT_SIZE_DATA_ATTR,\n FONT_SIZE_MARK_TYPE,\n fontSizeClass,\n isRichTextFontSize,\n} from '@elytracms/core/rich-text'\n\n/**\n * The CMS font-size mark (EC-411) — a project-owned TipTap mark over the semantic\n * size model in `@elytracms/core/rich-text`. It stores exactly `{ type: 'fontSize',\n * attrs: { size } }` where `size ∈ sm/lg/xl`, matching the storage vocabulary\n * one-for-one so the document round-trips through `parseRichTextValue` / the delivery\n * renderer with no format change (the same discipline as the rest of the extension\n * set). *Normal = no mark*: the toolbar removes the mark rather than storing a value.\n *\n * The mark carries no custom commands — the toolbar applies/removes it with the\n * built-in `setMark('fontSize', { size })` / `unsetMark('fontSize')`. Its DOM\n * rendering mirrors the delivery renderer's hook exactly (a `data-rich-text-size`\n * attribute + `rich-text-size-{size}` class), so the editing surface styles the steps\n * with the same CSS hook the delivery output exposes (no inline pixel styles).\n */\nexport const FontSize = Mark.create({\n name: FONT_SIZE_MARK_TYPE,\n\n addAttributes() {\n return {\n size: {\n default: null,\n parseHTML: (element) => element.getAttribute(FONT_SIZE_DATA_ATTR),\n renderHTML: (attributes) => {\n const size = attributes.size\n if (!isRichTextFontSize(size)) return {}\n return { [FONT_SIZE_DATA_ATTR]: size, class: fontSizeClass(size) }\n },\n },\n }\n },\n\n parseHTML() {\n return [{ tag: `span[${FONT_SIZE_DATA_ATTR}]` }]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['span', mergeAttributes(HTMLAttributes), 0]\n },\n})\n"]}
@@ -1,4 +1,5 @@
1
1
  import type { Editor } from '@tiptap/react';
2
+ import type { RichTextFontSize } from '@elytracms/core/rich-text';
2
3
  /** Snapshot of the active formatting state, recomputed reactively per selection. */
3
4
  export interface ToolbarState {
4
5
  bold: boolean;
@@ -12,6 +13,8 @@ export interface ToolbarState {
12
13
  codeBlock: boolean;
13
14
  paragraph: boolean;
14
15
  headingLevel: number | null;
16
+ /** The active semantic font-size step, or null for the markless normal size (EC-411). */
17
+ fontSize: RichTextFontSize | null;
15
18
  canUndo: boolean;
16
19
  canRedo: boolean;
17
20
  }
@@ -1,3 +1,4 @@
1
+ import { RICH_TEXT_FONT_SIZES } from '@elytracms/core/rich-text';
1
2
  /**
2
3
  * The toolbar's state when there is no live editor to read — every control
3
4
  * inactive and both history actions disabled. Returned by
@@ -16,6 +17,7 @@ export const DISABLED_TOOLBAR_STATE = {
16
17
  codeBlock: false,
17
18
  paragraph: false,
18
19
  headingLevel: null,
20
+ fontSize: null,
19
21
  canUndo: false,
20
22
  canRedo: false,
21
23
  };
@@ -55,6 +57,7 @@ export function toolbarStateFromEditor(editor) {
55
57
  codeBlock: editor.isActive('codeBlock'),
56
58
  paragraph: editor.isActive('paragraph'),
57
59
  headingLevel: [1, 2, 3, 4, 5, 6].find((l) => editor.isActive('heading', { level: l })) ?? null,
60
+ fontSize: RICH_TEXT_FONT_SIZES.find((size) => editor.isActive('fontSize', { size })) ?? null,
58
61
  canUndo: editor.can().undo(),
59
62
  canRedo: editor.can().redo(),
60
63
  };
@@ -1 +1 @@
1
- {"version":3,"file":"toolbar-state.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/toolbar-state.ts"],"names":[],"mappings":"AAmBA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAiB;IAClD,IAAI,EAAE,KAAK;IACX,MAAM,EAAE,KAAK;IACb,SAAS,EAAE,KAAK;IAChB,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,KAAK;IACX,UAAU,EAAE,KAAK;IACjB,WAAW,EAAE,KAAK;IAClB,UAAU,EAAE,KAAK;IACjB,SAAS,EAAE,KAAK;IAChB,SAAS,EAAE,KAAK;IAChB,YAAY,EAAE,IAAI;IAClB,OAAO,EAAE,KAAK;IACd,OAAO,EAAE,KAAK;CACf,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAiC;IACtE,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,WAAW;QAAE,OAAO,sBAAsB,CAAA;IAChE,IAAI,CAAC;QACH,OAAO;YACL,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC7B,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACjC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;YACvC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACjC,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC7B,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;YACzC,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;YAC3C,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;YACzC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;YACvC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;YACvC,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI;YAC9F,OAAO,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE;YAC5B,OAAO,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE;SAC7B,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,sBAAsB,CAAA;IAC/B,CAAC;AACH,CAAC","sourcesContent":["import type { Editor } from '@tiptap/react'\n\n/** Snapshot of the active formatting state, recomputed reactively per selection. */\nexport interface ToolbarState {\n bold: boolean\n italic: boolean\n underline: boolean\n strike: boolean\n code: boolean\n bulletList: boolean\n orderedList: boolean\n blockquote: boolean\n codeBlock: boolean\n paragraph: boolean\n headingLevel: number | null\n canUndo: boolean\n canRedo: boolean\n}\n\n/**\n * The toolbar's state when there is no live editor to read — every control\n * inactive and both history actions disabled. Returned by\n * {@link toolbarStateFromEditor} for a missing / not-yet-initialized / destroyed\n * editor so the toolbar renders inert instead of throwing.\n */\nexport const DISABLED_TOOLBAR_STATE: ToolbarState = {\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n code: false,\n bulletList: false,\n orderedList: false,\n blockquote: false,\n codeBlock: false,\n paragraph: false,\n headingLevel: null,\n canUndo: false,\n canRedo: false,\n}\n\n/**\n * Project a TipTap editor into the toolbar's active-state shape — NULL-SAFE, and\n * the pure function the toolbar's `useEditorState` selector delegates to.\n *\n * `useEditorState`'s selector runs through `useSyncExternalStoreWithSelector`, so\n * React can invoke it against an editor that is missing, not yet initialized, or\n * already DESTROYED — e.g. during React 19 concurrent teardown, or when the\n * EC-322 lazy/Suspense boundary unmounts the editor subtree and `useEditor`'s\n * cleanup has already called `editor.destroy()` while the external store fires the\n * selector one last time. TipTap v3 nulls the editor's `commandManager` (and\n * `editorView`) on destroy, so `editor.can()` then throws\n * `Cannot read properties of null (reading 'can')` — the published-package crash\n * Konditorei hit (`editor.isActive(...)` returns stale state and does NOT throw,\n * which is why the reported stack points specifically at `.can`).\n *\n * Guarding on `isDestroyed` (safe on a destroyed editor — it reads\n * `editorView?.isDestroyed ?? true`) short-circuits before any command access; a\n * defensive `try/catch` covers any other pre-init window. The result is the\n * disabled shape rather than a throw, independent of mount/unmount timing.\n */\nexport function toolbarStateFromEditor(editor: Editor | null | undefined): ToolbarState {\n if (!editor || editor.isDestroyed) return DISABLED_TOOLBAR_STATE\n try {\n return {\n bold: editor.isActive('bold'),\n italic: editor.isActive('italic'),\n underline: editor.isActive('underline'),\n strike: editor.isActive('strike'),\n code: editor.isActive('code'),\n bulletList: editor.isActive('bulletList'),\n orderedList: editor.isActive('orderedList'),\n blockquote: editor.isActive('blockquote'),\n codeBlock: editor.isActive('codeBlock'),\n paragraph: editor.isActive('paragraph'),\n headingLevel: [1, 2, 3, 4, 5, 6].find((l) => editor.isActive('heading', { level: l })) ?? null,\n canUndo: editor.can().undo(),\n canRedo: editor.can().redo(),\n }\n } catch {\n return DISABLED_TOOLBAR_STATE\n }\n}\n"]}
1
+ {"version":3,"file":"toolbar-state.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/toolbar-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAsBhE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAiB;IAClD,IAAI,EAAE,KAAK;IACX,MAAM,EAAE,KAAK;IACb,SAAS,EAAE,KAAK;IAChB,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,KAAK;IACX,UAAU,EAAE,KAAK;IACjB,WAAW,EAAE,KAAK;IAClB,UAAU,EAAE,KAAK;IACjB,SAAS,EAAE,KAAK;IAChB,SAAS,EAAE,KAAK;IAChB,YAAY,EAAE,IAAI;IAClB,QAAQ,EAAE,IAAI;IACd,OAAO,EAAE,KAAK;IACd,OAAO,EAAE,KAAK;CACf,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAiC;IACtE,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,WAAW;QAAE,OAAO,sBAAsB,CAAA;IAChE,IAAI,CAAC;QACH,OAAO;YACL,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC7B,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACjC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;YACvC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACjC,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC7B,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;YACzC,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;YAC3C,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;YACzC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;YACvC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;YACvC,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI;YAC9F,QAAQ,EAAE,oBAAoB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI;YAC5F,OAAO,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE;YAC5B,OAAO,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE;SAC7B,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,sBAAsB,CAAA;IAC/B,CAAC;AACH,CAAC","sourcesContent":["import type { Editor } from '@tiptap/react'\nimport { RICH_TEXT_FONT_SIZES } from '@elytracms/core/rich-text'\nimport type { RichTextFontSize } from '@elytracms/core/rich-text'\n\n/** Snapshot of the active formatting state, recomputed reactively per selection. */\nexport interface ToolbarState {\n bold: boolean\n italic: boolean\n underline: boolean\n strike: boolean\n code: boolean\n bulletList: boolean\n orderedList: boolean\n blockquote: boolean\n codeBlock: boolean\n paragraph: boolean\n headingLevel: number | null\n /** The active semantic font-size step, or null for the markless normal size (EC-411). */\n fontSize: RichTextFontSize | null\n canUndo: boolean\n canRedo: boolean\n}\n\n/**\n * The toolbar's state when there is no live editor to read — every control\n * inactive and both history actions disabled. Returned by\n * {@link toolbarStateFromEditor} for a missing / not-yet-initialized / destroyed\n * editor so the toolbar renders inert instead of throwing.\n */\nexport const DISABLED_TOOLBAR_STATE: ToolbarState = {\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n code: false,\n bulletList: false,\n orderedList: false,\n blockquote: false,\n codeBlock: false,\n paragraph: false,\n headingLevel: null,\n fontSize: null,\n canUndo: false,\n canRedo: false,\n}\n\n/**\n * Project a TipTap editor into the toolbar's active-state shape — NULL-SAFE, and\n * the pure function the toolbar's `useEditorState` selector delegates to.\n *\n * `useEditorState`'s selector runs through `useSyncExternalStoreWithSelector`, so\n * React can invoke it against an editor that is missing, not yet initialized, or\n * already DESTROYED — e.g. during React 19 concurrent teardown, or when the\n * EC-322 lazy/Suspense boundary unmounts the editor subtree and `useEditor`'s\n * cleanup has already called `editor.destroy()` while the external store fires the\n * selector one last time. TipTap v3 nulls the editor's `commandManager` (and\n * `editorView`) on destroy, so `editor.can()` then throws\n * `Cannot read properties of null (reading 'can')` — the published-package crash\n * Konditorei hit (`editor.isActive(...)` returns stale state and does NOT throw,\n * which is why the reported stack points specifically at `.can`).\n *\n * Guarding on `isDestroyed` (safe on a destroyed editor — it reads\n * `editorView?.isDestroyed ?? true`) short-circuits before any command access; a\n * defensive `try/catch` covers any other pre-init window. The result is the\n * disabled shape rather than a throw, independent of mount/unmount timing.\n */\nexport function toolbarStateFromEditor(editor: Editor | null | undefined): ToolbarState {\n if (!editor || editor.isDestroyed) return DISABLED_TOOLBAR_STATE\n try {\n return {\n bold: editor.isActive('bold'),\n italic: editor.isActive('italic'),\n underline: editor.isActive('underline'),\n strike: editor.isActive('strike'),\n code: editor.isActive('code'),\n bulletList: editor.isActive('bulletList'),\n orderedList: editor.isActive('orderedList'),\n blockquote: editor.isActive('blockquote'),\n codeBlock: editor.isActive('codeBlock'),\n paragraph: editor.isActive('paragraph'),\n headingLevel: [1, 2, 3, 4, 5, 6].find((l) => editor.isActive('heading', { level: l })) ?? null,\n fontSize: RICH_TEXT_FONT_SIZES.find((size) => editor.isActive('fontSize', { size })) ?? null,\n canUndo: editor.can().undo(),\n canRedo: editor.can().redo(),\n }\n } catch {\n return DISABLED_TOOLBAR_STATE\n }\n}\n"]}
@@ -11,6 +11,19 @@ import type { LinkTarget } from './link-control.js';
11
11
  * Pinned by `toolbar-layout.test.ts`.
12
12
  */
13
13
  export declare const RICH_TEXT_TOOLBAR_CLASS = "h-auto min-h-10 flex-wrap gap-0.5 rounded-none border-x-0 border-t-0 bg-muted/30 px-1.5 py-1";
14
+ /**
15
+ * The block-style dropdown trigger's class (EC-418). The load-bearing part is the
16
+ * FIXED width (`w-12`): the trigger shows the active style's icon glyph (not its
17
+ * variable-width localized label — "Absatz" ≪ "Überschrift 2"), so caret movement
18
+ * across Absatz/H1/H2/H3 can never change the trigger's width and reflow the wrapped
19
+ * strip beneath the cursor (the reported German-locale defect). `justify-between`
20
+ * seats the glyph and the ChevronDown in that fixed box. Every other control on the
21
+ * strip is already a fixed `size-7` square (`ToolbarButton`, `LinkControl`) whose
22
+ * active state only recolors (a `secondary` border sits inside the box-border square,
23
+ * never resizing it) — so this trigger was the sole selection-variable width.
24
+ * Pinned by `toolbar-layout.test.ts`.
25
+ */
26
+ export declare const RICH_TEXT_MENU_TRIGGER_CLASS = "h-7 w-12 justify-between gap-1 px-2";
14
27
  interface RichTextToolbarProps {
15
28
  editor: Editor;
16
29
  /** Internal-link destinations across the workspace (empty ⇒ external-only). */
@@ -1,12 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEditorState } from '@tiptap/react';
3
- import { Blocks, Bold, ChevronDown, Code, ImagePlus, Italic, Maximize2, Minimize2, Pilcrow, Redo2, Strikethrough, Underline, Undo2, } from 'lucide-react';
3
+ import { Blocks, Bold, ChevronDown, Code, ImagePlus, Italic, Maximize2, Minimize2, Redo2, Strikethrough, Underline, Undo2, } from 'lucide-react';
4
4
  import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Toolbar, ToolbarGroup, ToolbarSeparator, } from '@elytracms/studio/ui';
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
6
  import { ToolbarButton } from './toolbar-button.js';
7
7
  import { LinkControl } from './link-control.js';
8
8
  import { toolbarStateFromEditor } from './toolbar-state.js';
9
9
  import { BLOCK_COMMANDS, isBlockCommandActive } from './block-commands.js';
10
+ import { BLOCK_STYLES, BlockStyleGlyph, blockStyleLabel } from './block-style.js';
11
+ import { FontSizeControl } from './font-size-control.js';
10
12
  /**
11
13
  * The toolbar container classes (EC-391). The load-bearing part is the height
12
14
  * contract: `h-auto min-h-10` (NOT a fixed `h-10`/`h-11`) so that when the many
@@ -18,6 +20,19 @@ import { BLOCK_COMMANDS, isBlockCommandActive } from './block-commands.js';
18
20
  * Pinned by `toolbar-layout.test.ts`.
19
21
  */
20
22
  export const RICH_TEXT_TOOLBAR_CLASS = 'h-auto min-h-10 flex-wrap gap-0.5 rounded-none border-x-0 border-t-0 bg-muted/30 px-1.5 py-1';
23
+ /**
24
+ * The block-style dropdown trigger's class (EC-418). The load-bearing part is the
25
+ * FIXED width (`w-12`): the trigger shows the active style's icon glyph (not its
26
+ * variable-width localized label — "Absatz" ≪ "Überschrift 2"), so caret movement
27
+ * across Absatz/H1/H2/H3 can never change the trigger's width and reflow the wrapped
28
+ * strip beneath the cursor (the reported German-locale defect). `justify-between`
29
+ * seats the glyph and the ChevronDown in that fixed box. Every other control on the
30
+ * strip is already a fixed `size-7` square (`ToolbarButton`, `LinkControl`) whose
31
+ * active state only recolors (a `secondary` border sits inside the box-border square,
32
+ * never resizing it) — so this trigger was the sole selection-variable width.
33
+ * Pinned by `toolbar-layout.test.ts`.
34
+ */
35
+ export const RICH_TEXT_MENU_TRIGGER_CLASS = 'h-7 w-12 justify-between gap-1 px-2';
21
36
  /**
22
37
  * The CMS rich-text toolbar (EC-314) — a compact, Notion-grade control strip that
23
38
  * reads its active/enabled state reactively from the editor (`useEditorState`) so a
@@ -34,9 +49,6 @@ export function RichTextToolbar({ editor, targets, onOpenImage, onOpenEmbed, exp
34
49
  editor,
35
50
  selector: ({ editor }) => toolbarStateFromEditor(editor),
36
51
  });
37
- const styleLabel = state.headingLevel != null
38
- ? translate('richText.heading', { level: state.headingLevel })
39
- : translate('richText.paragraph');
40
- return (_jsxs(Toolbar, { className: RICH_TEXT_TOOLBAR_CLASS, children: [_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 gap-1 px-2 font-medium", onMouseDown: (e) => e.preventDefault(), children: [styleLabel, _jsx(ChevronDown, { className: "size-3 opacity-60" })] }) }), _jsxs(DropdownMenuContent, { align: "start", className: "min-w-40", children: [_jsxs(DropdownMenuItem, { onSelect: () => editor.chain().focus().setParagraph().run(), children: [_jsx(Pilcrow, { className: "size-3.5" }), " ", translate('richText.paragraph')] }), [1, 2, 3].map((level) => (_jsxs(DropdownMenuItem, { onSelect: () => editor.chain().focus().toggleHeading({ level }).run(), children: [_jsxs("span", { className: "w-3.5 text-center text-xs font-semibold", children: ["H", level] }), ' ', translate('richText.heading', { level })] }, level)))] })] }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: state.bold, label: translate('richText.bold'), onClick: () => editor.chain().focus().toggleBold().run(), children: _jsx(Bold, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.italic, label: translate('richText.italic'), onClick: () => editor.chain().focus().toggleItalic().run(), children: _jsx(Italic, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.underline, label: translate('richText.underline'), onClick: () => editor.chain().focus().toggleUnderline().run(), children: _jsx(Underline, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.strike, label: translate('richText.strike'), onClick: () => editor.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.code, label: translate('richText.code'), onClick: () => editor.chain().focus().toggleCode().run(), children: _jsx(Code, { className: "size-3.5" }) })] }), _jsx(ToolbarSeparator, {}), _jsx(ToolbarGroup, { children: BLOCK_COMMANDS.map((cmd) => (_jsx(ToolbarButton, { active: isBlockCommandActive(cmd, state), label: translate(cmd.labelKey), onClick: () => cmd.chain(editor.chain().focus()).run(), children: _jsx(cmd.icon, { className: "size-3.5" }) }, cmd.key))) }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(LinkControl, { editor: editor, targets: targets }), onOpenImage ? (_jsx(ToolbarButton, { label: translate('richText.image.add'), onClick: onOpenImage, children: _jsx(ImagePlus, { className: "size-3.5" }) })) : null, onOpenEmbed ? (_jsx(ToolbarButton, { label: translate('richText.embed.add'), onClick: onOpenEmbed, children: _jsx(Blocks, { className: "size-3.5" }) })) : null] }), _jsx("div", { className: "flex-1" }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: false, disabled: !state.canUndo, label: translate('richText.undo'), onClick: () => editor.chain().focus().undo().run(), children: _jsx(Undo2, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: false, disabled: !state.canRedo, label: translate('richText.redo'), onClick: () => editor.chain().focus().redo().run(), children: _jsx(Redo2, { className: "size-3.5" }) })] }), onToggleExpand ? (_jsxs(_Fragment, { children: [_jsx(ToolbarSeparator, {}), _jsx(ToolbarGroup, { children: _jsx(ToolbarButton, { active: expanded, label: expanded ? translate('richText.expand.collapse') : translate('richText.expand.expand'), onClick: onToggleExpand, children: expanded ? _jsx(Minimize2, { className: "size-3.5" }) : _jsx(Maximize2, { className: "size-3.5" }) }) })] })) : null] }));
52
+ return (_jsxs(Toolbar, { className: RICH_TEXT_TOOLBAR_CLASS, children: [_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: RICH_TEXT_MENU_TRIGGER_CLASS, "aria-label": blockStyleLabel(state.headingLevel), onMouseDown: (e) => e.preventDefault(), children: [_jsx(BlockStyleGlyph, { headingLevel: state.headingLevel }), _jsx(ChevronDown, { className: "size-3 opacity-60" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "min-w-40", children: BLOCK_STYLES.map((style) => (_jsxs(DropdownMenuItem, { onSelect: () => style.chain(editor.chain().focus()).run(), children: [_jsx(BlockStyleGlyph, { headingLevel: style.headingLevel }), ' ', blockStyleLabel(style.headingLevel)] }, style.headingLevel ?? 'paragraph'))) })] }), _jsx(FontSizeControl, { editor: editor, activeSize: state.fontSize }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: state.bold, label: translate('richText.bold'), onClick: () => editor.chain().focus().toggleBold().run(), children: _jsx(Bold, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.italic, label: translate('richText.italic'), onClick: () => editor.chain().focus().toggleItalic().run(), children: _jsx(Italic, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.underline, label: translate('richText.underline'), onClick: () => editor.chain().focus().toggleUnderline().run(), children: _jsx(Underline, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.strike, label: translate('richText.strike'), onClick: () => editor.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.code, label: translate('richText.code'), onClick: () => editor.chain().focus().toggleCode().run(), children: _jsx(Code, { className: "size-3.5" }) })] }), _jsx(ToolbarSeparator, {}), _jsx(ToolbarGroup, { children: BLOCK_COMMANDS.map((cmd) => (_jsx(ToolbarButton, { active: isBlockCommandActive(cmd, state), label: translate(cmd.labelKey), onClick: () => cmd.chain(editor.chain().focus()).run(), children: _jsx(cmd.icon, { className: "size-3.5" }) }, cmd.key))) }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(LinkControl, { editor: editor, targets: targets }), onOpenImage ? (_jsx(ToolbarButton, { label: translate('richText.image.add'), onClick: onOpenImage, children: _jsx(ImagePlus, { className: "size-3.5" }) })) : null, onOpenEmbed ? (_jsx(ToolbarButton, { label: translate('richText.embed.add'), onClick: onOpenEmbed, children: _jsx(Blocks, { className: "size-3.5" }) })) : null] }), _jsx("div", { className: "flex-1" }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: false, disabled: !state.canUndo, label: translate('richText.undo'), onClick: () => editor.chain().focus().undo().run(), children: _jsx(Undo2, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: false, disabled: !state.canRedo, label: translate('richText.redo'), onClick: () => editor.chain().focus().redo().run(), children: _jsx(Redo2, { className: "size-3.5" }) })] }), onToggleExpand ? (_jsxs(_Fragment, { children: [_jsx(ToolbarSeparator, {}), _jsx(ToolbarGroup, { children: _jsx(ToolbarButton, { active: expanded, label: expanded ? translate('richText.expand.collapse') : translate('richText.expand.expand'), onClick: onToggleExpand, children: expanded ? _jsx(Minimize2, { className: "size-3.5" }) : _jsx(Maximize2, { className: "size-3.5" }) }) })] })) : null] }));
41
53
  }
42
54
  //# sourceMappingURL=toolbar.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"toolbar.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/toolbar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEditorState } from '@tiptap/react'\nimport type { Editor } from '@tiptap/react'\nimport {\n Blocks,\n Bold,\n ChevronDown,\n Code,\n ImagePlus,\n Italic,\n Maximize2,\n Minimize2,\n Pilcrow,\n Redo2,\n Strikethrough,\n Underline,\n Undo2,\n} from 'lucide-react'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Toolbar,\n ToolbarGroup,\n ToolbarSeparator,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { ToolbarButton } from './toolbar-button'\nimport { LinkControl } from './link-control'\nimport type { LinkTarget } from './link-control'\nimport { toolbarStateFromEditor } from './toolbar-state'\nimport { BLOCK_COMMANDS, isBlockCommandActive } from './block-commands'\n\n/**\n * The toolbar container classes (EC-391). The load-bearing part is the height\n * contract: `h-auto min-h-10` (NOT a fixed `h-10`/`h-11`) so that when the many\n * controls wrap at narrow inspector widths the strip GROWS and reserves its real\n * layout height — the editable content always starts below it. A fixed height plus\n * `flex-wrap` was the bug: wrapped rows overflowed the locked box and painted over\n * the prose, so the buttons were un-hittable. `py-1` only bites once wrapped (a\n * single row is governed by `min-h-10` + `items-center`), padding the stacked rows.\n * Pinned by `toolbar-layout.test.ts`.\n */\nexport const RICH_TEXT_TOOLBAR_CLASS =\n 'h-auto min-h-10 flex-wrap gap-0.5 rounded-none border-x-0 border-t-0 bg-muted/30 px-1.5 py-1'\n\ninterface RichTextToolbarProps {\n editor: Editor\n /** Internal-link destinations across the workspace (empty ⇒ external-only). */\n targets: LinkTarget[]\n /** Open the shared image picker (omitted ⇒ no image button). */\n onOpenImage?: () => void\n /** Open the shared component-embed picker (omitted ⇒ no embed button). */\n onOpenEmbed?: () => void\n /** EC-388: whether the editor is currently in its expanded (sheet) surface. */\n expanded?: boolean\n /** EC-388: toggle the expanded editing surface (omitted ⇒ no expand affordance). */\n onToggleExpand?: () => void\n}\n\n/**\n * The CMS rich-text toolbar (EC-314) — a compact, Notion-grade control strip that\n * reads its active/enabled state reactively from the editor (`useEditorState`) so a\n * pressed control always mirrors the caret. Every button prevents the default\n * mousedown so clicking it never steals the editor selection.\n */\nexport function RichTextToolbar({\n editor,\n targets,\n onOpenImage,\n onOpenEmbed,\n expanded = false,\n onToggleExpand,\n}: RichTextToolbarProps) {\n // The selector delegates to a pure, NULL-SAFE projection: `useEditorState` runs\n // it through an external store that React can fire against a destroyed/pre-init\n // editor (concurrent teardown, the EC-322 lazy boundary), where `editor.can()`\n // would throw on TipTap v3's nulled `commandManager`. `toolbarStateFromEditor`\n // returns the disabled shape in that window instead of crashing the toolbar.\n const state = useEditorState({\n editor,\n selector: ({ editor }) => toolbarStateFromEditor(editor),\n })\n\n const styleLabel =\n state.headingLevel != null\n ? translate('richText.heading', { level: state.headingLevel })\n : translate('richText.paragraph')\n\n return (\n <Toolbar className={RICH_TEXT_TOOLBAR_CLASS}>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-7 gap-1 px-2 font-medium\"\n onMouseDown={(e) => e.preventDefault()}\n >\n {styleLabel}\n <ChevronDown className=\"size-3 opacity-60\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"min-w-40\">\n <DropdownMenuItem onSelect={() => editor.chain().focus().setParagraph().run()}>\n <Pilcrow className=\"size-3.5\" /> {translate('richText.paragraph')}\n </DropdownMenuItem>\n {([1, 2, 3] as const).map((level) => (\n <DropdownMenuItem\n key={level}\n onSelect={() => editor.chain().focus().toggleHeading({ level }).run()}\n >\n <span className=\"w-3.5 text-center text-xs font-semibold\">H{level}</span>{' '}\n {translate('richText.heading', { level })}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <ToolbarButton\n active={state.bold}\n label={translate('richText.bold')}\n onClick={() => editor.chain().focus().toggleBold().run()}\n >\n <Bold className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.italic}\n label={translate('richText.italic')}\n onClick={() => editor.chain().focus().toggleItalic().run()}\n >\n <Italic className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.underline}\n label={translate('richText.underline')}\n onClick={() => editor.chain().focus().toggleUnderline().run()}\n >\n <Underline className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.strike}\n label={translate('richText.strike')}\n onClick={() => editor.chain().focus().toggleStrike().run()}\n >\n <Strikethrough className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.code}\n label={translate('richText.code')}\n onClick={() => editor.chain().focus().toggleCode().run()}\n >\n <Code className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n {/* The shared table (EC-328) — the SAME descriptors `buildSlashItems`\n derives its bulletList/orderedList/blockquote/codeBlock/divider items from. */}\n {BLOCK_COMMANDS.map((cmd) => (\n <ToolbarButton\n key={cmd.key}\n active={isBlockCommandActive(cmd, state)}\n label={translate(cmd.labelKey)}\n onClick={() => cmd.chain(editor.chain().focus()).run()}\n >\n <cmd.icon className=\"size-3.5\" />\n </ToolbarButton>\n ))}\n </ToolbarGroup>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <LinkControl editor={editor} targets={targets} />\n {onOpenImage ? (\n <ToolbarButton label={translate('richText.image.add')} onClick={onOpenImage}>\n <ImagePlus className=\"size-3.5\" />\n </ToolbarButton>\n ) : null}\n {onOpenEmbed ? (\n <ToolbarButton label={translate('richText.embed.add')} onClick={onOpenEmbed}>\n <Blocks className=\"size-3.5\" />\n </ToolbarButton>\n ) : null}\n </ToolbarGroup>\n\n <div className=\"flex-1\" />\n\n <ToolbarGroup>\n <ToolbarButton\n active={false}\n disabled={!state.canUndo}\n label={translate('richText.undo')}\n onClick={() => editor.chain().focus().undo().run()}\n >\n <Undo2 className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={false}\n disabled={!state.canRedo}\n label={translate('richText.redo')}\n onClick={() => editor.chain().focus().redo().run()}\n >\n <Redo2 className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n\n {/* EC-388: the expand/collapse affordance — moves with the toolbar into the\n expanded sheet (where it reads as \"collapse\") and back to the inline\n editor (where it reads as \"expand\"). Same editor instance, same value\n flow; no separate save/cancel. */}\n {onToggleExpand ? (\n <>\n <ToolbarSeparator />\n <ToolbarGroup>\n <ToolbarButton\n active={expanded}\n label={\n expanded ? translate('richText.expand.collapse') : translate('richText.expand.expand')\n }\n onClick={onToggleExpand}\n >\n {expanded ? <Minimize2 className=\"size-3.5\" /> : <Maximize2 className=\"size-3.5\" />}\n </ToolbarButton>\n </ToolbarGroup>\n </>\n ) : null}\n </Toolbar>\n )\n}\n"]}
1
+ {"version":3,"file":"toolbar.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/toolbar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEditorState } from '@tiptap/react'\nimport type { Editor } from '@tiptap/react'\nimport {\n Blocks,\n Bold,\n ChevronDown,\n Code,\n ImagePlus,\n Italic,\n Maximize2,\n Minimize2,\n Redo2,\n Strikethrough,\n Underline,\n Undo2,\n} from 'lucide-react'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Toolbar,\n ToolbarGroup,\n ToolbarSeparator,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { ToolbarButton } from './toolbar-button'\nimport { LinkControl } from './link-control'\nimport type { LinkTarget } from './link-control'\nimport { toolbarStateFromEditor } from './toolbar-state'\nimport { BLOCK_COMMANDS, isBlockCommandActive } from './block-commands'\nimport { BLOCK_STYLES, BlockStyleGlyph, blockStyleLabel } from './block-style'\nimport { FontSizeControl } from './font-size-control'\n\n/**\n * The toolbar container classes (EC-391). The load-bearing part is the height\n * contract: `h-auto min-h-10` (NOT a fixed `h-10`/`h-11`) so that when the many\n * controls wrap at narrow inspector widths the strip GROWS and reserves its real\n * layout height — the editable content always starts below it. A fixed height plus\n * `flex-wrap` was the bug: wrapped rows overflowed the locked box and painted over\n * the prose, so the buttons were un-hittable. `py-1` only bites once wrapped (a\n * single row is governed by `min-h-10` + `items-center`), padding the stacked rows.\n * Pinned by `toolbar-layout.test.ts`.\n */\nexport const RICH_TEXT_TOOLBAR_CLASS =\n 'h-auto min-h-10 flex-wrap gap-0.5 rounded-none border-x-0 border-t-0 bg-muted/30 px-1.5 py-1'\n\n/**\n * The block-style dropdown trigger's class (EC-418). The load-bearing part is the\n * FIXED width (`w-12`): the trigger shows the active style's icon glyph (not its\n * variable-width localized label — \"Absatz\" ≪ \"Überschrift 2\"), so caret movement\n * across Absatz/H1/H2/H3 can never change the trigger's width and reflow the wrapped\n * strip beneath the cursor (the reported German-locale defect). `justify-between`\n * seats the glyph and the ChevronDown in that fixed box. Every other control on the\n * strip is already a fixed `size-7` square (`ToolbarButton`, `LinkControl`) whose\n * active state only recolors (a `secondary` border sits inside the box-border square,\n * never resizing it) — so this trigger was the sole selection-variable width.\n * Pinned by `toolbar-layout.test.ts`.\n */\nexport const RICH_TEXT_MENU_TRIGGER_CLASS = 'h-7 w-12 justify-between gap-1 px-2'\n\ninterface RichTextToolbarProps {\n editor: Editor\n /** Internal-link destinations across the workspace (empty ⇒ external-only). */\n targets: LinkTarget[]\n /** Open the shared image picker (omitted ⇒ no image button). */\n onOpenImage?: () => void\n /** Open the shared component-embed picker (omitted ⇒ no embed button). */\n onOpenEmbed?: () => void\n /** EC-388: whether the editor is currently in its expanded (sheet) surface. */\n expanded?: boolean\n /** EC-388: toggle the expanded editing surface (omitted ⇒ no expand affordance). */\n onToggleExpand?: () => void\n}\n\n/**\n * The CMS rich-text toolbar (EC-314) — a compact, Notion-grade control strip that\n * reads its active/enabled state reactively from the editor (`useEditorState`) so a\n * pressed control always mirrors the caret. Every button prevents the default\n * mousedown so clicking it never steals the editor selection.\n */\nexport function RichTextToolbar({\n editor,\n targets,\n onOpenImage,\n onOpenEmbed,\n expanded = false,\n onToggleExpand,\n}: RichTextToolbarProps) {\n // The selector delegates to a pure, NULL-SAFE projection: `useEditorState` runs\n // it through an external store that React can fire against a destroyed/pre-init\n // editor (concurrent teardown, the EC-322 lazy boundary), where `editor.can()`\n // would throw on TipTap v3's nulled `commandManager`. `toolbarStateFromEditor`\n // returns the disabled shape in that window instead of crashing the toolbar.\n const state = useEditorState({\n editor,\n selector: ({ editor }) => toolbarStateFromEditor(editor),\n })\n\n return (\n <Toolbar className={RICH_TEXT_TOOLBAR_CLASS}>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={RICH_TEXT_MENU_TRIGGER_CLASS}\n // The visible label moved into the opened menu (EC-418) — the trigger keeps\n // an accessible name for the ACTIVE style from the same catalog keys.\n aria-label={blockStyleLabel(state.headingLevel)}\n onMouseDown={(e) => e.preventDefault()}\n >\n <BlockStyleGlyph headingLevel={state.headingLevel} />\n <ChevronDown className=\"size-3 opacity-60\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"min-w-40\">\n {BLOCK_STYLES.map((style) => (\n <DropdownMenuItem\n key={style.headingLevel ?? 'paragraph'}\n onSelect={() => style.chain(editor.chain().focus()).run()}\n >\n <BlockStyleGlyph headingLevel={style.headingLevel} />{' '}\n {blockStyleLabel(style.headingLevel)}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n\n <FontSizeControl editor={editor} activeSize={state.fontSize} />\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <ToolbarButton\n active={state.bold}\n label={translate('richText.bold')}\n onClick={() => editor.chain().focus().toggleBold().run()}\n >\n <Bold className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.italic}\n label={translate('richText.italic')}\n onClick={() => editor.chain().focus().toggleItalic().run()}\n >\n <Italic className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.underline}\n label={translate('richText.underline')}\n onClick={() => editor.chain().focus().toggleUnderline().run()}\n >\n <Underline className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.strike}\n label={translate('richText.strike')}\n onClick={() => editor.chain().focus().toggleStrike().run()}\n >\n <Strikethrough className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.code}\n label={translate('richText.code')}\n onClick={() => editor.chain().focus().toggleCode().run()}\n >\n <Code className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n {/* The shared table (EC-328) — the SAME descriptors `buildSlashItems`\n derives its bulletList/orderedList/blockquote/codeBlock/divider items from. */}\n {BLOCK_COMMANDS.map((cmd) => (\n <ToolbarButton\n key={cmd.key}\n active={isBlockCommandActive(cmd, state)}\n label={translate(cmd.labelKey)}\n onClick={() => cmd.chain(editor.chain().focus()).run()}\n >\n <cmd.icon className=\"size-3.5\" />\n </ToolbarButton>\n ))}\n </ToolbarGroup>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <LinkControl editor={editor} targets={targets} />\n {onOpenImage ? (\n <ToolbarButton label={translate('richText.image.add')} onClick={onOpenImage}>\n <ImagePlus className=\"size-3.5\" />\n </ToolbarButton>\n ) : null}\n {onOpenEmbed ? (\n <ToolbarButton label={translate('richText.embed.add')} onClick={onOpenEmbed}>\n <Blocks className=\"size-3.5\" />\n </ToolbarButton>\n ) : null}\n </ToolbarGroup>\n\n <div className=\"flex-1\" />\n\n <ToolbarGroup>\n <ToolbarButton\n active={false}\n disabled={!state.canUndo}\n label={translate('richText.undo')}\n onClick={() => editor.chain().focus().undo().run()}\n >\n <Undo2 className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={false}\n disabled={!state.canRedo}\n label={translate('richText.redo')}\n onClick={() => editor.chain().focus().redo().run()}\n >\n <Redo2 className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n\n {/* EC-388: the expand/collapse affordance — moves with the toolbar into the\n expanded sheet (where it reads as \"collapse\") and back to the inline\n editor (where it reads as \"expand\"). Same editor instance, same value\n flow; no separate save/cancel. */}\n {onToggleExpand ? (\n <>\n <ToolbarSeparator />\n <ToolbarGroup>\n <ToolbarButton\n active={expanded}\n label={\n expanded ? translate('richText.expand.collapse') : translate('richText.expand.expand')\n }\n onClick={onToggleExpand}\n >\n {expanded ? <Minimize2 className=\"size-3.5\" /> : <Maximize2 className=\"size-3.5\" />}\n </ToolbarButton>\n </ToolbarGroup>\n </>\n ) : null}\n </Toolbar>\n )\n}\n"]}
@@ -11,7 +11,7 @@ import { TransferToDocumentDialog } from '@elytracms/studio/components/component
11
11
  import { ReferencesPanel } from '@elytracms/studio/components/references/references-panel';
12
12
  import { InspectorSidebar } from '@elytracms/studio/components/inspector/inspector-sidebar';
13
13
  import { useSetBreadcrumb } from '@elytracms/studio/components/breadcrumb-context';
14
- import { translate } from '@elytracms/studio/lib/i18n';
14
+ import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
15
15
  import { Badge, Button, Code, Heading, Row, Spacer, Stack, TabButton, Text } from '@elytracms/studio/ui';
16
16
  import { cn } from '@elytracms/studio/lib/utils';
17
17
  /**
@@ -26,7 +26,7 @@ export function ComponentDetail({ projectId, componentId, }) {
26
26
  const manifest = studio.registry.getManifest(componentId);
27
27
  // EC-214: the component is the active record — name it in the global header
28
28
  // breadcrumb (under the Components section crumb). Unconditional (hooks rules).
29
- useSetBreadcrumb([{ label: manifest?.title ?? manifest?.id ?? componentId }]);
29
+ useSetBreadcrumb([{ label: localizedLabel(manifest?.title, manifest?.id ?? componentId) }]);
30
30
  if (!manifest) {
31
31
  return (_jsx("div", { className: "grid h-full place-items-center p-8", children: _jsxs(Stack, { gap: "3", align: "center", className: "max-w-sm text-center", children: [_jsx(Heading, { level: 2, size: "h2", children: translate('components.detail.notFound.title') }), _jsx(Text, { tone: "muted", children: translate('components.detail.notFound.body', { id: componentId }) }), _jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: studioPath.components() }, className: "text-sm text-primary hover:underline", children: translate('components.detail.notFound.back') })] }) }));
32
32
  }
@@ -38,7 +38,7 @@ function ComponentDetailBody({ projectId, studio, manifest, }) {
38
38
  const [transferOpen, setTransferOpen] = useState(false);
39
39
  const status = componentStatus(studio, manifest);
40
40
  const namespace = isPrimitive(manifest.id) ? 'base' : 'project';
41
- const title = manifest.title ?? manifest.id;
41
+ const title = localizedLabel(manifest.title, manifest.id);
42
42
  // The Preview tab's right inspector: editable Props + declared Slots. Shares the
43
43
  // exact frame the document editor uses (EC-224), so a prop edit re-renders the
44
44
  // preview live through the same control set documents use.