@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.
- package/convex/cms.ts +8 -2
- package/dist/components/app-views/default-modules.d.ts +60 -5
- package/dist/components/app-views/default-modules.js +53 -15
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +2 -0
- package/dist/components/app-views/module.js +8 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/assets/asset-detail.d.ts +24 -0
- package/dist/components/assets/asset-detail.js +163 -0
- package/dist/components/assets/asset-detail.js.map +1 -0
- package/dist/components/assets/asset-grid.d.ts +28 -0
- package/dist/components/assets/asset-grid.js +68 -0
- package/dist/components/assets/asset-grid.js.map +1 -0
- package/dist/components/assets/asset-metadata.d.ts +19 -0
- package/dist/components/assets/asset-metadata.js +44 -0
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +54 -0
- package/dist/components/assets/assets-collection-view-source.js +186 -0
- package/dist/components/assets/assets-collection-view-source.js.map +1 -0
- package/dist/components/assets/assets-module.d.ts +18 -0
- package/dist/components/assets/assets-module.js +94 -0
- package/dist/components/assets/assets-module.js.map +1 -0
- package/dist/components/assets/assets-store.js +2 -7
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/assets-view-preference.d.ts +15 -0
- package/dist/components/assets/assets-view-preference.js +22 -0
- package/dist/components/assets/assets-view-preference.js.map +1 -0
- package/dist/components/builder/builder-store.d.ts +12 -0
- package/dist/components/builder/builder-store.js +18 -16
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +5 -14
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.d.ts +19 -0
- package/dist/components/builder/composition-field-editor-lazy.js +29 -0
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -0
- package/dist/components/builder/composition-field-editor.js +1 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +8 -0
- package/dist/components/builder/insert-context.js +1 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.js +1 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +7 -134
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +15 -4
- package/dist/components/cms/cms-store.js +42 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.d.ts +8 -1
- package/dist/components/cms/collection-table.js +75 -11
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/column-width.d.ts +25 -0
- package/dist/components/cms/column-width.js +47 -0
- package/dist/components/cms/column-width.js.map +1 -0
- package/dist/components/cms/detail-editor.js +143 -79
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/details-pane.d.ts +21 -8
- package/dist/components/cms/details-pane.js +31 -11
- package/dist/components/cms/details-pane.js.map +1 -1
- package/dist/components/cms/discard-draft.d.ts +32 -0
- package/dist/components/cms/discard-draft.js +13 -0
- package/dist/components/cms/discard-draft.js.map +1 -0
- package/dist/components/cms/document-staging.d.ts +7 -0
- package/dist/components/cms/document-staging.js +44 -0
- package/dist/components/cms/document-staging.js.map +1 -0
- package/dist/components/cms/field-connection.d.ts +22 -0
- package/dist/components/cms/field-connection.js +37 -0
- package/dist/components/cms/field-connection.js.map +1 -0
- package/dist/components/cms/field-control.js +167 -34
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +43 -0
- package/dist/components/cms/prop-fields.js +154 -0
- package/dist/components/cms/prop-fields.js.map +1 -0
- package/dist/components/cms/rich-text/component-embed.d.ts +17 -3
- package/dist/components/cms/rich-text/component-embed.js +29 -7
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.d.ts +49 -0
- package/dist/components/cms/rich-text/embed-props.js +106 -0
- package/dist/components/cms/rich-text/embed-props.js.map +1 -0
- package/dist/components/cms/rich-text/index.d.ts +6 -1
- package/dist/components/cms/rich-text/index.js +6 -1
- package/dist/components/cms/rich-text/index.js.map +1 -1
- package/dist/components/cms/rich-text/lazy.d.ts +19 -0
- package/dist/components/cms/rich-text/lazy.js +26 -0
- package/dist/components/cms/rich-text/lazy.js.map +1 -0
- package/dist/components/cms/rich-text/link-control.js +3 -1
- package/dist/components/cms/rich-text/link-control.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar-state.d.ts +45 -0
- package/dist/components/cms/rich-text/toolbar-state.js +66 -0
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar.js +7 -15
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/row-actions.d.ts +28 -0
- package/dist/components/cms/row-actions.js +20 -0
- package/dist/components/cms/row-actions.js.map +1 -0
- package/dist/components/cms/use-document-versions.d.ts +8 -1
- package/dist/components/cms/use-document-versions.js +31 -4
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.d.ts +1 -0
- package/dist/components/cms/version-compare-lazy.js +23 -0
- package/dist/components/cms/version-compare-lazy.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +46 -0
- package/dist/components/cms/version-compare-text.js +78 -0
- package/dist/components/cms/version-compare-text.js.map +1 -1
- package/dist/components/cms/version-compare.d.ts +9 -5
- package/dist/components/cms/version-compare.js +217 -64
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +10 -4
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/components/component-detail.d.ts +5 -3
- package/dist/components/components/component-detail.js +41 -8
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +97 -0
- package/dist/components/components/component-preview.js +202 -0
- package/dist/components/components/component-preview.js.map +1 -0
- package/dist/components/components/components-index.d.ts +5 -3
- package/dist/components/components/components-index.js +19 -24
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/registry-browser.d.ts +7 -0
- package/dist/components/components/registry-browser.js +20 -36
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.d.ts +51 -0
- package/dist/components/components/taxonomy.js +73 -0
- package/dist/components/components/taxonomy.js.map +1 -0
- package/dist/components/components/transfer-dialog.d.ts +20 -0
- package/dist/components/components/transfer-dialog.js +64 -0
- package/dist/components/components/transfer-dialog.js.map +1 -0
- package/dist/components/components/transfer.d.ts +43 -0
- package/dist/components/components/transfer.js +31 -0
- package/dist/components/components/transfer.js.map +1 -0
- package/dist/components/project-list.js +3 -4
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.js +3 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-store-status.d.ts +21 -0
- package/dist/components/rail-store-status.js +21 -0
- package/dist/components/rail-store-status.js.map +1 -0
- package/dist/components/redirects/redirects-module.js +4 -8
- package/dist/components/redirects/redirects-module.js.map +1 -1
- package/dist/components/redirects/redirects-store.js +2 -7
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/sign-in-needed-badge.d.ts +6 -0
- package/dist/components/sign-in-needed-badge.js +13 -0
- package/dist/components/sign-in-needed-badge.js.map +1 -0
- package/dist/components/studio-project-screen.js +1 -4
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +1 -4
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/users/users-module.js +5 -7
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/components/users/users-store.js +2 -7
- package/dist/components/users/users-store.js.map +1 -1
- package/dist/lib/auth/index.js +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +1 -3
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +0 -2
- package/dist/lib/cms-views/view-model.js +0 -7
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/diff-view-preference.d.ts +7 -0
- package/dist/lib/diff-view-preference.js +21 -0
- package/dist/lib/diff-view-preference.js.map +1 -0
- package/dist/lib/i18n/messages.en.js +94 -33
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/local-preference.d.ts +11 -0
- package/dist/lib/local-preference.js +26 -0
- package/dist/lib/local-preference.js.map +1 -0
- package/dist/lib/locale/fallback.d.ts +45 -0
- package/dist/lib/locale/fallback.js +85 -0
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +6 -0
- package/dist/lib/persona/labels.js +8 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/index.d.ts +1 -1
- package/dist/lib/query/index.js +1 -1
- package/dist/lib/query/index.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +14 -2
- package/dist/lib/query/operations-query.js +11 -6
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/rail.d.ts +0 -6
- package/dist/lib/rail.js +6 -25
- package/dist/lib/rail.js.map +1 -1
- package/dist/lib/theme.d.ts +0 -6
- package/dist/lib/theme.js +10 -13
- package/dist/lib/theme.js.map +1 -1
- package/dist/router/match.d.ts +1 -0
- package/dist/router/match.js +4 -0
- package/dist/router/match.js.map +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/package.json +22 -25
|
@@ -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(/ /gi, ' ')\n .replace(/&/gi, '&')\n .replace(/</gi, '<')\n .replace(/>/gi, '>')\n .replace(/"/gi, '\"')\n .replace(/'/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(/ /gi, ' ')\n .replace(/&/gi, '&')\n .replace(/</gi, '<')\n .replace(/>/gi, '>')\n .replace(/"/gi, '\"')\n .replace(/'/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):
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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;
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
|
3
3
|
import { ArrowRight, ChevronDown, ChevronLeft, ChevronRight, Image as ImageIcon, RotateCcw } from 'lucide-react';
|
|
4
4
|
import { diffDocuments } from '@elytracms/core/content';
|
|
5
|
-
import { isAssetNode, readAssetId, readDocumentRef } from '@elytracms/core/cms-core';
|
|
5
|
+
import { assetSubfieldsOf, isAssetNode, readAssetId, readDocumentRef } from '@elytracms/core/cms-core';
|
|
6
6
|
import { Badge, Button, Code, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
|
|
7
7
|
import { useOperationCache } from '@elytracms/studio/lib/query';
|
|
8
8
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
9
9
|
import { localeChipLabel } from '@elytracms/studio/lib/locale';
|
|
10
10
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
11
|
+
import { readDiffViewMode, storeDiffViewMode } from '@elytracms/studio/lib/diff-view-preference';
|
|
11
12
|
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
13
|
+
import { fieldLabel } from './cms-store';
|
|
12
14
|
import { toRefArray } from './cell';
|
|
13
|
-
import { richTextToText, wordDiff } from './version-compare-text';
|
|
15
|
+
import { DIFF_TRUNCATE_THRESHOLD, mergeWordDiff, richTextToText, truncateText, truncateTokens, wordDiff, } from './version-compare-text';
|
|
14
16
|
import { useDocumentEditor } from './document-editor-context';
|
|
15
17
|
import { useDocumentVersionsQuery } from './use-document-versions';
|
|
16
18
|
import { formatVersionTimestamp } from './versions-model';
|
|
@@ -19,11 +21,15 @@ import { formatVersionTimestamp } from './versions-model';
|
|
|
19
21
|
* (the Versions sidebar pane stays on as the navigator).
|
|
20
22
|
*
|
|
21
23
|
* A calm change-list grouped BY FIELD (a localized field's per-locale variants sit
|
|
22
|
-
* together):
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* together): a left **descriptor lane** (EC-317 — field label + locale) identifies
|
|
25
|
+
* each row, next to its before | after value (or one merged **unified** lane —
|
|
26
|
+
* a persisted transient view preference, EC-317). Text reads as a prose diff
|
|
27
|
+
* (changed words lit in place — rich text via its markdown projection); assets as
|
|
28
|
+
* thumbnails; relations as titles. A value past a length threshold collapses
|
|
29
|
+
* behind a per-row "Show more" (+ a global "Expand/Collapse all") — presentation
|
|
30
|
+
* only, the semantic/identity diff underneath (EC-298) is never touched. Rows
|
|
31
|
+
* zebra-stripe for scanability. Each field/locale (and each block, at property
|
|
32
|
+
* level) restores back to the base version with a single ↩.
|
|
27
33
|
*/
|
|
28
34
|
export function VersionCompare() {
|
|
29
35
|
const source = useCollectionView();
|
|
@@ -31,18 +37,63 @@ export function VersionCompare() {
|
|
|
31
37
|
const { versionsQuery, versionKey } = useDocumentVersionsQuery(docRef);
|
|
32
38
|
const cache = useOperationCache();
|
|
33
39
|
const [busy, setBusy] = useState(false);
|
|
40
|
+
// EC-317 — the unified/side-by-side toggle is a transient, personal-local view
|
|
41
|
+
// preference (same class as the theme/rail prefs): read once at mount, written
|
|
42
|
+
// back on every change, never round-tripped through the project graph.
|
|
43
|
+
const [mode, setModeState] = useState(() => readDiffViewMode());
|
|
44
|
+
const setMode = useCallback((next) => {
|
|
45
|
+
setModeState(next);
|
|
46
|
+
storeDiffViewMode(next);
|
|
47
|
+
}, []);
|
|
48
|
+
// EC-317 — long-value truncation state: an `expandAll` flag plus per-row
|
|
49
|
+
// overrides. Presentation-only (never recomputes the diff), reset is implicit
|
|
50
|
+
// (in-session state, like the rest of the compare selection).
|
|
51
|
+
const [expandAll, setExpandAll] = useState(false);
|
|
52
|
+
const [expandedRows, setExpandedRows] = useState(() => new Set());
|
|
53
|
+
const toggleExpand = useCallback((id) => {
|
|
54
|
+
setExpandedRows((prev) => {
|
|
55
|
+
const next = new Set(prev);
|
|
56
|
+
if (next.has(id))
|
|
57
|
+
next.delete(id);
|
|
58
|
+
else
|
|
59
|
+
next.add(id);
|
|
60
|
+
return next;
|
|
61
|
+
});
|
|
62
|
+
}, []);
|
|
63
|
+
const isExpanded = useCallback((id) => expandAll || expandedRows.has(id), [expandAll, expandedRows]);
|
|
64
|
+
const toggleExpandAll = useCallback(() => {
|
|
65
|
+
setExpandAll((prev) => {
|
|
66
|
+
const next = !prev;
|
|
67
|
+
// Collapsing all clears per-row overrides too, so "Collapse all" always
|
|
68
|
+
// means "everything is collapsed" rather than "back to before I expanded".
|
|
69
|
+
if (!next)
|
|
70
|
+
setExpandedRows(new Set());
|
|
71
|
+
return next;
|
|
72
|
+
});
|
|
73
|
+
}, []);
|
|
74
|
+
const diffDisplay = useMemo(() => ({ mode, expandAll, isExpanded, toggleExpand }), [mode, expandAll, isExpanded, toggleExpand]);
|
|
34
75
|
const view = versionsQuery.data ?? null;
|
|
35
76
|
const versions = useMemo(() => view?.versions ?? [], [view]);
|
|
36
77
|
const baseRecord = compare ? versions.find((v) => v.version === compare.base) : undefined;
|
|
37
|
-
const targetSnapshot = compare?.target === 'draft'
|
|
38
|
-
? draft
|
|
39
|
-
: versions.find((v) => v.version === compare?.target)?.snapshot;
|
|
78
|
+
const targetSnapshot = compare?.target === 'draft' ? draft : versions.find((v) => v.version === compare?.target)?.snapshot;
|
|
40
79
|
const diff = useMemo(() => baseRecord && targetSnapshot
|
|
41
80
|
? diffDocuments(collection, baseRecord.snapshot, targetSnapshot)
|
|
42
81
|
: { changes: [], unchangedCount: 0 }, [collection, baseRecord, targetSnapshot]);
|
|
43
82
|
const fieldOrder = useMemo(() => new Map(collection.fields.map((f, i) => [f.name, i])), [collection]);
|
|
83
|
+
const fieldByName = useMemo(() => new Map(collection.fields.map((f) => [f.name, f])), [collection]);
|
|
44
84
|
const localeRank = useMemo(() => new Map(source.localeConfig.locales.map((l, i) => [l, i])), [source.localeConfig]);
|
|
45
85
|
const groups = useMemo(() => groupByField(diff.changes, fieldOrder, localeRank), [diff.changes, fieldOrder, localeRank]);
|
|
86
|
+
// EC-317 zebra striping: a running index across every rendered row (not reset
|
|
87
|
+
// per field group), so consecutive groups keep alternating rather than each
|
|
88
|
+
// restarting on the same shade.
|
|
89
|
+
const groupRows = useMemo(() => {
|
|
90
|
+
let rowIndex = 0;
|
|
91
|
+
return groups.map((group) => {
|
|
92
|
+
const zebraStart = rowIndex;
|
|
93
|
+
rowIndex += group.entries.length;
|
|
94
|
+
return { group, field: fieldByName.get(group.field), zebraStart };
|
|
95
|
+
});
|
|
96
|
+
}, [groups, fieldByName]);
|
|
46
97
|
const restore = useCallback(async (scope) => {
|
|
47
98
|
if (!compare)
|
|
48
99
|
return;
|
|
@@ -90,79 +141,139 @@ export function VersionCompare() {
|
|
|
90
141
|
.filter((v) => v.version !== compare.base)
|
|
91
142
|
.map((v) => ({ value: v.version, label: `v${v.version}`, meta: formatVersionTimestamp(v.createdAt) })),
|
|
92
143
|
];
|
|
93
|
-
return (_jsxs(Stack, { gap: "3", className: "mx-auto w-full max-w-5xl p-4 sm:p-6", children: [_jsxs(Row, { gap: "2", align: "center", wrap: true, className: "rounded-md border border-border/70 bg-muted/30 px-3 py-2", children: [_jsx(VersionMenu, { caption: translate('cms.version.baseCaption'), current: `v${compare.base}`, meta: formatVersionTimestamp(baseRecord.createdAt), options: baseOptions, onSelect: (v) => setCompareBase(v) }), _jsx(ArrowRight, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx(VersionMenu, { caption: translate('cms.version.compareWithCaption'), current: targetLabel, options: targetOptions, onSelect: (v) => setCompareTarget(v) }), _jsx(Spacer, {}), _jsxs(Row, { gap: "1", children: [_jsx(Button, { variant: "outline", size: "icon-sm", onClick: () => newerBase !== undefined && setCompareBase(newerBase), disabled: newerBase === undefined, title: translate('cms.version.compareNewer'), "aria-label": translate('cms.version.compareNewer'), children: _jsx(ChevronLeft, {}) }), _jsx(Button, { variant: "outline", size: "icon-sm", onClick: () => olderBase !== undefined && setCompareBase(olderBase), disabled: olderBase === undefined, title: translate('cms.version.compareOlder'), "aria-label": translate('cms.version.compareOlder'), children: _jsx(ChevronRight, {}) })] }), canRestore ? (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => void restore({ kind: 'all' }), disabled: busy || diff.changes.length === 0, title: translate('cms.version.restoreWholeTitle', { version: compare.base }), children: [_jsx(RotateCcw, {}), " ", translate('cms.version.restoreVersion')] })) : null] }), _jsxs(Text, { size: "xs", tone: "muted", className: "px-0.5", children: [diff.changes.length === 0
|
|
144
|
+
return (_jsxs(Stack, { gap: "3", className: "mx-auto w-full max-w-5xl p-4 sm:p-6", children: [_jsxs(Row, { gap: "2", align: "center", wrap: true, className: "rounded-md border border-border/70 bg-muted/30 px-3 py-2", children: [_jsx(VersionMenu, { caption: translate('cms.version.baseCaption'), current: `v${compare.base}`, meta: formatVersionTimestamp(baseRecord.createdAt), options: baseOptions, onSelect: (v) => setCompareBase(v) }), _jsx(ArrowRight, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx(VersionMenu, { caption: translate('cms.version.compareWithCaption'), current: targetLabel, options: targetOptions, onSelect: (v) => setCompareTarget(v) }), _jsx(Spacer, {}), _jsxs(Row, { gap: "1", children: [_jsx(Button, { variant: "outline", size: "icon-sm", onClick: () => newerBase !== undefined && setCompareBase(newerBase), disabled: newerBase === undefined, title: translate('cms.version.compareNewer'), "aria-label": translate('cms.version.compareNewer'), children: _jsx(ChevronLeft, {}) }), _jsx(Button, { variant: "outline", size: "icon-sm", onClick: () => olderBase !== undefined && setCompareBase(olderBase), disabled: olderBase === undefined, title: translate('cms.version.compareOlder'), "aria-label": translate('cms.version.compareOlder'), children: _jsx(ChevronRight, {}) })] }), canRestore ? (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => void restore({ kind: 'all' }), disabled: busy || diff.changes.length === 0, title: translate('cms.version.restoreWholeTitle', { version: compare.base }), children: [_jsx(RotateCcw, {}), " ", translate('cms.version.restoreVersion')] })) : null] }), diff.changes.length > 0 ? (_jsxs(Row, { gap: "2", align: "center", justify: "between", wrap: true, className: "px-0.5", children: [_jsx(DiffViewToggle, { mode: mode, onChange: setMode }), _jsx(Button, { variant: "ghost", size: "sm", onClick: toggleExpandAll, children: translate(expandAll ? 'cms.field.collapseAll' : 'cms.field.expandAll') })] })) : null, _jsxs(Text, { size: "xs", tone: "muted", className: "px-0.5", children: [diff.changes.length === 0
|
|
94
145
|
? translate('cms.version.noDifferences')
|
|
95
|
-
: translate(diff.changes.length === 1
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
? translate('cms.version.comparingPastVersionsHint')
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
146
|
+
: translate(diff.changes.length === 1 ? 'cms.version.changeSummary.one' : 'cms.version.changeSummary.other', {
|
|
147
|
+
count: diff.changes.length,
|
|
148
|
+
unchanged: diff.unchangedCount,
|
|
149
|
+
}), !canRestore && diff.changes.length > 0 ? translate('cms.version.comparingPastVersionsHint') : ''] }), diff.changes.length === 0 ? (_jsx(CompareNotice, { children: translate('cms.version.versionsIdentical') })) : (_jsx(DiffDisplayContext.Provider, { value: diffDisplay, children: _jsxs("div", { className: "pb-1", children: [_jsxs("div", { className: cn(DIFF_ROW_GRID, 'px-2 pb-1.5'), children: [_jsx(Text, { size: "xs", tone: "subtle", className: "uppercase tracking-wide", children: translate('cms.version.fieldColumnHeader') }), mode === 'unified' ? (_jsx(Text, { size: "xs", tone: "subtle", className: "uppercase tracking-wide", children: translate('cms.version.unifiedCaption', { base: compare.base, target: targetLabel }) })) : (_jsx(TwoCol, { before: _jsx(Text, { size: "xs", tone: "subtle", className: "uppercase tracking-wide", children: translate('cms.version.beforeVersion', { version: compare.base }) }), after: _jsx(Text, { size: "xs", tone: "subtle", className: "uppercase tracking-wide", children: translate('cms.version.afterTarget', { target: targetLabel }) }) }))] }), groupRows.map(({ group, field, zebraStart }) => (_jsx(FieldGroup, { group: group, field: field, zebraStart: zebraStart, baseVersion: compare.base, canRestore: canRestore, busy: busy, onRestore: restore }, group.field)))] }) }))] }));
|
|
150
|
+
}
|
|
151
|
+
const DiffDisplayContext = createContext(null);
|
|
152
|
+
function useDiffDisplay() {
|
|
153
|
+
const ctx = useContext(DiffDisplayContext);
|
|
154
|
+
if (!ctx)
|
|
155
|
+
throw new Error('useDiffDisplay must be rendered under the VersionCompare diff area');
|
|
156
|
+
return ctx;
|
|
157
|
+
}
|
|
158
|
+
/** Shared grid template so the descriptor lane + header captions line up. */
|
|
159
|
+
const DIFF_ROW_GRID = 'grid grid-cols-[7.5rem_1fr] items-start gap-x-4';
|
|
160
|
+
/** A before | after grid with a clear central divider (0.0.39: a full-height ruled
|
|
161
|
+
* gutter — the same proportions in every row type, so the before/after split reads
|
|
162
|
+
* consistently across field rows, block-prop rows and word diffs). The columns keep
|
|
163
|
+
* a symmetric inset off the divider so values never touch it. */
|
|
103
164
|
function TwoCol({ before, after, className }) {
|
|
104
|
-
return (_jsxs("div", { className: cn('grid grid-cols-2 gap-x-
|
|
165
|
+
return (_jsxs("div", { className: cn('relative grid grid-cols-2 gap-x-8', className), children: [_jsx("div", { className: "pointer-events-none absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-border", "aria-hidden": true }), _jsx("div", { className: "min-w-0 pr-1", children: before }), _jsx("div", { className: "min-w-0 pl-1", children: after })] }));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Mode-aware value display (EC-317): side-by-side keeps the before | after
|
|
169
|
+
* columns; unified stacks a single "− before" / "+ after" pair in one lane,
|
|
170
|
+
* omitting whichever side doesn't exist (`added`/`removed` kind).
|
|
171
|
+
*/
|
|
172
|
+
function DiffColumns({ before, after, kind }) {
|
|
173
|
+
const { mode } = useDiffDisplay();
|
|
174
|
+
if (mode === 'unified') {
|
|
175
|
+
return (_jsxs(Stack, { gap: "1", children: [kind !== 'added' ? _jsx(UnifiedLine, { tone: "removed", children: before }) : null, kind !== 'removed' ? _jsx(UnifiedLine, { tone: "added", children: after }) : null] }));
|
|
176
|
+
}
|
|
177
|
+
return _jsx(TwoCol, { before: before, after: after });
|
|
178
|
+
}
|
|
179
|
+
function UnifiedLine({ tone, children }) {
|
|
180
|
+
return (_jsxs(Row, { gap: "2", align: "start", className: cn('rounded-sm px-1.5 py-1', tone === 'removed' ? 'bg-destructive/10' : 'bg-success/10'), children: [_jsx(Text, { size: "xs", mono: true, "aria-hidden": true, className: cn('mt-0.5 shrink-0 select-none', tone === 'removed' ? 'text-destructive' : 'text-success'), children: tone === 'removed' ? '−' : '+' }), _jsx("div", { className: "min-w-0 flex-1", children: children })] }));
|
|
181
|
+
}
|
|
182
|
+
/** Wraps truncatable content with its per-row "Show more"/"Show less" control —
|
|
183
|
+
* hidden while the global "expand all" is active (nothing left to toggle). */
|
|
184
|
+
function ExpandToggle({ id, isLong, expanded, children, }) {
|
|
185
|
+
const { expandAll, toggleExpand } = useDiffDisplay();
|
|
186
|
+
return (_jsxs("div", { children: [children, isLong && !expandAll ? (_jsx(Button, { variant: "link", size: "sm", className: "h-auto p-0 text-xs", onClick: () => toggleExpand(id), children: expanded ? translate('cms.version.showLess') : translate('cms.version.showMore') })) : null] }));
|
|
105
187
|
}
|
|
106
188
|
function VersionMenu({ caption, current, meta, options, onSelect, }) {
|
|
107
189
|
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "gap-1.5", children: [_jsx(Text, { size: "xs", tone: "subtle", className: "uppercase tracking-wide", children: caption }), _jsx(Text, { size: "sm", weight: "medium", mono: true, children: current }), meta ? (_jsx(Text, { size: "xs", tone: "muted", className: "hidden sm:inline", children: meta })) : null, _jsx(ChevronDown, { className: "size-3.5 text-muted-foreground" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "max-h-72 overflow-auto", children: options.map((o) => (_jsxs(DropdownMenuItem, { onClick: () => onSelect(o.value), className: "gap-2", children: [_jsx(Text, { size: "sm", weight: "medium", mono: o.value !== 'draft', children: o.label }), o.meta ? (_jsx(Text, { size: "xs", tone: "muted", children: o.meta })) : null] }, String(o.value)))) })] }));
|
|
108
190
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
191
|
+
/** The unified/side-by-side segmented toggle (EC-317) — same pill language as
|
|
192
|
+
* the header's `ViewTabs`/locale switcher, so it reads as one lens control. */
|
|
193
|
+
function DiffViewToggle({ mode, onChange }) {
|
|
194
|
+
return (_jsxs(Row, { gap: "px", className: "rounded-md bg-muted/50 p-0.5", role: "group", "aria-label": translate('cms.version.viewModeAriaLabel'), "data-testid": "diff-view-toggle", children: [_jsx(DiffViewOption, { active: mode === 'sideBySide', label: translate('cms.version.viewSideBySide'), onClick: () => onChange('sideBySide') }), _jsx(DiffViewOption, { active: mode === 'unified', label: translate('cms.version.viewUnified'), onClick: () => onChange('unified') })] }));
|
|
195
|
+
}
|
|
196
|
+
function DiffViewOption({ active, label, onClick }) {
|
|
197
|
+
return (_jsx("button", { type: "button", "aria-pressed": active, "data-testid": `diff-view-option-${active ? 'active' : 'inactive'}`, onClick: onClick, className: cn('rounded-[5px] px-2.5 py-1 text-xs font-medium transition-colors', active ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'), children: label }));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* One field's diff rows (EC-317: each locale variant is its own zebra-striped
|
|
201
|
+
* row with a descriptor lane — label reused from the studio's EC-241
|
|
202
|
+
* `fieldLabel` active-locale resolver where a schema field exists, falling
|
|
203
|
+
* back to the diff engine's default-locale label for a schema-drift key).
|
|
204
|
+
*/
|
|
205
|
+
function FieldGroup({ group, field, zebraStart, baseVersion, canRestore, busy, onRestore, }) {
|
|
206
|
+
const label = field ? fieldLabel(field) : group.label;
|
|
207
|
+
return (_jsx("div", { className: "border-t border-border/30", children: group.entries.map((entry, i) => {
|
|
208
|
+
const rowId = `${group.field}:${entry.locale ?? '_'}`;
|
|
209
|
+
// 0.0.39: ONE restore per row, right-aligned in the descriptor lane. A blocks
|
|
210
|
+
// field restores per BLOCK (each block owns its own restore in its header), so
|
|
211
|
+
// the field-level restore is dropped there — no more two icons per row.
|
|
212
|
+
const fieldRestore = canRestore && !entry.blocks ? (_jsx(RestoreIcon, { busy: busy, title: entry.locale
|
|
213
|
+
? translate('cms.version.restoreFieldLocale', {
|
|
214
|
+
field: label,
|
|
215
|
+
locale: localeChipLabel(entry.locale),
|
|
216
|
+
version: baseVersion,
|
|
217
|
+
})
|
|
218
|
+
: translate('cms.version.restoreField', { field: label, version: baseVersion }), onClick: () => onRestore(fieldScope(entry)) })) : null;
|
|
219
|
+
return (_jsx(FieldRow, { zebra: (zebraStart + i) % 2 === 1, descriptor: _jsx(FieldDescriptor, { label: label, locale: entry.locale, kind: entry.kind }), restore: fieldRestore, children: _jsx(EntryBody, { change: entry, rowId: rowId, baseVersion: baseVersion, canRestore: canRestore, busy: busy, onRestore: onRestore }) }, entry.locale ?? '__base__'));
|
|
220
|
+
}) }));
|
|
221
|
+
}
|
|
222
|
+
/** The descriptor lane + value area, one grid row (EC-317). Zebra alternates
|
|
223
|
+
* by the row's position across the WHOLE change-list, not per field group. */
|
|
224
|
+
function FieldRow({ descriptor, restore, zebra, children, }) {
|
|
225
|
+
return (_jsxs("div", { className: cn(DIFF_ROW_GRID, 'rounded-sm px-2 py-2.5', zebra && 'bg-muted/40'), children: [_jsxs("div", { className: "flex items-start justify-between gap-1", children: [_jsx("div", { className: "min-w-0", children: descriptor }), restore ? _jsx("div", { className: "shrink-0", children: restore }) : null] }), _jsx("div", { className: "min-w-0", children: children })] }));
|
|
226
|
+
}
|
|
227
|
+
/** The descriptor lane's content (EC-317): field label always, locale chip only
|
|
228
|
+
* when this row is a localized variant, plus the quiet added/removed kind tag. */
|
|
229
|
+
function FieldDescriptor({ label, locale, kind }) {
|
|
230
|
+
return (_jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "sm", weight: "medium", className: "min-w-0 truncate", children: label }), _jsxs(Row, { gap: "1", wrap: true, children: [locale ? (_jsx(Badge, { variant: "outline", className: "px-1 py-0 text-[0.6rem] uppercase tracking-wide text-muted-foreground", children: localeChipLabel(locale) })) : null, _jsx(KindTag, { kind: kind })] })] }));
|
|
231
|
+
}
|
|
232
|
+
function EntryBody({ change, rowId, baseVersion, canRestore, busy, onRestore, }) {
|
|
125
233
|
const localeScope = change.locale !== undefined ? { locale: change.locale } : {};
|
|
126
234
|
if (change.blocks) {
|
|
127
|
-
return (_jsx(Stack, { gap: "
|
|
235
|
+
return (_jsx(Stack, { gap: "2", className: "mt-1", children: change.blocks.map((block) => (_jsx(BlockSection, { block: block, rowId: `${rowId}|block:${block.blockId}`, baseVersion: baseVersion, canRestore: canRestore, busy: busy, onRestore: () => onRestore({ kind: 'block', field: change.field, blockId: block.blockId, ...localeScope }) }, block.blockId))) }));
|
|
128
236
|
}
|
|
129
237
|
if (change.type === 'richText') {
|
|
130
238
|
const before = richTextToText(change.before);
|
|
131
239
|
const after = richTextToText(change.after);
|
|
132
240
|
if (before === after)
|
|
133
|
-
return (_jsx(MutedNote, { children: before
|
|
134
|
-
|
|
135
|
-
: translate('cms.version.contentChanged') }));
|
|
136
|
-
return _jsx(WordDiffCols, { before: before, after: after });
|
|
241
|
+
return (_jsx(MutedNote, { children: before ? translate('cms.version.textUnchanged') : translate('cms.version.contentChanged') }));
|
|
242
|
+
return _jsx(WordDiffCols, { id: rowId, before: before, after: after });
|
|
137
243
|
}
|
|
138
244
|
if (change.kind === 'changed' && typeof change.before === 'string' && typeof change.after === 'string') {
|
|
139
|
-
return _jsx(WordDiffCols, { before: change.before, after: change.after });
|
|
245
|
+
return _jsx(WordDiffCols, { id: rowId, before: change.before, after: change.after });
|
|
140
246
|
}
|
|
141
|
-
return (_jsx(
|
|
247
|
+
return (_jsx(DiffColumns, { kind: change.kind, before: _jsx(SideValue, { value: change.before, other: change.after, typeHint: change.type, id: `${rowId}|before` }), after: _jsx(SideValue, { value: change.after, other: change.before, typeHint: change.type, id: `${rowId}|after` }) }));
|
|
142
248
|
}
|
|
143
|
-
function BlockSection({ block, canRestore, busy, onRestore, }) {
|
|
249
|
+
function BlockSection({ block, rowId, baseVersion, canRestore, busy, onRestore, }) {
|
|
144
250
|
const name = block.componentId.split('.').pop() ?? block.componentId;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
251
|
+
// 0.0.39: each block is its own owned row — a header band (block name + id + kind,
|
|
252
|
+
// restore right-aligned) over the prop changes, indented under it with a left rail
|
|
253
|
+
// so the nesting (field → block → props) reads at a glance.
|
|
254
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-md border border-border/60", children: [_jsxs(Row, { gap: "2", align: "center", className: "border-b border-border/50 bg-muted/30 px-2 py-1.5", children: [_jsx(Text, { size: "xs", weight: "semibold", className: "min-w-0 truncate", children: name }), _jsx(Code, { tone: "muted", children: block.componentId }), _jsx(KindTag, { kind: block.kind }), _jsx(Spacer, {}), canRestore ? (_jsx(RestoreIcon, { busy: busy, title: translate('cms.version.restoreBlock', { version: baseVersion }), onClick: onRestore })) : null] }), _jsxs("div", { className: "px-2 py-2", children: [block.props && block.props.length > 0 ? (_jsx(Stack, { gap: "3", className: "border-l-2 border-border/50 pl-3", children: block.props.map((prop) => (_jsx(PropRow, { prop: prop, rowId: `${rowId}|prop:${prop.name}` }, prop.name))) })) : null, block.slotsChanged ? _jsx(MutedNote, { children: translate('cms.version.nestedContentChanged') }) : null, block.kind !== 'changed' && !block.props?.length ? (_jsx(MutedNote, { children: block.kind === 'added' ? translate('cms.version.blockAdded') : translate('cms.version.blockRemoved') })) : null] })] }));
|
|
148
255
|
}
|
|
149
|
-
function PropRow({ prop }) {
|
|
256
|
+
function PropRow({ prop, rowId }) {
|
|
150
257
|
const useWordDiff = prop.kind === 'changed' && typeof prop.before === 'string' && typeof prop.after === 'string';
|
|
151
|
-
return (_jsxs("div", { children: [_jsx(Text, { size: "xs", tone: "subtle", className: "mb-0.5 block uppercase tracking-wide", children: prettyName(prop.name) }), useWordDiff ? (_jsx(WordDiffCols, { before: String(prop.before), after: String(prop.after) })) : (_jsx(
|
|
258
|
+
return (_jsxs("div", { children: [_jsx(Text, { size: "xs", tone: "subtle", className: "mb-0.5 block uppercase tracking-wide", children: prettyName(prop.name) }), useWordDiff ? (_jsx(WordDiffCols, { id: rowId, before: String(prop.before), after: String(prop.after) })) : (_jsx(DiffColumns, { kind: prop.kind, before: _jsx(SideValue, { value: prop.before, other: prop.after, id: `${rowId}|before` }), after: _jsx(SideValue, { value: prop.after, other: prop.before, id: `${rowId}|after` }) }))] }));
|
|
152
259
|
}
|
|
153
260
|
/** One side of a non-text change — the value, or an "empty" placeholder. */
|
|
154
|
-
function SideValue({ value, other, typeHint }) {
|
|
261
|
+
function SideValue({ value, other, typeHint, id, }) {
|
|
155
262
|
if (value === undefined || value === null || value === '') {
|
|
156
263
|
return (_jsx(Text, { size: "sm", tone: "subtle", className: "italic", children: translate('cms.version.emptyValue') }));
|
|
157
264
|
}
|
|
158
|
-
return _jsx(ValueView, { value: value, other: other, typeHint: typeHint });
|
|
265
|
+
return _jsx(ValueView, { value: value, other: other, typeHint: typeHint, id: id });
|
|
159
266
|
}
|
|
160
267
|
/**
|
|
161
268
|
* Read-only value display, dispatching on the field type where known and on the
|
|
162
269
|
* self-describing value shape otherwise (block props carry no schema) — assets as
|
|
163
|
-
* thumbnails, relations as titles, everything else as calm text
|
|
270
|
+
* thumbnails, relations as titles, everything else as calm text (EC-317: the
|
|
271
|
+
* plain-text fallback truncates past the shared length threshold).
|
|
164
272
|
*/
|
|
165
|
-
function ValueView({ value, other, typeHint }) {
|
|
273
|
+
function ValueView({ value, other, typeHint, id, }) {
|
|
274
|
+
// Called unconditionally up front (rules-of-hooks) even though only the plain-
|
|
275
|
+
// text fallback branch below needs it — the early returns above it don't hook.
|
|
276
|
+
const { isExpanded } = useDiffDisplay();
|
|
166
277
|
if (typeHint === 'asset' || isAssetNode(value))
|
|
167
278
|
return _jsx(AssetValue, { value: value });
|
|
168
279
|
if (typeHint === 'relation' || readDocumentRef(value))
|
|
@@ -175,9 +286,13 @@ function ValueView({ value, other, typeHint }) {
|
|
|
175
286
|
return (_jsx(Text, { size: "sm", mono: true, children: String(value).slice(0, 10) }));
|
|
176
287
|
if (typeof value === 'object')
|
|
177
288
|
return (_jsx(Text, { size: "sm", tone: "muted", children: Array.isArray(value)
|
|
178
|
-
? translate(value.length === 1 ? 'cms.version.itemCount.one' : 'cms.version.itemCount.other', {
|
|
289
|
+
? translate(value.length === 1 ? 'cms.version.itemCount.one' : 'cms.version.itemCount.other', {
|
|
290
|
+
count: value.length,
|
|
291
|
+
})
|
|
179
292
|
: translate('cms.version.objectValue') }));
|
|
180
|
-
|
|
293
|
+
const expanded = isExpanded(id);
|
|
294
|
+
const { shown, isLong } = truncateText(String(value), expanded);
|
|
295
|
+
return (_jsx(ExpandToggle, { id: id, isLong: isLong, expanded: expanded, children: _jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words", children: shown }) }));
|
|
181
296
|
}
|
|
182
297
|
function RelationValue({ value, other }) {
|
|
183
298
|
const source = useCollectionView();
|
|
@@ -196,13 +311,29 @@ function RelationValue({ value, other }) {
|
|
|
196
311
|
.join(', ');
|
|
197
312
|
return _jsx(Text, { size: "sm", children: label });
|
|
198
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Asset-record-owned keys — resolved off the asset RECORD at render (url, alt,
|
|
316
|
+
* dimensions, focal point, …), never authored on the referencing document. Mirrors
|
|
317
|
+
* the diff engine's own denylist (`document-diff.ts`): those keys never register as a
|
|
318
|
+
* change, so they are never worth SHOWING as one either. Only authored per-use
|
|
319
|
+
* sub-fields (EC-301, e.g. a placement's `caption`) surface beneath the thumbnail.
|
|
320
|
+
*/
|
|
321
|
+
const ASSET_RECORD_KEYS = new Set([
|
|
322
|
+
'url', 'alt', 'width', 'height', 'focalPoint', 'contentType', 'mimeType',
|
|
323
|
+
'byteSize', 'storageKey', 'title', 'filename', 'meta', 'createdAt', 'updatedAt',
|
|
324
|
+
]);
|
|
325
|
+
function authoredAssetSubfields(value) {
|
|
326
|
+
return Object.entries(assetSubfieldsOf(value)).filter(([key]) => !ASSET_RECORD_KEYS.has(key));
|
|
327
|
+
}
|
|
199
328
|
function AssetValue({ value }) {
|
|
200
|
-
const
|
|
201
|
-
.map(readAssetId)
|
|
202
|
-
.filter((
|
|
203
|
-
if (
|
|
329
|
+
const items = (Array.isArray(value) ? value : [value])
|
|
330
|
+
.map((entry) => ({ id: readAssetId(entry), sub: authoredAssetSubfields(entry) }))
|
|
331
|
+
.filter((a) => a.id !== undefined);
|
|
332
|
+
if (items.length === 0)
|
|
204
333
|
return (_jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words", children: String(value) }));
|
|
205
|
-
return (_jsx("div", { className: "flex flex-wrap gap-2", children:
|
|
334
|
+
return (_jsx("div", { className: "flex flex-wrap gap-2", children: items.map(({ id, sub }, i) => (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(AssetThumb, { id: id }), sub.length > 0 ? (_jsx(Stack, { gap: "1", className: "pl-0.5", children: sub.map(([key, val]) => (_jsxs(Text, { size: "xs", tone: "muted", className: "break-words", children: [_jsxs("span", { className: "uppercase tracking-wide text-muted-foreground/70", children: [prettyName(key), ":"] }), ' ', typeof val === 'object' && val !== null
|
|
335
|
+
? translate('cms.version.objectValue')
|
|
336
|
+
: String(val)] }, key))) })) : null] }, `${id}-${i}`))) }));
|
|
206
337
|
}
|
|
207
338
|
/** An asset as a thumbnail (image with a resolvable URL) or a labelled badge —
|
|
208
339
|
* the visual diff for a cover-image change, not two identical filenames. */
|
|
@@ -219,16 +350,38 @@ function AssetThumb({ id }) {
|
|
|
219
350
|
return (_jsxs(Badge, { variant: "outline", children: [_jsx(ImageIcon, { className: "size-3" }), " ", label] }));
|
|
220
351
|
}
|
|
221
352
|
// --- Prose word diff --------------------------------------------------------------
|
|
222
|
-
/**
|
|
223
|
-
|
|
353
|
+
/**
|
|
354
|
+
* Prose diff across a row (EC-317): side-by-side reads as two word-diffed
|
|
355
|
+
* columns; unified merges into ONE reading lane (`mergeWordDiff`). Either way,
|
|
356
|
+
* a value past the length threshold collapses behind the row's expand control
|
|
357
|
+
* — the full `wordDiff`/`mergeWordDiff` computation underneath is untouched.
|
|
358
|
+
*/
|
|
359
|
+
function WordDiffCols({ id, before, after }) {
|
|
360
|
+
const { mode, isExpanded } = useDiffDisplay();
|
|
361
|
+
const expanded = isExpanded(id);
|
|
224
362
|
const wd = useMemo(() => wordDiff(before, after), [before, after]);
|
|
225
|
-
|
|
363
|
+
const merged = useMemo(() => mergeWordDiff(wd.before, wd.after), [wd]);
|
|
364
|
+
if (mode === 'unified') {
|
|
365
|
+
const { shown, isLong } = truncateTokens(merged, expanded, DIFF_TRUNCATE_THRESHOLD);
|
|
366
|
+
return (_jsx(ExpandToggle, { id: id, isLong: isLong, expanded: expanded, children: _jsx(UnifiedWordTokens, { tokens: shown }) }));
|
|
367
|
+
}
|
|
368
|
+
const beforeSlice = truncateTokens(wd.before, expanded, DIFF_TRUNCATE_THRESHOLD);
|
|
369
|
+
const afterSlice = truncateTokens(wd.after, expanded, DIFF_TRUNCATE_THRESHOLD);
|
|
370
|
+
const isLong = beforeSlice.isLong || afterSlice.isLong;
|
|
371
|
+
return (_jsx(ExpandToggle, { id: id, isLong: isLong, expanded: expanded, children: _jsx(TwoCol, { before: _jsx(WordTokens, { tokens: beforeSlice.shown, side: "before" }), after: _jsx(WordTokens, { tokens: afterSlice.shown, side: "after" }) }) }));
|
|
226
372
|
}
|
|
227
373
|
function WordTokens({ tokens, side }) {
|
|
228
374
|
return (_jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words leading-relaxed", children: tokens.map((token, i) => token.changed ? (_jsx("mark", { className: cn('rounded-sm px-0.5', side === 'before'
|
|
229
375
|
? 'bg-destructive/15 text-destructive line-through decoration-destructive/40'
|
|
230
376
|
: 'bg-success/20 text-foreground'), children: token.text }, i)) : (_jsx("span", { className: "text-muted-foreground", children: token.text }, i))) }));
|
|
231
377
|
}
|
|
378
|
+
/** The unified lane's merged token stream — same marks as `WordTokens`, one
|
|
379
|
+
* continuous reading order instead of two columns. */
|
|
380
|
+
function UnifiedWordTokens({ tokens }) {
|
|
381
|
+
return (_jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words leading-relaxed", children: tokens.map((token, i) => token.kind === 'same' ? (_jsx("span", { className: "text-muted-foreground", children: token.text }, i)) : (_jsx("mark", { className: cn('rounded-sm px-0.5', token.kind === 'removed'
|
|
382
|
+
? 'bg-destructive/15 text-destructive line-through decoration-destructive/40'
|
|
383
|
+
: 'bg-success/20 text-foreground'), children: token.text }, i))) }));
|
|
384
|
+
}
|
|
232
385
|
// --- Small shared bits ------------------------------------------------------------
|
|
233
386
|
/** A quiet "· new" / "· removed" tag; nothing for a plain change (implicit). */
|
|
234
387
|
function KindTag({ kind }) {
|