@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
@@ -18,14 +18,14 @@ export function ColumnV2AnimationTab({
18
18
  section: PageSectionV2;
19
19
  column: SectionColumn;
20
20
  }) {
21
- const store = useBuilderStore();
21
+ const updateColumnEnterAnimation = useBuilderStore((s) => s.updateColumnEnterAnimation);
22
22
 
23
23
  return (
24
24
  <EnterAnimationPicker
25
25
  mode={{ level: "column", parentConfig: section.settings.enter_animation }}
26
26
  config={column.enter_animation}
27
27
  onChange={(cfg) => {
28
- store.updateColumnEnterAnimation(section._key, column._key, cfg);
28
+ updateColumnEnterAnimation(section._key, column._key, cfg);
29
29
  }}
30
30
  />
31
31
  );
@@ -35,15 +35,18 @@ export function ColumnV2LayoutTab({
35
35
  section: PageSectionV2;
36
36
  column: SectionColumn;
37
37
  }) {
38
- const store = useBuilderStore();
38
+ const activeViewport = useBuilderStore((s) => s.activeViewport);
39
+ const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
40
+ const updateColumnV2Layout = useBuilderStore((s) => s.updateColumnV2Layout);
41
+ const setColorPickerPreview = useBuilderStore((s) => s.setColorPickerPreview);
42
+ const clearColorPickerPreview = useBuilderStore((s) => s.clearColorPickerPreview);
39
43
  const paletteSwatches = usePaletteSwatches();
40
- const activeViewport = store.activeViewport;
41
44
 
42
45
  const isResponsive = activeViewport !== "desktop";
43
46
 
44
47
  const update = (field: LayoutField, value: unknown) => {
45
- store._pushSnapshot();
46
- store.updateColumnV2Layout(section._key, column._key, {
48
+ _pushSnapshot();
49
+ updateColumnV2Layout(section._key, column._key, {
47
50
  [field]: value,
48
51
  } as Partial<SectionColumn>);
49
52
  };
@@ -54,7 +57,7 @@ export function ColumnV2LayoutTab({
54
57
  };
55
58
 
56
59
  const handleBgPreview = (val: import("../../../lib/sanity/types").ColorField) => {
57
- store.setColorPickerPreview({ sectionKey: section._key, field: "column_background_color", value: val });
60
+ setColorPickerPreview({ sectionKey: section._key, field: "column_background_color", value: val });
58
61
  };
59
62
 
60
63
  const bgIsGradient = isGradient(parseColorField(getValue<string>("background_color", "")));
@@ -77,7 +80,7 @@ export function ColumnV2LayoutTab({
77
80
  <ColorSwatchPicker
78
81
  value={parseColorField(getValue<string>("background_color", ""))}
79
82
  onChange={(val) => {
80
- store.clearColorPickerPreview();
83
+ clearColorPickerPreview();
81
84
  update("background_color", serializeColorField(val));
82
85
  }}
83
86
  swatches={paletteSwatches}
@@ -111,7 +114,7 @@ export function ColumnV2LayoutTab({
111
114
  <SettingsField label="Image">
112
115
  <AssetPathInput
113
116
  value={getValue<string>("background_image", "")}
114
- onFocus={() => store._pushSnapshot()}
117
+ onFocus={() => _pushSnapshot()}
115
118
  onChange={(v) => update("background_image", v)}
116
119
  placeholder="path/to/image.jpg"
117
120
  filterType="image"
@@ -168,7 +171,7 @@ export function ColumnV2LayoutTab({
168
171
  <ColorSwatchPicker
169
172
  value={parseColorField(getValue<string>("border_color", ""))}
170
173
  onChange={(val) => {
171
- store.clearColorPickerPreview();
174
+ clearColorPickerPreview();
172
175
  update("border_color", serializeColorField(val));
173
176
  }}
174
177
  swatches={paletteSwatches}
@@ -36,8 +36,9 @@ export default function ColumnV2Settings({
36
36
  section: PageSectionV2;
37
37
  column: SectionColumn;
38
38
  }) {
39
- const store = useBuilderStore();
40
- const activeViewport = store.activeViewport;
39
+ const activeViewport = useBuilderStore((s) => s.activeViewport);
40
+ const updateSectionV2Responsive = useBuilderStore((s) => s.updateSectionV2Responsive);
41
+ const resizeColumnV2 = useBuilderStore((s) => s.resizeColumnV2);
41
42
  const isResponsive = activeViewport !== "desktop";
42
43
  const gridColumns = section.settings.grid_columns;
43
44
 
@@ -66,10 +67,10 @@ export default function ColumnV2Settings({
66
67
  const responsive = buildSingleColumnV2Override(section, activeViewport, column._key, {
67
68
  span: clamped,
68
69
  });
69
- store.updateSectionV2Responsive(section._key, responsive ?? undefined);
70
+ updateSectionV2Responsive(section._key, responsive ?? undefined);
70
71
  } else {
71
72
  // Desktop: use cascade engine
72
- store.resizeColumnV2(section._key, column._key, clamped);
73
+ resizeColumnV2(section._key, column._key, clamped);
73
74
  }
74
75
  };
75
76
 
@@ -78,7 +79,7 @@ export default function ColumnV2Settings({
78
79
  const responsive = buildSingleColumnV2Override(section, activeViewport, column._key, {
79
80
  span: undefined,
80
81
  });
81
- store.updateSectionV2Responsive(section._key, responsive ?? undefined);
82
+ updateSectionV2Responsive(section._key, responsive ?? undefined);
82
83
  };
83
84
 
84
85
  return (
@@ -15,11 +15,12 @@ interface CoverSectionLayoutTabProps {
15
15
  }
16
16
 
17
17
  export default function CoverSectionLayoutTab({ section }: CoverSectionLayoutTabProps) {
18
- const store = useBuilderStore();
18
+ const updateCoverSettings = useBuilderStore((s) => s.updateCoverSettings);
19
+ const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
19
20
  const s = section.settings;
20
21
 
21
22
  const update = (fields: Record<string, string | undefined>) => {
22
- store.updateCoverSettings(section._key, fields as Partial<typeof s>);
23
+ updateCoverSettings(section._key, fields as Partial<typeof s>);
23
24
  };
24
25
 
25
26
  return (
@@ -56,7 +57,7 @@ export default function CoverSectionLayoutTab({ section }: CoverSectionLayoutTab
56
57
  max={50}
57
58
  step={1}
58
59
  value={parseInt(s.border_radius || "0", 10) || 0}
59
- onMouseDown={() => store._pushSnapshot()}
60
+ onMouseDown={() => _pushSnapshot()}
60
61
  onChange={(e) => update({ border_radius: e.target.value === "0" ? undefined : e.target.value })}
61
62
  className="flex-1 accent-[#3580f9]"
62
63
  />
@@ -74,7 +74,12 @@ interface CoverSectionSettingsProps {
74
74
  }
75
75
 
76
76
  export default function CoverSectionSettings({ section }: CoverSectionSettingsProps) {
77
- const store = useBuilderStore();
77
+ const updateCoverBackground = useBuilderStore((s) => s.updateCoverBackground);
78
+ const updateCoverHeight = useBuilderStore((s) => s.updateCoverHeight);
79
+ const updateCoverRowAlign = useBuilderStore((s) => s.updateCoverRowAlign);
80
+ const removeCoverRow = useBuilderStore((s) => s.removeCoverRow);
81
+ const addCoverRow = useBuilderStore((s) => s.addCoverRow);
82
+ const updateCoverSettings = useBuilderStore((s) => s.updateCoverSettings);
78
83
  const paletteSwatches = usePaletteSwatches();
79
84
 
80
85
  const bgType = section.background_type || "image";
@@ -89,7 +94,7 @@ export default function CoverSectionSettings({ section }: CoverSectionSettingsPr
89
94
  "background_overlay_color" | "background_overlay_opacity" |
90
95
  "nav_color"
91
96
  >>) => {
92
- store.updateCoverBackground(section._key, fields);
97
+ updateCoverBackground(section._key, fields);
93
98
  };
94
99
 
95
100
  return (
@@ -223,7 +228,7 @@ export default function CoverSectionSettings({ section }: CoverSectionSettingsPr
223
228
  <SettingsField label="Height">
224
229
  <select
225
230
  value={section.height}
226
- onChange={(e) => store.updateCoverHeight(section._key, e.target.value as CoverSection["height"])}
231
+ onChange={(e) => updateCoverHeight(section._key, e.target.value as CoverSection["height"])}
227
232
  className={SELECT_CLASS}
228
233
  >
229
234
  {HEIGHT_OPTIONS.map((opt) => (
@@ -250,7 +255,7 @@ export default function CoverSectionSettings({ section }: CoverSectionSettingsPr
250
255
  <select
251
256
  value={row.vertical_align}
252
257
  onChange={(e) =>
253
- store.updateCoverRowAlign(
258
+ updateCoverRowAlign(
254
259
  section._key,
255
260
  row._key,
256
261
  e.target.value as "start" | "center" | "end"
@@ -264,7 +269,7 @@ export default function CoverSectionSettings({ section }: CoverSectionSettingsPr
264
269
  </select>
265
270
  {section.cover_rows.length > 1 && (
266
271
  <button
267
- onClick={() => store.removeCoverRow(section._key, row._key)}
272
+ onClick={() => removeCoverRow(section._key, row._key)}
268
273
  className="group/bb relative text-neutral-300 hover:text-red-500 transition-colors text-xs shrink-0"
269
274
  aria-label="Remove row"
270
275
  >
@@ -278,7 +283,7 @@ export default function CoverSectionSettings({ section }: CoverSectionSettingsPr
278
283
 
279
284
  {section.cover_rows.length < 5 && (
280
285
  <button
281
- onClick={() => store.addCoverRow(section._key)}
286
+ onClick={() => addCoverRow(section._key)}
282
287
  className="w-full mt-2 rounded-lg border border-dashed border-neutral-300 py-2 text-[11px] font-medium text-neutral-400 hover:text-neutral-600 hover:border-neutral-400 transition-colors"
283
288
  >
284
289
  + Add Row
@@ -300,7 +305,7 @@ export default function CoverSectionSettings({ section }: CoverSectionSettingsPr
300
305
  max={60}
301
306
  step={2}
302
307
  value={section.settings.col_gap ?? 20}
303
- onChange={(e) => store.updateCoverSettings(section._key, { col_gap: parseInt(e.target.value) })}
308
+ onChange={(e) => updateCoverSettings(section._key, { col_gap: parseInt(e.target.value) })}
304
309
  className="flex-1 accent-[#3580f9]"
305
310
  />
306
311
  <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
@@ -317,7 +322,7 @@ export default function CoverSectionSettings({ section }: CoverSectionSettingsPr
317
322
  max={60}
318
323
  step={2}
319
324
  value={section.settings.row_gap ?? 20}
320
- onChange={(e) => store.updateCoverSettings(section._key, { row_gap: parseInt(e.target.value) })}
325
+ onChange={(e) => updateCoverSettings(section._key, { row_gap: parseInt(e.target.value) })}
321
326
  className="flex-1 accent-[#3580f9]"
322
327
  />
323
328
  <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
@@ -330,7 +335,7 @@ export default function CoverSectionSettings({ section }: CoverSectionSettingsPr
330
335
  {/* Stagger */}
331
336
  <StaggerSettings
332
337
  stagger={section.settings.stagger}
333
- onChange={(s) => store.updateCoverSettings(section._key, { stagger: s })}
338
+ onChange={(s) => updateCoverSettings(section._key, { stagger: s })}
334
339
  />
335
340
  </>
336
341
  );
@@ -12,7 +12,9 @@ import { BUILDER_VIOLET } from "../../../lib/builder/constants";
12
12
  import type { CustomSectionInstance, PageSectionV2 } from "../../../lib/sanity/types";
13
13
 
14
14
  export function CustomSectionSettings({ instance }: { instance: CustomSectionInstance }) {
15
- const store = useBuilderStore();
15
+ const updateCustomSectionInstanceTitle = useBuilderStore((s) => s.updateCustomSectionInstanceTitle);
16
+ const enterSectionEditor = useBuilderStore((s) => s.enterSectionEditor);
17
+ const detachCustomSectionInstance = useBuilderStore((s) => s.detachCustomSectionInstance);
16
18
  const refetchTick = useBuilderStore((s) => s._customSectionRefetchTick);
17
19
  const [showDetachConfirm, setShowDetachConfirm] = useState(false);
18
20
  const [sectionData, setSectionData] = useState<PageSectionV2 | null>(null);
@@ -40,17 +42,17 @@ export function CustomSectionSettings({ instance }: { instance: CustomSectionIns
40
42
 
41
43
  const remoteTitle = data.section.title;
42
44
  if (remoteTitle && remoteTitle !== instance.custom_section_title) {
43
- store.updateCustomSectionInstanceTitle(instance._key, remoteTitle);
45
+ updateCustomSectionInstanceTitle(instance._key, remoteTitle);
44
46
  }
45
47
 
46
- store.enterSectionEditor(
48
+ enterSectionEditor(
47
49
  instance.custom_section_slug,
48
50
  remoteTitle,
49
51
  data.section.section
50
52
  );
51
53
  } catch {
52
54
  if (sectionData) {
53
- store.enterSectionEditor(
55
+ enterSectionEditor(
54
56
  instance.custom_section_slug,
55
57
  instance.custom_section_title,
56
58
  sectionData
@@ -59,13 +61,13 @@ export function CustomSectionSettings({ instance }: { instance: CustomSectionIns
59
61
  } finally {
60
62
  setLoadingEdit(false);
61
63
  }
62
- }, [instance, sectionData, store]);
64
+ }, [instance, sectionData, updateCustomSectionInstanceTitle, enterSectionEditor]);
63
65
 
64
66
  const handleDetach = useCallback(() => {
65
67
  if (!sectionData) return;
66
- store.detachCustomSectionInstance(instance._key, sectionData);
68
+ detachCustomSectionInstance(instance._key, sectionData);
67
69
  setShowDetachConfirm(false);
68
- }, [instance._key, sectionData, store]);
70
+ }, [instance._key, sectionData, detachCustomSectionInstance]);
69
71
 
70
72
  return (
71
73
  <div className="px-4 py-3 space-y-3">
@@ -39,7 +39,13 @@ function slugify(text: string): string {
39
39
  }
40
40
 
41
41
  export default function PageSettings() {
42
- const store = useBuilderStore();
42
+ const pageTitle = useBuilderStore((s) => s.pageTitle);
43
+ const pageSlug = useBuilderStore((s) => s.pageSlug);
44
+ const pageType = useBuilderStore((s) => s.pageType);
45
+ const pageSettings = useBuilderStore((s) => s.pageSettings);
46
+ const setPageTitle = useBuilderStore((s) => s.setPageTitle);
47
+ const setPageSlug = useBuilderStore((s) => s.setPageSlug);
48
+ const updatePageSettings = useBuilderStore((s) => s.updatePageSettings);
43
49
  const paletteSwatches = usePaletteSwatches();
44
50
  const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);
45
51
 
@@ -49,13 +55,13 @@ export default function PageSettings() {
49
55
  <SettingsField label="Title">
50
56
  <input
51
57
  type="text"
52
- value={store.pageTitle}
58
+ value={pageTitle}
53
59
  onChange={(e) => {
54
60
  const newTitle = e.target.value;
55
- store.setPageTitle(newTitle);
61
+ setPageTitle(newTitle);
56
62
  // Auto-update slug from title unless user manually edited the slug
57
63
  if (!slugManuallyEdited) {
58
- store.setPageSlug(slugify(newTitle));
64
+ setPageSlug(slugify(newTitle));
59
65
  }
60
66
  }}
61
67
  className={INPUT_CLASS}
@@ -64,17 +70,17 @@ export default function PageSettings() {
64
70
  <SettingsField label="Slug">
65
71
  <input
66
72
  type="text"
67
- value={store.pageSlug}
73
+ value={pageSlug}
68
74
  onChange={(e) => {
69
75
  setSlugManuallyEdited(true);
70
- store.setPageSlug(e.target.value);
76
+ setPageSlug(e.target.value);
71
77
  }}
72
78
  className={INPUT_CLASS}
73
79
  />
74
80
  </SettingsField>
75
81
  <SettingsField label="Type">
76
82
  <p className="text-xs text-neutral-900 py-[7px]">
77
- {store.pageType}
83
+ {pageType}
78
84
  </p>
79
85
  </SettingsField>
80
86
  </SettingsSection>
@@ -82,16 +88,16 @@ export default function PageSettings() {
82
88
  <SettingsSection title="Appearance" defaultOpen icon={<AppearanceIcon />}>
83
89
  <SettingsField label="Background">
84
90
  <ColorSwatchPicker
85
- value={parseColorField(store.pageSettings.background_color || "")}
86
- onChange={(val) => store.updatePageSettings({ background_color: serializeColorField(val) || "transparent" })}
91
+ value={parseColorField(pageSettings.background_color || "")}
92
+ onChange={(val) => updatePageSettings({ background_color: serializeColorField(val) || "transparent" })}
87
93
  swatches={paletteSwatches}
88
94
  allowGradients
89
95
  />
90
96
  </SettingsField>
91
97
  <SettingsField label="Text Color">
92
98
  <ColorSwatchPicker
93
- value={store.pageSettings.text_color || ""}
94
- onChange={(val) => store.updatePageSettings({ text_color: typeof val === "string" ? val : "" })}
99
+ value={pageSettings.text_color || ""}
100
+ onChange={(val) => updatePageSettings({ text_color: typeof val === "string" ? val : "" })}
95
101
  swatches={paletteSwatches}
96
102
  />
97
103
  </SettingsField>
@@ -100,15 +106,15 @@ export default function PageSettings() {
100
106
  <SettingsSection title="Navigation" icon={<NavigationIcon />}>
101
107
  <SettingsField label="Nav Color">
102
108
  <ColorSwatchPicker
103
- value={store.pageSettings.nav_color || ""}
104
- onChange={(val) => store.updatePageSettings({ nav_color: typeof val === "string" ? val : "" })}
109
+ value={pageSettings.nav_color || ""}
110
+ onChange={(val) => updatePageSettings({ nav_color: typeof val === "string" ? val : "" })}
105
111
  swatches={paletteSwatches}
106
112
  />
107
113
  </SettingsField>
108
114
  <SettingsField label="Animation Override" hint="Override global nav entrance for this page">
109
115
  <select
110
- value={store.pageSettings.nav_entrance_animation || ""}
111
- onChange={(e) => store.updatePageSettings({ nav_entrance_animation: e.target.value as "" | "fade-in" | "slide-down" | "blur-in" })}
116
+ value={pageSettings.nav_entrance_animation || ""}
117
+ onChange={(e) => updatePageSettings({ nav_entrance_animation: e.target.value as "" | "fade-in" | "slide-down" | "blur-in" })}
112
118
  className={INPUT_CLASS}
113
119
  >
114
120
  <option value="">Use Global</option>
@@ -117,27 +123,27 @@ export default function PageSettings() {
117
123
  <option value="blur-in">Blur In</option>
118
124
  </select>
119
125
  </SettingsField>
120
- {store.pageSettings.nav_entrance_animation && (
126
+ {pageSettings.nav_entrance_animation && (
121
127
  <>
122
- <SettingsField label={`Duration: ${store.pageSettings.nav_entrance_duration || 600}ms`}>
128
+ <SettingsField label={`Duration: ${pageSettings.nav_entrance_duration || 600}ms`}>
123
129
  <input
124
130
  type="range"
125
131
  min={200}
126
132
  max={5000}
127
133
  step={50}
128
- value={store.pageSettings.nav_entrance_duration || 600}
129
- onChange={(e) => store.updatePageSettings({ nav_entrance_duration: Number(e.target.value) })}
134
+ value={pageSettings.nav_entrance_duration || 600}
135
+ onChange={(e) => updatePageSettings({ nav_entrance_duration: Number(e.target.value) })}
130
136
  className="w-full accent-[#3580f9]"
131
137
  />
132
138
  </SettingsField>
133
- <SettingsField label={`Delay: ${store.pageSettings.nav_entrance_delay || 0}ms`}>
139
+ <SettingsField label={`Delay: ${pageSettings.nav_entrance_delay || 0}ms`}>
134
140
  <input
135
141
  type="range"
136
142
  min={0}
137
143
  max={5000}
138
144
  step={50}
139
- value={store.pageSettings.nav_entrance_delay || 0}
140
- onChange={(e) => store.updatePageSettings({ nav_entrance_delay: Number(e.target.value) })}
145
+ value={pageSettings.nav_entrance_delay || 0}
146
+ onChange={(e) => updatePageSettings({ nav_entrance_delay: Number(e.target.value) })}
141
147
  className="w-full accent-[#3580f9]"
142
148
  />
143
149
  </SettingsField>
@@ -146,14 +152,14 @@ export default function PageSettings() {
146
152
  <SettingsField label="Disable Animation" hint="No nav animation on this page">
147
153
  <button
148
154
  type="button"
149
- onClick={() => store.updatePageSettings({ nav_entrance_disabled: !store.pageSettings.nav_entrance_disabled })}
155
+ onClick={() => updatePageSettings({ nav_entrance_disabled: !pageSettings.nav_entrance_disabled })}
150
156
  className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
151
- store.pageSettings.nav_entrance_disabled ? "bg-[#3580f9]" : "bg-neutral-300"
157
+ pageSettings.nav_entrance_disabled ? "bg-[#3580f9]" : "bg-neutral-300"
152
158
  }`}
153
159
  >
154
160
  <span
155
161
  className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white transition-transform ${
156
- store.pageSettings.nav_entrance_disabled ? "translate-x-[18px]" : "translate-x-[3px]"
162
+ pageSettings.nav_entrance_disabled ? "translate-x-[18px]" : "translate-x-[3px]"
157
163
  }`}
158
164
  />
159
165
  </button>
@@ -166,7 +172,8 @@ export default function PageSettings() {
166
172
 
167
173
  /** SEO settings — rendered in the dedicated SEO tab at page level. */
168
174
  export function PageSeoSettings() {
169
- const store = useBuilderStore();
175
+ const metadata = useBuilderStore((s) => s.metadata);
176
+ const setMetadata = useBuilderStore((s) => s.setMetadata);
170
177
 
171
178
  return (
172
179
  <>
@@ -174,17 +181,17 @@ export function PageSeoSettings() {
174
181
  <SettingsField label="SEO Title">
175
182
  <input
176
183
  type="text"
177
- value={store.metadata.seo_title || ""}
178
- onChange={(e) => store.setMetadata({ seo_title: e.target.value })}
184
+ value={metadata.seo_title || ""}
185
+ onChange={(e) => setMetadata({ seo_title: e.target.value })}
179
186
  className={INPUT_CLASS}
180
187
  placeholder="Page title for search engines"
181
188
  />
182
189
  </SettingsField>
183
190
  <SettingsField label="Description">
184
191
  <textarea
185
- value={store.metadata.seo_description || ""}
192
+ value={metadata.seo_description || ""}
186
193
  onChange={(e) =>
187
- store.setMetadata({ seo_description: e.target.value })
194
+ setMetadata({ seo_description: e.target.value })
188
195
  }
189
196
  rows={2}
190
197
  className={`${INPUT_CLASS} resize-y`}
@@ -194,9 +201,9 @@ export function PageSeoSettings() {
194
201
  <SettingsField label="OG Image" hint="Social sharing image path">
195
202
  <input
196
203
  type="text"
197
- value={store.metadata.og_image_path || ""}
204
+ value={metadata.og_image_path || ""}
198
205
  onChange={(e) =>
199
- store.setMetadata({ og_image_path: e.target.value })
206
+ setMetadata({ og_image_path: e.target.value })
200
207
  }
201
208
  className={INPUT_CLASS}
202
209
  placeholder="og/page-image.jpg"
@@ -55,7 +55,7 @@ interface ParallaxGroupSettingsProps {
55
55
  export default function ParallaxGroupSettings({
56
56
  group,
57
57
  }: ParallaxGroupSettingsProps) {
58
- const store = useBuilderStore();
58
+ const updateParallaxGroupSettings = useBuilderStore((s) => s.updateParallaxGroupSettings);
59
59
  const activeEffect = group.transition_effect || "parallax";
60
60
 
61
61
  return (
@@ -68,7 +68,7 @@ export default function ParallaxGroupSettings({
68
68
  <button
69
69
  key={effect.value}
70
70
  onClick={() =>
71
- store.updateParallaxGroupSettings(group._key, {
71
+ updateParallaxGroupSettings(group._key, {
72
72
  transition_effect: effect.value,
73
73
  })
74
74
  }
@@ -60,11 +60,11 @@ export default function ParallaxSlideSettings({
60
60
  group,
61
61
  slide,
62
62
  }: ParallaxSlideSettingsProps) {
63
- const store = useBuilderStore();
63
+ const updateParallaxSlideBackground = useBuilderStore((s) => s.updateParallaxSlideBackground);
64
64
  const paletteSwatches = usePaletteSwatches();
65
65
 
66
66
  const updateBg = (fields: Partial<ParallaxSlideV2>) => {
67
- store.updateParallaxSlideBackground(group._key, slide._key, fields);
67
+ updateParallaxSlideBackground(group._key, slide._key, fields);
68
68
  };
69
69
 
70
70
  const bgType = slide.background_type || "image";
@@ -22,17 +22,19 @@ import {
22
22
  } from "./responsive-helpers";
23
23
 
24
24
  export function SectionV2AnimationTab({ section }: { section: PageSectionV2 }) {
25
- const store = useBuilderStore();
26
- const activeViewport = store.activeViewport;
25
+ const activeViewport = useBuilderStore((s) => s.activeViewport);
26
+ const updateSectionV2Settings = useBuilderStore((s) => s.updateSectionV2Settings);
27
+ const updateSectionV2Responsive = useBuilderStore((s) => s.updateSectionV2Responsive);
28
+ const pageSettings = useBuilderStore((s) => s.pageSettings);
27
29
  const isResponsive = activeViewport !== "desktop";
28
30
 
29
31
  /** Viewport-aware update: desktop writes to settings, tablet/phone to responsive */
30
32
  const updateAnimSetting = (property: "enter_animation" | "stagger", value: unknown) => {
31
33
  if (activeViewport === "desktop") {
32
- store.updateSectionV2Settings(section._key, { [property]: value } as Partial<SectionV2SettingsType>);
34
+ updateSectionV2Settings(section._key, { [property]: value } as Partial<SectionV2SettingsType>);
33
35
  } else {
34
36
  const responsive = buildSectionV2SettingOverride(section, activeViewport, property, value);
35
- store.updateSectionV2Responsive(section._key, responsive ?? undefined);
37
+ updateSectionV2Responsive(section._key, responsive ?? undefined);
36
38
  }
37
39
  };
38
40
 
@@ -73,7 +75,7 @@ export function SectionV2AnimationTab({ section }: { section: PageSectionV2 }) {
73
75
  </div>
74
76
  )}
75
77
  <EnterAnimationPicker
76
- mode={{ level: "section", parentConfig: store.pageSettings.enter_animation }}
78
+ mode={{ level: "section", parentConfig: pageSettings.enter_animation }}
77
79
  config={effectiveEnterAnim}
78
80
  onChange={(cfg) => updateAnimSetting("enter_animation", cfg)}
79
81
  />
@@ -62,17 +62,21 @@ function SectionOverrideBadge({
62
62
  }
63
63
 
64
64
  export function SectionV2LayoutTab({ section }: { section: PageSectionV2 }) {
65
- const store = useBuilderStore();
65
+ const activeViewport = useBuilderStore((s) => s.activeViewport);
66
+ const updateSectionV2Settings = useBuilderStore((s) => s.updateSectionV2Settings);
67
+ const updateSectionV2Responsive = useBuilderStore((s) => s.updateSectionV2Responsive);
68
+ const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
69
+ const setColorPickerPreview = useBuilderStore((s) => s.setColorPickerPreview);
70
+ const clearColorPickerPreview = useBuilderStore((s) => s.clearColorPickerPreview);
66
71
  const paletteSwatches = usePaletteSwatches();
67
72
  const settings = section.settings;
68
- const activeViewport = store.activeViewport;
69
73
 
70
74
  // Live preview callbacks (Phase 4)
71
75
  const handleBgPreview = (val: import("../../../lib/sanity/types").ColorField) => {
72
- store.setColorPickerPreview({ sectionKey: section._key, field: "background_color", value: val });
76
+ setColorPickerPreview({ sectionKey: section._key, field: "background_color", value: val });
73
77
  };
74
78
  const handleBorderPreview = (val: import("../../../lib/sanity/types").ColorField) => {
75
- store.setColorPickerPreview({ sectionKey: section._key, field: "border_color", value: val });
79
+ setColorPickerPreview({ sectionKey: section._key, field: "border_color", value: val });
76
80
  };
77
81
 
78
82
  const viewportLabel = activeViewport !== "desktop"
@@ -85,7 +89,7 @@ export function SectionV2LayoutTab({ section }: { section: PageSectionV2 }) {
85
89
  */
86
90
  const updateSettingResponsive = (property: string, value: unknown) => {
87
91
  if (activeViewport === "desktop") {
88
- store.updateSectionV2Settings(section._key, { [property]: value } as Partial<SectionV2SettingsType>);
92
+ updateSectionV2Settings(section._key, { [property]: value } as Partial<SectionV2SettingsType>);
89
93
  } else {
90
94
  const existing = section.responsive || {};
91
95
  const vp = activeViewport as "tablet" | "phone";
@@ -95,7 +99,7 @@ export function SectionV2LayoutTab({ section }: { section: PageSectionV2 }) {
95
99
  const responsive = { ...existing, [vp]: vpOverride };
96
100
  // Clean up empty viewport override
97
101
  if (!vpOverride.columns?.length && !vpOverride.settings) delete responsive[vp];
98
- store.updateSectionV2Responsive(section._key, Object.keys(responsive).length ? responsive : undefined);
102
+ updateSectionV2Responsive(section._key, Object.keys(responsive).length ? responsive : undefined);
99
103
  }
100
104
  };
101
105
 
@@ -183,7 +187,7 @@ export function SectionV2LayoutTab({ section }: { section: PageSectionV2 }) {
183
187
  <SettingsField label="Color">
184
188
  <ColorSwatchPicker
185
189
  value={parseColorField(getSettingValue<string>("background_color", ""))}
186
- onChange={(val) => { store.clearColorPickerPreview(); updateSettingResponsive("background_color", serializeColorField(val)); }}
190
+ onChange={(val) => { clearColorPickerPreview(); updateSettingResponsive("background_color", serializeColorField(val)); }}
187
191
  swatches={paletteSwatches}
188
192
  allowGradients
189
193
  onPreview={handleBgPreview}
@@ -215,7 +219,7 @@ export function SectionV2LayoutTab({ section }: { section: PageSectionV2 }) {
215
219
  <SettingsField label="Image">
216
220
  <AssetPathInput
217
221
  value={getSettingValue<string>("background_image", "")}
218
- onFocus={() => store._pushSnapshot()}
222
+ onFocus={() => _pushSnapshot()}
219
223
  onChange={(v) => updateSettingResponsive("background_image", v)}
220
224
  placeholder="path/to/image.jpg"
221
225
  filterType="image"
@@ -277,7 +281,7 @@ export function SectionV2LayoutTab({ section }: { section: PageSectionV2 }) {
277
281
  <SettingsField label="Color">
278
282
  <ColorSwatchPicker
279
283
  value={parseColorField(getSettingValue<string>("border_color", ""))}
280
- onChange={(val) => { store.clearColorPickerPreview(); updateSettingResponsive("border_color", serializeColorField(val)); }}
284
+ onChange={(val) => { clearColorPickerPreview(); updateSettingResponsive("border_color", serializeColorField(val)); }}
281
285
  swatches={paletteSwatches}
282
286
  allowGradients
283
287
  onPreview={handleBorderPreview}