@morphika/andami 0.5.4 → 0.5.5

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 (41) hide show
  1. package/app/admin/assets/page.tsx +3 -2
  2. package/app/admin/layout.tsx +4 -0
  3. package/components/admin/nav-builder/NavBuilder.tsx +2 -1
  4. package/components/admin/styles/FontsEditor.tsx +2 -1
  5. package/components/builder/CoverSectionCanvas.tsx +7 -6
  6. package/components/builder/SettingsPanel.tsx +14 -8
  7. package/components/builder/SortableBlock.tsx +4 -0
  8. package/components/builder/SortableRow.tsx +2 -0
  9. package/components/builder/asset-browser/useR2Operations.ts +5 -4
  10. package/components/builder/editors/AudioBlockEditor.tsx +10 -8
  11. package/components/builder/editors/BeforeAfterBlockEditor.tsx +10 -8
  12. package/components/builder/editors/ButtonBlockEditor.tsx +9 -7
  13. package/components/builder/editors/ImageBlockEditor.tsx +10 -8
  14. package/components/builder/editors/ImageGridBlockEditor.tsx +10 -8
  15. package/components/builder/editors/SpacerBlockEditor.tsx +4 -4
  16. package/components/builder/editors/TextBlockEditor.tsx +471 -468
  17. package/components/builder/editors/VideoBlockEditor.tsx +10 -8
  18. package/components/builder/settings-panel/AnimationTab.tsx +11 -8
  19. package/components/builder/settings-panel/BlockLayoutTab.tsx +514 -511
  20. package/components/builder/settings-panel/ColumnV2AnimationTab.tsx +2 -2
  21. package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +11 -8
  22. package/components/builder/settings-panel/ColumnV2Settings.tsx +6 -5
  23. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +4 -3
  24. package/components/builder/settings-panel/CoverSectionSettings.tsx +14 -9
  25. package/components/builder/settings-panel/CustomSectionSettings.tsx +9 -7
  26. package/components/builder/settings-panel/PageSettings.tsx +39 -32
  27. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +2 -2
  28. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  29. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +7 -5
  30. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +13 -9
  31. package/components/builder/settings-panel/SectionV2Settings.tsx +7 -6
  32. package/components/builder/settings-panel/TRBLInputs.tsx +2 -2
  33. package/components/builder/settings-panel/useSettingsPanelSelection.ts +16 -13
  34. package/components/ui/ToastStack.tsx +142 -0
  35. package/lib/auth-token.ts +5 -1
  36. package/lib/bot-guard.ts +6 -0
  37. package/lib/builder/constants.ts +0 -7
  38. package/lib/toast/index.ts +56 -0
  39. package/lib/toast/store.ts +56 -0
  40. package/lib/version.ts +1 -1
  41. package/package.json +3 -1
@@ -3,6 +3,7 @@
3
3
  import { useState, useEffect, useCallback } from "react";
4
4
  import type { RegisteredAsset, AssetRegistry, RelinkLogEntry } from "../../../lib/sanity/types";
5
5
  import { formatDate, formatBytes } from "../../../lib/format-utils";
6
+ import { toast } from "../../../lib/toast";
6
7
 
7
8
  function getStatusColor(status: string): string {
8
9
  switch (status) {
@@ -83,8 +84,8 @@ export default function AdminAssetsPage() {
83
84
  const data = await res.json();
84
85
  if (!res.ok) throw new Error(data.error || "Scan failed");
85
86
  await fetchRegistry();
86
- alert(
87
- `Scan complete: ${data.scanned_count} files found, ${data.new_assets} new, ${data.missing_assets} missing`
87
+ toast.success(
88
+ `Scan complete: ${data.scanned_count} files, ${data.new_assets} new, ${data.missing_assets} missing`
88
89
  );
89
90
  } catch (err) {
90
91
  setError(err instanceof Error ? err.message : "Scan failed");
@@ -4,6 +4,7 @@ import { usePathname, useRouter } from "next/navigation";
4
4
  import Link from "next/link";
5
5
  import { getSiteConfig } from "../../lib/config";
6
6
  import { ANDAMI_VERSION } from "../../lib/version";
7
+ import ToastStack from "../../components/ui/ToastStack";
7
8
 
8
9
  // ============================================
9
10
  // Navigation Configuration — grouped by section
@@ -327,6 +328,9 @@ export default function AdminLayout({
327
328
  {children}
328
329
  </main>
329
330
  </div>
331
+
332
+ {/* Global toast stack — mounted at admin shell so all admin pages get it */}
333
+ <ToastStack />
330
334
  </div>
331
335
  );
332
336
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useCallback, useEffect } from "react";
4
4
  import type { NavItem, NavDesign, PageListItem } from "../../../lib/sanity/types";
5
+ import { toast } from "../../../lib/toast";
5
6
  import { migrateNavItems, validateLayout } from "./nav-builder-utils";
6
7
  import NavBuilderGrid from "./NavBuilderGrid";
7
8
  import NavLivePreview from "./NavLivePreview";
@@ -151,7 +152,7 @@ export default function NavBuilder({
151
152
  const handleSave = useCallback(async () => {
152
153
  const validation = validateLayout(items);
153
154
  if (!validation.valid) {
154
- alert(`Layout conflicts:\n${validation.conflicts.join("\n")}`);
155
+ toast.error(`Layout conflicts: ${validation.conflicts.join(", ")}`);
155
156
  return;
156
157
  }
157
158
  await onSave(items, design);
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useRef } from "react";
4
4
  import { csrfHeaders } from "../../../lib/csrf-client";
5
+ import { toast } from "../../../lib/toast";
5
6
  import type { FontFamily, FontVariant } from "../../../lib/sanity/types";
6
7
  import { Section, SaveButton } from "./shared";
7
8
  import { BubbleTooltip } from "../../builder/BubbleIcons";
@@ -73,7 +74,7 @@ export function FontsEditor({ fonts, onSave, saving }: { fonts: FontFamily[]; on
73
74
  )
74
75
  );
75
76
  } catch (err) {
76
- alert(err instanceof Error ? err.message : "Upload failed");
77
+ toast.error(err instanceof Error ? err.message : "Upload failed");
77
78
  } finally {
78
79
  setUploading(false);
79
80
  }
@@ -60,9 +60,10 @@ export default function CoverSectionCanvas({
60
60
  section,
61
61
  onAddBlockTarget,
62
62
  }: CoverSectionCanvasProps) {
63
- const store = useBuilderStore();
64
- const activeViewport = store.activeViewport || "desktop";
65
- const previewMode = store.previewMode;
63
+ const storeActiveViewport = useBuilderStore((s) => s.activeViewport);
64
+ const updateSectionV2Responsive = useBuilderStore((s) => s.updateSectionV2Responsive);
65
+ const addColumnV2 = useBuilderStore((s) => s.addColumnV2);
66
+ const activeViewport = storeActiveViewport || "desktop";
66
67
  const [isSectionHovered, setIsSectionHovered] = useState(false);
67
68
 
68
69
  const vhPixels = DEVICE_HEIGHTS[activeViewport];
@@ -182,12 +183,12 @@ export default function CoverSectionCanvas({
182
183
  // store action (it writes to any section by _key); the runtime shapes align
183
184
  // on `{ tablet?, phone? }` — the extra `cover_rows` field on CoverSection
184
185
  // is carried through without touching the V2-shaped write path.
185
- store.updateSectionV2Responsive(
186
+ updateSectionV2Responsive(
186
187
  section._key,
187
188
  finalResponsive as PageSectionV2["responsive"],
188
189
  );
189
190
  },
190
- [section, store],
191
+ [section, updateSectionV2Responsive],
191
192
  );
192
193
 
193
194
  return (
@@ -323,7 +324,7 @@ export default function CoverSectionCanvas({
323
324
  <button
324
325
  onClick={(e) => {
325
326
  e.stopPropagation();
326
- store.addColumnV2(section._key, rowNumber, 1, section.settings.grid_columns || 12);
327
+ addColumnV2(section._key, rowNumber, 1, section.settings.grid_columns || 12);
327
328
  }}
328
329
  className="rounded-full text-[10px] font-medium transition-all hover:scale-105"
329
330
  style={{
@@ -46,7 +46,13 @@ import { BubbleTooltip } from "./BubbleIcons";
46
46
  type SettingsTab = "settings" | "layout" | "seo" | "animation";
47
47
 
48
48
  export default function SettingsPanel() {
49
- const store = useBuilderStore();
49
+ const selectedRowKey = useBuilderStore((s) => s.selectedRowKey);
50
+ const selectedColumnKey = useBuilderStore((s) => s.selectedColumnKey);
51
+ const selectedBlockKey = useBuilderStore((s) => s.selectedBlockKey);
52
+ const editorMode = useBuilderStore((s) => s.editorMode);
53
+ const deleteBlock = useBuilderStore((s) => s.deleteBlock);
54
+ const deleteColumnV2 = useBuilderStore((s) => s.deleteColumnV2);
55
+ const deleteSection = useBuilderStore((s) => s.deleteSection);
50
56
  const [activeTab, setActiveTab] = useState<SettingsTab>("settings");
51
57
 
52
58
  const sel = useSettingsPanelSelection();
@@ -68,7 +74,7 @@ export default function SettingsPanel() {
68
74
  } = sel;
69
75
 
70
76
  // Reset to "settings" tab when selection changes
71
- const selectionKey = `${store.selectedRowKey}-${store.selectedColumnKey}-${store.selectedBlockKey}`;
77
+ const selectionKey = `${selectedRowKey}-${selectedColumnKey}-${selectedBlockKey}`;
72
78
  const prevSelectionKey = useRef(selectionKey);
73
79
  useEffect(() => {
74
80
  if (prevSelectionKey.current !== selectionKey) {
@@ -115,19 +121,19 @@ export default function SettingsPanel() {
115
121
  let deleteTitle = "";
116
122
 
117
123
  if (selectedBlock && !selectedBlock.isSection) {
118
- onDelete = () => store.deleteBlock(selectedBlock.block._key);
124
+ onDelete = () => deleteBlock(selectedBlock.block._key);
119
125
  deleteTitle = "Delete";
120
126
  } else if (selectedColumnV2 && effectiveSectionV2) {
121
- onDelete = () => store.deleteColumnV2(effectiveSectionV2._key, selectedColumnV2._key);
127
+ onDelete = () => deleteColumnV2(effectiveSectionV2._key, selectedColumnV2._key);
122
128
  deleteTitle = "Delete Column";
123
129
  } else if (selectedParallaxGroup && !selectedParallaxSlide) {
124
- onDelete = () => store.deleteSection(selectedParallaxGroup._key);
130
+ onDelete = () => deleteSection(selectedParallaxGroup._key);
125
131
  deleteTitle = "Delete Parallax Group";
126
132
  } else if (selectedCoverSection) {
127
- onDelete = () => store.deleteSection(selectedCoverSection._key);
133
+ onDelete = () => deleteSection(selectedCoverSection._key);
128
134
  deleteTitle = "Delete Cover Section";
129
135
  } else if (selectedSectionV2) {
130
- onDelete = () => store.deleteSection(selectedSectionV2._key);
136
+ onDelete = () => deleteSection(selectedSectionV2._key);
131
137
  deleteTitle = "Delete Section";
132
138
  }
133
139
 
@@ -215,7 +221,7 @@ export default function SettingsPanel() {
215
221
  if (animIdx >= 0) tabs.splice(animIdx, 1);
216
222
  }
217
223
  // Custom section editor mode: only Settings (Layout & Animation are per-instance on the parent page)
218
- if (store.editorMode === "customSection") {
224
+ if (editorMode === "customSection") {
219
225
  const layoutIdx = tabs.findIndex((t) => t.id === "layout");
220
226
  if (layoutIdx >= 0) tabs.splice(layoutIdx, 1);
221
227
  const animIdx = tabs.findIndex((t) => t.id === "animation");
@@ -111,6 +111,8 @@ export default function SortableBlock({
111
111
  ref={setNodeRef}
112
112
  style={style}
113
113
  className="relative"
114
+ aria-roledescription="Draggable block"
115
+ aria-label={info?.label || block._type}
114
116
  onClick={(e) => { e.stopPropagation(); onSelect(); }}
115
117
  >
116
118
  <BlockLivePreview block={block} viewport={activeViewport} editable />
@@ -128,6 +130,8 @@ export default function SortableBlock({
128
130
  ? "ring-2 ring-[#3580f9] ring-offset-1 ring-offset-transparent rounded"
129
131
  : ""
130
132
  }`}
133
+ aria-roledescription="Draggable block"
134
+ aria-label={info?.label || block._type}
131
135
  onClick={(e) => {
132
136
  e.stopPropagation();
133
137
  // If user clicked inside a contentEditable element (inline text editing),
@@ -232,6 +232,8 @@ export default function SortableRow({
232
232
  className={`relative transition-[opacity,box-shadow] ${
233
233
  isDragging ? "ring-2 ring-[#93278f] ring-offset-2 ring-offset-[#0a0a0a]" : ""
234
234
  }`}
235
+ aria-roledescription="Draggable section"
236
+ aria-label={sectionLabel || "Section"}
235
237
  onClick={(e) => {
236
238
  e.stopPropagation();
237
239
  if (isLockedSection) {
@@ -3,6 +3,7 @@
3
3
  import { useState, useCallback, useRef } from "react";
4
4
  import type { RegisteredAsset } from "../../../lib/sanity/types";
5
5
  import { csrfHeaders } from "../../../lib/csrf-client";
6
+ import { toast } from "../../../lib/toast";
6
7
  import type { FolderNode } from "./types";
7
8
 
8
9
  // ============================================
@@ -80,10 +81,10 @@ export function useR2Operations({
80
81
  });
81
82
  if (!res.ok) {
82
83
  const data = await res.json().catch(() => ({}));
83
- alert(data.error || "Delete failed");
84
+ toast.error(data.error || "Delete failed");
84
85
  }
85
86
  } catch {
86
- alert("Delete failed — network error");
87
+ toast.error("Delete failed — network error");
87
88
  } finally {
88
89
  setActionLoading(false);
89
90
  onRetry();
@@ -125,10 +126,10 @@ export function useR2Operations({
125
126
 
126
127
  if (!res.ok) {
127
128
  const data = await res.json().catch(() => ({}));
128
- alert(data.error || "Rename failed");
129
+ toast.error(data.error || "Rename failed");
129
130
  }
130
131
  } catch {
131
- alert("Rename failed — network error");
132
+ toast.error("Rename failed — network error");
132
133
  } finally {
133
134
  setRenameTarget(null);
134
135
  setRenameValue("");
@@ -28,32 +28,34 @@ interface Props {
28
28
  }
29
29
 
30
30
  export default function AudioBlockEditor({ block }: Props) {
31
- const store = useBuilderStore();
31
+ const updateBlock = useBuilderStore((s) => s.updateBlock);
32
+ const updateBlockDebounced = useBuilderStore((s) => s.updateBlockDebounced);
33
+ const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
32
34
  const viewport = useActiveViewport();
33
35
  const paletteSwatches = usePaletteSwatches();
34
36
 
35
- const snapshotOnFocus = () => store._pushSnapshot();
37
+ const snapshotOnFocus = () => _pushSnapshot();
36
38
 
37
39
  const updateResponsive = (property: string, value: unknown) => {
38
40
  if (viewport === "desktop") {
39
- store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
41
+ updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
40
42
  } else {
41
43
  const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
42
- store.updateBlock(block._key, overrides as Partial<ContentBlock>);
44
+ updateBlock(block._key, overrides as Partial<ContentBlock>);
43
45
  }
44
46
  };
45
47
 
46
48
  const resetOverride = (property: string) => {
47
49
  const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, undefined);
48
- store.updateBlock(block._key, overrides as Partial<ContentBlock>);
50
+ updateBlock(block._key, overrides as Partial<ContentBlock>);
49
51
  };
50
52
 
51
53
  const update = (updates: Partial<AudioBlock>) => {
52
- store.updateBlock(block._key, updates as Partial<ContentBlock>);
54
+ updateBlock(block._key, updates as Partial<ContentBlock>);
53
55
  };
54
56
 
55
57
  const updateDebounced = (updates: Partial<AudioBlock>) => {
56
- store.updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
58
+ updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
57
59
  };
58
60
 
59
61
  const effectiveWidth = getEffectiveValue<string>(
@@ -181,7 +183,7 @@ export default function AudioBlockEditor({ block }: Props) {
181
183
  value={String(getEffectiveValue<string>(block as ContentBlock, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
182
184
  onFocus={snapshotOnFocus}
183
185
  onChange={(e) => {
184
- store._pushSnapshot();
186
+ _pushSnapshot();
185
187
  updateResponsive("border_radius", e.target.value.replace(/[^0-9]/g, ""));
186
188
  }}
187
189
  className={INPUT_CLASS}
@@ -29,32 +29,34 @@ interface Props {
29
29
  }
30
30
 
31
31
  export default function BeforeAfterBlockEditor({ block }: Props) {
32
- const store = useBuilderStore();
32
+ const updateBlock = useBuilderStore((s) => s.updateBlock);
33
+ const updateBlockDebounced = useBuilderStore((s) => s.updateBlockDebounced);
34
+ const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
33
35
  const viewport = useActiveViewport();
34
36
  const paletteSwatches = usePaletteSwatches();
35
37
 
36
- const snapshotOnFocus = () => store._pushSnapshot();
38
+ const snapshotOnFocus = () => _pushSnapshot();
37
39
 
38
40
  const updateResponsive = (property: string, value: unknown) => {
39
41
  if (viewport === "desktop") {
40
- store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
42
+ updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
41
43
  } else {
42
44
  const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
43
- store.updateBlock(block._key, overrides as Partial<ContentBlock>);
45
+ updateBlock(block._key, overrides as Partial<ContentBlock>);
44
46
  }
45
47
  };
46
48
 
47
49
  const resetOverride = (property: string) => {
48
50
  const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, undefined);
49
- store.updateBlock(block._key, overrides as Partial<ContentBlock>);
51
+ updateBlock(block._key, overrides as Partial<ContentBlock>);
50
52
  };
51
53
 
52
54
  const update = (updates: Partial<BeforeAfterBlock>) => {
53
- store.updateBlock(block._key, updates as Partial<ContentBlock>);
55
+ updateBlock(block._key, updates as Partial<ContentBlock>);
54
56
  };
55
57
 
56
58
  const updateDebounced = (updates: Partial<BeforeAfterBlock>) => {
57
- store.updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
59
+ updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
58
60
  };
59
61
 
60
62
  const beforeType = block.before_media_type || "image";
@@ -297,7 +299,7 @@ export default function BeforeAfterBlockEditor({ block }: Props) {
297
299
  value={String(getEffectiveValue<string>(block as ContentBlock, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
298
300
  onFocus={snapshotOnFocus}
299
301
  onChange={(e) => {
300
- store._pushSnapshot();
302
+ _pushSnapshot();
301
303
  updateResponsive("border_radius", e.target.value.replace(/[^0-9]/g, ""));
302
304
  }}
303
305
  className={INPUT_CLASS}
@@ -23,32 +23,34 @@ interface Props {
23
23
  }
24
24
 
25
25
  export default function ButtonBlockEditor({ block }: Props) {
26
- const store = useBuilderStore();
26
+ const updateBlock = useBuilderStore((s) => s.updateBlock);
27
+ const updateBlockDebounced = useBuilderStore((s) => s.updateBlockDebounced);
28
+ const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
27
29
  const viewport = useActiveViewport();
28
- const snapshotOnFocus = () => store._pushSnapshot();
30
+ const snapshotOnFocus = () => _pushSnapshot();
29
31
 
30
32
  // Responsive-aware update
31
33
  const updateResponsive = (property: string, value: unknown) => {
32
34
  if (viewport === "desktop") {
33
- store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
35
+ updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
34
36
  } else {
35
37
  const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
36
- store.updateBlock(block._key, overrides as Partial<ContentBlock>);
38
+ updateBlock(block._key, overrides as Partial<ContentBlock>);
37
39
  }
38
40
  };
39
41
 
40
42
  const resetOverride = (property: string) => {
41
43
  const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, undefined);
42
- store.updateBlock(block._key, overrides as Partial<ContentBlock>);
44
+ updateBlock(block._key, overrides as Partial<ContentBlock>);
43
45
  };
44
46
 
45
47
  // Direct update (base block, not responsive)
46
48
  const update = (updates: Partial<ButtonBlock>) => {
47
- store.updateBlock(block._key, updates as Partial<ContentBlock>);
49
+ updateBlock(block._key, updates as Partial<ContentBlock>);
48
50
  };
49
51
 
50
52
  const updateDebounced = (updates: Partial<ButtonBlock>) => {
51
- store.updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
53
+ updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
52
54
  };
53
55
 
54
56
  // Effective values for the active viewport
@@ -25,33 +25,35 @@ interface Props {
25
25
  }
26
26
 
27
27
  export default function ImageBlockEditor({ block }: Props) {
28
- const store = useBuilderStore();
28
+ const updateBlock = useBuilderStore((s) => s.updateBlock);
29
+ const updateBlockDebounced = useBuilderStore((s) => s.updateBlockDebounced);
30
+ const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
29
31
  const viewport = useActiveViewport();
30
32
 
31
- const snapshotOnFocus = () => store._pushSnapshot();
33
+ const snapshotOnFocus = () => _pushSnapshot();
32
34
 
33
35
  // Responsive-aware update for layout/appearance properties
34
36
  const updateResponsive = (property: string, value: unknown) => {
35
37
  if (viewport === "desktop") {
36
- store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
38
+ updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
37
39
  } else {
38
40
  const overrides = setResponsiveOverride(block, viewport, property, value);
39
- store.updateBlock(block._key, overrides as Partial<ContentBlock>);
41
+ updateBlock(block._key, overrides as Partial<ContentBlock>);
40
42
  }
41
43
  };
42
44
 
43
45
  const resetOverride = (property: string) => {
44
46
  const overrides = setResponsiveOverride(block, viewport, property, undefined);
45
- store.updateBlock(block._key, overrides as Partial<ContentBlock>);
47
+ updateBlock(block._key, overrides as Partial<ContentBlock>);
46
48
  };
47
49
 
48
50
  // Direct update (base block, not responsive)
49
51
  const update = (updates: Partial<ImageBlock>) => {
50
- store.updateBlock(block._key, updates as Partial<ContentBlock>);
52
+ updateBlock(block._key, updates as Partial<ContentBlock>);
51
53
  };
52
54
 
53
55
  const updateDebounced = (updates: Partial<ImageBlock>) => {
54
- store.updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
56
+ updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
55
57
  };
56
58
 
57
59
  // Effective values for the active viewport
@@ -166,7 +168,7 @@ export default function ImageBlockEditor({ block }: Props) {
166
168
  value={String(getEffectiveValue<string>(block, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
167
169
  onFocus={snapshotOnFocus}
168
170
  onChange={(e) => {
169
- store._pushSnapshot();
171
+ _pushSnapshot();
170
172
  updateResponsive("border_radius", e.target.value.replace(/[^0-9]/g, ""));
171
173
  }}
172
174
  className={INPUT_CLASS}
@@ -95,35 +95,37 @@ function ImageThumb({
95
95
  // ============================================
96
96
 
97
97
  export default function ImageGridBlockEditor({ block }: Props) {
98
- const store = useBuilderStore();
98
+ const updateBlock = useBuilderStore((s) => s.updateBlock);
99
+ const updateBlockDebounced = useBuilderStore((s) => s.updateBlockDebounced);
100
+ const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
99
101
  const viewport = useActiveViewport();
100
102
  const images = block.images || [];
101
103
  const [browserOpen, setBrowserOpen] = useState(false);
102
104
 
103
- const snapshotOnFocus = () => store._pushSnapshot();
105
+ const snapshotOnFocus = () => _pushSnapshot();
104
106
 
105
107
  // Responsive update helper
106
108
  const updateResponsive = (property: string, value: unknown) => {
107
109
  if (viewport === "desktop") {
108
- store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
110
+ updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
109
111
  } else {
110
112
  const overrides = setResponsiveOverride(block, viewport, property, value);
111
- store.updateBlock(block._key, overrides as Partial<ContentBlock>);
113
+ updateBlock(block._key, overrides as Partial<ContentBlock>);
112
114
  }
113
115
  };
114
116
 
115
117
  const resetOverride = (property: string) => {
116
118
  const overrides = setResponsiveOverride(block, viewport, property, undefined);
117
- store.updateBlock(block._key, overrides as Partial<ContentBlock>);
119
+ updateBlock(block._key, overrides as Partial<ContentBlock>);
118
120
  };
119
121
 
120
122
  // Direct update (always base block — for images content)
121
123
  const update = (updates: Partial<ImageGridBlock>) => {
122
- store.updateBlock(block._key, updates as Partial<ContentBlock>);
124
+ updateBlock(block._key, updates as Partial<ContentBlock>);
123
125
  };
124
126
 
125
127
  const updateDebounced = (updates: Partial<ImageGridBlock>) => {
126
- store.updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
128
+ updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
127
129
  };
128
130
 
129
131
  // Add multiple images at once from browser
@@ -378,7 +380,7 @@ export default function ImageGridBlockEditor({ block }: Props) {
378
380
  value={String(getEffectiveValue<string>(block, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
379
381
  onFocus={snapshotOnFocus}
380
382
  onChange={(v) => {
381
- store._pushSnapshot();
383
+ _pushSnapshot();
382
384
  updateResponsive("border_radius", v.replace(/[^0-9]/g, ""));
383
385
  }}
384
386
  placeholder="0"
@@ -39,21 +39,21 @@ export function getSpacerPx(block: SpacerBlock): number {
39
39
  }
40
40
 
41
41
  export default function SpacerBlockEditor({ block }: Props) {
42
- const store = useBuilderStore();
42
+ const updateBlock = useBuilderStore((s) => s.updateBlock);
43
43
  const viewport = useActiveViewport();
44
44
 
45
45
  const updateResponsive = (property: string, value: unknown) => {
46
46
  if (viewport === "desktop") {
47
- store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
47
+ updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
48
48
  } else {
49
49
  const overrides = setResponsiveOverride(block, viewport, property, value);
50
- store.updateBlock(block._key, overrides as Partial<ContentBlock>);
50
+ updateBlock(block._key, overrides as Partial<ContentBlock>);
51
51
  }
52
52
  };
53
53
 
54
54
  const resetOverride = (property: string) => {
55
55
  const overrides = setResponsiveOverride(block, viewport, property, undefined);
56
- store.updateBlock(block._key, overrides as Partial<ContentBlock>);
56
+ updateBlock(block._key, overrides as Partial<ContentBlock>);
57
57
  };
58
58
 
59
59
  const effectiveHeight = getEffectiveValue<string>(