@elytracms/studio 0.0.38 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) 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 +62 -10
  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 +101 -68
  55. package/dist/components/cms/detail-editor.js.map +1 -1
  56. package/dist/components/cms/discard-draft.d.ts +32 -0
  57. package/dist/components/cms/discard-draft.js +13 -0
  58. package/dist/components/cms/discard-draft.js.map +1 -0
  59. package/dist/components/cms/document-staging.d.ts +7 -0
  60. package/dist/components/cms/document-staging.js +44 -0
  61. package/dist/components/cms/document-staging.js.map +1 -0
  62. package/dist/components/cms/field-connection.d.ts +22 -0
  63. package/dist/components/cms/field-connection.js +37 -0
  64. package/dist/components/cms/field-connection.js.map +1 -0
  65. package/dist/components/cms/field-control.js +158 -34
  66. package/dist/components/cms/field-control.js.map +1 -1
  67. package/dist/components/cms/prop-fields.d.ts +43 -0
  68. package/dist/components/cms/prop-fields.js +130 -0
  69. package/dist/components/cms/prop-fields.js.map +1 -0
  70. package/dist/components/cms/rich-text/component-embed.d.ts +17 -3
  71. package/dist/components/cms/rich-text/component-embed.js +29 -7
  72. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  73. package/dist/components/cms/rich-text/embed-props.d.ts +49 -0
  74. package/dist/components/cms/rich-text/embed-props.js +106 -0
  75. package/dist/components/cms/rich-text/embed-props.js.map +1 -0
  76. package/dist/components/cms/rich-text/index.d.ts +6 -1
  77. package/dist/components/cms/rich-text/index.js +6 -1
  78. package/dist/components/cms/rich-text/index.js.map +1 -1
  79. package/dist/components/cms/rich-text/lazy.d.ts +19 -0
  80. package/dist/components/cms/rich-text/lazy.js +26 -0
  81. package/dist/components/cms/rich-text/lazy.js.map +1 -0
  82. package/dist/components/cms/row-actions.d.ts +28 -0
  83. package/dist/components/cms/row-actions.js +20 -0
  84. package/dist/components/cms/row-actions.js.map +1 -0
  85. package/dist/components/cms/use-document-versions.d.ts +8 -1
  86. package/dist/components/cms/use-document-versions.js +31 -4
  87. package/dist/components/cms/use-document-versions.js.map +1 -1
  88. package/dist/components/cms/version-compare-lazy.d.ts +1 -0
  89. package/dist/components/cms/version-compare-lazy.js +23 -0
  90. package/dist/components/cms/version-compare-lazy.js.map +1 -0
  91. package/dist/components/cms/version-compare-text.d.ts +46 -0
  92. package/dist/components/cms/version-compare-text.js +78 -0
  93. package/dist/components/cms/version-compare-text.js.map +1 -1
  94. package/dist/components/cms/version-compare.d.ts +9 -5
  95. package/dist/components/cms/version-compare.js +184 -59
  96. package/dist/components/cms/version-compare.js.map +1 -1
  97. package/dist/components/cms/versions-pane.js +10 -4
  98. package/dist/components/cms/versions-pane.js.map +1 -1
  99. package/dist/components/components/component-detail.d.ts +5 -3
  100. package/dist/components/components/component-detail.js +41 -8
  101. package/dist/components/components/component-detail.js.map +1 -1
  102. package/dist/components/components/component-preview.d.ts +61 -0
  103. package/dist/components/components/component-preview.js +103 -0
  104. package/dist/components/components/component-preview.js.map +1 -0
  105. package/dist/components/components/components-index.d.ts +5 -3
  106. package/dist/components/components/components-index.js +19 -24
  107. package/dist/components/components/components-index.js.map +1 -1
  108. package/dist/components/components/registry-browser.d.ts +7 -0
  109. package/dist/components/components/registry-browser.js +20 -36
  110. package/dist/components/components/registry-browser.js.map +1 -1
  111. package/dist/components/components/taxonomy.d.ts +51 -0
  112. package/dist/components/components/taxonomy.js +73 -0
  113. package/dist/components/components/taxonomy.js.map +1 -0
  114. package/dist/components/components/transfer-dialog.d.ts +20 -0
  115. package/dist/components/components/transfer-dialog.js +64 -0
  116. package/dist/components/components/transfer-dialog.js.map +1 -0
  117. package/dist/components/components/transfer.d.ts +43 -0
  118. package/dist/components/components/transfer.js +31 -0
  119. package/dist/components/components/transfer.js.map +1 -0
  120. package/dist/components/project-list.js +3 -4
  121. package/dist/components/project-list.js.map +1 -1
  122. package/dist/components/project-switcher.js +3 -2
  123. package/dist/components/project-switcher.js.map +1 -1
  124. package/dist/components/rail-store-status.d.ts +21 -0
  125. package/dist/components/rail-store-status.js +21 -0
  126. package/dist/components/rail-store-status.js.map +1 -0
  127. package/dist/components/redirects/redirects-module.js +4 -8
  128. package/dist/components/redirects/redirects-module.js.map +1 -1
  129. package/dist/components/redirects/redirects-store.js +2 -7
  130. package/dist/components/redirects/redirects-store.js.map +1 -1
  131. package/dist/components/sign-in-needed-badge.d.ts +6 -0
  132. package/dist/components/sign-in-needed-badge.js +13 -0
  133. package/dist/components/sign-in-needed-badge.js.map +1 -0
  134. package/dist/components/studio-project-screen.js +1 -4
  135. package/dist/components/studio-project-screen.js.map +1 -1
  136. package/dist/components/studio-shell.js +1 -4
  137. package/dist/components/studio-shell.js.map +1 -1
  138. package/dist/components/users/users-module.js +5 -7
  139. package/dist/components/users/users-module.js.map +1 -1
  140. package/dist/components/users/users-store.js +2 -7
  141. package/dist/components/users/users-store.js.map +1 -1
  142. package/dist/lib/auth/index.js +1 -1
  143. package/dist/lib/auth/index.js.map +1 -1
  144. package/dist/lib/clipboard/paste-target.js +1 -3
  145. package/dist/lib/clipboard/paste-target.js.map +1 -1
  146. package/dist/lib/cms-views/view-model.d.ts +0 -2
  147. package/dist/lib/cms-views/view-model.js +0 -7
  148. package/dist/lib/cms-views/view-model.js.map +1 -1
  149. package/dist/lib/diff-view-preference.d.ts +7 -0
  150. package/dist/lib/diff-view-preference.js +21 -0
  151. package/dist/lib/diff-view-preference.js.map +1 -0
  152. package/dist/lib/i18n/messages.en.js +79 -30
  153. package/dist/lib/i18n/messages.en.js.map +1 -1
  154. package/dist/lib/local-preference.d.ts +11 -0
  155. package/dist/lib/local-preference.js +26 -0
  156. package/dist/lib/local-preference.js.map +1 -0
  157. package/dist/lib/locale/fallback.d.ts +45 -0
  158. package/dist/lib/locale/fallback.js +85 -0
  159. package/dist/lib/locale/fallback.js.map +1 -1
  160. package/dist/lib/persona/labels.d.ts +6 -0
  161. package/dist/lib/persona/labels.js +8 -0
  162. package/dist/lib/persona/labels.js.map +1 -1
  163. package/dist/lib/query/index.d.ts +1 -1
  164. package/dist/lib/query/index.js +1 -1
  165. package/dist/lib/query/index.js.map +1 -1
  166. package/dist/lib/query/operations-query.d.ts +14 -2
  167. package/dist/lib/query/operations-query.js +11 -6
  168. package/dist/lib/query/operations-query.js.map +1 -1
  169. package/dist/lib/rail.d.ts +0 -6
  170. package/dist/lib/rail.js +6 -25
  171. package/dist/lib/rail.js.map +1 -1
  172. package/dist/lib/theme.d.ts +0 -6
  173. package/dist/lib/theme.js +10 -13
  174. package/dist/lib/theme.js.map +1 -1
  175. package/dist/router/match.d.ts +1 -0
  176. package/dist/router/match.js +4 -0
  177. package/dist/router/match.js.map +1 -1
  178. package/dist/ui/button.d.ts +1 -1
  179. package/package.json +22 -25
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Suspense, lazy } from 'react';
3
+ import { translate } from '@elytracms/studio/lib/i18n';
4
+ /**
5
+ * Lazy boundary for the CMS rich-text editor (EC-322 load-time pass).
6
+ *
7
+ * The TipTap editor + its ProseMirror runtime and extension set is a large
8
+ * dependency, and `field-control` sits on the critical path of *every* CMS view.
9
+ * Importing the editor statically pulled all of TipTap into the studio's main
10
+ * route chunk even for documents with no `richText` field on screen. Splitting it
11
+ * behind {@link lazy} moves the whole editor subtree into an on-demand chunk that
12
+ * only downloads when a `richText` field actually renders — the field control's
13
+ * seam is unchanged (it still imports `RichTextEditor` from `./rich-text`).
14
+ *
15
+ * The fallback mirrors the loaded editor's outer frame (same border/rounding + a
16
+ * toolbar-and-body min-height) so the swap-in is layout-jank-free, and carries an
17
+ * accessible loading label from the message catalog.
18
+ */
19
+ const RichTextEditorImpl = lazy(() => import('./editor').then((module) => ({ default: module.RichTextEditor })));
20
+ function RichTextEditorFallback() {
21
+ return (_jsx("div", { className: "ec-rte min-h-[7.5rem] animate-pulse rounded-md border border-input bg-muted/20 shadow-sm", role: "status", "aria-busy": "true", children: _jsx("span", { className: "sr-only", children: translate('richText.loading') }) }));
22
+ }
23
+ export function RichTextEditor(props) {
24
+ return (_jsx(Suspense, { fallback: _jsx(RichTextEditorFallback, {}), children: _jsx(RichTextEditorImpl, { ...props }) }));
25
+ }
26
+ //# sourceMappingURL=lazy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lazy.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/lazy.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,EAAE,CACnC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC,CAC1E,CAAA;AAED,SAAS,sBAAsB;IAC7B,OAAO,CACL,cACE,SAAS,EAAC,0FAA0F,EACpG,IAAI,EAAC,QAAQ,eACH,MAAM,YAEhB,eAAM,SAAS,EAAC,SAAS,YAAE,SAAS,CAAC,kBAAkB,CAAC,GAAQ,GAC5D,CACP,CAAA;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAgD;IAC7E,OAAO,CACL,KAAC,QAAQ,IAAC,QAAQ,EAAE,KAAC,sBAAsB,KAAG,YAC5C,KAAC,kBAAkB,OAAK,KAAK,GAAI,GACxB,CACZ,CAAA;AACH,CAAC","sourcesContent":["import { Suspense, lazy } from 'react'\nimport type { ComponentProps } from 'react'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Lazy boundary for the CMS rich-text editor (EC-322 load-time pass).\n *\n * The TipTap editor + its ProseMirror runtime and extension set is a large\n * dependency, and `field-control` sits on the critical path of *every* CMS view.\n * Importing the editor statically pulled all of TipTap into the studio's main\n * route chunk even for documents with no `richText` field on screen. Splitting it\n * behind {@link lazy} moves the whole editor subtree into an on-demand chunk that\n * only downloads when a `richText` field actually renders — the field control's\n * seam is unchanged (it still imports `RichTextEditor` from `./rich-text`).\n *\n * The fallback mirrors the loaded editor's outer frame (same border/rounding + a\n * toolbar-and-body min-height) so the swap-in is layout-jank-free, and carries an\n * accessible loading label from the message catalog.\n */\nconst RichTextEditorImpl = lazy(() =>\n import('./editor').then((module) => ({ default: module.RichTextEditor })),\n)\n\nfunction RichTextEditorFallback() {\n return (\n <div\n className=\"ec-rte min-h-[7.5rem] animate-pulse rounded-md border border-input bg-muted/20 shadow-sm\"\n role=\"status\"\n aria-busy=\"true\"\n >\n <span className=\"sr-only\">{translate('richText.loading')}</span>\n </div>\n )\n}\n\nexport function RichTextEditor(props: ComponentProps<typeof RichTextEditorImpl>) {\n return (\n <Suspense fallback={<RichTextEditorFallback />}>\n <RichTextEditorImpl {...props} />\n </Suspense>\n )\n}\n"]}
@@ -0,0 +1,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
@@ -55,6 +55,84 @@ export function wordDiff(before, after) {
55
55
  afterTokens.push({ text: b[j++], changed: true });
56
56
  return { before: beforeTokens, after: afterTokens };
57
57
  }
58
+ /**
59
+ * Merge a {@link wordDiff} pair into ONE reading lane (EC-317 unified view) —
60
+ * a single stream of `same` / `removed` / `added` tokens instead of two
61
+ * columns. Sound by construction: `wordDiff` pushes an `unchanged` token to
62
+ * BOTH sides in the SAME loop iteration (so the Nth unchanged token in
63
+ * `before` and the Nth unchanged token in `after` are always the same anchor),
64
+ * and every stretch between two anchors is a contiguous `changed` run on
65
+ * either side (possibly empty on one, or both — a pure add/remove — or both
66
+ * nonempty — a replace). Walking both arrays in lockstep and draining each
67
+ * side's changed run between anchors reconstructs that reading order exactly.
68
+ */
69
+ export function mergeWordDiff(before, after) {
70
+ const merged = [];
71
+ let i = 0;
72
+ let j = 0;
73
+ while (i < before.length && j < after.length) {
74
+ if (!before[i].changed && !after[j].changed) {
75
+ merged.push({ text: before[i].text, kind: 'same' });
76
+ i++;
77
+ j++;
78
+ continue;
79
+ }
80
+ while (i < before.length && before[i].changed) {
81
+ merged.push({ text: before[i].text, kind: 'removed' });
82
+ i++;
83
+ }
84
+ while (j < after.length && after[j].changed) {
85
+ merged.push({ text: after[j].text, kind: 'added' });
86
+ j++;
87
+ }
88
+ }
89
+ // A trailing changed run on whichever side still has tokens once the other
90
+ // is exhausted (wordDiff's own trailing while-loops produce exactly this).
91
+ while (i < before.length)
92
+ merged.push({ text: before[i++].text, kind: 'removed' });
93
+ while (j < after.length)
94
+ merged.push({ text: after[j++].text, kind: 'added' });
95
+ return merged;
96
+ }
97
+ /**
98
+ * A sensible default length threshold (characters) past which the EC-317
99
+ * compare view collapses a value behind a per-row "Show more" — long enough to
100
+ * read a couple of sentences at a glance, short enough that a full rich-text
101
+ * body doesn't dominate the change-list.
102
+ */
103
+ export const DIFF_TRUNCATE_THRESHOLD = 400;
104
+ /**
105
+ * Collapse a plain text value for display (EC-317) — presentation only, never
106
+ * touching the semantic/identity diff underneath: the caller always has the
107
+ * full string, this just decides how much of it to render.
108
+ */
109
+ export function truncateText(text, expanded, threshold = DIFF_TRUNCATE_THRESHOLD) {
110
+ const isLong = text.length > threshold;
111
+ if (expanded || !isLong)
112
+ return { shown: text, isLong };
113
+ return { shown: `${text.slice(0, threshold).trimEnd()}…`, isLong };
114
+ }
115
+ /**
116
+ * The token-array counterpart of {@link truncateText} — used for word-diff /
117
+ * merged-diff token streams so a collapsed row still ends on a whole token
118
+ * (never mid-word) and the change markers (`changed`/`kind`) it slices off
119
+ * are simply not rendered, not recomputed.
120
+ */
121
+ export function truncateTokens(tokens, expanded, threshold = DIFF_TRUNCATE_THRESHOLD) {
122
+ const totalLength = tokens.reduce((sum, token) => sum + token.text.length, 0);
123
+ const isLong = totalLength > threshold;
124
+ if (expanded || !isLong)
125
+ return { shown: [...tokens], isLong };
126
+ const shown = [];
127
+ let used = 0;
128
+ for (const token of tokens) {
129
+ if (used >= threshold)
130
+ break;
131
+ shown.push(token);
132
+ used += token.text.length;
133
+ }
134
+ return { shown, isLong };
135
+ }
58
136
  /**
59
137
  * A "rich text" value as readable text for the prose diff — FORMAT-AGNOSTIC, since
60
138
  * the stored value can be anything the field's editor writes: a plain/HTML STRING
@@ -1 +1 @@
1
- {"version":3,"file":"version-compare-text.js","sourceRoot":"","sources":["../../../src/components/cms/version-compare-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure text helpers for the EC-298 compare view — extracted so the fiddly bits\n * (word-level LCS, format-agnostic \"rich text\" extraction) are unit-tested apart\n * from the React surface.\n */\n\nexport interface WordToken {\n text: string\n changed: boolean\n}\n\nfunction tokenize(s: string): string[] {\n return s.split(/(\\s+)/).filter((t) => t.length > 0)\n}\n\n/**\n * Word-level LCS diff (prose, not code): tokens NOT on the longest common\n * subsequence are marked — removed on the before side, added on the after side.\n * Whitespace tokens participate, so spacing is preserved on render.\n */\nexport function wordDiff(before: string, after: string): { before: WordToken[]; after: WordToken[] } {\n const a = tokenize(before)\n const b = tokenize(after)\n // Bound the DP so a pathological pair can't stall the editor.\n if (a.length + b.length > 4000) {\n return {\n before: a.map((text) => ({ text, changed: true })),\n after: b.map((text) => ({ text, changed: true })),\n }\n }\n const m = a.length\n const n = b.length\n const dp: number[][] = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0))\n for (let i = m - 1; i >= 0; i--) {\n for (let j = n - 1; j >= 0; j--) {\n dp[i]![j] = a[i] === b[j] ? dp[i + 1]![j + 1]! + 1 : Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!)\n }\n }\n const beforeTokens: WordToken[] = []\n const afterTokens: WordToken[] = []\n let i = 0\n let j = 0\n while (i < m && j < n) {\n if (a[i] === b[j]) {\n beforeTokens.push({ text: a[i]!, changed: false })\n afterTokens.push({ text: b[j]!, changed: false })\n i++\n j++\n } else if (dp[i + 1]![j]! >= dp[i]![j + 1]!) {\n beforeTokens.push({ text: a[i]!, changed: true })\n i++\n } else {\n afterTokens.push({ text: b[j]!, changed: true })\n j++\n }\n }\n while (i < m) beforeTokens.push({ text: a[i++]!, changed: true })\n while (j < n) afterTokens.push({ text: b[j++]!, changed: true })\n return { before: beforeTokens, after: afterTokens }\n}\n\n/**\n * A \"rich text\" value as readable text for the prose diff — FORMAT-AGNOSTIC, since\n * the stored value can be anything the field's editor writes: a plain/HTML STRING\n * (a simple text input), a TipTap `{version, doc}` tree, or a bare doc. We show\n * whatever is entered rather than assuming a structure (EC-298).\n */\nexport function richTextToText(value: unknown): string {\n if (typeof value === 'string') return looksLikeHtml(value) ? htmlToText(value) : value\n if (value && typeof value === 'object') {\n const wrapper = value as { doc?: unknown; content?: unknown }\n const doc = (wrapper.doc ?? wrapper) as { content?: unknown }\n if (Array.isArray(doc.content)) return plainTextFromNodes(doc.content)\n }\n return ''\n}\n\nexport function looksLikeHtml(s: string): boolean {\n return /<[a-z!/][\\s\\S]*>/i.test(s)\n}\n\n/** Strip HTML to readable text, keeping block breaks so paragraphs still separate. */\nexport function htmlToText(html: string): string {\n return html\n .replace(/<\\s*br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|h[1-6]|blockquote)\\s*>/gi, '\\n\\n')\n .replace(/<\\/(li|tr|ul|ol)\\s*>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/&nbsp;/gi, ' ')\n .replace(/&amp;/gi, '&')\n .replace(/&lt;/gi, '<')\n .replace(/&gt;/gi, '>')\n .replace(/&quot;/gi, '\"')\n .replace(/&#39;/gi, \"'\")\n .replace(/[ \\t]+/g, ' ')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\n/** Plain text from ProseMirror/TipTap nodes: text runs joined, blocks separated. */\nexport function plainTextFromNodes(nodes: unknown[]): string {\n const blockText = (node: unknown): string => {\n const parts: string[] = []\n const walk = (n: unknown) => {\n if (!n || typeof n !== 'object') return\n const rec = n as { text?: unknown; content?: unknown }\n if (typeof rec.text === 'string') parts.push(rec.text)\n if (Array.isArray(rec.content)) rec.content.forEach(walk)\n }\n walk(node)\n return parts.join('')\n }\n return nodes.map(blockText).filter(Boolean).join('\\n\\n')\n}\n"]}
1
+ {"version":3,"file":"version-compare-text.js","sourceRoot":"","sources":["../../../src/components/cms/version-compare-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure text helpers for the EC-298 compare view — extracted so the fiddly bits\n * (word-level LCS, format-agnostic \"rich text\" extraction) are unit-tested apart\n * from the React surface.\n */\n\nexport interface WordToken {\n text: string\n changed: boolean\n}\n\n/** A token in the EC-317 unified (single-lane) prose diff: `same` reads plain,\n * `removed`/`added` are the struck/highlighted halves of a changed run. */\nexport interface MergedDiffToken {\n text: string\n kind: 'same' | 'removed' | 'added'\n}\n\nfunction tokenize(s: string): string[] {\n return s.split(/(\\s+)/).filter((t) => t.length > 0)\n}\n\n/**\n * Word-level LCS diff (prose, not code): tokens NOT on the longest common\n * subsequence are marked — removed on the before side, added on the after side.\n * Whitespace tokens participate, so spacing is preserved on render.\n */\nexport function wordDiff(before: string, after: string): { before: WordToken[]; after: WordToken[] } {\n const a = tokenize(before)\n const b = tokenize(after)\n // Bound the DP so a pathological pair can't stall the editor.\n if (a.length + b.length > 4000) {\n return {\n before: a.map((text) => ({ text, changed: true })),\n after: b.map((text) => ({ text, changed: true })),\n }\n }\n const m = a.length\n const n = b.length\n const dp: number[][] = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0))\n for (let i = m - 1; i >= 0; i--) {\n for (let j = n - 1; j >= 0; j--) {\n dp[i]![j] = a[i] === b[j] ? dp[i + 1]![j + 1]! + 1 : Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!)\n }\n }\n const beforeTokens: WordToken[] = []\n const afterTokens: WordToken[] = []\n let i = 0\n let j = 0\n while (i < m && j < n) {\n if (a[i] === b[j]) {\n beforeTokens.push({ text: a[i]!, changed: false })\n afterTokens.push({ text: b[j]!, changed: false })\n i++\n j++\n } else if (dp[i + 1]![j]! >= dp[i]![j + 1]!) {\n beforeTokens.push({ text: a[i]!, changed: true })\n i++\n } else {\n afterTokens.push({ text: b[j]!, changed: true })\n j++\n }\n }\n while (i < m) beforeTokens.push({ text: a[i++]!, changed: true })\n while (j < n) afterTokens.push({ text: b[j++]!, changed: true })\n return { before: beforeTokens, after: afterTokens }\n}\n\n/**\n * Merge a {@link wordDiff} pair into ONE reading lane (EC-317 unified view) —\n * a single stream of `same` / `removed` / `added` tokens instead of two\n * columns. Sound by construction: `wordDiff` pushes an `unchanged` token to\n * BOTH sides in the SAME loop iteration (so the Nth unchanged token in\n * `before` and the Nth unchanged token in `after` are always the same anchor),\n * and every stretch between two anchors is a contiguous `changed` run on\n * either side (possibly empty on one, or both — a pure add/remove — or both\n * nonempty — a replace). Walking both arrays in lockstep and draining each\n * side's changed run between anchors reconstructs that reading order exactly.\n */\nexport function mergeWordDiff(before: WordToken[], after: WordToken[]): MergedDiffToken[] {\n const merged: MergedDiffToken[] = []\n let i = 0\n let j = 0\n while (i < before.length && j < after.length) {\n if (!before[i]!.changed && !after[j]!.changed) {\n merged.push({ text: before[i]!.text, kind: 'same' })\n i++\n j++\n continue\n }\n while (i < before.length && before[i]!.changed) {\n merged.push({ text: before[i]!.text, kind: 'removed' })\n i++\n }\n while (j < after.length && after[j]!.changed) {\n merged.push({ text: after[j]!.text, kind: 'added' })\n j++\n }\n }\n // A trailing changed run on whichever side still has tokens once the other\n // is exhausted (wordDiff's own trailing while-loops produce exactly this).\n while (i < before.length) merged.push({ text: before[i++]!.text, kind: 'removed' })\n while (j < after.length) merged.push({ text: after[j++]!.text, kind: 'added' })\n return merged\n}\n\n/**\n * A sensible default length threshold (characters) past which the EC-317\n * compare view collapses a value behind a per-row \"Show more\" — long enough to\n * read a couple of sentences at a glance, short enough that a full rich-text\n * body doesn't dominate the change-list.\n */\nexport const DIFF_TRUNCATE_THRESHOLD = 400\n\n/**\n * Collapse a plain text value for display (EC-317) — presentation only, never\n * touching the semantic/identity diff underneath: the caller always has the\n * full string, this just decides how much of it to render.\n */\nexport function truncateText(\n text: string,\n expanded: boolean,\n threshold = DIFF_TRUNCATE_THRESHOLD,\n): { shown: string; isLong: boolean } {\n const isLong = text.length > threshold\n if (expanded || !isLong) return { shown: text, isLong }\n return { shown: `${text.slice(0, threshold).trimEnd()}…`, isLong }\n}\n\n/**\n * The token-array counterpart of {@link truncateText} — used for word-diff /\n * merged-diff token streams so a collapsed row still ends on a whole token\n * (never mid-word) and the change markers (`changed`/`kind`) it slices off\n * are simply not rendered, not recomputed.\n */\nexport function truncateTokens<T extends { text: string }>(\n tokens: readonly T[],\n expanded: boolean,\n threshold = DIFF_TRUNCATE_THRESHOLD,\n): { shown: T[]; isLong: boolean } {\n const totalLength = tokens.reduce((sum, token) => sum + token.text.length, 0)\n const isLong = totalLength > threshold\n if (expanded || !isLong) return { shown: [...tokens], isLong }\n const shown: T[] = []\n let used = 0\n for (const token of tokens) {\n if (used >= threshold) break\n shown.push(token)\n used += token.text.length\n }\n return { shown, isLong }\n}\n\n/**\n * A \"rich text\" value as readable text for the prose diff — FORMAT-AGNOSTIC, since\n * the stored value can be anything the field's editor writes: a plain/HTML STRING\n * (a simple text input), a TipTap `{version, doc}` tree, or a bare doc. We show\n * whatever is entered rather than assuming a structure (EC-298).\n */\nexport function richTextToText(value: unknown): string {\n if (typeof value === 'string') return looksLikeHtml(value) ? htmlToText(value) : value\n if (value && typeof value === 'object') {\n const wrapper = value as { doc?: unknown; content?: unknown }\n const doc = (wrapper.doc ?? wrapper) as { content?: unknown }\n if (Array.isArray(doc.content)) return plainTextFromNodes(doc.content)\n }\n return ''\n}\n\nexport function looksLikeHtml(s: string): boolean {\n return /<[a-z!/][\\s\\S]*>/i.test(s)\n}\n\n/** Strip HTML to readable text, keeping block breaks so paragraphs still separate. */\nexport function htmlToText(html: string): string {\n return html\n .replace(/<\\s*br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|h[1-6]|blockquote)\\s*>/gi, '\\n\\n')\n .replace(/<\\/(li|tr|ul|ol)\\s*>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/&nbsp;/gi, ' ')\n .replace(/&amp;/gi, '&')\n .replace(/&lt;/gi, '<')\n .replace(/&gt;/gi, '>')\n .replace(/&quot;/gi, '\"')\n .replace(/&#39;/gi, \"'\")\n .replace(/[ \\t]+/g, ' ')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\n/** Plain text from ProseMirror/TipTap nodes: text runs joined, blocks separated. */\nexport function plainTextFromNodes(nodes: unknown[]): string {\n const blockText = (node: unknown): string => {\n const parts: string[] = []\n const walk = (n: unknown) => {\n if (!n || typeof n !== 'object') return\n const rec = n as { text?: unknown; content?: unknown }\n if (typeof rec.text === 'string') parts.push(rec.text)\n if (Array.isArray(rec.content)) rec.content.forEach(walk)\n }\n walk(node)\n return parts.join('')\n }\n return nodes.map(blockText).filter(Boolean).join('\\n\\n')\n}\n"]}
@@ -3,10 +3,14 @@
3
3
  * (the Versions sidebar pane stays on as the navigator).
4
4
  *
5
5
  * A calm change-list grouped BY FIELD (a localized field's per-locale variants sit
6
- * together): before | after across ONE continuous central divider. Text reads as a
7
- * prose diff (changed words lit in place — rich text via its markdown projection so
8
- * the content, not just "changed", is visible); assets as thumbnails; relations as
9
- * titles. Each field/locale (and each block, at property level) restores back to the
10
- * base version with a single ↩.
6
+ * together): a left **descriptor lane** (EC-317 — field label + locale) identifies
7
+ * each row, next to its before | after value (or one merged **unified** lane —
8
+ * a persisted transient view preference, EC-317). Text reads as a prose diff
9
+ * (changed words lit in place — rich text via its markdown projection); assets as
10
+ * thumbnails; relations as titles. A value past a length threshold collapses
11
+ * behind a per-row "Show more" (+ a global "Expand/Collapse all") — presentation
12
+ * only, the semantic/identity diff underneath (EC-298) is never touched. Rows
13
+ * zebra-stripe for scanability. Each field/locale (and each block, at property
14
+ * level) restores back to the base version with a single ↩.
11
15
  */
12
16
  export declare function VersionCompare(): import("react").JSX.Element | null;