@prenta/admin 0.93.0 → 0.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/__tests__/lib/version-diff.test.d.ts +2 -0
  3. package/dist/__tests__/lib/version-diff.test.d.ts.map +1 -0
  4. package/dist/__tests__/lib/version-diff.test.js +230 -0
  5. package/dist/__tests__/lib/version-diff.test.js.map +1 -0
  6. package/dist/__tests__/lib/version-history.test.d.ts +2 -0
  7. package/dist/__tests__/lib/version-history.test.d.ts.map +1 -0
  8. package/dist/__tests__/lib/version-history.test.js +125 -0
  9. package/dist/__tests__/lib/version-history.test.js.map +1 -0
  10. package/dist/__tests__/styles/responsive-layer.test.d.ts +2 -0
  11. package/dist/__tests__/styles/responsive-layer.test.d.ts.map +1 -0
  12. package/dist/__tests__/styles/responsive-layer.test.js +127 -0
  13. package/dist/__tests__/styles/responsive-layer.test.js.map +1 -0
  14. package/dist/__tests__/views/canvas-review-mode.render.test.d.ts +2 -0
  15. package/dist/__tests__/views/canvas-review-mode.render.test.d.ts.map +1 -0
  16. package/dist/__tests__/views/canvas-review-mode.render.test.js +70 -0
  17. package/dist/__tests__/views/canvas-review-mode.render.test.js.map +1 -0
  18. package/dist/__tests__/views/editor-top-bar.render.test.js +29 -0
  19. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  20. package/dist/__tests__/views/version-history-panel.render.test.d.ts +2 -0
  21. package/dist/__tests__/views/version-history-panel.render.test.d.ts.map +1 -0
  22. package/dist/__tests__/views/version-history-panel.render.test.js +153 -0
  23. package/dist/__tests__/views/version-history-panel.render.test.js.map +1 -0
  24. package/dist/components/EditorRail.js +3 -3
  25. package/dist/components/EditorRail.js.map +1 -1
  26. package/dist/components/page-tab/DirectiveSwitch.d.ts.map +1 -1
  27. package/dist/components/page-tab/DirectiveSwitch.js +5 -1
  28. package/dist/components/page-tab/DirectiveSwitch.js.map +1 -1
  29. package/dist/layout/primitives/AdminShell.d.ts.map +1 -1
  30. package/dist/layout/primitives/AdminShell.js +2 -2
  31. package/dist/layout/primitives/AdminShell.js.map +1 -1
  32. package/dist/lib/version-diff.d.ts +50 -0
  33. package/dist/lib/version-diff.d.ts.map +1 -0
  34. package/dist/lib/version-diff.js +294 -0
  35. package/dist/lib/version-diff.js.map +1 -0
  36. package/dist/lib/version-history.d.ts +73 -0
  37. package/dist/lib/version-history.d.ts.map +1 -0
  38. package/dist/lib/version-history.js +126 -0
  39. package/dist/lib/version-history.js.map +1 -0
  40. package/dist/prenta-admin.css +1 -1
  41. package/dist/views/page-editor/EditorCanvas.d.ts +14 -1
  42. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  43. package/dist/views/page-editor/EditorCanvas.js +3 -3
  44. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  45. package/dist/views/page-editor/EditorTopBar.d.ts +8 -2
  46. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  47. package/dist/views/page-editor/EditorTopBar.js +8 -6
  48. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  49. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  50. package/dist/views/page-editor/PageSectionEditor.js +115 -32
  51. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  52. package/dist/views/page-editor/ReviewBar.d.ts +21 -0
  53. package/dist/views/page-editor/ReviewBar.d.ts.map +1 -0
  54. package/dist/views/page-editor/ReviewBar.js +22 -0
  55. package/dist/views/page-editor/ReviewBar.js.map +1 -0
  56. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  57. package/dist/views/page-editor/SectionInspector.js +1 -1
  58. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  59. package/dist/views/page-editor/SectionsPanel.js +1 -1
  60. package/dist/views/page-editor/SectionsPanel.js.map +1 -1
  61. package/dist/views/page-editor/VersionHistoryPanel.d.ts +29 -0
  62. package/dist/views/page-editor/VersionHistoryPanel.d.ts.map +1 -0
  63. package/dist/views/page-editor/VersionHistoryPanel.js +81 -0
  64. package/dist/views/page-editor/VersionHistoryPanel.js.map +1 -0
  65. package/dist/views/page-editor/sections/SectionRenderer.d.ts +13 -1
  66. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  67. package/dist/views/page-editor/sections/SectionRenderer.js +23 -8
  68. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  69. package/package.json +3 -3
  70. package/src/__tests__/lib/version-diff.test.ts +278 -0
  71. package/src/__tests__/lib/version-history.test.ts +166 -0
  72. package/src/__tests__/styles/responsive-layer.test.ts +152 -0
  73. package/src/__tests__/views/canvas-review-mode.render.test.tsx +94 -0
  74. package/src/__tests__/views/editor-top-bar.render.test.tsx +42 -0
  75. package/src/__tests__/views/version-history-panel.render.test.tsx +195 -0
  76. package/src/components/EditorRail.tsx +5 -5
  77. package/src/components/page-tab/DirectiveSwitch.tsx +4 -1
  78. package/src/layout/primitives/AdminShell.tsx +13 -1
  79. package/src/lib/version-diff.ts +376 -0
  80. package/src/lib/version-history.ts +192 -0
  81. package/src/styles/responsive.css +281 -0
  82. package/src/styles/theme.css +8 -0
  83. package/src/views/page-editor/EditorCanvas.tsx +34 -7
  84. package/src/views/page-editor/EditorTopBar.tsx +20 -8
  85. package/src/views/page-editor/PageSectionEditor.tsx +162 -43
  86. package/src/views/page-editor/ReviewBar.tsx +76 -0
  87. package/src/views/page-editor/SectionInspector.tsx +2 -1
  88. package/src/views/page-editor/SectionsPanel.tsx +2 -2
  89. package/src/views/page-editor/VersionHistoryPanel.tsx +302 -0
  90. package/src/views/page-editor/sections/SectionRenderer.tsx +53 -12
package/CHANGELOG.md CHANGED
@@ -1,5 +1,116 @@
1
1
  # @actuate-media/cms-admin
2
2
 
3
+ ## 0.94.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 385dd10: Responsive + accessibility layer
8
+
9
+ Ports `design-system/cms-responsive.css` from the page-builder design bundle,
10
+ which the prototype now links and which post-dates the handoff copy in
11
+ `design_handoff_page_builder/` (followup 7: the bundle in hand had one `:focus`
12
+ rule, no `:focus-visible`, no reduced-motion, no contrast preference). None of
13
+ it existed in the admin.
14
+
15
+ The prototype's selectors (`.ni`, `.sec-panel`, `.icbtn`) do not exist here —
16
+ this admin is Tailwind over the same tokens — so the **rules** are ported and
17
+ re-expressed against the real structure, in one stylesheet scoped throughout to
18
+ `.prenta-admin`. The admin renders inside consumer applications; an unscoped
19
+ `@media print` or `:focus-visible` rule would restyle the host's own pages, and
20
+ a test asserts every selector in the file carries the scope.
21
+ - **Visible focus everywhere**, as a floor. Base-layer, so any component with
22
+ its own `focus-visible:ring` still wins and this only fills the gaps.
23
+ - **A skip link** — the first focusable thing in the admin, so a keyboard user's
24
+ first Tab offers a jump past ~20 sidebar items.
25
+ - **`prefers-reduced-motion`** honoured as a media query, not only through the
26
+ Appearance setting and the `motion-safe:` utilities that were written with it.
27
+ - **`prefers-contrast: more`** raises the three tertiary tokens, in both themes,
28
+ and a test asserts it raises rather than lowers — the shipped `--muted` is
29
+ already an AA correction over the handoff's value, and a "more contrast"
30
+ preference that moved it the other way would be worse than ignoring it.
31
+ - **44px touch targets under a coarse pointer**, opt-in per control rather than
32
+ a blanket `button` rule. The admin is full of dense tables where growing every
33
+ button reflows the layout around it; the design bundle carries a curated list
34
+ for the same reason. The switch is the carve-out both hit: its thumb is
35
+ positioned against the track, so the **hit area** grows and the control paints
36
+ at its designed 34×19.
37
+ - **The editor stacks below 900px** — rail above canvas, both full width, drag
38
+ handle gone. The width override needs `!important`: the rail's width is an
39
+ inline style because it is drag-resizable and persisted.
40
+ - **Print** drops the chrome and lets the canvas print at natural size instead
41
+ of as a clipped, transformed thumbnail.
42
+
43
+ Two rules deliberately did not come across. The prototype turns the sidebar into
44
+ a fixed bottom bar under 768px, where `AdminShell` already has a working
45
+ slide-in overlay driven by `matchMedia` — replacing a working navigation pattern
46
+ was not part of finishing the page builder. And the blanket `min-height` list is
47
+ the per-control class described above.
48
+
49
+ Every way this breaks is invisible: a stylesheet that stops being imported, a
50
+ class hook renamed in a component, or a token override placed before the token
51
+ it overrides all render perfectly and simply lack the behaviour. Unit tests pin
52
+ the import order and assert every hook is applied by some component; e2e tests
53
+ measure the result in a browser, because "the CSS says 44px" and "the button is
54
+ 44px" are different claims. The second caught a real gap — the history button's
55
+ conditional className had been missed by the edit that added the class to its
56
+ four neighbours.
57
+
58
+ - 385dd10: Version history: a mode, not a popup
59
+
60
+ The page editor's history was a right-hand dialog with a flat list and a
61
+ Restore button. The handoff's stage 6 asks for something different in kind:
62
+ history is about comparing **renders**, so selecting a version puts it on the
63
+ canvas and marks what changed. A modal covers the thing it exists to let you
64
+ look at; a drawer resizes it. This overlays the rail instead, so the canvas
65
+ does not move a pixel and the only thing that differs between two versions is
66
+ the page itself. An e2e test measures exactly that.
67
+
68
+ **The diff is computed, not stored.** Field-level, from the full snapshots the
69
+ versions API already returns, and the same result drives three surfaces: the
70
+ per-field list in the panel, the dashed outlines on the canvas, and the review
71
+ bar's count. One computation, so they cannot disagree. Field names come from
72
+ the section registry — the same definitions the inspector renders from — so a
73
+ diff reads in the words the editor typed under, and a custom section type
74
+ falls back to its raw key rather than dropping the row.
75
+
76
+ Whether a field changed is decided **structurally**, and the display summary is
77
+ used only to render it. Comparing the summaries is the obvious shortcut and it
78
+ loses real edits: the summary collapses an array to "N items", so retyping a
79
+ stats row while keeping the row count formats identically on both sides and the
80
+ field reports no change — no panel row and no canvas outline, on exactly the
81
+ edit a reviewer opened history to find. Repeaters are descended into row by
82
+ row, labelled through the registry's own nouns ("Stats · Stat 2 · Value"), so a
83
+ collapsed summary never has to stand in for a change it cannot express.
84
+
85
+ Visibility is diffed alongside content: hiding a section is the one structural
86
+ change a design-owned CMS gives an editor, and a history that omitted it would
87
+ be missing the only structural edit there is.
88
+
89
+ **Review mode is its own mode, deliberately.** It and site-preview mode both
90
+ drop the cursor, the hover outline and the section label, which makes one
91
+ shared suppression rule very tempting — and that rule hides the changed-section
92
+ label review mode exists to show. They are separate flags, with a test that
93
+ holds them apart. Editing is frozen throughout: every control that writes reads
94
+ one flag, so no surface can mutate the current page while the canvas shows a
95
+ different one.
96
+
97
+ **Two things found by testing this against a real browser rather than a mock:**
98
+ - Restore was nested inside the diff block, so a version with no computed
99
+ changes — the oldest snapshot held, or a metadata-only save — was reviewable
100
+ and not restorable. That is backwards: the oldest version is the one a
101
+ rollback most often wants. The prototype nests it the same way.
102
+ - Selection and disclosure were separate state, so the review bar's "Back to
103
+ current" could leave the panel describing one version while the canvas
104
+ rendered another, with Restore stranded on a row nobody could see. They are
105
+ the same act and are now one piece of state.
106
+
107
+ **And one the review bots found.** The inline site preview is a second editing
108
+ surface — an iframe of the real site with click-to-edit wired into the same
109
+ section state — and its handlers checked the user's role and nothing else. The
110
+ review lock, which every control in the rail honours, stopped at the iframe
111
+ boundary; so did the read-only lock on Page-Builder-owned pages. Both now read
112
+ the same single gate. A lock that covers only the rail is not a lock.
113
+
3
114
  ## 0.93.0
4
115
 
5
116
  ### Minor Changes
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=version-diff.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-diff.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/lib/version-diff.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,230 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { changedSectionIds, diffVersions, formatValue, groupChangesBySection, } from '../../lib/version-diff.js';
3
+ function section(overrides) {
4
+ return {
5
+ sectionType: 'hero',
6
+ name: 'Hero Banner',
7
+ visible: true,
8
+ content: {},
9
+ settings: {},
10
+ ...overrides,
11
+ };
12
+ }
13
+ describe('formatValue', () => {
14
+ it('renders absent and blank values as an em dash so an empty diff cell is never blank', () => {
15
+ expect(formatValue(null)).toBe('—');
16
+ expect(formatValue(undefined)).toBe('—');
17
+ expect(formatValue('')).toBe('—');
18
+ expect(formatValue(' ')).toBe('—');
19
+ expect(formatValue([])).toBe('—');
20
+ expect(formatValue({})).toBe('—');
21
+ });
22
+ it('summarises structured values rather than dumping them', () => {
23
+ expect(formatValue([{ value: '1' }, { value: '2' }])).toBe('2 items');
24
+ expect(formatValue([{ value: '1' }])).toBe('1 item');
25
+ expect(formatValue({ name: 'hero.jpg', url: '/m/hero.jpg' })).toBe('hero.jpg');
26
+ // No name-ish key: fall back to a shape description, not "[object Object]".
27
+ expect(formatValue({ x: 1, y: 2 })).toBe('2 fields');
28
+ });
29
+ it('reads booleans and numbers as words the panel can show', () => {
30
+ expect(formatValue(true)).toBe('Yes');
31
+ expect(formatValue(false)).toBe('No');
32
+ expect(formatValue(0)).toBe('0');
33
+ });
34
+ });
35
+ describe('diffVersions', () => {
36
+ it('labels content fields through the section registry, not by raw key', () => {
37
+ const before = [section({ id: 's1', content: { heading: 'Old headline' } })];
38
+ const after = [section({ id: 's1', content: { heading: 'New headline' } })];
39
+ const changes = diffVersions(after, before);
40
+ expect(changes).toEqual([
41
+ {
42
+ sectionId: 's1',
43
+ sectionName: 'Hero Banner',
44
+ // 'heading' is `label: 'Heading'` on the hero type in the registry.
45
+ field: 'Heading',
46
+ from: 'Old headline',
47
+ to: 'New headline',
48
+ },
49
+ ]);
50
+ });
51
+ it('falls back to the raw key for a type the registry does not describe', () => {
52
+ const before = [section({ id: 's1', sectionType: 'opal-pricing', content: { tiers: 'a' } })];
53
+ const after = [section({ id: 's1', sectionType: 'opal-pricing', content: { tiers: 'b' } })];
54
+ expect(diffVersions(after, before)[0]?.field).toBe('tiers');
55
+ });
56
+ it('reports a field cleared to nothing — the key exists only in the older snapshot', () => {
57
+ const before = [section({ id: 's1', content: { heading: 'Something' } })];
58
+ const after = [section({ id: 's1', content: {} })];
59
+ expect(diffVersions(after, before)).toEqual([
60
+ { sectionId: 's1', sectionName: 'Hero Banner', field: 'Heading', from: 'Something', to: '—' },
61
+ ]);
62
+ });
63
+ it('reports visibility as a state, because hiding a section is the one structural edit there is', () => {
64
+ const before = [section({ id: 's1', visible: true })];
65
+ const after = [section({ id: 's1', visible: false })];
66
+ expect(diffVersions(after, before)).toEqual([
67
+ {
68
+ sectionId: 's1',
69
+ sectionName: 'Hero Banner',
70
+ field: 'Visibility',
71
+ from: 'Shown',
72
+ to: 'Hidden',
73
+ },
74
+ ]);
75
+ });
76
+ it('reports added and removed sections', () => {
77
+ const before = [section({ id: 's1' })];
78
+ const after = [section({ id: 's1' }), section({ id: 's2', name: 'CTA', sectionType: 'cta' })];
79
+ expect(diffVersions(after, before)).toEqual([
80
+ { sectionId: 's2', sectionName: 'CTA', field: 'Section', from: '—', to: 'Added' },
81
+ ]);
82
+ expect(diffVersions(before, after)).toEqual([
83
+ { sectionId: 's2', sectionName: 'CTA', field: 'Section', from: 'Present', to: 'Removed' },
84
+ ]);
85
+ });
86
+ it('reports nothing when the snapshots match', () => {
87
+ const same = [section({ id: 's1', content: { heading: 'Same' } })];
88
+ expect(diffVersions(same, [section({ id: 's1', content: { heading: 'Same' } })])).toEqual([]);
89
+ });
90
+ it('reports nothing against a missing predecessor', () => {
91
+ // The oldest version we hold is not the moment the page was created, so
92
+ // reporting every field as "added" would state something false.
93
+ expect(diffVersions([section({ id: 's1', content: { heading: 'x' } })], undefined)).toEqual([]);
94
+ expect(diffVersions([section({ id: 's1' })], null)).toEqual([]);
95
+ });
96
+ it('does not report a settings key that was merely absent versus explicitly default', () => {
97
+ // `{}` and `{ backgroundType: undefined }` format identically, so neither
98
+ // produces a row. Without this the first save after a schema addition
99
+ // would light up every section on the canvas.
100
+ const before = [section({ id: 's1', settings: {} })];
101
+ const after = [section({ id: 's1', settings: { backgroundType: undefined } })];
102
+ expect(diffVersions(after, before)).toEqual([]);
103
+ });
104
+ it('labels known settings and reports a real background change', () => {
105
+ const before = [section({ id: 's1', settings: { backgroundType: 'none' } })];
106
+ const after = [section({ id: 's1', settings: { backgroundType: 'solid' } })];
107
+ expect(diffVersions(after, before)).toEqual([
108
+ {
109
+ sectionId: 's1',
110
+ sectionName: 'Hero Banner',
111
+ field: 'Background type',
112
+ from: 'none',
113
+ to: 'solid',
114
+ },
115
+ ]);
116
+ });
117
+ });
118
+ describe('diffVersions — structured fields', () => {
119
+ const stats = (rows) => section({ id: 's1', sectionType: 'stats', name: 'By The Numbers', content: { stats: rows } });
120
+ it('catches an edit that keeps the row count', () => {
121
+ // The bug this exists for: deciding "did it change" from the DISPLAY
122
+ // summary collapses an array to "2 items", so retyping a row while keeping
123
+ // the count formats identically on both sides and the field reports no
124
+ // change — no panel row AND no outline on the canvas, which is exactly the
125
+ // edit a reviewer opened history to find.
126
+ const before = [
127
+ stats([
128
+ { value: '10k', label: 'Users' },
129
+ { value: '99%', label: 'Uptime' },
130
+ ]),
131
+ ];
132
+ const after = [
133
+ stats([
134
+ { value: '20k', label: 'Users' },
135
+ { value: '99%', label: 'Uptime' },
136
+ ]),
137
+ ];
138
+ const changes = diffVersions(after, before);
139
+ expect(changes).toHaveLength(1);
140
+ expect(changes[0]?.from).toBe('10k');
141
+ expect(changes[0]?.to).toBe('20k');
142
+ // Named down to the row and the sub-field, so "which one" has an answer.
143
+ expect(changes[0]?.field).toBe('Stats · Stat 1 · Value');
144
+ expect(changedSectionIds(changes).has('s1')).toBe(true);
145
+ });
146
+ it('never emits a row whose before and after both read as a bare count', () => {
147
+ const before = [
148
+ stats([
149
+ { value: 'a', label: 'x' },
150
+ { value: 'b', label: 'y' },
151
+ ]),
152
+ ];
153
+ const after = [
154
+ stats([
155
+ { value: 'c', label: 'x' },
156
+ { value: 'd', label: 'y' },
157
+ ]),
158
+ ];
159
+ for (const change of diffVersions(after, before)) {
160
+ expect(change.from).not.toBe(change.to);
161
+ expect(change.from).not.toMatch(/^\d+ items?$/);
162
+ }
163
+ });
164
+ it('reports added and removed rows', () => {
165
+ const before = [stats([{ value: '10k', label: 'Users' }])];
166
+ const after = [
167
+ stats([
168
+ { value: '10k', label: 'Users' },
169
+ { value: '99%', label: 'Uptime' },
170
+ ]),
171
+ ];
172
+ expect(diffVersions(after, before)).toEqual([
173
+ expect.objectContaining({ field: 'Stats · Stat 2', from: '—', to: 'Added' }),
174
+ ]);
175
+ expect(diffVersions(before, after)).toEqual([
176
+ expect.objectContaining({ field: 'Stats · Stat 2', from: 'Present', to: 'Removed' }),
177
+ ]);
178
+ });
179
+ it('catches a nested change inside a media object', () => {
180
+ // Same failure one level down: two media objects with the same `name` used
181
+ // to format identically while pointing at different files.
182
+ const media = (url) => ({ name: 'hero.jpg', url });
183
+ const before = [section({ id: 's1', content: { imageUrl: media('/a.jpg') } })];
184
+ const after = [section({ id: 's1', content: { imageUrl: media('/b.jpg') } })];
185
+ expect(diffVersions(after, before)).toHaveLength(1);
186
+ });
187
+ it('treats every spelling of blank as unchanged', () => {
188
+ // Tightening the comparison must not start reporting "— → —".
189
+ for (const [before, after] of [
190
+ ['', null],
191
+ [null, undefined],
192
+ [undefined, ' '],
193
+ [[], undefined],
194
+ [{}, null],
195
+ ]) {
196
+ const changes = diffVersions([section({ id: 's1', content: { heading: after } })], [section({ id: 's1', content: { heading: before } })]);
197
+ expect(changes, `${JSON.stringify(before)} → ${JSON.stringify(after)}`).toEqual([]);
198
+ }
199
+ });
200
+ it('reports an unchanged repeater as no change at all', () => {
201
+ const rows = [{ value: '10k', label: 'Users' }];
202
+ expect(diffVersions([stats(rows)], [stats([...rows.map((r) => ({ ...r }))])])).toEqual([]);
203
+ });
204
+ });
205
+ describe('groupChangesBySection', () => {
206
+ it('groups by section in first-seen order', () => {
207
+ const before = [
208
+ section({ id: 's1', content: { heading: 'a', body: 'a' } }),
209
+ section({ id: 's2', name: 'CTA', sectionType: 'cta', content: { heading: 'a' } }),
210
+ ];
211
+ const after = [
212
+ section({ id: 's1', content: { heading: 'b', body: 'b' } }),
213
+ section({ id: 's2', name: 'CTA', sectionType: 'cta', content: { heading: 'b' } }),
214
+ ];
215
+ const groups = groupChangesBySection(diffVersions(after, before));
216
+ expect(groups.map((g) => g.sectionId)).toEqual(['s1', 's2']);
217
+ expect(groups[0]?.changes).toHaveLength(2);
218
+ expect(groups[1]?.changes).toHaveLength(1);
219
+ });
220
+ });
221
+ describe('changedSectionIds', () => {
222
+ it('collapses several changes on one section to a single outline', () => {
223
+ const before = [section({ id: 's1', content: { heading: 'a', body: 'a' } })];
224
+ const after = [section({ id: 's1', content: { heading: 'b', body: 'b' } })];
225
+ const ids = changedSectionIds(diffVersions(after, before));
226
+ expect(ids.size).toBe(1);
227
+ expect(ids.has('s1')).toBe(true);
228
+ });
229
+ });
230
+ //# sourceMappingURL=version-diff.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-diff.test.js","sourceRoot":"","sources":["../../../src/__tests__/lib/version-diff.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=version-history.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-history.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/lib/version-history.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,125 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { dayLabel, groupVersionsByDay, initials, toPageVersion, versionKind, } from '../../lib/version-history.js';
3
+ const NOW = new Date('2026-08-02T17:00:00Z');
4
+ function version(id, createdAt) {
5
+ return {
6
+ id,
7
+ changeType: 'UPDATE',
8
+ createdAt,
9
+ author: { id: 'u1', name: 'Sarah Mills' },
10
+ sections: [],
11
+ };
12
+ }
13
+ describe('toPageVersion', () => {
14
+ it('lifts the section snapshot out of the version payload', () => {
15
+ const mapped = toPageVersion({
16
+ id: 'v1',
17
+ changeType: 'PUBLISH',
18
+ createdAt: '2026-08-02T12:00:00Z',
19
+ data: {
20
+ sections: [
21
+ { id: 's1', sectionType: 'hero', name: 'Hero', visible: true, content: {}, settings: {} },
22
+ ],
23
+ },
24
+ changedBy: { id: 'u1', name: 'Sarah Mills', email: 's@example.com' },
25
+ });
26
+ expect(mapped.sections).toHaveLength(1);
27
+ expect(mapped.sections[0]?.id).toBe('s1');
28
+ expect(mapped.changeType).toBe('PUBLISH');
29
+ expect(mapped.author.name).toBe('Sarah Mills');
30
+ });
31
+ it('preserves an unmanaged section rather than dropping it from the snapshot', () => {
32
+ // Coercing a snapshot more strictly than the live document reads would
33
+ // make an externally-managed section look deleted in every version.
34
+ const mapped = toPageVersion({
35
+ id: 'v1',
36
+ changeType: 'UPDATE',
37
+ createdAt: '2026-08-02T12:00:00Z',
38
+ data: {
39
+ sections: [
40
+ { id: 's1', sectionType: 'opal-pricing', name: 'Pricing', visible: true, content: {} },
41
+ ],
42
+ },
43
+ });
44
+ expect(mapped.sections).toHaveLength(1);
45
+ expect(mapped.sections[0]?.sectionType).toBe('opal-pricing');
46
+ });
47
+ it('falls back through name → email → a real word for the avatar', () => {
48
+ expect(toPageVersion({ id: 'v', changeType: 'UPDATE', createdAt: '', data: null }).author.name).toBe('Unknown');
49
+ expect(toPageVersion({
50
+ id: 'v',
51
+ changeType: 'UPDATE',
52
+ createdAt: '',
53
+ data: null,
54
+ changedBy: { id: 'u', name: ' ', email: 'a@b.com' },
55
+ }).author.name).toBe('a@b.com');
56
+ });
57
+ it('yields no sections for a version whose payload has none', () => {
58
+ expect(toPageVersion({ id: 'v', changeType: 'DELETE', createdAt: '', data: {} }).sections).toEqual([]);
59
+ });
60
+ });
61
+ describe('versionKind', () => {
62
+ it('marks a published snapshot as the one a visitor could have seen', () => {
63
+ expect(versionKind('PUBLISH')).toEqual({ label: 'Published', tone: 'live' });
64
+ });
65
+ it('calls UPDATE "Saved", never "Draft"', () => {
66
+ // Every row written before cms-core started recording PUBLISH is an UPDATE
67
+ // whether or not it went live, so "Draft" would be false for those.
68
+ expect(versionKind('UPDATE').label).toBe('Saved');
69
+ });
70
+ it('distinguishes a rollback from authored content', () => {
71
+ expect(versionKind('RESTORE').label).toBe('Restored');
72
+ });
73
+ it('degrades to a neutral label for an unrecognised type', () => {
74
+ expect(versionKind('SOMETHING_NEW').label).toBe('Saved');
75
+ });
76
+ });
77
+ describe('dayLabel', () => {
78
+ it('names today and yesterday', () => {
79
+ expect(dayLabel('2026-08-02T09:00:00', NOW)).toBe('Today');
80
+ expect(dayLabel('2026-08-01T23:59:00', NOW)).toBe('Yesterday');
81
+ });
82
+ it('drops the year within the current one and keeps it outside', () => {
83
+ expect(dayLabel('2026-07-27T09:00:00', NOW)).not.toMatch(/2026/);
84
+ expect(dayLabel('2025-07-27T09:00:00', NOW)).toMatch(/2025/);
85
+ });
86
+ it('does not throw on an unparseable timestamp', () => {
87
+ expect(dayLabel('not-a-date', NOW)).toBe('Unknown date');
88
+ });
89
+ });
90
+ describe('groupVersionsByDay', () => {
91
+ it('groups consecutive runs and keeps newest-first order', () => {
92
+ const groups = groupVersionsByDay([
93
+ version('v5', '2026-08-02T16:12:00'),
94
+ version('v4', '2026-08-02T11:03:00'),
95
+ version('v3', '2026-08-01T17:47:00'),
96
+ version('v1', '2026-07-27T09:30:00'),
97
+ ], NOW);
98
+ expect(groups.map((g) => g.label)).toEqual(['Today', 'Yesterday', expect.stringMatching(/Jul/)]);
99
+ expect(groups[0]?.versions.map((v) => v.id)).toEqual(['v5', 'v4']);
100
+ });
101
+ it('does not merge two non-adjacent runs that share a label', () => {
102
+ // A Map keyed on the label would silently reorder an unsorted page into
103
+ // something that reads as chronological but is not.
104
+ const groups = groupVersionsByDay([
105
+ version('a', '2026-08-02T16:00:00'),
106
+ version('b', '2026-08-01T16:00:00'),
107
+ version('c', '2026-08-02T09:00:00'),
108
+ ], NOW);
109
+ expect(groups.map((g) => g.label)).toEqual(['Today', 'Yesterday', 'Today']);
110
+ });
111
+ it('returns nothing for an empty list', () => {
112
+ expect(groupVersionsByDay([], NOW)).toEqual([]);
113
+ });
114
+ });
115
+ describe('initials', () => {
116
+ it('takes first and last for a full name, two letters for one word', () => {
117
+ expect(initials('Sarah Mills')).toBe('SM');
118
+ expect(initials('Sarah Jane Mills')).toBe('SM');
119
+ expect(initials('sarah')).toBe('SA');
120
+ });
121
+ it('never renders an empty monogram', () => {
122
+ expect(initials(' ')).toBe('?');
123
+ });
124
+ });
125
+ //# sourceMappingURL=version-history.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-history.test.js","sourceRoot":"","sources":["../../../src/__tests__/lib/version-history.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=responsive-layer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"responsive-layer.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/styles/responsive-layer.test.ts"],"names":[],"mappings":""}