@morphika/andami 0.5.4 → 0.5.6

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 (46) 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/ColumnDragOverlay.tsx +4 -4
  6. package/components/builder/CoverSectionCanvas.tsx +10 -9
  7. package/components/builder/InsertionLines.tsx +3 -3
  8. package/components/builder/SectionV2Canvas.tsx +3 -3
  9. package/components/builder/SectionV2Column.tsx +20 -20
  10. package/components/builder/SettingsPanel.tsx +14 -8
  11. package/components/builder/SortableBlock.tsx +4 -0
  12. package/components/builder/SortableRow.tsx +2 -0
  13. package/components/builder/asset-browser/useR2Operations.ts +5 -4
  14. package/components/builder/editors/AudioBlockEditor.tsx +10 -8
  15. package/components/builder/editors/BeforeAfterBlockEditor.tsx +10 -8
  16. package/components/builder/editors/ButtonBlockEditor.tsx +9 -7
  17. package/components/builder/editors/ImageBlockEditor.tsx +10 -8
  18. package/components/builder/editors/ImageGridBlockEditor.tsx +10 -8
  19. package/components/builder/editors/SpacerBlockEditor.tsx +4 -4
  20. package/components/builder/editors/TextBlockEditor.tsx +471 -468
  21. package/components/builder/editors/VideoBlockEditor.tsx +10 -8
  22. package/components/builder/live-preview/drag-utils.tsx +5 -3
  23. package/components/builder/settings-panel/AnimationTab.tsx +11 -8
  24. package/components/builder/settings-panel/BlockLayoutTab.tsx +514 -511
  25. package/components/builder/settings-panel/ColumnV2AnimationTab.tsx +2 -2
  26. package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +11 -8
  27. package/components/builder/settings-panel/ColumnV2Settings.tsx +6 -5
  28. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +4 -3
  29. package/components/builder/settings-panel/CoverSectionSettings.tsx +14 -9
  30. package/components/builder/settings-panel/CustomSectionSettings.tsx +9 -7
  31. package/components/builder/settings-panel/PageSettings.tsx +39 -32
  32. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +2 -2
  33. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  34. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +7 -5
  35. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +13 -9
  36. package/components/builder/settings-panel/SectionV2Settings.tsx +10 -9
  37. package/components/builder/settings-panel/TRBLInputs.tsx +2 -2
  38. package/components/builder/settings-panel/useSettingsPanelSelection.ts +16 -13
  39. package/components/ui/ToastStack.tsx +142 -0
  40. package/lib/auth-token.ts +5 -1
  41. package/lib/bot-guard.ts +6 -0
  42. package/lib/builder/constants.ts +5 -10
  43. package/lib/toast/index.ts +56 -0
  44. package/lib/toast/store.ts +56 -0
  45. package/lib/version.ts +1 -1
  46. 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
  }
@@ -5,7 +5,7 @@ import { createPortal } from "react-dom";
5
5
  import { useBuilderStore } from "../../lib/builder/store";
6
6
  import type { PageSectionV2, CoverSection, SectionColumn } from "../../lib/sanity/types";
7
7
  import { isPageSectionV2, isCoverSection } from "../../lib/sanity/types";
8
- import { BUILDER_BLUE } from "../../lib/builder/constants";
8
+ import { BUILDER_YELLOW } from "../../lib/builder/constants";
9
9
 
10
10
  /** Color used when the current drop target is invalid (cross-section
11
11
  * swap, or a target that is not V2/Cover). Red 500 with compatible
@@ -57,9 +57,9 @@ const ColumnDragOverlay = memo(function ColumnDragOverlay({
57
57
 
58
58
  const blockCount = (col.blocks || []).length;
59
59
 
60
- // Pick accent based on drop validity.
61
- const accentColor = isValidDrop ? BUILDER_BLUE : INVALID_RED;
62
- const accentRgb = isValidDrop ? "71, 148, 226" : INVALID_RED_RGB;
60
+ // Pick accent based on drop validity. Column drag = BUILDER_YELLOW.
61
+ const accentColor = isValidDrop ? BUILDER_YELLOW : INVALID_RED;
62
+ const accentRgb = isValidDrop ? "214, 137, 0" : INVALID_RED_RGB;
63
63
 
64
64
  const overlay = (
65
65
  <div
@@ -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,14 +324,14 @@ 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={{
330
331
  padding: "5px 16px",
331
- background: `rgba(53, 128, 249, 0.10)`,
332
- color: "#3580f9",
333
- border: "1.5px dashed rgba(53, 128, 249, 0.4)",
332
+ background: `rgba(214, 137, 0, 0.10)`,
333
+ color: "#d68900",
334
+ border: "1.5px dashed rgba(214, 137, 0, 0.4)",
334
335
  opacity: isSectionHovered ? 1 : 0,
335
336
  pointerEvents: isSectionHovered ? "auto" : "none",
336
337
  transition: "opacity 150ms",
@@ -3,7 +3,7 @@
3
3
  import { useMemo, memo } from "react";
4
4
  import type { CascadeColumn } from "../../lib/builder/cascade-helpers";
5
5
  import type { DropTarget } from "./hooks/useColumnDrag";
6
- import { BUILDER_BLUE } from "../../lib/builder/constants";
6
+ import { BUILDER_YELLOW } from "../../lib/builder/constants";
7
7
 
8
8
  // ============================================
9
9
  // InsertionLines — Visual insertion indicators between adjacent columns
@@ -154,7 +154,7 @@ export const InsertionLines = memo(function InsertionLines({
154
154
  bottom: 4,
155
155
  width: isActive ? 4 : 0,
156
156
  transform: "translateX(-50%)",
157
- background: BUILDER_BLUE,
157
+ background: BUILDER_YELLOW,
158
158
  borderRadius: 2,
159
159
  transition: "width 100ms ease-out, opacity 100ms ease-out",
160
160
  opacity: isActive ? 1 : 0,
@@ -171,7 +171,7 @@ export const InsertionLines = memo(function InsertionLines({
171
171
  width: 14,
172
172
  height: 14,
173
173
  borderRadius: "50%",
174
- background: BUILDER_BLUE,
174
+ background: BUILDER_YELLOW,
175
175
  border: "2px solid white",
176
176
  boxShadow: "0 1px 4px rgba(53, 128, 249, 0.4)",
177
177
  display: "flex",
@@ -306,11 +306,11 @@ export default function SectionV2Canvas({
306
306
  }}
307
307
  className={`rounded-lg border-2 border-dashed text-xs font-medium transition-all flex items-center justify-center cursor-pointer ${
308
308
  isGapTarget
309
- ? "border-blue-500 text-blue-500 bg-blue-500/10 opacity-100"
309
+ ? "border-[#d68900] text-[#d68900] bg-[#d68900]/10 opacity-100"
310
310
  : showAsDropTarget
311
- ? "border-blue-500/40 text-blue-500/60 bg-blue-500/5 opacity-100"
311
+ ? "border-[#d68900]/40 text-[#d68900]/60 bg-[#d68900]/5 opacity-100"
312
312
  : isSectionHovered
313
- ? "border-[#3580f9]/25 text-[#3580f9]/50 hover:text-[#3580f9] hover:border-[#3580f9]/60 hover:bg-[#3580f9]/5 opacity-100"
313
+ ? "border-[#d68900]/25 text-[#d68900]/50 hover:text-[#d68900] hover:border-[#d68900]/60 hover:bg-[#d68900]/5 opacity-100"
314
314
  : "border-transparent text-transparent opacity-0 pointer-events-none"
315
315
  }`}
316
316
  >
@@ -15,7 +15,7 @@ import {
15
15
  getBorderStyles,
16
16
  } from "../../lib/builder/layout-styles";
17
17
  import { isSectionBlockType } from "../../lib/builder/types";
18
- import { BUILDER_BLUE } from "../../lib/builder/constants";
18
+ import { BUILDER_YELLOW, BUILDER_YELLOW_LIGHT } from "../../lib/builder/constants";
19
19
  import { BubbleTooltip, CloseIcon, DragDropIcon } from "./BubbleIcons";
20
20
 
21
21
  // ============================================
@@ -89,17 +89,17 @@ function ResizeHandle({
89
89
  height: isActive ? 16 : isHoveredEdge ? 56 : showChrome ? 56 : 32,
90
90
  borderRadius: isActive ? "50%" : 999,
91
91
  backgroundColor: isActive
92
- ? "rgba(53, 128, 249, 0.9)"
92
+ ? "rgba(214, 137, 0, 0.9)"
93
93
  : isHoveredEdge
94
- ? "rgba(53, 128, 249, 0.7)"
94
+ ? "rgba(214, 137, 0, 0.7)"
95
95
  : showChrome
96
- ? "rgba(53, 128, 249, 0.5)"
97
- : "rgba(53, 128, 249, 0.2)",
96
+ ? "rgba(214, 137, 0, 0.5)"
97
+ : "rgba(214, 137, 0, 0.2)",
98
98
  transition: "width 150ms ease-out, height 150ms ease-out, border-radius 150ms ease-out, background-color 150ms, box-shadow 150ms",
99
99
  boxShadow: isActive
100
- ? "0 0 10px rgba(53, 128, 249, 0.5)"
100
+ ? "0 0 10px rgba(214, 137, 0, 0.5)"
101
101
  : isHoveredEdge
102
- ? "0 0 6px rgba(53, 128, 249, 0.2)"
102
+ ? "0 0 6px rgba(214, 137, 0, 0.2)"
103
103
  : undefined,
104
104
  }}
105
105
  />
@@ -304,15 +304,15 @@ export default function SectionV2Column({
304
304
  style={{
305
305
  transition: "box-shadow 150ms",
306
306
  ...(isSwapTarget
307
- ? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}`, background: "rgba(53, 128, 249, 0.08)" }
307
+ ? { boxShadow: `inset 0 0 0 2px ${BUILDER_YELLOW}`, background: "rgba(214, 137, 0, 0.08)" }
308
308
  : isBlockOver
309
- ? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}` }
309
+ ? { boxShadow: `inset 0 0 0 2px ${BUILDER_YELLOW}` }
310
310
  : isSelected
311
- ? { boxShadow: `inset 0 0 0 2px rgba(53, 128, 249, 0.6)` }
311
+ ? { boxShadow: `inset 0 0 0 2px rgba(214, 137, 0, 0.6)` }
312
312
  : isHovered
313
- ? { boxShadow: `inset 0 0 0 1.5px rgba(53, 128, 249, 0.5)` }
313
+ ? { boxShadow: `inset 0 0 0 1.5px rgba(214, 137, 0, 0.5)` }
314
314
  : showFaintOutline
315
- ? { boxShadow: `inset 0 0 0 1px rgba(53, 128, 249, 0.2)`, borderRadius: 4 }
315
+ ? { boxShadow: `inset 0 0 0 1px rgba(214, 137, 0, 0.2)`, borderRadius: 4 }
316
316
  : undefined),
317
317
  }}
318
318
  />
@@ -383,14 +383,14 @@ export default function SectionV2Column({
383
383
  <div
384
384
  className="flex items-center rounded-[5px]"
385
385
  style={{
386
- background: "#c0d7ff",
387
- border: "1.5px solid #3580f9",
386
+ background: BUILDER_YELLOW_LIGHT,
387
+ border: `1.5px solid ${BUILDER_YELLOW}`,
388
388
  }}
389
389
  >
390
390
  {/* Drag handle — starts a column drag via @dnd-kit, selects on click */}
391
391
  <button
392
392
  type="button"
393
- className="group/bb relative text-[#3580f9] hover:bg-[#3580f9]/15 transition-colors px-1.5 py-1 flex items-center justify-center cursor-grab active:cursor-grabbing rounded-l-[3px]"
393
+ className="group/bb relative text-[#d68900] hover:bg-[#d68900]/15 transition-colors px-1.5 py-1 flex items-center justify-center cursor-grab active:cursor-grabbing rounded-l-[3px]"
394
394
  aria-label="Move column"
395
395
  onMouseDown={(e) => {
396
396
  e.stopPropagation();
@@ -404,22 +404,22 @@ export default function SectionV2Column({
404
404
  <DragDropIcon size={14} />
405
405
  <BubbleTooltip>Drag to move</BubbleTooltip>
406
406
  </button>
407
- <div className="w-px self-stretch my-1" style={{ background: "#3580f9" }} />
407
+ <div className="w-px self-stretch my-1" style={{ background: BUILDER_YELLOW }} />
408
408
  {/* Label — clicking selects the column */}
409
409
  <button
410
410
  type="button"
411
411
  onClick={(e) => { e.stopPropagation(); onSelect(); }}
412
- className="text-[11px] px-2 py-0.5 font-medium hover:bg-[#3580f9]/15 transition-colors"
413
- style={{ color: "#3580f9" }}
412
+ className="text-[11px] px-2 py-0.5 font-medium hover:bg-[#d68900]/15 transition-colors"
413
+ style={{ color: BUILDER_YELLOW }}
414
414
  aria-label="Select column"
415
415
  >
416
416
  Col
417
417
  </button>
418
- <div className="w-px self-stretch my-1" style={{ background: "#3580f9" }} />
418
+ <div className="w-px self-stretch my-1" style={{ background: BUILDER_YELLOW }} />
419
419
  {/* Delete */}
420
420
  <button
421
421
  onClick={handleDelete}
422
- className="group/bb relative text-[#3580f9] hover:text-red-500 hover:bg-red-500/10 transition-colors px-1.5 py-1 flex items-center justify-center rounded-r-[3px]"
422
+ className="group/bb relative text-[#d68900] hover:text-red-500 hover:bg-red-500/10 transition-colors px-1.5 py-1 flex items-center justify-center rounded-r-[3px]"
423
423
  aria-label="Delete column"
424
424
  >
425
425
  <CloseIcon size={14} />
@@ -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}