@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.
- package/convex/cms.ts +8 -2
- package/dist/components/app-views/default-modules.d.ts +60 -5
- package/dist/components/app-views/default-modules.js +53 -15
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +2 -0
- package/dist/components/app-views/module.js +8 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/assets/asset-detail.d.ts +24 -0
- package/dist/components/assets/asset-detail.js +163 -0
- package/dist/components/assets/asset-detail.js.map +1 -0
- package/dist/components/assets/asset-grid.d.ts +28 -0
- package/dist/components/assets/asset-grid.js +68 -0
- package/dist/components/assets/asset-grid.js.map +1 -0
- package/dist/components/assets/asset-metadata.d.ts +19 -0
- package/dist/components/assets/asset-metadata.js +44 -0
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +54 -0
- package/dist/components/assets/assets-collection-view-source.js +186 -0
- package/dist/components/assets/assets-collection-view-source.js.map +1 -0
- package/dist/components/assets/assets-module.d.ts +18 -0
- package/dist/components/assets/assets-module.js +94 -0
- package/dist/components/assets/assets-module.js.map +1 -0
- package/dist/components/assets/assets-store.js +2 -7
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/assets-view-preference.d.ts +15 -0
- package/dist/components/assets/assets-view-preference.js +22 -0
- package/dist/components/assets/assets-view-preference.js.map +1 -0
- package/dist/components/builder/builder-store.d.ts +12 -0
- package/dist/components/builder/builder-store.js +18 -16
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +5 -14
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.d.ts +19 -0
- package/dist/components/builder/composition-field-editor-lazy.js +29 -0
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -0
- package/dist/components/builder/composition-field-editor.js +1 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +8 -0
- package/dist/components/builder/insert-context.js +1 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.js +1 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +7 -134
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +15 -4
- package/dist/components/cms/cms-store.js +42 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.d.ts +8 -1
- package/dist/components/cms/collection-table.js +62 -10
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/column-width.d.ts +25 -0
- package/dist/components/cms/column-width.js +47 -0
- package/dist/components/cms/column-width.js.map +1 -0
- package/dist/components/cms/detail-editor.js +101 -68
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/discard-draft.d.ts +32 -0
- package/dist/components/cms/discard-draft.js +13 -0
- package/dist/components/cms/discard-draft.js.map +1 -0
- package/dist/components/cms/document-staging.d.ts +7 -0
- package/dist/components/cms/document-staging.js +44 -0
- package/dist/components/cms/document-staging.js.map +1 -0
- package/dist/components/cms/field-connection.d.ts +22 -0
- package/dist/components/cms/field-connection.js +37 -0
- package/dist/components/cms/field-connection.js.map +1 -0
- package/dist/components/cms/field-control.js +158 -34
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +43 -0
- package/dist/components/cms/prop-fields.js +130 -0
- package/dist/components/cms/prop-fields.js.map +1 -0
- package/dist/components/cms/rich-text/component-embed.d.ts +17 -3
- package/dist/components/cms/rich-text/component-embed.js +29 -7
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/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/row-actions.d.ts +28 -0
- package/dist/components/cms/row-actions.js +20 -0
- package/dist/components/cms/row-actions.js.map +1 -0
- package/dist/components/cms/use-document-versions.d.ts +8 -1
- package/dist/components/cms/use-document-versions.js +31 -4
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.d.ts +1 -0
- package/dist/components/cms/version-compare-lazy.js +23 -0
- package/dist/components/cms/version-compare-lazy.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +46 -0
- package/dist/components/cms/version-compare-text.js +78 -0
- package/dist/components/cms/version-compare-text.js.map +1 -1
- package/dist/components/cms/version-compare.d.ts +9 -5
- package/dist/components/cms/version-compare.js +184 -59
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +10 -4
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/components/component-detail.d.ts +5 -3
- package/dist/components/components/component-detail.js +41 -8
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +61 -0
- package/dist/components/components/component-preview.js +103 -0
- package/dist/components/components/component-preview.js.map +1 -0
- package/dist/components/components/components-index.d.ts +5 -3
- package/dist/components/components/components-index.js +19 -24
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/registry-browser.d.ts +7 -0
- package/dist/components/components/registry-browser.js +20 -36
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.d.ts +51 -0
- package/dist/components/components/taxonomy.js +73 -0
- package/dist/components/components/taxonomy.js.map +1 -0
- package/dist/components/components/transfer-dialog.d.ts +20 -0
- package/dist/components/components/transfer-dialog.js +64 -0
- package/dist/components/components/transfer-dialog.js.map +1 -0
- package/dist/components/components/transfer.d.ts +43 -0
- package/dist/components/components/transfer.js +31 -0
- package/dist/components/components/transfer.js.map +1 -0
- package/dist/components/project-list.js +3 -4
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.js +3 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-store-status.d.ts +21 -0
- package/dist/components/rail-store-status.js +21 -0
- package/dist/components/rail-store-status.js.map +1 -0
- package/dist/components/redirects/redirects-module.js +4 -8
- package/dist/components/redirects/redirects-module.js.map +1 -1
- package/dist/components/redirects/redirects-store.js +2 -7
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/sign-in-needed-badge.d.ts +6 -0
- package/dist/components/sign-in-needed-badge.js +13 -0
- package/dist/components/sign-in-needed-badge.js.map +1 -0
- package/dist/components/studio-project-screen.js +1 -4
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +1 -4
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/users/users-module.js +5 -7
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/components/users/users-store.js +2 -7
- package/dist/components/users/users-store.js.map +1 -1
- package/dist/lib/auth/index.js +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +1 -3
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +0 -2
- package/dist/lib/cms-views/view-model.js +0 -7
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/diff-view-preference.d.ts +7 -0
- package/dist/lib/diff-view-preference.js +21 -0
- package/dist/lib/diff-view-preference.js.map +1 -0
- package/dist/lib/i18n/messages.en.js +79 -30
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/local-preference.d.ts +11 -0
- package/dist/lib/local-preference.js +26 -0
- package/dist/lib/local-preference.js.map +1 -0
- package/dist/lib/locale/fallback.d.ts +45 -0
- package/dist/lib/locale/fallback.js +85 -0
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +6 -0
- package/dist/lib/persona/labels.js +8 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/index.d.ts +1 -1
- package/dist/lib/query/index.js +1 -1
- package/dist/lib/query/index.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +14 -2
- package/dist/lib/query/operations-query.js +11 -6
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/rail.d.ts +0 -6
- package/dist/lib/rail.js +6 -25
- package/dist/lib/rail.js.map +1 -1
- package/dist/lib/theme.d.ts +0 -6
- package/dist/lib/theme.js +10 -13
- package/dist/lib/theme.js.map +1 -1
- package/dist/router/match.d.ts +1 -0
- package/dist/router/match.js +4 -0
- package/dist/router/match.js.map +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/package.json +22 -25
|
@@ -1,5 +1,5 @@
|
|
|
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
5
|
import { isAssetNode, readAssetId, readDocumentRef } from '@elytracms/core/cms-core';
|
|
@@ -8,9 +8,11 @@ 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,129 @@ 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
|
-
: ''] }), diff.changes.length === 0 ? (_jsx(CompareNotice, { children: translate('cms.version.versionsIdentical') })) : (_jsxs("div", { className: "relative", children: [_jsx("div", { className: "pointer-events-none absolute inset-y-0 left-1/2 w-0.5 -translate-x-1/2 bg-border", "aria-hidden": true }), _jsx(TwoCol, { className: "px-0.5 pb-1", 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 }) }) }), groups.map((group) => (_jsx(FieldGroup, { group: group, canRestore: canRestore, busy: busy, onRestore: restore }, group.field)))] }))] }));
|
|
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, canRestore: canRestore, busy: busy, onRestore: restore }, group.field)))] }) }))] }));
|
|
100
150
|
}
|
|
101
|
-
|
|
102
|
-
|
|
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; a divider sits at the midline (its own, scoped to this
|
|
161
|
+
* instance — every row uses the same proportions, so they all line up). */
|
|
103
162
|
function TwoCol({ before, after, className }) {
|
|
104
|
-
return (_jsxs("div", { className: cn('grid grid-cols-2 gap-x-6', className), children: [_jsx("div", { className: "min-w-0", children: before }), _jsx("div", { className: "min-w-0", children: after })] }));
|
|
163
|
+
return (_jsxs("div", { className: cn('relative grid grid-cols-2 gap-x-6', 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", children: before }), _jsx("div", { className: "min-w-0", children: after })] }));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Mode-aware value display (EC-317): side-by-side keeps the before | after
|
|
167
|
+
* columns; unified stacks a single "− before" / "+ after" pair in one lane,
|
|
168
|
+
* omitting whichever side doesn't exist (`added`/`removed` kind).
|
|
169
|
+
*/
|
|
170
|
+
function DiffColumns({ before, after, kind }) {
|
|
171
|
+
const { mode } = useDiffDisplay();
|
|
172
|
+
if (mode === 'unified') {
|
|
173
|
+
return (_jsxs(Stack, { gap: "1", children: [kind !== 'added' ? _jsx(UnifiedLine, { tone: "removed", children: before }) : null, kind !== 'removed' ? _jsx(UnifiedLine, { tone: "added", children: after }) : null] }));
|
|
174
|
+
}
|
|
175
|
+
return _jsx(TwoCol, { before: before, after: after });
|
|
176
|
+
}
|
|
177
|
+
function UnifiedLine({ tone, children }) {
|
|
178
|
+
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 })] }));
|
|
179
|
+
}
|
|
180
|
+
/** Wraps truncatable content with its per-row "Show more"/"Show less" control —
|
|
181
|
+
* hidden while the global "expand all" is active (nothing left to toggle). */
|
|
182
|
+
function ExpandToggle({ id, isLong, expanded, children, }) {
|
|
183
|
+
const { expandAll, toggleExpand } = useDiffDisplay();
|
|
184
|
+
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
185
|
}
|
|
106
186
|
function VersionMenu({ caption, current, meta, options, onSelect, }) {
|
|
107
187
|
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
188
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (single) {
|
|
114
|
-
const entry = group.entries[0];
|
|
115
|
-
return (_jsxs("div", { className: "border-t border-border/30 py-3", children: [_jsxs(Row, { gap: "2", align: "center", className: "mb-1.5", children: [_jsx(Text, { size: "sm", weight: "medium", className: "min-w-0 truncate", children: group.label }), _jsx(KindTag, { kind: entry.kind }), _jsx(Spacer, {}), canRestore ? (_jsx(RestoreIcon, { busy: busy, title: translate('cms.version.restoreField', { field: group.label }), onClick: () => onRestore(fieldScope(entry)) })) : null] }), _jsx(EntryBody, { change: entry, canRestore: canRestore, busy: busy, onRestore: onRestore })] }));
|
|
116
|
-
}
|
|
117
|
-
return (_jsxs("div", { className: "border-t border-border/30 py-3", children: [_jsx(Text, { size: "sm", weight: "medium", className: "mb-2 block", children: group.label }), _jsx(Stack, { gap: "3", children: group.entries.map((entry) => (_jsxs("div", { children: [_jsxs(Row, { gap: "2", align: "center", className: "mb-1", children: [entry.locale ? (_jsx(Badge, { variant: "outline", className: "px-1 py-0 text-[0.6rem] uppercase tracking-wide text-muted-foreground", children: localeChipLabel(entry.locale) })) : null, _jsx(KindTag, { kind: entry.kind }), _jsx(Spacer, {}), canRestore ? (_jsx(RestoreIcon, { busy: busy, title: entry.locale
|
|
118
|
-
? translate('cms.version.restoreFieldLocale', {
|
|
119
|
-
field: group.label,
|
|
120
|
-
locale: localeChipLabel(entry.locale),
|
|
121
|
-
})
|
|
122
|
-
: translate('cms.version.restoreField', { field: group.label }), onClick: () => onRestore(fieldScope(entry)) })) : null] }), _jsx(EntryBody, { change: entry, canRestore: canRestore, busy: busy, onRestore: onRestore })] }, entry.locale ?? '__base__'))) })] }));
|
|
189
|
+
/** The unified/side-by-side segmented toggle (EC-317) — same pill language as
|
|
190
|
+
* the header's `ViewTabs`/locale switcher, so it reads as one lens control. */
|
|
191
|
+
function DiffViewToggle({ mode, onChange }) {
|
|
192
|
+
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') })] }));
|
|
123
193
|
}
|
|
124
|
-
function
|
|
194
|
+
function DiffViewOption({ active, label, onClick }) {
|
|
195
|
+
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 }));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* One field's diff rows (EC-317: each locale variant is its own zebra-striped
|
|
199
|
+
* row with a descriptor lane — label reused from the studio's EC-241
|
|
200
|
+
* `fieldLabel` active-locale resolver where a schema field exists, falling
|
|
201
|
+
* back to the diff engine's default-locale label for a schema-drift key).
|
|
202
|
+
*/
|
|
203
|
+
function FieldGroup({ group, field, zebraStart, canRestore, busy, onRestore, }) {
|
|
204
|
+
const label = field ? fieldLabel(field) : group.label;
|
|
205
|
+
return (_jsx("div", { className: "border-t border-border/30", children: group.entries.map((entry, i) => {
|
|
206
|
+
const rowId = `${group.field}:${entry.locale ?? '_'}`;
|
|
207
|
+
return (_jsx(FieldRow, { zebra: (zebraStart + i) % 2 === 1, descriptor: _jsx(FieldDescriptor, { label: label, locale: entry.locale, kind: entry.kind }), restore: canRestore ? (_jsx(RestoreIcon, { busy: busy, title: entry.locale
|
|
208
|
+
? translate('cms.version.restoreFieldLocale', {
|
|
209
|
+
field: label,
|
|
210
|
+
locale: localeChipLabel(entry.locale),
|
|
211
|
+
})
|
|
212
|
+
: translate('cms.version.restoreField', { field: label }), onClick: () => onRestore(fieldScope(entry)) })) : null, children: _jsx(EntryBody, { change: entry, rowId: rowId, canRestore: canRestore, busy: busy, onRestore: onRestore }) }, entry.locale ?? '__base__'));
|
|
213
|
+
}) }));
|
|
214
|
+
}
|
|
215
|
+
/** The descriptor lane + value area, one grid row (EC-317). Zebra alternates
|
|
216
|
+
* by the row's position across the WHOLE change-list, not per field group. */
|
|
217
|
+
function FieldRow({ descriptor, restore, zebra, children, }) {
|
|
218
|
+
return (_jsxs("div", { className: cn(DIFF_ROW_GRID, 'rounded-sm px-2 py-2.5', zebra && 'bg-muted/40'), children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [descriptor, restore] }), _jsx("div", { className: "min-w-0", children: children })] }));
|
|
219
|
+
}
|
|
220
|
+
/** The descriptor lane's content (EC-317): field label always, locale chip only
|
|
221
|
+
* when this row is a localized variant, plus the quiet added/removed kind tag. */
|
|
222
|
+
function FieldDescriptor({ label, locale, kind }) {
|
|
223
|
+
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 })] })] }));
|
|
224
|
+
}
|
|
225
|
+
function EntryBody({ change, rowId, canRestore, busy, onRestore, }) {
|
|
125
226
|
const localeScope = change.locale !== undefined ? { locale: change.locale } : {};
|
|
126
227
|
if (change.blocks) {
|
|
127
|
-
return (_jsx(Stack, { gap: "3", className: "mt-1", children: change.blocks.map((block) => (_jsx(BlockSection, { block: block, canRestore: canRestore, busy: busy, onRestore: () => onRestore({ kind: 'block', field: change.field, blockId: block.blockId, ...localeScope }) }, block.blockId))) }));
|
|
228
|
+
return (_jsx(Stack, { gap: "3", className: "mt-1", children: change.blocks.map((block) => (_jsx(BlockSection, { block: block, rowId: `${rowId}|block:${block.blockId}`, canRestore: canRestore, busy: busy, onRestore: () => onRestore({ kind: 'block', field: change.field, blockId: block.blockId, ...localeScope }) }, block.blockId))) }));
|
|
128
229
|
}
|
|
129
230
|
if (change.type === 'richText') {
|
|
130
231
|
const before = richTextToText(change.before);
|
|
131
232
|
const after = richTextToText(change.after);
|
|
132
233
|
if (before === after)
|
|
133
|
-
return (_jsx(MutedNote, { children: before
|
|
134
|
-
|
|
135
|
-
: translate('cms.version.contentChanged') }));
|
|
136
|
-
return _jsx(WordDiffCols, { before: before, after: after });
|
|
234
|
+
return (_jsx(MutedNote, { children: before ? translate('cms.version.textUnchanged') : translate('cms.version.contentChanged') }));
|
|
235
|
+
return _jsx(WordDiffCols, { id: rowId, before: before, after: after });
|
|
137
236
|
}
|
|
138
237
|
if (change.kind === 'changed' && typeof change.before === 'string' && typeof change.after === 'string') {
|
|
139
|
-
return _jsx(WordDiffCols, { before: change.before, after: change.after });
|
|
238
|
+
return _jsx(WordDiffCols, { id: rowId, before: change.before, after: change.after });
|
|
140
239
|
}
|
|
141
|
-
return (_jsx(
|
|
240
|
+
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
241
|
}
|
|
143
|
-
function BlockSection({ block, canRestore, busy, onRestore, }) {
|
|
242
|
+
function BlockSection({ block, rowId, canRestore, busy, onRestore, }) {
|
|
144
243
|
const name = block.componentId.split('.').pop() ?? block.componentId;
|
|
145
|
-
return (_jsxs("div", { children: [_jsxs(Row, { gap: "2", align: "center", className: "mb-1", children: [_jsx(Text, { size: "xs", weight: "medium", 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'), onClick: onRestore })) : null] }), block.props && block.props.length > 0 ? (_jsx(Stack, { gap: "2", children: block.props.map((prop) => (_jsx(PropRow, { prop: prop }, 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'
|
|
146
|
-
? translate('cms.version.blockAdded')
|
|
147
|
-
: translate('cms.version.blockRemoved') })) : null] }));
|
|
244
|
+
return (_jsxs("div", { children: [_jsxs(Row, { gap: "2", align: "center", className: "mb-1", children: [_jsx(Text, { size: "xs", weight: "medium", 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'), onClick: onRestore })) : null] }), block.props && block.props.length > 0 ? (_jsx(Stack, { gap: "2", 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
245
|
}
|
|
149
|
-
function PropRow({ prop }) {
|
|
246
|
+
function PropRow({ prop, rowId }) {
|
|
150
247
|
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(
|
|
248
|
+
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
249
|
}
|
|
153
250
|
/** One side of a non-text change — the value, or an "empty" placeholder. */
|
|
154
|
-
function SideValue({ value, other, typeHint }) {
|
|
251
|
+
function SideValue({ value, other, typeHint, id, }) {
|
|
155
252
|
if (value === undefined || value === null || value === '') {
|
|
156
253
|
return (_jsx(Text, { size: "sm", tone: "subtle", className: "italic", children: translate('cms.version.emptyValue') }));
|
|
157
254
|
}
|
|
158
|
-
return _jsx(ValueView, { value: value, other: other, typeHint: typeHint });
|
|
255
|
+
return _jsx(ValueView, { value: value, other: other, typeHint: typeHint, id: id });
|
|
159
256
|
}
|
|
160
257
|
/**
|
|
161
258
|
* Read-only value display, dispatching on the field type where known and on the
|
|
162
259
|
* self-describing value shape otherwise (block props carry no schema) — assets as
|
|
163
|
-
* thumbnails, relations as titles, everything else as calm text
|
|
260
|
+
* thumbnails, relations as titles, everything else as calm text (EC-317: the
|
|
261
|
+
* plain-text fallback truncates past the shared length threshold).
|
|
164
262
|
*/
|
|
165
|
-
function ValueView({ value, other, typeHint }) {
|
|
263
|
+
function ValueView({ value, other, typeHint, id, }) {
|
|
264
|
+
// Called unconditionally up front (rules-of-hooks) even though only the plain-
|
|
265
|
+
// text fallback branch below needs it — the early returns above it don't hook.
|
|
266
|
+
const { isExpanded } = useDiffDisplay();
|
|
166
267
|
if (typeHint === 'asset' || isAssetNode(value))
|
|
167
268
|
return _jsx(AssetValue, { value: value });
|
|
168
269
|
if (typeHint === 'relation' || readDocumentRef(value))
|
|
@@ -175,9 +276,13 @@ function ValueView({ value, other, typeHint }) {
|
|
|
175
276
|
return (_jsx(Text, { size: "sm", mono: true, children: String(value).slice(0, 10) }));
|
|
176
277
|
if (typeof value === 'object')
|
|
177
278
|
return (_jsx(Text, { size: "sm", tone: "muted", children: Array.isArray(value)
|
|
178
|
-
? translate(value.length === 1 ? 'cms.version.itemCount.one' : 'cms.version.itemCount.other', {
|
|
279
|
+
? translate(value.length === 1 ? 'cms.version.itemCount.one' : 'cms.version.itemCount.other', {
|
|
280
|
+
count: value.length,
|
|
281
|
+
})
|
|
179
282
|
: translate('cms.version.objectValue') }));
|
|
180
|
-
|
|
283
|
+
const expanded = isExpanded(id);
|
|
284
|
+
const { shown, isLong } = truncateText(String(value), expanded);
|
|
285
|
+
return (_jsx(ExpandToggle, { id: id, isLong: isLong, expanded: expanded, children: _jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words", children: shown }) }));
|
|
181
286
|
}
|
|
182
287
|
function RelationValue({ value, other }) {
|
|
183
288
|
const source = useCollectionView();
|
|
@@ -197,9 +302,7 @@ function RelationValue({ value, other }) {
|
|
|
197
302
|
return _jsx(Text, { size: "sm", children: label });
|
|
198
303
|
}
|
|
199
304
|
function AssetValue({ value }) {
|
|
200
|
-
const ids = (Array.isArray(value) ? value : [value])
|
|
201
|
-
.map(readAssetId)
|
|
202
|
-
.filter((id) => id !== undefined);
|
|
305
|
+
const ids = (Array.isArray(value) ? value : [value]).map(readAssetId).filter((id) => id !== undefined);
|
|
203
306
|
if (ids.length === 0)
|
|
204
307
|
return (_jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words", children: String(value) }));
|
|
205
308
|
return (_jsx("div", { className: "flex flex-wrap gap-2", children: ids.map((id) => (_jsx(AssetThumb, { id: id }, id))) }));
|
|
@@ -219,16 +322,38 @@ function AssetThumb({ id }) {
|
|
|
219
322
|
return (_jsxs(Badge, { variant: "outline", children: [_jsx(ImageIcon, { className: "size-3" }), " ", label] }));
|
|
220
323
|
}
|
|
221
324
|
// --- Prose word diff --------------------------------------------------------------
|
|
222
|
-
/**
|
|
223
|
-
|
|
325
|
+
/**
|
|
326
|
+
* Prose diff across a row (EC-317): side-by-side reads as two word-diffed
|
|
327
|
+
* columns; unified merges into ONE reading lane (`mergeWordDiff`). Either way,
|
|
328
|
+
* a value past the length threshold collapses behind the row's expand control
|
|
329
|
+
* — the full `wordDiff`/`mergeWordDiff` computation underneath is untouched.
|
|
330
|
+
*/
|
|
331
|
+
function WordDiffCols({ id, before, after }) {
|
|
332
|
+
const { mode, isExpanded } = useDiffDisplay();
|
|
333
|
+
const expanded = isExpanded(id);
|
|
224
334
|
const wd = useMemo(() => wordDiff(before, after), [before, after]);
|
|
225
|
-
|
|
335
|
+
const merged = useMemo(() => mergeWordDiff(wd.before, wd.after), [wd]);
|
|
336
|
+
if (mode === 'unified') {
|
|
337
|
+
const { shown, isLong } = truncateTokens(merged, expanded, DIFF_TRUNCATE_THRESHOLD);
|
|
338
|
+
return (_jsx(ExpandToggle, { id: id, isLong: isLong, expanded: expanded, children: _jsx(UnifiedWordTokens, { tokens: shown }) }));
|
|
339
|
+
}
|
|
340
|
+
const beforeSlice = truncateTokens(wd.before, expanded, DIFF_TRUNCATE_THRESHOLD);
|
|
341
|
+
const afterSlice = truncateTokens(wd.after, expanded, DIFF_TRUNCATE_THRESHOLD);
|
|
342
|
+
const isLong = beforeSlice.isLong || afterSlice.isLong;
|
|
343
|
+
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
344
|
}
|
|
227
345
|
function WordTokens({ tokens, side }) {
|
|
228
346
|
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
347
|
? 'bg-destructive/15 text-destructive line-through decoration-destructive/40'
|
|
230
348
|
: 'bg-success/20 text-foreground'), children: token.text }, i)) : (_jsx("span", { className: "text-muted-foreground", children: token.text }, i))) }));
|
|
231
349
|
}
|
|
350
|
+
/** The unified lane's merged token stream — same marks as `WordTokens`, one
|
|
351
|
+
* continuous reading order instead of two columns. */
|
|
352
|
+
function UnifiedWordTokens({ tokens }) {
|
|
353
|
+
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'
|
|
354
|
+
? 'bg-destructive/15 text-destructive line-through decoration-destructive/40'
|
|
355
|
+
: 'bg-success/20 text-foreground'), children: token.text }, i))) }));
|
|
356
|
+
}
|
|
232
357
|
// --- Small shared bits ------------------------------------------------------------
|
|
233
358
|
/** A quiet "· new" / "· removed" tag; nothing for a plain change (implicit). */
|
|
234
359
|
function KindTag({ kind }) {
|