@elytracms/studio 0.0.38 → 0.0.40

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 (189) hide show
  1. package/convex/cms.ts +8 -2
  2. package/dist/components/app-views/default-modules.d.ts +60 -5
  3. package/dist/components/app-views/default-modules.js +53 -15
  4. package/dist/components/app-views/default-modules.js.map +1 -1
  5. package/dist/components/app-views/module.d.ts +2 -0
  6. package/dist/components/app-views/module.js +8 -0
  7. package/dist/components/app-views/module.js.map +1 -1
  8. package/dist/components/assets/asset-detail.d.ts +24 -0
  9. package/dist/components/assets/asset-detail.js +163 -0
  10. package/dist/components/assets/asset-detail.js.map +1 -0
  11. package/dist/components/assets/asset-grid.d.ts +28 -0
  12. package/dist/components/assets/asset-grid.js +68 -0
  13. package/dist/components/assets/asset-grid.js.map +1 -0
  14. package/dist/components/assets/asset-metadata.d.ts +19 -0
  15. package/dist/components/assets/asset-metadata.js +44 -0
  16. package/dist/components/assets/asset-metadata.js.map +1 -1
  17. package/dist/components/assets/assets-collection-view-source.d.ts +54 -0
  18. package/dist/components/assets/assets-collection-view-source.js +186 -0
  19. package/dist/components/assets/assets-collection-view-source.js.map +1 -0
  20. package/dist/components/assets/assets-module.d.ts +18 -0
  21. package/dist/components/assets/assets-module.js +94 -0
  22. package/dist/components/assets/assets-module.js.map +1 -0
  23. package/dist/components/assets/assets-store.js +2 -7
  24. package/dist/components/assets/assets-store.js.map +1 -1
  25. package/dist/components/assets/assets-view-preference.d.ts +15 -0
  26. package/dist/components/assets/assets-view-preference.js +22 -0
  27. package/dist/components/assets/assets-view-preference.js.map +1 -0
  28. package/dist/components/builder/builder-store.d.ts +12 -0
  29. package/dist/components/builder/builder-store.js +18 -16
  30. package/dist/components/builder/builder-store.js.map +1 -1
  31. package/dist/components/builder/clipboard-actions.js +5 -14
  32. package/dist/components/builder/clipboard-actions.js.map +1 -1
  33. package/dist/components/builder/composition-field-editor-lazy.d.ts +19 -0
  34. package/dist/components/builder/composition-field-editor-lazy.js +29 -0
  35. package/dist/components/builder/composition-field-editor-lazy.js.map +1 -0
  36. package/dist/components/builder/composition-field-editor.js +1 -4
  37. package/dist/components/builder/composition-field-editor.js.map +1 -1
  38. package/dist/components/builder/insert-context.d.ts +8 -0
  39. package/dist/components/builder/insert-context.js +1 -3
  40. package/dist/components/builder/insert-context.js.map +1 -1
  41. package/dist/components/builder/page-tree.js +1 -3
  42. package/dist/components/builder/page-tree.js.map +1 -1
  43. package/dist/components/builder/property-inspector.js +7 -134
  44. package/dist/components/builder/property-inspector.js.map +1 -1
  45. package/dist/components/cms/cms-store.d.ts +15 -4
  46. package/dist/components/cms/cms-store.js +42 -24
  47. package/dist/components/cms/cms-store.js.map +1 -1
  48. package/dist/components/cms/collection-table.d.ts +8 -1
  49. package/dist/components/cms/collection-table.js +75 -11
  50. package/dist/components/cms/collection-table.js.map +1 -1
  51. package/dist/components/cms/column-width.d.ts +25 -0
  52. package/dist/components/cms/column-width.js +47 -0
  53. package/dist/components/cms/column-width.js.map +1 -0
  54. package/dist/components/cms/detail-editor.js +143 -79
  55. package/dist/components/cms/detail-editor.js.map +1 -1
  56. package/dist/components/cms/details-pane.d.ts +21 -8
  57. package/dist/components/cms/details-pane.js +31 -11
  58. package/dist/components/cms/details-pane.js.map +1 -1
  59. package/dist/components/cms/discard-draft.d.ts +32 -0
  60. package/dist/components/cms/discard-draft.js +13 -0
  61. package/dist/components/cms/discard-draft.js.map +1 -0
  62. package/dist/components/cms/document-staging.d.ts +7 -0
  63. package/dist/components/cms/document-staging.js +44 -0
  64. package/dist/components/cms/document-staging.js.map +1 -0
  65. package/dist/components/cms/field-connection.d.ts +22 -0
  66. package/dist/components/cms/field-connection.js +37 -0
  67. package/dist/components/cms/field-connection.js.map +1 -0
  68. package/dist/components/cms/field-control.js +167 -34
  69. package/dist/components/cms/field-control.js.map +1 -1
  70. package/dist/components/cms/prop-fields.d.ts +43 -0
  71. package/dist/components/cms/prop-fields.js +154 -0
  72. package/dist/components/cms/prop-fields.js.map +1 -0
  73. package/dist/components/cms/rich-text/component-embed.d.ts +17 -3
  74. package/dist/components/cms/rich-text/component-embed.js +29 -7
  75. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  76. package/dist/components/cms/rich-text/embed-props.d.ts +49 -0
  77. package/dist/components/cms/rich-text/embed-props.js +106 -0
  78. package/dist/components/cms/rich-text/embed-props.js.map +1 -0
  79. package/dist/components/cms/rich-text/index.d.ts +6 -1
  80. package/dist/components/cms/rich-text/index.js +6 -1
  81. package/dist/components/cms/rich-text/index.js.map +1 -1
  82. package/dist/components/cms/rich-text/lazy.d.ts +19 -0
  83. package/dist/components/cms/rich-text/lazy.js +26 -0
  84. package/dist/components/cms/rich-text/lazy.js.map +1 -0
  85. package/dist/components/cms/rich-text/link-control.js +3 -1
  86. package/dist/components/cms/rich-text/link-control.js.map +1 -1
  87. package/dist/components/cms/rich-text/toolbar-state.d.ts +45 -0
  88. package/dist/components/cms/rich-text/toolbar-state.js +66 -0
  89. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -0
  90. package/dist/components/cms/rich-text/toolbar.js +7 -15
  91. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  92. package/dist/components/cms/row-actions.d.ts +28 -0
  93. package/dist/components/cms/row-actions.js +20 -0
  94. package/dist/components/cms/row-actions.js.map +1 -0
  95. package/dist/components/cms/use-document-versions.d.ts +8 -1
  96. package/dist/components/cms/use-document-versions.js +31 -4
  97. package/dist/components/cms/use-document-versions.js.map +1 -1
  98. package/dist/components/cms/version-compare-lazy.d.ts +1 -0
  99. package/dist/components/cms/version-compare-lazy.js +23 -0
  100. package/dist/components/cms/version-compare-lazy.js.map +1 -0
  101. package/dist/components/cms/version-compare-text.d.ts +46 -0
  102. package/dist/components/cms/version-compare-text.js +78 -0
  103. package/dist/components/cms/version-compare-text.js.map +1 -1
  104. package/dist/components/cms/version-compare.d.ts +9 -5
  105. package/dist/components/cms/version-compare.js +217 -64
  106. package/dist/components/cms/version-compare.js.map +1 -1
  107. package/dist/components/cms/versions-pane.js +10 -4
  108. package/dist/components/cms/versions-pane.js.map +1 -1
  109. package/dist/components/components/component-detail.d.ts +5 -3
  110. package/dist/components/components/component-detail.js +41 -8
  111. package/dist/components/components/component-detail.js.map +1 -1
  112. package/dist/components/components/component-preview.d.ts +97 -0
  113. package/dist/components/components/component-preview.js +202 -0
  114. package/dist/components/components/component-preview.js.map +1 -0
  115. package/dist/components/components/components-index.d.ts +5 -3
  116. package/dist/components/components/components-index.js +19 -24
  117. package/dist/components/components/components-index.js.map +1 -1
  118. package/dist/components/components/registry-browser.d.ts +7 -0
  119. package/dist/components/components/registry-browser.js +20 -36
  120. package/dist/components/components/registry-browser.js.map +1 -1
  121. package/dist/components/components/taxonomy.d.ts +51 -0
  122. package/dist/components/components/taxonomy.js +73 -0
  123. package/dist/components/components/taxonomy.js.map +1 -0
  124. package/dist/components/components/transfer-dialog.d.ts +20 -0
  125. package/dist/components/components/transfer-dialog.js +64 -0
  126. package/dist/components/components/transfer-dialog.js.map +1 -0
  127. package/dist/components/components/transfer.d.ts +43 -0
  128. package/dist/components/components/transfer.js +31 -0
  129. package/dist/components/components/transfer.js.map +1 -0
  130. package/dist/components/project-list.js +3 -4
  131. package/dist/components/project-list.js.map +1 -1
  132. package/dist/components/project-switcher.js +3 -2
  133. package/dist/components/project-switcher.js.map +1 -1
  134. package/dist/components/rail-store-status.d.ts +21 -0
  135. package/dist/components/rail-store-status.js +21 -0
  136. package/dist/components/rail-store-status.js.map +1 -0
  137. package/dist/components/redirects/redirects-module.js +4 -8
  138. package/dist/components/redirects/redirects-module.js.map +1 -1
  139. package/dist/components/redirects/redirects-store.js +2 -7
  140. package/dist/components/redirects/redirects-store.js.map +1 -1
  141. package/dist/components/sign-in-needed-badge.d.ts +6 -0
  142. package/dist/components/sign-in-needed-badge.js +13 -0
  143. package/dist/components/sign-in-needed-badge.js.map +1 -0
  144. package/dist/components/studio-project-screen.js +1 -4
  145. package/dist/components/studio-project-screen.js.map +1 -1
  146. package/dist/components/studio-shell.js +1 -4
  147. package/dist/components/studio-shell.js.map +1 -1
  148. package/dist/components/users/users-module.js +5 -7
  149. package/dist/components/users/users-module.js.map +1 -1
  150. package/dist/components/users/users-store.js +2 -7
  151. package/dist/components/users/users-store.js.map +1 -1
  152. package/dist/lib/auth/index.js +1 -1
  153. package/dist/lib/auth/index.js.map +1 -1
  154. package/dist/lib/clipboard/paste-target.js +1 -3
  155. package/dist/lib/clipboard/paste-target.js.map +1 -1
  156. package/dist/lib/cms-views/view-model.d.ts +0 -2
  157. package/dist/lib/cms-views/view-model.js +0 -7
  158. package/dist/lib/cms-views/view-model.js.map +1 -1
  159. package/dist/lib/diff-view-preference.d.ts +7 -0
  160. package/dist/lib/diff-view-preference.js +21 -0
  161. package/dist/lib/diff-view-preference.js.map +1 -0
  162. package/dist/lib/i18n/messages.en.js +94 -33
  163. package/dist/lib/i18n/messages.en.js.map +1 -1
  164. package/dist/lib/local-preference.d.ts +11 -0
  165. package/dist/lib/local-preference.js +26 -0
  166. package/dist/lib/local-preference.js.map +1 -0
  167. package/dist/lib/locale/fallback.d.ts +45 -0
  168. package/dist/lib/locale/fallback.js +85 -0
  169. package/dist/lib/locale/fallback.js.map +1 -1
  170. package/dist/lib/persona/labels.d.ts +6 -0
  171. package/dist/lib/persona/labels.js +8 -0
  172. package/dist/lib/persona/labels.js.map +1 -1
  173. package/dist/lib/query/index.d.ts +1 -1
  174. package/dist/lib/query/index.js +1 -1
  175. package/dist/lib/query/index.js.map +1 -1
  176. package/dist/lib/query/operations-query.d.ts +14 -2
  177. package/dist/lib/query/operations-query.js +11 -6
  178. package/dist/lib/query/operations-query.js.map +1 -1
  179. package/dist/lib/rail.d.ts +0 -6
  180. package/dist/lib/rail.js +6 -25
  181. package/dist/lib/rail.js.map +1 -1
  182. package/dist/lib/theme.d.ts +0 -6
  183. package/dist/lib/theme.js +10 -13
  184. package/dist/lib/theme.js.map +1 -1
  185. package/dist/router/match.d.ts +1 -0
  186. package/dist/router/match.js +4 -0
  187. package/dist/router/match.js.map +1 -1
  188. package/dist/ui/button.d.ts +1 -1
  189. package/package.json +22 -25
@@ -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"]}
@@ -17,7 +17,9 @@ export function LinkControl({ editor, targets }) {
17
17
  const [open, setOpen] = useState(false);
18
18
  const [href, setHref] = useState('');
19
19
  const [query, setQuery] = useState('');
20
- const isLink = editor.isActive('link');
20
+ // Null-safe like the toolbar's state selector: a destroyed editor (concurrent
21
+ // teardown / lazy-boundary unmount) still renders this control for a frame.
22
+ const isLink = !editor.isDestroyed && editor.isActive('link');
21
23
  const results = useMemo(() => {
22
24
  const q = query.trim().toLowerCase();
23
25
  const matched = q
@@ -1 +1 @@
1
- {"version":3,"file":"link-control.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/link-control.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { FileText, Link2, Trash2 } from 'lucide-react'\nimport type { Editor } from '@tiptap/react'\nimport { referenceNode } from '@elytracms/core/cms-core'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport {\n Button,\n Input,\n Popover,\n PopoverContent,\n PopoverTrigger,\n Row,\n Separator,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/** A pickable internal-link destination: a document plus its display labels. */\nexport interface LinkTarget {\n ref: DocumentRef\n label: string\n collectionLabel: string\n}\n\ninterface LinkControlProps {\n editor: Editor\n /** Documents linkable across the workspace (empty ⇒ external-only). */\n targets: LinkTarget[]\n}\n\nconst MAX_RESULTS = 40\n\n/**\n * The rich-text link control (EC-314): set/clear a link on the selection, either as\n * an external URL (a literal `href`) or an internal document — which writes a\n * structured `attrs.ref` (`referenceNode`, EC-290) so the link stays indexed like a\n * relation and resolves to a live path in delivery. Applying an empty URL clears the\n * link, so the one control both sets and removes.\n */\nexport function LinkControl({ editor, targets }: LinkControlProps) {\n const [open, setOpen] = useState(false)\n const [href, setHref] = useState('')\n const [query, setQuery] = useState('')\n const isLink = editor.isActive('link')\n\n const results = useMemo(() => {\n const q = query.trim().toLowerCase()\n const matched = q\n ? targets.filter((t) => t.label.toLowerCase().includes(q) || t.collectionLabel.toLowerCase().includes(q))\n : targets\n return matched.slice(0, MAX_RESULTS)\n }, [targets, query])\n\n function onOpenChange(next: boolean) {\n if (next) {\n const current = editor.getAttributes('link').href\n setHref(typeof current === 'string' ? current : '')\n setQuery('')\n }\n setOpen(next)\n }\n\n function applyExternal() {\n const url = href.trim()\n const chain = editor.chain().focus().extendMarkRange('link')\n if (url) chain.setLink({ href: url }).run()\n else chain.unsetLink().run()\n setOpen(false)\n }\n\n function applyDocument(target: LinkTarget) {\n editor\n .chain()\n .focus()\n .extendMarkRange('link')\n .setMark('link', { href: null, ref: referenceNode(target.ref) })\n .run()\n setOpen(false)\n }\n\n function remove() {\n editor.chain().focus().extendMarkRange('link').unsetLink().run()\n setOpen(false)\n }\n\n return (\n <Popover open={open} onOpenChange={onOpenChange}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant={isLink ? 'secondary' : 'ghost'}\n size=\"icon-sm\"\n aria-label={translate('richText.link.add')}\n aria-pressed={isLink}\n title={translate('richText.link.add')}\n onMouseDown={(e) => e.preventDefault()}\n >\n <Link2 className=\"size-3.5\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-80\">\n <Stack gap=\"2\">\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"uppercase tracking-wide\">\n {translate('richText.link.external')}\n </Text>\n <Row gap=\"2\" align=\"center\">\n <Input\n autoFocus\n value={href}\n placeholder={translate('richText.link.externalPlaceholder')}\n onChange={(e) => setHref(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n applyExternal()\n }\n }}\n />\n <Button type=\"button\" size=\"sm\" onClick={applyExternal}>\n {translate('richText.link.apply')}\n </Button>\n </Row>\n\n {targets.length > 0 ? (\n <>\n <Separator className=\"my-0.5\" />\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"uppercase tracking-wide\">\n {translate('richText.link.document')}\n </Text>\n <Input\n value={query}\n placeholder={translate('richText.link.documentSearch')}\n onChange={(e) => setQuery(e.target.value)}\n />\n <Stack gap=\"none\" className=\"max-h-52 overflow-y-auto\">\n {results.length === 0 ? (\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"px-1 py-2\">\n {translate('richText.link.documentEmpty')}\n </Text>\n ) : (\n results.map((target) => (\n <button\n key={`${target.ref.collection}:${target.ref.id}`}\n type=\"button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => applyDocument(target)}\n className={cn(\n 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm',\n 'hover:bg-muted focus-visible:bg-muted outline-none',\n )}\n >\n <FileText className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate\">{target.label}</span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">{target.collectionLabel}</span>\n </button>\n ))\n )}\n </Stack>\n </>\n ) : null}\n\n {isLink ? (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"self-start\" onClick={remove}>\n <Trash2 className=\"size-3.5\" /> {translate('richText.link.remove')}\n </Button>\n ) : null}\n </Stack>\n </PopoverContent>\n </Popover>\n )\n}\n"]}
1
+ {"version":3,"file":"link-control.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/link-control.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { FileText, Link2, Trash2 } from 'lucide-react'\nimport type { Editor } from '@tiptap/react'\nimport { referenceNode } from '@elytracms/core/cms-core'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport {\n Button,\n Input,\n Popover,\n PopoverContent,\n PopoverTrigger,\n Row,\n Separator,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/** A pickable internal-link destination: a document plus its display labels. */\nexport interface LinkTarget {\n ref: DocumentRef\n label: string\n collectionLabel: string\n}\n\ninterface LinkControlProps {\n editor: Editor\n /** Documents linkable across the workspace (empty ⇒ external-only). */\n targets: LinkTarget[]\n}\n\nconst MAX_RESULTS = 40\n\n/**\n * The rich-text link control (EC-314): set/clear a link on the selection, either as\n * an external URL (a literal `href`) or an internal document — which writes a\n * structured `attrs.ref` (`referenceNode`, EC-290) so the link stays indexed like a\n * relation and resolves to a live path in delivery. Applying an empty URL clears the\n * link, so the one control both sets and removes.\n */\nexport function LinkControl({ editor, targets }: LinkControlProps) {\n const [open, setOpen] = useState(false)\n const [href, setHref] = useState('')\n const [query, setQuery] = useState('')\n // Null-safe like the toolbar's state selector: a destroyed editor (concurrent\n // teardown / lazy-boundary unmount) still renders this control for a frame.\n const isLink = !editor.isDestroyed && editor.isActive('link')\n\n const results = useMemo(() => {\n const q = query.trim().toLowerCase()\n const matched = q\n ? targets.filter((t) => t.label.toLowerCase().includes(q) || t.collectionLabel.toLowerCase().includes(q))\n : targets\n return matched.slice(0, MAX_RESULTS)\n }, [targets, query])\n\n function onOpenChange(next: boolean) {\n if (next) {\n const current = editor.getAttributes('link').href\n setHref(typeof current === 'string' ? current : '')\n setQuery('')\n }\n setOpen(next)\n }\n\n function applyExternal() {\n const url = href.trim()\n const chain = editor.chain().focus().extendMarkRange('link')\n if (url) chain.setLink({ href: url }).run()\n else chain.unsetLink().run()\n setOpen(false)\n }\n\n function applyDocument(target: LinkTarget) {\n editor\n .chain()\n .focus()\n .extendMarkRange('link')\n .setMark('link', { href: null, ref: referenceNode(target.ref) })\n .run()\n setOpen(false)\n }\n\n function remove() {\n editor.chain().focus().extendMarkRange('link').unsetLink().run()\n setOpen(false)\n }\n\n return (\n <Popover open={open} onOpenChange={onOpenChange}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant={isLink ? 'secondary' : 'ghost'}\n size=\"icon-sm\"\n aria-label={translate('richText.link.add')}\n aria-pressed={isLink}\n title={translate('richText.link.add')}\n onMouseDown={(e) => e.preventDefault()}\n >\n <Link2 className=\"size-3.5\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-80\">\n <Stack gap=\"2\">\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"uppercase tracking-wide\">\n {translate('richText.link.external')}\n </Text>\n <Row gap=\"2\" align=\"center\">\n <Input\n autoFocus\n value={href}\n placeholder={translate('richText.link.externalPlaceholder')}\n onChange={(e) => setHref(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n applyExternal()\n }\n }}\n />\n <Button type=\"button\" size=\"sm\" onClick={applyExternal}>\n {translate('richText.link.apply')}\n </Button>\n </Row>\n\n {targets.length > 0 ? (\n <>\n <Separator className=\"my-0.5\" />\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"uppercase tracking-wide\">\n {translate('richText.link.document')}\n </Text>\n <Input\n value={query}\n placeholder={translate('richText.link.documentSearch')}\n onChange={(e) => setQuery(e.target.value)}\n />\n <Stack gap=\"none\" className=\"max-h-52 overflow-y-auto\">\n {results.length === 0 ? (\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"px-1 py-2\">\n {translate('richText.link.documentEmpty')}\n </Text>\n ) : (\n results.map((target) => (\n <button\n key={`${target.ref.collection}:${target.ref.id}`}\n type=\"button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => applyDocument(target)}\n className={cn(\n 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm',\n 'hover:bg-muted focus-visible:bg-muted outline-none',\n )}\n >\n <FileText className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate\">{target.label}</span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">{target.collectionLabel}</span>\n </button>\n ))\n )}\n </Stack>\n </>\n ) : null}\n\n {isLink ? (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"self-start\" onClick={remove}>\n <Trash2 className=\"size-3.5\" /> {translate('richText.link.remove')}\n </Button>\n ) : null}\n </Stack>\n </PopoverContent>\n </Popover>\n )\n}\n"]}
@@ -0,0 +1,45 @@
1
+ import type { Editor } from '@tiptap/react';
2
+ /** Snapshot of the active formatting state, recomputed reactively per selection. */
3
+ export interface ToolbarState {
4
+ bold: boolean;
5
+ italic: boolean;
6
+ underline: boolean;
7
+ strike: boolean;
8
+ code: boolean;
9
+ bulletList: boolean;
10
+ orderedList: boolean;
11
+ blockquote: boolean;
12
+ codeBlock: boolean;
13
+ paragraph: boolean;
14
+ headingLevel: number | null;
15
+ canUndo: boolean;
16
+ canRedo: boolean;
17
+ }
18
+ /**
19
+ * The toolbar's state when there is no live editor to read — every control
20
+ * inactive and both history actions disabled. Returned by
21
+ * {@link toolbarStateFromEditor} for a missing / not-yet-initialized / destroyed
22
+ * editor so the toolbar renders inert instead of throwing.
23
+ */
24
+ export declare const DISABLED_TOOLBAR_STATE: ToolbarState;
25
+ /**
26
+ * Project a TipTap editor into the toolbar's active-state shape — NULL-SAFE, and
27
+ * the pure function the toolbar's `useEditorState` selector delegates to.
28
+ *
29
+ * `useEditorState`'s selector runs through `useSyncExternalStoreWithSelector`, so
30
+ * React can invoke it against an editor that is missing, not yet initialized, or
31
+ * already DESTROYED — e.g. during React 19 concurrent teardown, or when the
32
+ * EC-322 lazy/Suspense boundary unmounts the editor subtree and `useEditor`'s
33
+ * cleanup has already called `editor.destroy()` while the external store fires the
34
+ * selector one last time. TipTap v3 nulls the editor's `commandManager` (and
35
+ * `editorView`) on destroy, so `editor.can()` then throws
36
+ * `Cannot read properties of null (reading 'can')` — the published-package crash
37
+ * Konditorei hit (`editor.isActive(...)` returns stale state and does NOT throw,
38
+ * which is why the reported stack points specifically at `.can`).
39
+ *
40
+ * Guarding on `isDestroyed` (safe on a destroyed editor — it reads
41
+ * `editorView?.isDestroyed ?? true`) short-circuits before any command access; a
42
+ * defensive `try/catch` covers any other pre-init window. The result is the
43
+ * disabled shape rather than a throw, independent of mount/unmount timing.
44
+ */
45
+ export declare function toolbarStateFromEditor(editor: Editor | null | undefined): ToolbarState;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The toolbar's state when there is no live editor to read — every control
3
+ * inactive and both history actions disabled. Returned by
4
+ * {@link toolbarStateFromEditor} for a missing / not-yet-initialized / destroyed
5
+ * editor so the toolbar renders inert instead of throwing.
6
+ */
7
+ export const DISABLED_TOOLBAR_STATE = {
8
+ bold: false,
9
+ italic: false,
10
+ underline: false,
11
+ strike: false,
12
+ code: false,
13
+ bulletList: false,
14
+ orderedList: false,
15
+ blockquote: false,
16
+ codeBlock: false,
17
+ paragraph: false,
18
+ headingLevel: null,
19
+ canUndo: false,
20
+ canRedo: false,
21
+ };
22
+ /**
23
+ * Project a TipTap editor into the toolbar's active-state shape — NULL-SAFE, and
24
+ * the pure function the toolbar's `useEditorState` selector delegates to.
25
+ *
26
+ * `useEditorState`'s selector runs through `useSyncExternalStoreWithSelector`, so
27
+ * React can invoke it against an editor that is missing, not yet initialized, or
28
+ * already DESTROYED — e.g. during React 19 concurrent teardown, or when the
29
+ * EC-322 lazy/Suspense boundary unmounts the editor subtree and `useEditor`'s
30
+ * cleanup has already called `editor.destroy()` while the external store fires the
31
+ * selector one last time. TipTap v3 nulls the editor's `commandManager` (and
32
+ * `editorView`) on destroy, so `editor.can()` then throws
33
+ * `Cannot read properties of null (reading 'can')` — the published-package crash
34
+ * Konditorei hit (`editor.isActive(...)` returns stale state and does NOT throw,
35
+ * which is why the reported stack points specifically at `.can`).
36
+ *
37
+ * Guarding on `isDestroyed` (safe on a destroyed editor — it reads
38
+ * `editorView?.isDestroyed ?? true`) short-circuits before any command access; a
39
+ * defensive `try/catch` covers any other pre-init window. The result is the
40
+ * disabled shape rather than a throw, independent of mount/unmount timing.
41
+ */
42
+ export function toolbarStateFromEditor(editor) {
43
+ if (!editor || editor.isDestroyed)
44
+ return DISABLED_TOOLBAR_STATE;
45
+ try {
46
+ return {
47
+ bold: editor.isActive('bold'),
48
+ italic: editor.isActive('italic'),
49
+ underline: editor.isActive('underline'),
50
+ strike: editor.isActive('strike'),
51
+ code: editor.isActive('code'),
52
+ bulletList: editor.isActive('bulletList'),
53
+ orderedList: editor.isActive('orderedList'),
54
+ blockquote: editor.isActive('blockquote'),
55
+ codeBlock: editor.isActive('codeBlock'),
56
+ paragraph: editor.isActive('paragraph'),
57
+ headingLevel: [1, 2, 3, 4, 5, 6].find((l) => editor.isActive('heading', { level: l })) ?? null,
58
+ canUndo: editor.can().undo(),
59
+ canRedo: editor.can().redo(),
60
+ };
61
+ }
62
+ catch {
63
+ return DISABLED_TOOLBAR_STATE;
64
+ }
65
+ }
66
+ //# sourceMappingURL=toolbar-state.js.map
@@ -0,0 +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"]}
@@ -5,6 +5,7 @@ import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMe
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
6
  import { ToolbarButton } from './toolbar-button';
7
7
  import { LinkControl } from './link-control';
8
+ import { toolbarStateFromEditor } from './toolbar-state';
8
9
  /**
9
10
  * The CMS rich-text toolbar (EC-314) — a compact, Notion-grade control strip that
10
11
  * reads its active/enabled state reactively from the editor (`useEditorState`) so a
@@ -12,23 +13,14 @@ import { LinkControl } from './link-control';
12
13
  * mousedown so clicking it never steals the editor selection.
13
14
  */
14
15
  export function RichTextToolbar({ editor, targets, onOpenImage, onOpenEmbed }) {
16
+ // The selector delegates to a pure, NULL-SAFE projection: `useEditorState` runs
17
+ // it through an external store that React can fire against a destroyed/pre-init
18
+ // editor (concurrent teardown, the EC-322 lazy boundary), where `editor.can()`
19
+ // would throw on TipTap v3's nulled `commandManager`. `toolbarStateFromEditor`
20
+ // returns the disabled shape in that window instead of crashing the toolbar.
15
21
  const state = useEditorState({
16
22
  editor,
17
- selector: ({ editor }) => ({
18
- bold: editor.isActive('bold'),
19
- italic: editor.isActive('italic'),
20
- underline: editor.isActive('underline'),
21
- strike: editor.isActive('strike'),
22
- code: editor.isActive('code'),
23
- bulletList: editor.isActive('bulletList'),
24
- orderedList: editor.isActive('orderedList'),
25
- blockquote: editor.isActive('blockquote'),
26
- codeBlock: editor.isActive('codeBlock'),
27
- paragraph: editor.isActive('paragraph'),
28
- headingLevel: [1, 2, 3, 4, 5, 6].find((l) => editor.isActive('heading', { level: l })) ?? null,
29
- canUndo: editor.can().undo(),
30
- canRedo: editor.can().redo(),
31
- }),
23
+ selector: ({ editor }) => toolbarStateFromEditor(editor),
32
24
  });
33
25
  const styleLabel = state.headingLevel != null
34
26
  ? translate('richText.heading', { level: state.headingLevel })
@@ -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 List,\n ListOrdered,\n Minus,\n Pilcrow,\n Quote,\n Redo2,\n SquareCode,\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'\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}\n\n/** Snapshot of the active formatting state, recomputed reactively per selection. */\ninterface 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 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({ editor, targets, onOpenImage, onOpenEmbed }: RichTextToolbarProps) {\n const state = useEditorState({\n editor,\n selector: ({ editor }): ToolbarState => ({\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 })\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=\"h-10 flex-wrap gap-0.5 rounded-none border-x-0 border-t-0 bg-muted/30 px-1.5\">\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 <ToolbarButton\n active={state.bulletList}\n label={translate('richText.bulletList')}\n onClick={() => editor.chain().focus().toggleBulletList().run()}\n >\n <List className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.orderedList}\n label={translate('richText.orderedList')}\n onClick={() => editor.chain().focus().toggleOrderedList().run()}\n >\n <ListOrdered className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.blockquote}\n label={translate('richText.blockquote')}\n onClick={() => editor.chain().focus().toggleBlockquote().run()}\n >\n <Quote className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.codeBlock}\n label={translate('richText.codeBlock')}\n onClick={() => editor.chain().focus().toggleCodeBlock().run()}\n >\n <SquareCode className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={false}\n label={translate('richText.horizontalRule')}\n onClick={() => editor.chain().focus().setHorizontalRule().run()}\n >\n <Minus className=\"size-3.5\" />\n </ToolbarButton>\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 </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 List,\n ListOrdered,\n Minus,\n Pilcrow,\n Quote,\n Redo2,\n SquareCode,\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'\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}\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({ editor, targets, onOpenImage, onOpenEmbed }: 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=\"h-10 flex-wrap gap-0.5 rounded-none border-x-0 border-t-0 bg-muted/30 px-1.5\">\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 <ToolbarButton\n active={state.bulletList}\n label={translate('richText.bulletList')}\n onClick={() => editor.chain().focus().toggleBulletList().run()}\n >\n <List className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.orderedList}\n label={translate('richText.orderedList')}\n onClick={() => editor.chain().focus().toggleOrderedList().run()}\n >\n <ListOrdered className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.blockquote}\n label={translate('richText.blockquote')}\n onClick={() => editor.chain().focus().toggleBlockquote().run()}\n >\n <Quote className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.codeBlock}\n label={translate('richText.codeBlock')}\n onClick={() => editor.chain().focus().toggleCodeBlock().run()}\n >\n <SquareCode className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={false}\n label={translate('richText.horizontalRule')}\n onClick={() => editor.chain().focus().setHorizontalRule().run()}\n >\n <Minus className=\"size-3.5\" />\n </ToolbarButton>\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 </Toolbar>\n )\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import type { CollectionViewCapabilities } from '@elytracms/studio/lib/collection-view/collection-view-source';
2
+ /**
3
+ * EC-316 — the right-edge row `⋮` menu's action vocabulary: the SAME bulk-action
4
+ * set the toolbar's selection actions offer (`bulk-actions.ts`'s `BulkAction`),
5
+ * minus `set-field` (a bulk-only composer over a multi-row selection — a single
6
+ * row edits its fields directly in the table), plus `open` (jump to the detail
7
+ * editor — the row's own affordance the primary-column link already offers, but
8
+ * explicit in the menu for a keyboard/menu-driven workflow).
9
+ */
10
+ export type RowAction = 'open' | 'publish' | 'unpublish' | 'delete';
11
+ export interface RowActionsInput {
12
+ /** The rail's capability matrix (AD-19) — the SAME gate the list/edit shells use. */
13
+ capabilities: Pick<CollectionViewCapabilities, 'publish' | 'remove'>;
14
+ /** Whether this row has a navigable detail route (a rail with no detail surface omits it). */
15
+ hasDetailPath: boolean;
16
+ /** Whether this document IS published — decides `publish` vs `unpublish`. */
17
+ published: boolean;
18
+ }
19
+ /**
20
+ * The gated action list for one row (EC-316). Every entry mirrors a capability
21
+ * the rail actually declares — a source without `publish` never offers
22
+ * publish/unpublish, one without `remove` never offers delete, and a rail with
23
+ * no detail surface never offers `open` — so the SAME menu serves the users
24
+ * rail (EC-240), redirects, and any future `CollectionViewSource` without a
25
+ * bespoke per-rail menu. An empty result means the row menu itself renders
26
+ * nothing (never an empty, useless trigger).
27
+ */
28
+ export declare function rowActionsFor({ capabilities, hasDetailPath, published }: RowActionsInput): RowAction[];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The gated action list for one row (EC-316). Every entry mirrors a capability
3
+ * the rail actually declares — a source without `publish` never offers
4
+ * publish/unpublish, one without `remove` never offers delete, and a rail with
5
+ * no detail surface never offers `open` — so the SAME menu serves the users
6
+ * rail (EC-240), redirects, and any future `CollectionViewSource` without a
7
+ * bespoke per-rail menu. An empty result means the row menu itself renders
8
+ * nothing (never an empty, useless trigger).
9
+ */
10
+ export function rowActionsFor({ capabilities, hasDetailPath, published }) {
11
+ const actions = [];
12
+ if (hasDetailPath)
13
+ actions.push('open');
14
+ if (capabilities.publish)
15
+ actions.push(published ? 'unpublish' : 'publish');
16
+ if (capabilities.remove)
17
+ actions.push('delete');
18
+ return actions;
19
+ }
20
+ //# sourceMappingURL=row-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"row-actions.js","sourceRoot":"","sources":["../../../src/components/cms/row-actions.ts"],"names":[],"mappings":"AAqBA;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAmB;IACvF,MAAM,OAAO,GAAgB,EAAE,CAAA;IAC/B,IAAI,aAAa;QAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACvC,IAAI,YAAY,CAAC,OAAO;QAAE,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;IAC3E,IAAI,YAAY,CAAC,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC/C,OAAO,OAAO,CAAA;AAChB,CAAC","sourcesContent":["import type { CollectionViewCapabilities } from '@elytracms/studio/lib/collection-view/collection-view-source'\n\n/**\n * EC-316 — the right-edge row `⋮` menu's action vocabulary: the SAME bulk-action\n * set the toolbar's selection actions offer (`bulk-actions.ts`'s `BulkAction`),\n * minus `set-field` (a bulk-only composer over a multi-row selection — a single\n * row edits its fields directly in the table), plus `open` (jump to the detail\n * editor — the row's own affordance the primary-column link already offers, but\n * explicit in the menu for a keyboard/menu-driven workflow).\n */\nexport type RowAction = 'open' | 'publish' | 'unpublish' | 'delete'\n\nexport interface RowActionsInput {\n /** The rail's capability matrix (AD-19) — the SAME gate the list/edit shells use. */\n capabilities: Pick<CollectionViewCapabilities, 'publish' | 'remove'>\n /** Whether this row has a navigable detail route (a rail with no detail surface omits it). */\n hasDetailPath: boolean\n /** Whether this document IS published — decides `publish` vs `unpublish`. */\n published: boolean\n}\n\n/**\n * The gated action list for one row (EC-316). Every entry mirrors a capability\n * the rail actually declares — a source without `publish` never offers\n * publish/unpublish, one without `remove` never offers delete, and a rail with\n * no detail surface never offers `open` — so the SAME menu serves the users\n * rail (EC-240), redirects, and any future `CollectionViewSource` without a\n * bespoke per-rail menu. An empty result means the row menu itself renders\n * nothing (never an empty, useless trigger).\n */\nexport function rowActionsFor({ capabilities, hasDetailPath, published }: RowActionsInput): RowAction[] {\n const actions: RowAction[] = []\n if (hasDetailPath) actions.push('open')\n if (capabilities.publish) actions.push(published ? 'unpublish' : 'publish')\n if (capabilities.remove) actions.push('delete')\n return actions\n}\n"]}
@@ -9,8 +9,15 @@ export declare function documentVersionsKey(projectId: string | undefined, ref:
9
9
  * so every fetch — initial, invalidated, or refetch — lists an authoritative
10
10
  * history regardless of which surface triggered the change. Two mounts with the
11
11
  * same document share one fetch (TanStack dedup on the key).
12
+ *
13
+ * `enabled` (EC-316, default `true`) lets a caller that only needs this for a
14
+ * gating check — e.g. the header's Discard-draft enablement, which isn't
15
+ * otherwise on the version-browsing path — skip the fetch until it actually
16
+ * applies (an unpublished/non-versioned document never needs its history).
12
17
  */
13
- export declare function useDocumentVersionsQuery(ref: DocumentRef): {
18
+ export declare function useDocumentVersionsQuery(ref: DocumentRef, options?: {
19
+ enabled?: boolean;
20
+ }): {
14
21
  versionsQuery: import("@tanstack/react-query").UseQueryResult<DocumentVersionsView>;
15
22
  versionKey: readonly ["documentVersions", string, string, string];
16
23
  };
@@ -1,9 +1,30 @@
1
1
  import { useMemo } from 'react';
2
2
  import { useOperationQuery } from '@elytracms/studio/lib/query';
3
3
  import { useCollectionView } from '../collection-view/collection-view-context';
4
- /** Recorded-version freshness: any document command (publish/restore/upsert) may
5
- * change the history, so re-read on the whole `documents` domain. */
6
- const VERSION_RELOAD_OPS = ['documents'];
4
+ /**
5
+ * Recorded-version freshness (EC-322): re-read on exactly the commands that
6
+ * change a document's version history or its live pin — NOT the whole `documents`
7
+ * domain. The draft the editor types (`documents.upsert`, fired per keystroke by
8
+ * autosave) creates no version and moves no pin (versions are appended only on
9
+ * publish/snapshot/restore — see `documentVersions` in `convex/schema.ts`), so
10
+ * listening on the whole domain refetched `listDocumentVersions` on every
11
+ * keystroke while the Versions pane was open. The comparison the pane shows
12
+ * (`liveMatchesDraft`) reads the live snapshot from THIS query and the draft from
13
+ * local editor state, so a draft edit re-renders it without any refetch.
14
+ *
15
+ * Every command that DOES change the view is enumerated (a new version-affecting
16
+ * op must be added here): `snapshot`/`restore` append a version; `publish` appends
17
+ * one and moves the pin; `unpublish`/`pinVersion` move the pin; `delete` cascades
18
+ * the history away.
19
+ */
20
+ const VERSION_RELOAD_OPS = [
21
+ 'documents.snapshot',
22
+ 'documents.publish',
23
+ 'documents.unpublish',
24
+ 'documents.pinVersion',
25
+ 'documents.restore',
26
+ 'documents.delete',
27
+ ];
7
28
  /** The one cache key for a document's version history — shared so the Versions
8
29
  * pane and the EC-298 compare view read through a single deduped query. */
9
30
  export function documentVersionsKey(projectId, ref) {
@@ -15,8 +36,13 @@ export function documentVersionsKey(projectId, ref) {
15
36
  * so every fetch — initial, invalidated, or refetch — lists an authoritative
16
37
  * history regardless of which surface triggered the change. Two mounts with the
17
38
  * same document share one fetch (TanStack dedup on the key).
39
+ *
40
+ * `enabled` (EC-316, default `true`) lets a caller that only needs this for a
41
+ * gating check — e.g. the header's Discard-draft enablement, which isn't
42
+ * otherwise on the version-browsing path — skip the fetch until it actually
43
+ * applies (an unpublished/non-versioned document never needs its history).
18
44
  */
19
- export function useDocumentVersionsQuery(ref) {
45
+ export function useDocumentVersionsQuery(ref, options) {
20
46
  const source = useCollectionView();
21
47
  const versionKey = useMemo(() => documentVersionsKey(source.projectId, ref), [source.projectId, ref.collection, ref.id]);
22
48
  const versionsQuery = useOperationQuery({
@@ -25,6 +51,7 @@ export function useDocumentVersionsQuery(ref) {
25
51
  // without versions (never mounts this) degrades to an empty history.
26
52
  loader: async () => (await source.versioning?.load(ref)) ?? { versions: [] },
27
53
  invalidateOn: VERSION_RELOAD_OPS,
54
+ ...(options?.enabled !== undefined ? { enabled: options.enabled } : {}),
28
55
  });
29
56
  return { versionsQuery, versionKey };
30
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"use-document-versions.js","sourceRoot":"","sources":["../../../src/components/cms/use-document-versions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAG9E;sEACsE;AACtE,MAAM,kBAAkB,GAAG,CAAC,WAAW,CAAU,CAAA;AAEjD;4EAC4E;AAC5E,MAAM,UAAU,mBAAmB,CAAC,SAA6B,EAAE,GAAgB;IACjF,OAAO,CAAC,kBAAkB,EAAE,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAU,CAAA;AAChF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAgB;IACvD,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,UAAU,GAAG,OAAO,CACxB,GAAG,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,EAChD,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAC3C,CAAA;IACD,MAAM,aAAa,GAAG,iBAAiB,CAAuB;QAC5D,QAAQ,EAAE,UAAU;QACpB,0EAA0E;QAC1E,qEAAqE;QACrE,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC5E,YAAY,EAAE,kBAAkB;KACjC,CAAC,CAAA;IACF,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,CAAA;AACtC,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport type { DocumentVersionsView } from './cms-store'\n\n/** Recorded-version freshness: any document command (publish/restore/upsert) may\n * change the history, so re-read on the whole `documents` domain. */\nconst VERSION_RELOAD_OPS = ['documents'] as const\n\n/** The one cache key for a document's version history — shared so the Versions\n * pane and the EC-298 compare view read through a single deduped query. */\nexport function documentVersionsKey(projectId: string | undefined, ref: DocumentRef) {\n return ['documentVersions', projectId ?? '∅', ref.collection, ref.id] as const\n}\n\n/**\n * A document's recorded version history, read through the one operations-backed\n * query cache (EC-308/AD-17). The flush-before-read ordering lives IN the loader,\n * so every fetch — initial, invalidated, or refetch — lists an authoritative\n * history regardless of which surface triggered the change. Two mounts with the\n * same document share one fetch (TanStack dedup on the key).\n */\nexport function useDocumentVersionsQuery(ref: DocumentRef) {\n const source = useCollectionView()\n const versionKey = useMemo(\n () => documentVersionsKey(source.projectId, ref),\n [source.projectId, ref.collection, ref.id],\n )\n const versionsQuery = useOperationQuery<DocumentVersionsView>({\n queryKey: versionKey,\n // The versioning capability owns the flush-before-read ordering; a source\n // without versions (never mounts this) degrades to an empty history.\n loader: async () => (await source.versioning?.load(ref)) ?? { versions: [] },\n invalidateOn: VERSION_RELOAD_OPS,\n })\n return { versionsQuery, versionKey }\n}\n"]}
1
+ {"version":3,"file":"use-document-versions.js","sourceRoot":"","sources":["../../../src/components/cms/use-document-versions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAG9E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,kBAAkB,GAAG;IACzB,oBAAoB;IACpB,mBAAmB;IACnB,qBAAqB;IACrB,sBAAsB;IACtB,mBAAmB;IACnB,kBAAkB;CACV,CAAA;AAEV;4EAC4E;AAC5E,MAAM,UAAU,mBAAmB,CAAC,SAA6B,EAAE,GAAgB;IACjF,OAAO,CAAC,kBAAkB,EAAE,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAU,CAAA;AAChF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAgB,EAAE,OAA+B;IACxF,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,UAAU,GAAG,OAAO,CACxB,GAAG,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,EAChD,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAC3C,CAAA;IACD,MAAM,aAAa,GAAG,iBAAiB,CAAuB;QAC5D,QAAQ,EAAE,UAAU;QACpB,0EAA0E;QAC1E,qEAAqE;QACrE,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC5E,YAAY,EAAE,kBAAkB;QAChC,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxE,CAAC,CAAA;IACF,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,CAAA;AACtC,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport type { DocumentVersionsView } from './cms-store'\n\n/**\n * Recorded-version freshness (EC-322): re-read on exactly the commands that\n * change a document's version history or its live pin — NOT the whole `documents`\n * domain. The draft the editor types (`documents.upsert`, fired per keystroke by\n * autosave) creates no version and moves no pin (versions are appended only on\n * publish/snapshot/restore — see `documentVersions` in `convex/schema.ts`), so\n * listening on the whole domain refetched `listDocumentVersions` on every\n * keystroke while the Versions pane was open. The comparison the pane shows\n * (`liveMatchesDraft`) reads the live snapshot from THIS query and the draft from\n * local editor state, so a draft edit re-renders it without any refetch.\n *\n * Every command that DOES change the view is enumerated (a new version-affecting\n * op must be added here): `snapshot`/`restore` append a version; `publish` appends\n * one and moves the pin; `unpublish`/`pinVersion` move the pin; `delete` cascades\n * the history away.\n */\nconst VERSION_RELOAD_OPS = [\n 'documents.snapshot',\n 'documents.publish',\n 'documents.unpublish',\n 'documents.pinVersion',\n 'documents.restore',\n 'documents.delete',\n] as const\n\n/** The one cache key for a document's version history — shared so the Versions\n * pane and the EC-298 compare view read through a single deduped query. */\nexport function documentVersionsKey(projectId: string | undefined, ref: DocumentRef) {\n return ['documentVersions', projectId ?? '∅', ref.collection, ref.id] as const\n}\n\n/**\n * A document's recorded version history, read through the one operations-backed\n * query cache (EC-308/AD-17). The flush-before-read ordering lives IN the loader,\n * so every fetch — initial, invalidated, or refetch — lists an authoritative\n * history regardless of which surface triggered the change. Two mounts with the\n * same document share one fetch (TanStack dedup on the key).\n *\n * `enabled` (EC-316, default `true`) lets a caller that only needs this for a\n * gating check — e.g. the header's Discard-draft enablement, which isn't\n * otherwise on the version-browsing path — skip the fetch until it actually\n * applies (an unpublished/non-versioned document never needs its history).\n */\nexport function useDocumentVersionsQuery(ref: DocumentRef, options?: { enabled?: boolean }) {\n const source = useCollectionView()\n const versionKey = useMemo(\n () => documentVersionsKey(source.projectId, ref),\n [source.projectId, ref.collection, ref.id],\n )\n const versionsQuery = useOperationQuery<DocumentVersionsView>({\n queryKey: versionKey,\n // The versioning capability owns the flush-before-read ordering; a source\n // without versions (never mounts this) degrades to an empty history.\n loader: async () => (await source.versioning?.load(ref)) ?? { versions: [] },\n invalidateOn: VERSION_RELOAD_OPS,\n ...(options?.enabled !== undefined ? { enabled: options.enabled } : {}),\n })\n return { versionsQuery, versionKey }\n}\n"]}
@@ -0,0 +1 @@
1
+ export declare function VersionCompare(): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Suspense, lazy } from 'react';
3
+ import { Text } from '@elytracms/studio/ui';
4
+ import { translate } from '@elytracms/studio/lib/i18n';
5
+ /**
6
+ * Lazy boundary for the version-compare surface (EC-322 load-time pass).
7
+ *
8
+ * The compare view is a large, genuinely occasional surface — it only renders on
9
+ * the detail editor's Compare tab. Importing it statically kept its whole diff UI
10
+ * on the CMS detail-view critical path for every document, most of which are never
11
+ * compared. Splitting it behind {@link lazy} defers it to the moment the tab opens.
12
+ *
13
+ * The shared `@elytracms/core/content` diff engine it uses stays eager (many other
14
+ * critical-path modules import it), so what defers here is the compare UI itself.
15
+ */
16
+ const VersionCompareImpl = lazy(() => import('./version-compare').then((module) => ({ default: module.VersionCompare })));
17
+ function CompareFallback() {
18
+ return (_jsx("div", { className: "grid min-h-0 flex-1 place-items-center p-8", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('cms.version.loadingCompare') }) }));
19
+ }
20
+ export function VersionCompare() {
21
+ return (_jsx(Suspense, { fallback: _jsx(CompareFallback, {}), children: _jsx(VersionCompareImpl, {}) }));
22
+ }
23
+ //# sourceMappingURL=version-compare-lazy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-compare-lazy.js","sourceRoot":"","sources":["../../../src/components/cms/version-compare-lazy.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,EAAE,CACnC,MAAM,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC,CACnF,CAAA;AAED,SAAS,eAAe;IACtB,OAAO,CACL,cAAK,SAAS,EAAC,4CAA4C,YACzD,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,4BAA4B,CAAC,GACnC,GACH,CACP,CAAA;AACH,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,CACL,KAAC,QAAQ,IAAC,QAAQ,EAAE,KAAC,eAAe,KAAG,YACrC,KAAC,kBAAkB,KAAG,GACb,CACZ,CAAA;AACH,CAAC","sourcesContent":["import { Suspense, lazy } from 'react'\nimport { Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Lazy boundary for the version-compare surface (EC-322 load-time pass).\n *\n * The compare view is a large, genuinely occasional surface — it only renders on\n * the detail editor's Compare tab. Importing it statically kept its whole diff UI\n * on the CMS detail-view critical path for every document, most of which are never\n * compared. Splitting it behind {@link lazy} defers it to the moment the tab opens.\n *\n * The shared `@elytracms/core/content` diff engine it uses stays eager (many other\n * critical-path modules import it), so what defers here is the compare UI itself.\n */\nconst VersionCompareImpl = lazy(() =>\n import('./version-compare').then((module) => ({ default: module.VersionCompare })),\n)\n\nfunction CompareFallback() {\n return (\n <div className=\"grid min-h-0 flex-1 place-items-center p-8\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.version.loadingCompare')}\n </Text>\n </div>\n )\n}\n\nexport function VersionCompare() {\n return (\n <Suspense fallback={<CompareFallback />}>\n <VersionCompareImpl />\n </Suspense>\n )\n}\n"]}
@@ -7,6 +7,12 @@ export interface WordToken {
7
7
  text: string;
8
8
  changed: boolean;
9
9
  }
10
+ /** A token in the EC-317 unified (single-lane) prose diff: `same` reads plain,
11
+ * `removed`/`added` are the struck/highlighted halves of a changed run. */
12
+ export interface MergedDiffToken {
13
+ text: string;
14
+ kind: 'same' | 'removed' | 'added';
15
+ }
10
16
  /**
11
17
  * Word-level LCS diff (prose, not code): tokens NOT on the longest common
12
18
  * subsequence are marked — removed on the before side, added on the after side.
@@ -16,6 +22,46 @@ export declare function wordDiff(before: string, after: string): {
16
22
  before: WordToken[];
17
23
  after: WordToken[];
18
24
  };
25
+ /**
26
+ * Merge a {@link wordDiff} pair into ONE reading lane (EC-317 unified view) —
27
+ * a single stream of `same` / `removed` / `added` tokens instead of two
28
+ * columns. Sound by construction: `wordDiff` pushes an `unchanged` token to
29
+ * BOTH sides in the SAME loop iteration (so the Nth unchanged token in
30
+ * `before` and the Nth unchanged token in `after` are always the same anchor),
31
+ * and every stretch between two anchors is a contiguous `changed` run on
32
+ * either side (possibly empty on one, or both — a pure add/remove — or both
33
+ * nonempty — a replace). Walking both arrays in lockstep and draining each
34
+ * side's changed run between anchors reconstructs that reading order exactly.
35
+ */
36
+ export declare function mergeWordDiff(before: WordToken[], after: WordToken[]): MergedDiffToken[];
37
+ /**
38
+ * A sensible default length threshold (characters) past which the EC-317
39
+ * compare view collapses a value behind a per-row "Show more" — long enough to
40
+ * read a couple of sentences at a glance, short enough that a full rich-text
41
+ * body doesn't dominate the change-list.
42
+ */
43
+ export declare const DIFF_TRUNCATE_THRESHOLD = 400;
44
+ /**
45
+ * Collapse a plain text value for display (EC-317) — presentation only, never
46
+ * touching the semantic/identity diff underneath: the caller always has the
47
+ * full string, this just decides how much of it to render.
48
+ */
49
+ export declare function truncateText(text: string, expanded: boolean, threshold?: number): {
50
+ shown: string;
51
+ isLong: boolean;
52
+ };
53
+ /**
54
+ * The token-array counterpart of {@link truncateText} — used for word-diff /
55
+ * merged-diff token streams so a collapsed row still ends on a whole token
56
+ * (never mid-word) and the change markers (`changed`/`kind`) it slices off
57
+ * are simply not rendered, not recomputed.
58
+ */
59
+ export declare function truncateTokens<T extends {
60
+ text: string;
61
+ }>(tokens: readonly T[], expanded: boolean, threshold?: number): {
62
+ shown: T[];
63
+ isLong: boolean;
64
+ };
19
65
  /**
20
66
  * A "rich text" value as readable text for the prose diff — FORMAT-AGNOSTIC, since
21
67
  * the stored value can be anything the field's editor writes: a plain/HTML STRING