@morphika/andami 0.5.3 → 0.5.4

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.
@@ -1,8 +1,11 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useCallback } from "react";
3
+ import { useEffect, useCallback, useRef } from "react";
4
4
  import { assetUrl } from "../../lib/assets";
5
5
 
6
+ const FOCUSABLE_SELECTOR =
7
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), iframe, [tabindex]:not([tabindex="-1"])';
8
+
6
9
  // ============================================
7
10
  // Helpers
8
11
  // ============================================
@@ -42,22 +45,51 @@ export default function NavContentLightbox({
42
45
  contentUrl,
43
46
  onClose,
44
47
  }: NavContentLightboxProps) {
45
- // Close on Escape
48
+ const containerRef = useRef<HTMLDivElement | null>(null);
49
+ const closeButtonRef = useRef<HTMLButtonElement | null>(null);
50
+
51
+ // Focus trap: cycle Tab between focusable children, close on Escape
46
52
  const handleKey = useCallback(
47
53
  (e: KeyboardEvent) => {
48
- if (e.key === "Escape") onClose();
54
+ if (e.key === "Escape") {
55
+ onClose();
56
+ return;
57
+ }
58
+ if (e.key !== "Tab" || !containerRef.current) return;
59
+
60
+ const focusables = containerRef.current.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
61
+ if (focusables.length === 0) return;
62
+
63
+ const first = focusables[0];
64
+ const last = focusables[focusables.length - 1];
65
+ const active = document.activeElement as HTMLElement | null;
66
+
67
+ if (e.shiftKey && active === first) {
68
+ e.preventDefault();
69
+ last.focus();
70
+ } else if (!e.shiftKey && active === last) {
71
+ e.preventDefault();
72
+ first.focus();
73
+ }
49
74
  },
50
75
  [onClose]
51
76
  );
52
77
 
53
78
  useEffect(() => {
79
+ // Remember where focus came from so we can restore it on close
80
+ const previouslyFocused = document.activeElement as HTMLElement | null;
81
+
54
82
  window.addEventListener("keydown", handleKey);
55
- // Lock body scroll
56
83
  const prev = document.body.style.overflow;
57
84
  document.body.style.overflow = "hidden";
85
+
86
+ // Move focus into the lightbox (close button is always present)
87
+ closeButtonRef.current?.focus();
88
+
58
89
  return () => {
59
90
  window.removeEventListener("keydown", handleKey);
60
91
  document.body.style.overflow = prev;
92
+ previouslyFocused?.focus?.();
61
93
  };
62
94
  }, [handleKey]);
63
95
 
@@ -78,11 +110,16 @@ export default function NavContentLightbox({
78
110
 
79
111
  return (
80
112
  <div
113
+ ref={containerRef}
114
+ role="dialog"
115
+ aria-modal="true"
116
+ aria-label="Media lightbox"
81
117
  className="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 backdrop-blur-sm"
82
118
  onClick={onClose}
83
119
  >
84
120
  {/* Close button */}
85
121
  <button
122
+ ref={closeButtonRef}
86
123
  onClick={onClose}
87
124
  className="absolute top-5 right-5 w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors z-10"
88
125
  aria-label="Close lightbox"
@@ -60,7 +60,6 @@ export function createBlockActions(set: StoreSet, get: StoreGet): BlockSliceActi
60
60
  },
61
61
 
62
62
  deleteBlock: (blockKey: string): void => {
63
- get()._pushSnapshot();
64
63
  const filterBlocks = (cols: import("../../lib/sanity/types").SectionColumn[]) =>
65
64
  cols.map((c) => ({ ...c, blocks: c.blocks.filter((b) => b._key !== blockKey) }));
66
65
 
@@ -88,12 +87,13 @@ export function createBlockActions(set: StoreSet, get: StoreGet): BlockSliceActi
88
87
  rows: finalRows,
89
88
  selectedBlockKey: state.selectedBlockKey === blockKey ? null : state.selectedBlockKey,
90
89
  isDirty: true,
90
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
91
+ _future: [],
91
92
  };
92
93
  });
93
94
  },
94
95
 
95
96
  duplicateBlock: (blockKey: string): void => {
96
- get()._pushSnapshot();
97
97
  set((state) => {
98
98
  const newKey = generateKey();
99
99
  const dupInColumns = (cols: import("../../lib/sanity/types").SectionColumn[]) =>
@@ -125,7 +125,13 @@ export function createBlockActions(set: StoreSet, get: StoreGet): BlockSliceActi
125
125
  }
126
126
  return item;
127
127
  });
128
- return { rows, selectedBlockKey: newKey, isDirty: true };
128
+ return {
129
+ rows,
130
+ selectedBlockKey: newKey,
131
+ isDirty: true,
132
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
133
+ _future: [],
134
+ };
129
135
  });
130
136
  },
131
137
 
@@ -148,7 +154,6 @@ export function createBlockActions(set: StoreSet, get: StoreGet): BlockSliceActi
148
154
  },
149
155
 
150
156
  reorderBlocks: (sectionKey: string, columnKey: string, fromIndex: number, toIndex: number): void => {
151
- get()._pushSnapshot();
152
157
  set((state) => {
153
158
  const path = findSectionPath(state.rows, sectionKey);
154
159
  if (!path) return state;
@@ -162,7 +167,12 @@ export function createBlockActions(set: StoreSet, get: StoreGet): BlockSliceActi
162
167
  return { ...c, blocks };
163
168
  }),
164
169
  }));
165
- return { rows, isDirty: true };
170
+ return {
171
+ rows,
172
+ isDirty: true,
173
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
174
+ _future: [],
175
+ };
166
176
  });
167
177
  },
168
178
 
@@ -19,6 +19,7 @@ import type {
19
19
  import { isCoverSection } from "../../lib/sanity/types";
20
20
  import { generateKey } from "./utils";
21
21
  import { createDefaultCoverSection, createDefaultCoverRow } from "./defaults";
22
+ import { pushSnapshot } from "./history";
22
23
 
23
24
  type StoreSet = (
24
25
  partial: Partial<BuilderState> | ((state: BuilderState) => Partial<BuilderState>)
@@ -82,7 +83,6 @@ function updateCoverInRows(
82
83
  export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActions {
83
84
  return {
84
85
  addCoverSection: (afterRowKey?: string | null): void => {
85
- get()._pushSnapshot();
86
86
  const section = createDefaultCoverSection();
87
87
  set((state) => {
88
88
  const rows = [...state.rows];
@@ -96,12 +96,17 @@ export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActi
96
96
  } else {
97
97
  rows.push(section);
98
98
  }
99
- return { rows, isDirty: true, selectedRowKey: section._key };
99
+ return {
100
+ rows,
101
+ isDirty: true,
102
+ selectedRowKey: section._key,
103
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
104
+ _future: [],
105
+ };
100
106
  });
101
107
  },
102
108
 
103
109
  addCoverRow: (sectionKey: string): void => {
104
- get()._pushSnapshot();
105
110
  set((state) => ({
106
111
  rows: updateCoverInRows(state.rows, sectionKey, (section) => {
107
112
  if (section.cover_rows.length >= 5) return section;
@@ -137,11 +142,12 @@ export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActi
137
142
  return { ...section, cover_rows: newRows };
138
143
  }),
139
144
  isDirty: true,
145
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
146
+ _future: [],
140
147
  }));
141
148
  },
142
149
 
143
150
  removeCoverRow: (sectionKey: string, rowKey: string): void => {
144
- get()._pushSnapshot();
145
151
  set((state) => ({
146
152
  rows: updateCoverInRows(state.rows, sectionKey, (section) => {
147
153
  if (section.cover_rows.length <= 1) return section;
@@ -183,6 +189,8 @@ export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActi
183
189
  };
184
190
  }),
185
191
  isDirty: true,
192
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
193
+ _future: [],
186
194
  }));
187
195
  },
188
196
 
@@ -248,13 +256,14 @@ export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActi
248
256
  "nav_color"
249
257
  >>
250
258
  ): void => {
251
- get()._pushSnapshot();
252
259
  set((state) => ({
253
260
  rows: updateCoverInRows(state.rows, sectionKey, (section) => ({
254
261
  ...section,
255
262
  ...fields,
256
263
  })),
257
264
  isDirty: true,
265
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
266
+ _future: [],
258
267
  }));
259
268
  },
260
269
 
@@ -275,13 +284,14 @@ export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActi
275
284
  sectionKey: string,
276
285
  height: CoverSection["height"]
277
286
  ): void => {
278
- get()._pushSnapshot();
279
287
  set((state) => ({
280
288
  rows: updateCoverInRows(state.rows, sectionKey, (section) => ({
281
289
  ...section,
282
290
  height,
283
291
  })),
284
292
  isDirty: true,
293
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
294
+ _future: [],
285
295
  }));
286
296
  },
287
297
  };
@@ -41,6 +41,7 @@ import {
41
41
  moveColumnBetweenSectionsInState,
42
42
  swapColumnsBetweenSectionsInState,
43
43
  } from "./store-helpers";
44
+ import { pushSnapshot } from "./history";
44
45
 
45
46
  type StoreSet = (
46
47
  partial: Partial<BuilderState> | ((state: BuilderState) => Partial<BuilderState>)
@@ -60,7 +61,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
60
61
  * the data migration in Session 165.
61
62
  */
62
63
  addSection: (blockType: SectionBlockType, afterRowKey?: string | null): void => {
63
- get()._pushSnapshot();
64
64
  const block = createDefaultBlock(blockType);
65
65
  const gridColumns = 12;
66
66
 
@@ -99,22 +99,31 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
99
99
  } else {
100
100
  rows.push(newSection);
101
101
  }
102
- return { rows, isDirty: true, selectedRowKey: newSection._key };
102
+ return {
103
+ rows,
104
+ isDirty: true,
105
+ selectedRowKey: newSection._key,
106
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
107
+ _future: [],
108
+ };
103
109
  });
104
110
  },
105
111
 
106
112
  reorderRows: (fromIndex: number, toIndex: number): void => {
107
- get()._pushSnapshot();
108
113
  set((state) => {
109
114
  const rows = [...state.rows];
110
115
  const [moved] = rows.splice(fromIndex, 1);
111
116
  rows.splice(toIndex, 0, moved);
112
- return { rows, isDirty: true };
117
+ return {
118
+ rows,
119
+ isDirty: true,
120
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
121
+ _future: [],
122
+ };
113
123
  });
114
124
  },
115
125
 
116
126
  deleteSection: (sectionKey: string): void => {
117
- get()._pushSnapshot();
118
127
  set((state) => ({
119
128
  rows: state.rows.filter((item) => item._key !== sectionKey),
120
129
  selectedRowKey:
@@ -124,11 +133,12 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
124
133
  selectedBlockKey:
125
134
  state.selectedRowKey === sectionKey ? null : state.selectedBlockKey,
126
135
  isDirty: true,
136
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
137
+ _future: [],
127
138
  }));
128
139
  },
129
140
 
130
141
  duplicateSection: (sectionKey: string): void => {
131
- get()._pushSnapshot();
132
142
  set((state) => {
133
143
  const idx = state.rows.findIndex((item) => item._key === sectionKey);
134
144
  if (idx === -1) return state;
@@ -173,6 +183,8 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
173
183
  selectedColumnKey: null,
174
184
  selectedBlockKey: null,
175
185
  isDirty: true,
186
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
187
+ _future: [],
176
188
  };
177
189
  });
178
190
  },
@@ -180,15 +192,19 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
180
192
  // ---- V2 Section operations ----
181
193
 
182
194
  addSectionV2: (preset: SectionV2Preset, afterRowKey?: string | null): void => {
183
- get()._pushSnapshot();
184
-
185
- const result = addSectionV2InState(get().rows, preset, afterRowKey);
186
- set({ rows: result.rows, isDirty: true, selectedRowKey: result.newSectionKey });
195
+ set((state) => {
196
+ const result = addSectionV2InState(state.rows, preset, afterRowKey);
197
+ return {
198
+ rows: result.rows,
199
+ isDirty: true,
200
+ selectedRowKey: result.newSectionKey,
201
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
202
+ _future: [],
203
+ };
204
+ });
187
205
  },
188
206
 
189
207
  addColumnV2: (sectionKey: string, gridRow: number, gridColumn: number, span: number): void => {
190
- get()._pushSnapshot();
191
-
192
208
  set((state) => {
193
209
  const path = findSectionPath(state.rows, sectionKey);
194
210
  if (!path) return state;
@@ -216,13 +232,16 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
216
232
  settings: { ...section.settings, preset: newPreset },
217
233
  };
218
234
  });
219
- return { rows, isDirty: true };
235
+ return {
236
+ rows,
237
+ isDirty: true,
238
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
239
+ _future: [],
240
+ };
220
241
  });
221
242
  },
222
243
 
223
244
  deleteColumnV2: (sectionKey: string, columnKey: string): void => {
224
- get()._pushSnapshot();
225
-
226
245
  set((state) => {
227
246
  const path = findSectionPath(state.rows, sectionKey);
228
247
  if (!path) return state;
@@ -251,13 +270,13 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
251
270
  selectedColumnKey:
252
271
  state.selectedColumnKey === columnKey ? null : state.selectedColumnKey,
253
272
  isDirty: true,
273
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
274
+ _future: [],
254
275
  };
255
276
  });
256
277
  },
257
278
 
258
279
  resizeColumnV2: (sectionKey: string, columnKey: string, newSpan: number): void => {
259
- get()._pushSnapshot();
260
-
261
280
  set((state) => {
262
281
  const path = findSectionPath(state.rows, sectionKey);
263
282
  if (!path) return state;
@@ -282,7 +301,12 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
282
301
  settings: { ...section.settings, preset: newPreset },
283
302
  };
284
303
  });
285
- return { rows, isDirty: true };
304
+ return {
305
+ rows,
306
+ isDirty: true,
307
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
308
+ _future: [],
309
+ };
286
310
  });
287
311
  },
288
312
 
@@ -295,8 +319,12 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
295
319
  );
296
320
  if (!result) return; // Blocked by cascade or section not found
297
321
 
298
- get()._pushSnapshot();
299
- set({ rows: result.rows, isDirty: true });
322
+ set((state) => ({
323
+ rows: result.rows,
324
+ isDirty: true,
325
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
326
+ _future: [],
327
+ }));
300
328
  },
301
329
 
302
330
  moveColumnV2: (sectionKey: string, columnKey: string, targetRow: number, targetColumn: number): void => {
@@ -309,8 +337,12 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
309
337
  );
310
338
  if (!result) return;
311
339
 
312
- get()._pushSnapshot();
313
- set({ rows: result.rows, isDirty: true });
340
+ set((state) => ({
341
+ rows: result.rows,
342
+ isDirty: true,
343
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
344
+ _future: [],
345
+ }));
314
346
  },
315
347
 
316
348
  swapColumnV2: (sectionKey: string, draggedKey: string, targetKey: string): void => {
@@ -322,8 +354,12 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
322
354
  );
323
355
  if (!result) return;
324
356
 
325
- get()._pushSnapshot();
326
- set({ rows: result.rows, isDirty: true });
357
+ set((state) => ({
358
+ rows: result.rows,
359
+ isDirty: true,
360
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
361
+ _future: [],
362
+ }));
327
363
  },
328
364
 
329
365
  moveColumnToGapV2: (sectionKey: string, columnKey: string, targetRow: number, targetColumn: number, targetSpan: number): void => {
@@ -337,8 +373,12 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
337
373
  );
338
374
  if (!result) return;
339
375
 
340
- get()._pushSnapshot();
341
- set({ rows: result.rows, isDirty: true });
376
+ set((state) => ({
377
+ rows: result.rows,
378
+ isDirty: true,
379
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
380
+ _future: [],
381
+ }));
342
382
  },
343
383
 
344
384
  moveColumnBetweenSections: (
@@ -360,16 +400,17 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
360
400
  );
361
401
  if (!result) return;
362
402
 
363
- get()._pushSnapshot();
364
403
  // Clear any selection referencing the moved column in its old section —
365
404
  // the column key remains but the "selected section" context has changed.
366
- set({
405
+ set((state) => ({
367
406
  rows: result.rows,
368
407
  isDirty: true,
369
408
  selectedColumnKey: columnKey,
370
409
  selectedRowKey: targetSectionKey,
371
410
  selectedBlockKey: null,
372
- });
411
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
412
+ _future: [],
413
+ }));
373
414
  },
374
415
 
375
416
  swapColumnsBetweenSections: (
@@ -387,23 +428,22 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
387
428
  );
388
429
  if (!result) return;
389
430
 
390
- get()._pushSnapshot();
391
431
  // After swap: the dragged column now lives in the target section.
392
432
  // Select it there (match mental model: "I just put this column here").
393
- set({
433
+ set((state) => ({
394
434
  rows: result.rows,
395
435
  isDirty: true,
396
436
  selectedColumnKey: sourceColumnKey,
397
437
  selectedRowKey: targetSectionKey,
398
438
  selectedBlockKey: null,
399
- });
439
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
440
+ _future: [],
441
+ }));
400
442
  },
401
443
 
402
444
  applyPresetV2: (sectionKey: string, preset: SectionV2Preset): void => {
403
445
  if (preset === "custom") return;
404
446
 
405
- get()._pushSnapshot();
406
-
407
447
  set((state) => {
408
448
  const path = findSectionPath(state.rows, sectionKey);
409
449
  if (!path) return state;
@@ -424,7 +464,12 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
424
464
  settings: { ...section.settings, preset },
425
465
  };
426
466
  });
427
- return { rows, isDirty: true };
467
+ return {
468
+ rows,
469
+ isDirty: true,
470
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
471
+ _future: [],
472
+ };
428
473
  });
429
474
  },
430
475
 
@@ -492,7 +537,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
492
537
  },
493
538
 
494
539
  addBlockV2: (sectionKey: string, columnKey: string, blockType: BlockType, insertIndex?: number): void => {
495
- get()._pushSnapshot();
496
540
  const newBlock = createDefaultBlock(blockType);
497
541
 
498
542
  set((state) => {
@@ -514,7 +558,13 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
514
558
  return { ...col, blocks: [...col.blocks, newBlock] };
515
559
  }),
516
560
  }));
517
- return { rows, selectedBlockKey: newBlock._key, isDirty: true };
561
+ return {
562
+ rows,
563
+ selectedBlockKey: newBlock._key,
564
+ isDirty: true,
565
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
566
+ _future: [],
567
+ };
518
568
  });
519
569
  },
520
570
 
@@ -570,7 +620,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
570
620
  title: string,
571
621
  afterRowKey?: string | null
572
622
  ) => {
573
- get()._pushSnapshot();
574
623
  const instance: CustomSectionInstance = {
575
624
  _type: "customSectionInstance",
576
625
  _key: generateKey(),
@@ -591,7 +640,13 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
591
640
  } else {
592
641
  rows.push(instance);
593
642
  }
594
- return { rows, isDirty: true, selectedRowKey: instance._key };
643
+ return {
644
+ rows,
645
+ isDirty: true,
646
+ selectedRowKey: instance._key,
647
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
648
+ _future: [],
649
+ };
595
650
  });
596
651
  },
597
652
 
@@ -599,7 +654,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
599
654
  instanceKey: string,
600
655
  sectionData: PageSectionV2
601
656
  ) => {
602
- get()._pushSnapshot();
603
657
  set((state) => ({
604
658
  rows: state.rows.map((item) =>
605
659
  item._key === instanceKey
@@ -607,6 +661,8 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
607
661
  : item
608
662
  ),
609
663
  isDirty: true,
664
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
665
+ _future: [],
610
666
  }));
611
667
  },
612
668
 
@@ -632,7 +688,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
632
688
  instanceKey: string,
633
689
  updates: Partial<SectionV2Settings>
634
690
  ) => {
635
- get()._pushSnapshot();
636
691
  set((state) => ({
637
692
  rows: state.rows.map((item) =>
638
693
  item._key === instanceKey && isCustomSectionInstance(item)
@@ -646,6 +701,8 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
646
701
  : item
647
702
  ),
648
703
  isDirty: true,
704
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
705
+ _future: [],
649
706
  }));
650
707
  },
651
708
 
@@ -661,13 +718,19 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
661
718
  // ---- Parallax Group operations (Session 123) ----
662
719
 
663
720
  addParallaxGroup: (afterRowKey?: string | null): void => {
664
- get()._pushSnapshot();
665
- const result = addParallaxGroupInState(get().rows, afterRowKey);
666
- set({ rows: result.rows, isDirty: true, selectedRowKey: result.newGroupKey });
721
+ set((state) => {
722
+ const result = addParallaxGroupInState(state.rows, afterRowKey);
723
+ return {
724
+ rows: result.rows,
725
+ isDirty: true,
726
+ selectedRowKey: result.newGroupKey,
727
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
728
+ _future: [],
729
+ };
730
+ });
667
731
  },
668
732
 
669
733
  addParallaxSlide: (groupKey: string): void => {
670
- get()._pushSnapshot();
671
734
  set((state) => ({
672
735
  rows: state.rows.map((item) => {
673
736
  if (item._key !== groupKey || !isParallaxGroup(item)) return item;
@@ -676,11 +739,12 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
676
739
  return { ...group, slides: [...group.slides, newSlide] } as ContentItem;
677
740
  }),
678
741
  isDirty: true,
742
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
743
+ _future: [],
679
744
  }));
680
745
  },
681
746
 
682
747
  removeParallaxSlide: (groupKey: string, slideKey: string): void => {
683
- get()._pushSnapshot();
684
748
  set((state) => {
685
749
  let changed = false;
686
750
  const rows = state.rows.map((item) => {
@@ -694,7 +758,14 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
694
758
  slides: group.slides.filter((s) => s._key !== slideKey),
695
759
  } as ContentItem;
696
760
  });
697
- return changed ? { rows, isDirty: true } : {};
761
+ return changed
762
+ ? {
763
+ rows,
764
+ isDirty: true,
765
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
766
+ _future: [],
767
+ }
768
+ : {};
698
769
  });
699
770
  },
700
771
 
@@ -703,7 +774,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
703
774
  slideKey: string,
704
775
  direction: "up" | "down"
705
776
  ) => {
706
- get()._pushSnapshot();
707
777
  set((state) => {
708
778
  let changed = false;
709
779
  const rows = state.rows.map((item) => {
@@ -718,7 +788,14 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
718
788
  changed = true;
719
789
  return { ...group, slides } as ContentItem;
720
790
  });
721
- return changed ? { rows, isDirty: true } : {};
791
+ return changed
792
+ ? {
793
+ rows,
794
+ isDirty: true,
795
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
796
+ _future: [],
797
+ }
798
+ : {};
722
799
  });
723
800
  },
724
801
 
@@ -727,7 +804,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
727
804
  slideKey: string,
728
805
  fields: Partial<ParallaxSlideV2>
729
806
  ) => {
730
- get()._pushSnapshot();
731
807
  set((state) => ({
732
808
  rows: state.rows.map((item) => {
733
809
  if (item._key !== groupKey || !isParallaxGroup(item)) return item;
@@ -740,6 +816,8 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
740
816
  } as ContentItem;
741
817
  }),
742
818
  isDirty: true,
819
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
820
+ _future: [],
743
821
  }));
744
822
  },
745
823
 
@@ -749,13 +827,14 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
749
827
  Pick<ParallaxGroup, "transition_effect" | "snap_enabled" | "parallax_intensity">
750
828
  >
751
829
  ) => {
752
- get()._pushSnapshot();
753
830
  set((state) => ({
754
831
  rows: state.rows.map((item) => {
755
832
  if (item._key !== groupKey || !isParallaxGroup(item)) return item;
756
833
  return { ...item, ...fields } as ContentItem;
757
834
  }),
758
835
  isDirty: true,
836
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
837
+ _future: [],
759
838
  }));
760
839
  },
761
840
  };
package/lib/version.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * Exposed as a plain constant so it can be imported without reading
7
7
  * package.json at runtime.
8
8
  */
9
- export const ANDAMI_VERSION = "0.5.3";
9
+ export const ANDAMI_VERSION = "0.5.4";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphika/andami",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Visual Page Builder — core library. A reusable website builder with visual editing, CMS integration, and asset management.",
5
5
  "type": "module",
6
6
  "license": "MIT",