@morphika/andami 0.5.2 → 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.
Files changed (68) hide show
  1. package/README.md +27 -2
  2. package/app/admin/layout.tsx +26 -14
  3. package/app/admin/pages/[slug]/page.tsx +39 -22
  4. package/app/admin/pages/page.tsx +13 -8
  5. package/app/admin/projects/page.tsx +17 -8
  6. package/app/api/admin/assets/register/route.ts +51 -14
  7. package/app/api/admin/assets/registry/route.ts +4 -1
  8. package/app/api/admin/assets/relink/confirm/route.ts +4 -1
  9. package/app/api/admin/assets/relink/route.ts +4 -1
  10. package/app/api/admin/assets/scan/route.ts +4 -1
  11. package/app/api/admin/backups/restore-data/route.ts +4 -1
  12. package/app/api/admin/r2/connect/route.ts +4 -1
  13. package/app/api/admin/r2/delete/route.ts +4 -1
  14. package/app/api/admin/r2/rename/route.ts +4 -1
  15. package/app/api/admin/r2/upload-url/route.ts +4 -1
  16. package/app/api/admin/revalidate/route.ts +4 -1
  17. package/app/api/admin/storage/switch/route.ts +4 -1
  18. package/app/api/custom-sections/[id]/route.ts +5 -6
  19. package/components/admin/PublishToggle.tsx +2 -2
  20. package/components/admin/nav-builder/NavGridItem.tsx +4 -2
  21. package/components/admin/nav-builder/NavSettingsFields.tsx +10 -6
  22. package/components/admin/styles/ColorsEditor.tsx +7 -6
  23. package/components/admin/styles/FontsEditor.tsx +3 -1
  24. package/components/blocks/CoverSectionRenderer.tsx +7 -1
  25. package/components/blocks/SectionV2Renderer.tsx +8 -1
  26. package/components/builder/BubbleIcons.tsx +14 -0
  27. package/components/builder/CanvasMinimap.tsx +66 -49
  28. package/components/builder/CanvasToolbar.tsx +31 -41
  29. package/components/builder/SectionEditorBar.tsx +4 -2
  30. package/components/builder/SectionTypePicker.tsx +4 -2
  31. package/components/builder/SectionV2Column.tsx +13 -1
  32. package/components/builder/SettingsPanel.tsx +21 -17
  33. package/components/builder/SortableBlock.tsx +2 -2
  34. package/components/builder/SortableRow.tsx +6 -9
  35. package/components/builder/VirtualAssetGrid.tsx +8 -2
  36. package/components/builder/asset-browser/R2BrowserContent.tsx +8 -4
  37. package/components/builder/color-picker/EyedropperButton.tsx +7 -6
  38. package/components/builder/color-picker/SwatchBar.tsx +11 -6
  39. package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
  40. package/components/builder/editors/ImageGridBlockEditor.tsx +4 -2
  41. package/components/builder/editors/MarqueeBlockEditor.tsx +3 -2
  42. package/components/builder/editors/ProjectGridEditor.tsx +12 -7
  43. package/components/builder/editors/SpacerBlockEditor.tsx +25 -23
  44. package/components/builder/editors/TextBlockEditor.tsx +19 -14
  45. package/components/builder/editors/shared.tsx +4 -2
  46. package/components/builder/live-preview/LiveImagePreview.tsx +3 -1
  47. package/components/builder/live-preview/ProjectCardWrapper.tsx +3 -1
  48. package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
  49. package/components/builder/live-preview/shared.tsx +5 -2
  50. package/components/builder/settings-panel/BlockLayoutTab.tsx +4 -2
  51. package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
  52. package/components/builder/settings-panel/CoverSectionSettings.tsx +4 -2
  53. package/components/builder/settings-panel/SectionV2Settings.tsx +13 -8
  54. package/components/builder/settings-panel/index.ts +1 -0
  55. package/components/ui/NavContentLightbox.tsx +41 -4
  56. package/lib/builder/serializer/normalizers.ts +14 -0
  57. package/lib/builder/serializer/serializers.ts +27 -0
  58. package/lib/builder/store-blocks.ts +15 -5
  59. package/lib/builder/store-cover.ts +16 -6
  60. package/lib/builder/store-sections.ts +151 -51
  61. package/lib/builder/types-slices.ts +14 -0
  62. package/lib/sanity/queries.ts +48 -0
  63. package/lib/sanity/types.ts +14 -0
  64. package/lib/version.ts +1 -1
  65. package/package.json +7 -5
  66. package/sanity/schemas/objects/coverSection.ts +32 -0
  67. package/sanity/schemas/objects/parallaxSlide.ts +32 -0
  68. package/sanity/schemas/pageSectionV2.ts +32 -0
@@ -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
 
@@ -541,6 +591,27 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
541
591
  });
542
592
  },
543
593
 
594
+ // Update desktop-level column layout fields (spacing/background/border).
595
+ // Undefined values in `updates` clear that field.
596
+ updateColumnV2Layout: (sectionKey, colKey, updates) => {
597
+ set((state) => {
598
+ const path = findSectionPath(state.rows, sectionKey);
599
+ if (!path) return state;
600
+ const rows = updateSectionAtPath(state.rows, path, (section) => ({
601
+ ...section,
602
+ columns: section.columns.map((col) => {
603
+ if (col._key !== colKey) return col;
604
+ const merged = { ...col, ...updates };
605
+ for (const [k, v] of Object.entries(updates)) {
606
+ if (v === undefined) delete (merged as Record<string, unknown>)[k];
607
+ }
608
+ return merged;
609
+ }),
610
+ }));
611
+ return { rows, isDirty: true };
612
+ });
613
+ },
614
+
544
615
  // ---- Custom Section Instance operations (Session 108) ----
545
616
 
546
617
  addCustomSectionInstance: (
@@ -549,7 +620,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
549
620
  title: string,
550
621
  afterRowKey?: string | null
551
622
  ) => {
552
- get()._pushSnapshot();
553
623
  const instance: CustomSectionInstance = {
554
624
  _type: "customSectionInstance",
555
625
  _key: generateKey(),
@@ -570,7 +640,13 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
570
640
  } else {
571
641
  rows.push(instance);
572
642
  }
573
- 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
+ };
574
650
  });
575
651
  },
576
652
 
@@ -578,7 +654,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
578
654
  instanceKey: string,
579
655
  sectionData: PageSectionV2
580
656
  ) => {
581
- get()._pushSnapshot();
582
657
  set((state) => ({
583
658
  rows: state.rows.map((item) =>
584
659
  item._key === instanceKey
@@ -586,6 +661,8 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
586
661
  : item
587
662
  ),
588
663
  isDirty: true,
664
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
665
+ _future: [],
589
666
  }));
590
667
  },
591
668
 
@@ -611,7 +688,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
611
688
  instanceKey: string,
612
689
  updates: Partial<SectionV2Settings>
613
690
  ) => {
614
- get()._pushSnapshot();
615
691
  set((state) => ({
616
692
  rows: state.rows.map((item) =>
617
693
  item._key === instanceKey && isCustomSectionInstance(item)
@@ -625,6 +701,8 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
625
701
  : item
626
702
  ),
627
703
  isDirty: true,
704
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
705
+ _future: [],
628
706
  }));
629
707
  },
630
708
 
@@ -640,13 +718,19 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
640
718
  // ---- Parallax Group operations (Session 123) ----
641
719
 
642
720
  addParallaxGroup: (afterRowKey?: string | null): void => {
643
- get()._pushSnapshot();
644
- const result = addParallaxGroupInState(get().rows, afterRowKey);
645
- 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
+ });
646
731
  },
647
732
 
648
733
  addParallaxSlide: (groupKey: string): void => {
649
- get()._pushSnapshot();
650
734
  set((state) => ({
651
735
  rows: state.rows.map((item) => {
652
736
  if (item._key !== groupKey || !isParallaxGroup(item)) return item;
@@ -655,11 +739,12 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
655
739
  return { ...group, slides: [...group.slides, newSlide] } as ContentItem;
656
740
  }),
657
741
  isDirty: true,
742
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
743
+ _future: [],
658
744
  }));
659
745
  },
660
746
 
661
747
  removeParallaxSlide: (groupKey: string, slideKey: string): void => {
662
- get()._pushSnapshot();
663
748
  set((state) => {
664
749
  let changed = false;
665
750
  const rows = state.rows.map((item) => {
@@ -673,7 +758,14 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
673
758
  slides: group.slides.filter((s) => s._key !== slideKey),
674
759
  } as ContentItem;
675
760
  });
676
- 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
+ : {};
677
769
  });
678
770
  },
679
771
 
@@ -682,7 +774,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
682
774
  slideKey: string,
683
775
  direction: "up" | "down"
684
776
  ) => {
685
- get()._pushSnapshot();
686
777
  set((state) => {
687
778
  let changed = false;
688
779
  const rows = state.rows.map((item) => {
@@ -697,7 +788,14 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
697
788
  changed = true;
698
789
  return { ...group, slides } as ContentItem;
699
790
  });
700
- 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
+ : {};
701
799
  });
702
800
  },
703
801
 
@@ -706,7 +804,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
706
804
  slideKey: string,
707
805
  fields: Partial<ParallaxSlideV2>
708
806
  ) => {
709
- get()._pushSnapshot();
710
807
  set((state) => ({
711
808
  rows: state.rows.map((item) => {
712
809
  if (item._key !== groupKey || !isParallaxGroup(item)) return item;
@@ -719,6 +816,8 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
719
816
  } as ContentItem;
720
817
  }),
721
818
  isDirty: true,
819
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
820
+ _future: [],
722
821
  }));
723
822
  },
724
823
 
@@ -728,13 +827,14 @@ export function createSectionActions(set: StoreSet, get: StoreGet): SectionSlice
728
827
  Pick<ParallaxGroup, "transition_effect" | "snap_enabled" | "parallax_intensity">
729
828
  >
730
829
  ) => {
731
- get()._pushSnapshot();
732
830
  set((state) => ({
733
831
  rows: state.rows.map((item) => {
734
832
  if (item._key !== groupKey || !isParallaxGroup(item)) return item;
735
833
  return { ...item, ...fields } as ContentItem;
736
834
  }),
737
835
  isDirty: true,
836
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
837
+ _future: [],
738
838
  }));
739
839
  },
740
840
  };
@@ -31,6 +31,7 @@ import type {
31
31
  PageSectionV2,
32
32
  SectionV2Settings,
33
33
  SectionV2Preset,
34
+ SectionColumn,
34
35
  PageMetadata,
35
36
  ColorField,
36
37
  CoverSection,
@@ -168,6 +169,19 @@ export interface SectionSliceActions {
168
169
  colKey: string,
169
170
  config: import("../../lib/animation/enter-types").EnterAnimationConfig | undefined,
170
171
  ) => void;
172
+ /** Update desktop-level layout fields on a V2/cover/parallax-slide column.
173
+ * Columns only support background + border — spacing belongs to the
174
+ * section (row_gap/col_gap) or to block-level padding. */
175
+ updateColumnV2Layout: (
176
+ sectionKey: string,
177
+ colKey: string,
178
+ updates: Partial<Pick<
179
+ SectionColumn,
180
+ | "background_color" | "background_opacity" | "background_image"
181
+ | "background_size" | "background_position" | "background_repeat"
182
+ | "border_color" | "border_width" | "border_style" | "border_sides" | "border_radius"
183
+ >>,
184
+ ) => void;
171
185
 
172
186
  /** Select a V2 column — sets selectedRowKey + selectedColumnKey. */
173
187
  selectColumnV2: (sectionKey: string | null, columnKey: string | null) => void;
@@ -20,6 +20,18 @@ const blockExpansion = `
20
20
  grid_row,
21
21
  span,
22
22
  enter_animation,
23
+ // Column-level background + border (Session 184)
24
+ background_color,
25
+ background_opacity,
26
+ background_image,
27
+ background_size,
28
+ background_position,
29
+ background_repeat,
30
+ border_color,
31
+ border_width,
32
+ border_style,
33
+ border_sides,
34
+ border_radius,
23
35
  blocks[] {
24
36
  _type,
25
37
  _key,
@@ -60,6 +72,18 @@ const blockExpansion = `
60
72
  grid_column,
61
73
  grid_row,
62
74
  span,
75
+ // Column-level background + border (Session 184)
76
+ background_color,
77
+ background_opacity,
78
+ background_image,
79
+ background_size,
80
+ background_position,
81
+ background_repeat,
82
+ border_color,
83
+ border_width,
84
+ border_style,
85
+ border_sides,
86
+ border_radius,
63
87
  blocks[] {
64
88
  _type,
65
89
  _key,
@@ -429,6 +453,18 @@ export const customSectionBySlugQuery = groq`
429
453
  grid_column,
430
454
  grid_row,
431
455
  span,
456
+ enter_animation,
457
+ background_color,
458
+ background_opacity,
459
+ background_image,
460
+ background_size,
461
+ background_position,
462
+ background_repeat,
463
+ border_color,
464
+ border_width,
465
+ border_style,
466
+ border_sides,
467
+ border_radius,
432
468
  blocks[] {
433
469
  _type,
434
470
  _key,
@@ -456,6 +492,18 @@ export const customSectionByIdQuery = groq`
456
492
  grid_column,
457
493
  grid_row,
458
494
  span,
495
+ enter_animation,
496
+ background_color,
497
+ background_opacity,
498
+ background_image,
499
+ background_size,
500
+ background_position,
501
+ background_repeat,
502
+ border_color,
503
+ border_width,
504
+ border_style,
505
+ border_sides,
506
+ border_radius,
459
507
  blocks[] {
460
508
  _type,
461
509
  _key,
@@ -559,6 +559,20 @@ export interface SectionColumn {
559
559
  blocks: ContentBlock[]; // same block types as today
560
560
  // NEW (Session 116) — column-level enter animation for 4-level cascade
561
561
  enter_animation?: import("../../lib/animation/enter-types").EnterAnimationConfig;
562
+ // Column-level layout — desktop-only for now. Background + border only
563
+ // (no spacing — section row_gap/col_gap + block padding cover that).
564
+ // Viewport overrides are TBD: the current ColumnOverride type is position-only.
565
+ background_color?: string;
566
+ background_opacity?: number;
567
+ background_image?: string;
568
+ background_size?: string;
569
+ background_position?: string;
570
+ background_repeat?: string;
571
+ border_color?: string;
572
+ border_width?: string;
573
+ border_style?: string;
574
+ border_sides?: string;
575
+ border_radius?: string;
562
576
  }
563
577
 
564
578
  export interface ColumnOverride {
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.2";
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.2",
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",
@@ -193,7 +193,6 @@
193
193
  "react-dom": ">=19.0.0"
194
194
  },
195
195
  "dependencies": {
196
- "archiver": "^7.0.1",
197
196
  "@aws-sdk/client-s3": "^3.1021.0",
198
197
  "@aws-sdk/s3-request-presigner": "^3.1021.0",
199
198
  "@dnd-kit/core": "^6.3.1",
@@ -210,10 +209,11 @@
210
209
  "@tiptap/starter-kit": "^2.12.0",
211
210
  "@types/archiver": "^6.0.3",
212
211
  "@types/unzipper": "^0.10.10",
212
+ "archiver": "^7.0.1",
213
213
  "jszip": "^3.10.1",
214
- "next-sanity": "^12.1.5",
214
+ "next-sanity": "^12.3.0",
215
215
  "ogl": "^1.0.8",
216
- "sanity": "^5.17.1",
216
+ "sanity": "^5.21.0",
217
217
  "unzipper": "^0.12.3",
218
218
  "zustand": "^5.0.12"
219
219
  },
@@ -228,10 +228,12 @@
228
228
  "@types/react-dom": "^19",
229
229
  "eslint": "^9",
230
230
  "eslint-config-next": "16.2.1",
231
+ "framer-motion": "^12.38.0",
231
232
  "jsdom": "^26.1.0",
232
- "next": "16.2.1",
233
+ "next": "^16.2.4",
233
234
  "react": "19.2.4",
234
235
  "react-dom": "19.2.4",
236
+ "styled-components": "^6.4.0",
235
237
  "tailwindcss": "^4",
236
238
  "typescript": "^5",
237
239
  "vitest": "^4.1.2"