@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.
- package/app/admin/assets/page.tsx +3 -2
- package/app/admin/layout.tsx +4 -0
- package/components/admin/nav-builder/NavBuilder.tsx +2 -1
- package/components/admin/styles/FontsEditor.tsx +2 -1
- package/components/builder/CoverSectionCanvas.tsx +7 -6
- package/components/builder/SettingsPanel.tsx +14 -8
- package/components/builder/SortableBlock.tsx +4 -0
- package/components/builder/SortableRow.tsx +2 -0
- package/components/builder/asset-browser/useR2Operations.ts +5 -4
- package/components/builder/editors/AudioBlockEditor.tsx +10 -8
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +10 -8
- package/components/builder/editors/ButtonBlockEditor.tsx +9 -7
- package/components/builder/editors/ImageBlockEditor.tsx +10 -8
- package/components/builder/editors/ImageGridBlockEditor.tsx +10 -8
- package/components/builder/editors/SpacerBlockEditor.tsx +4 -4
- package/components/builder/editors/TextBlockEditor.tsx +471 -468
- package/components/builder/editors/VideoBlockEditor.tsx +10 -8
- package/components/builder/settings-panel/AnimationTab.tsx +11 -8
- package/components/builder/settings-panel/BlockLayoutTab.tsx +514 -511
- package/components/builder/settings-panel/ColumnV2AnimationTab.tsx +2 -2
- package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +11 -8
- package/components/builder/settings-panel/ColumnV2Settings.tsx +6 -5
- package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +4 -3
- package/components/builder/settings-panel/CoverSectionSettings.tsx +14 -9
- package/components/builder/settings-panel/CustomSectionSettings.tsx +9 -7
- package/components/builder/settings-panel/PageSettings.tsx +39 -32
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +2 -2
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +7 -5
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +13 -9
- package/components/builder/settings-panel/SectionV2Settings.tsx +7 -6
- package/components/builder/settings-panel/TRBLInputs.tsx +2 -2
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +16 -13
- package/components/ui/ToastStack.tsx +142 -0
- package/lib/auth-token.ts +5 -1
- package/lib/bot-guard.ts +6 -0
- package/lib/builder/constants.ts +0 -7
- package/lib/toast/index.ts +56 -0
- package/lib/toast/store.ts +56 -0
- package/lib/version.ts +1 -1
- package/package.json +3 -1
|
@@ -18,14 +18,14 @@ export function ColumnV2AnimationTab({
|
|
|
18
18
|
section: PageSectionV2;
|
|
19
19
|
column: SectionColumn;
|
|
20
20
|
}) {
|
|
21
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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={() =>
|
|
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
|
-
|
|
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
|
|
40
|
-
const
|
|
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
|
-
|
|
70
|
+
updateSectionV2Responsive(section._key, responsive ?? undefined);
|
|
70
71
|
} else {
|
|
71
72
|
// Desktop: use cascade engine
|
|
72
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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={() =>
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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={() =>
|
|
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={() =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
45
|
+
updateCustomSectionInstanceTitle(instance._key, remoteTitle);
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
|
|
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
|
-
|
|
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,
|
|
64
|
+
}, [instance, sectionData, updateCustomSectionInstanceTitle, enterSectionEditor]);
|
|
63
65
|
|
|
64
66
|
const handleDetach = useCallback(() => {
|
|
65
67
|
if (!sectionData) return;
|
|
66
|
-
|
|
68
|
+
detachCustomSectionInstance(instance._key, sectionData);
|
|
67
69
|
setShowDetachConfirm(false);
|
|
68
|
-
}, [instance._key, sectionData,
|
|
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
|
|
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={
|
|
58
|
+
value={pageTitle}
|
|
53
59
|
onChange={(e) => {
|
|
54
60
|
const newTitle = e.target.value;
|
|
55
|
-
|
|
61
|
+
setPageTitle(newTitle);
|
|
56
62
|
// Auto-update slug from title unless user manually edited the slug
|
|
57
63
|
if (!slugManuallyEdited) {
|
|
58
|
-
|
|
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={
|
|
73
|
+
value={pageSlug}
|
|
68
74
|
onChange={(e) => {
|
|
69
75
|
setSlugManuallyEdited(true);
|
|
70
|
-
|
|
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
|
-
{
|
|
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(
|
|
86
|
-
onChange={(val) =>
|
|
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={
|
|
94
|
-
onChange={(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={
|
|
104
|
-
onChange={(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={
|
|
111
|
-
onChange={(e) =>
|
|
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
|
-
{
|
|
126
|
+
{pageSettings.nav_entrance_animation && (
|
|
121
127
|
<>
|
|
122
|
-
<SettingsField label={`Duration: ${
|
|
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={
|
|
129
|
-
onChange={(e) =>
|
|
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: ${
|
|
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={
|
|
140
|
-
onChange={(e) =>
|
|
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={() =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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={
|
|
178
|
-
onChange={(e) =>
|
|
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={
|
|
192
|
+
value={metadata.seo_description || ""}
|
|
186
193
|
onChange={(e) =>
|
|
187
|
-
|
|
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={
|
|
204
|
+
value={metadata.og_image_path || ""}
|
|
198
205
|
onChange={(e) =>
|
|
199
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
63
|
+
const updateParallaxSlideBackground = useBuilderStore((s) => s.updateParallaxSlideBackground);
|
|
64
64
|
const paletteSwatches = usePaletteSwatches();
|
|
65
65
|
|
|
66
66
|
const updateBg = (fields: Partial<ParallaxSlideV2>) => {
|
|
67
|
-
|
|
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
|
|
26
|
-
const
|
|
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
|
-
|
|
34
|
+
updateSectionV2Settings(section._key, { [property]: value } as Partial<SectionV2SettingsType>);
|
|
33
35
|
} else {
|
|
34
36
|
const responsive = buildSectionV2SettingOverride(section, activeViewport, property, value);
|
|
35
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
76
|
+
setColorPickerPreview({ sectionKey: section._key, field: "background_color", value: val });
|
|
73
77
|
};
|
|
74
78
|
const handleBorderPreview = (val: import("../../../lib/sanity/types").ColorField) => {
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
|
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={() =>
|
|
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) => {
|
|
284
|
+
onChange={(val) => { clearColorPickerPreview(); updateSettingResponsive("border_color", serializeColorField(val)); }}
|
|
281
285
|
swatches={paletteSwatches}
|
|
282
286
|
allowGradients
|
|
283
287
|
onPreview={handleBorderPreview}
|
|
@@ -164,9 +164,10 @@ function PresetGrid({ section }: { section: PageSectionV2 }) {
|
|
|
164
164
|
// ============================================
|
|
165
165
|
|
|
166
166
|
export default function SectionV2Settings({ section }: { section: PageSectionV2 }) {
|
|
167
|
-
const
|
|
167
|
+
const activeViewport = useBuilderStore((s) => s.activeViewport);
|
|
168
|
+
const updateSectionV2Settings = useBuilderStore((s) => s.updateSectionV2Settings);
|
|
169
|
+
const updateSectionV2Responsive = useBuilderStore((s) => s.updateSectionV2Responsive);
|
|
168
170
|
const settings = section.settings;
|
|
169
|
-
const activeViewport = store.activeViewport;
|
|
170
171
|
const isResponsive = activeViewport !== "desktop";
|
|
171
172
|
|
|
172
173
|
const hasColOverrides = hasAnyColumnV2Overrides(section, activeViewport);
|
|
@@ -176,10 +177,10 @@ export default function SectionV2Settings({ section }: { section: PageSectionV2
|
|
|
176
177
|
/** Viewport-aware update: desktop writes to settings, tablet/phone to responsive */
|
|
177
178
|
const updateSettingResponsive = (property: keyof SectionV2SettingsOverridable, value: unknown) => {
|
|
178
179
|
if (activeViewport === "desktop") {
|
|
179
|
-
|
|
180
|
+
updateSectionV2Settings(section._key, { [property]: value } as Partial<SectionV2SettingsType>);
|
|
180
181
|
} else {
|
|
181
182
|
const responsive = buildSectionV2SettingOverride(section, activeViewport, property, value);
|
|
182
|
-
|
|
183
|
+
updateSectionV2Responsive(section._key, responsive ?? undefined);
|
|
183
184
|
}
|
|
184
185
|
};
|
|
185
186
|
|
|
@@ -190,7 +191,7 @@ export default function SectionV2Settings({ section }: { section: PageSectionV2
|
|
|
190
191
|
const handleStack = () => {
|
|
191
192
|
const colOverrides = buildStackOverride(section);
|
|
192
193
|
const responsive = buildColumnV2Overrides(section, activeViewport, colOverrides);
|
|
193
|
-
|
|
194
|
+
updateSectionV2Responsive(section._key, responsive ?? undefined);
|
|
194
195
|
};
|
|
195
196
|
|
|
196
197
|
const handleReset = () => {
|
|
@@ -199,7 +200,7 @@ export default function SectionV2Settings({ section }: { section: PageSectionV2
|
|
|
199
200
|
const vp = activeViewport as "tablet" | "phone";
|
|
200
201
|
const responsive = { ...existing };
|
|
201
202
|
delete responsive[vp];
|
|
202
|
-
|
|
203
|
+
updateSectionV2Responsive(section._key, Object.keys(responsive).length ? responsive : undefined);
|
|
203
204
|
};
|
|
204
205
|
|
|
205
206
|
return (
|