@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
@@ -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"
@@ -31,6 +31,19 @@ export function normalizePageSectionV2(section: Partial<PageSectionV2> & { _key:
31
31
  // Migrate column-level enter_animation (new field, no old equivalent for columns)
32
32
  const colEnterAnimation = colRecord.enter_animation as EnterAnimationConfig | undefined;
33
33
 
34
+ // Column-level background + border — carry through from Sanity (desktop-only).
35
+ const colLayoutFields: Partial<SectionColumn> = {};
36
+ for (const field of [
37
+ "background_color", "background_opacity", "background_image",
38
+ "background_size", "background_position", "background_repeat",
39
+ "border_color", "border_width", "border_style", "border_sides", "border_radius",
40
+ ] as const) {
41
+ const val = colRecord[field];
42
+ if (val !== undefined && val !== null) {
43
+ (colLayoutFields as Record<string, unknown>)[field] = val;
44
+ }
45
+ }
46
+
34
47
  return {
35
48
  _key: col._key || generateKey(),
36
49
  grid_column: col.grid_column || 1,
@@ -55,6 +68,7 @@ export function normalizePageSectionV2(section: Partial<PageSectionV2> & { _key:
55
68
  return b as ContentBlock;
56
69
  }),
57
70
  ...(colEnterAnimation ? { enter_animation: colEnterAnimation } : {}),
71
+ ...colLayoutFields,
58
72
  };
59
73
  });
60
74
 
@@ -134,6 +134,21 @@ function serializePageSectionV2(section: PageSectionV2): Record<string, unknown>
134
134
  if (col.enter_animation && col.enter_animation.preset && col.enter_animation.preset !== "none") {
135
135
  colData.enter_animation = col.enter_animation;
136
136
  }
137
+ // Column-level background + border (desktop-only).
138
+ const colLayout = stripUndefined({
139
+ background_color: col.background_color,
140
+ background_opacity: col.background_opacity,
141
+ background_image: col.background_image,
142
+ background_size: col.background_size,
143
+ background_position: col.background_position,
144
+ background_repeat: col.background_repeat,
145
+ border_color: col.border_color,
146
+ border_width: col.border_width,
147
+ border_style: col.border_style,
148
+ border_sides: col.border_sides,
149
+ border_radius: col.border_radius,
150
+ });
151
+ if (colLayout) Object.assign(colData, colLayout);
137
152
  return colData;
138
153
  }),
139
154
  settings: s ? stripUndefined({
@@ -223,6 +238,18 @@ function serializeCoverSection(section: CoverSection): Record<string, unknown> {
223
238
  span: col.span,
224
239
  blocks: (col.blocks || []).map((b) => serializeBlock(b)),
225
240
  enter_animation: col.enter_animation,
241
+ // Column-level background + border (desktop-only).
242
+ background_color: col.background_color,
243
+ background_opacity: col.background_opacity,
244
+ background_image: col.background_image,
245
+ background_size: col.background_size,
246
+ background_position: col.background_position,
247
+ background_repeat: col.background_repeat,
248
+ border_color: col.border_color,
249
+ border_width: col.border_width,
250
+ border_style: col.border_style,
251
+ border_sides: col.border_sides,
252
+ border_radius: col.border_radius,
226
253
  } as Record<string, unknown>));
227
254
 
228
255
  return stripUndefined({
@@ -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
  };