@morphika/andami 0.1.8 → 0.1.10
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/README.md +3 -0
- package/components/admin/nav-builder/NavBuilder.tsx +90 -14
- package/components/admin/nav-builder/NavGeneralSettings.tsx +521 -271
- package/components/admin/nav-builder/NavItemSettings.tsx +331 -312
- package/components/admin/nav-builder/NavMobileSettings.tsx +159 -140
- package/components/admin/nav-builder/NavSettingsFields.tsx +287 -21
- package/components/admin/nav-builder/NavSettingsPanel.tsx +137 -127
- package/components/blocks/TextBlockRenderer.tsx +1 -1
- package/components/builder/SettingsPanel.tsx +29 -543
- package/components/builder/editors/ButtonBlockEditor.tsx +8 -3
- package/components/builder/editors/CoverBlockEditor.tsx +14 -6
- package/components/builder/editors/ImageBlockEditor.tsx +8 -3
- package/components/builder/editors/ImageGridBlockEditor.tsx +8 -3
- package/components/builder/editors/ProjectGridEditor.tsx +7 -46
- package/components/builder/editors/SpacerBlockEditor.tsx +4 -1
- package/components/builder/editors/StaggerSettings.tsx +2 -1
- package/components/builder/editors/TextBlockEditor.tsx +8 -3
- package/components/builder/editors/VideoBlockEditor.tsx +10 -4
- package/components/builder/editors/section-icons.tsx +492 -0
- package/components/builder/editors/shared.tsx +23 -4
- package/components/builder/live-preview/GhostCard.tsx +84 -0
- package/components/builder/live-preview/LiveProjectGridPreview.tsx +294 -1010
- package/components/builder/live-preview/LiveTextEditor.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -0
- package/components/builder/live-preview/drag-utils.tsx +89 -0
- package/components/builder/live-preview/useDragReorder.ts +370 -0
- package/components/builder/settings-panel/AnimationTab.tsx +152 -0
- package/components/builder/settings-panel/BlockLayoutTab.tsx +13 -58
- package/components/builder/settings-panel/CardEntranceSection.tsx +114 -0
- package/components/builder/settings-panel/ColumnV2AnimationTab.tsx +32 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +4 -1
- package/components/builder/settings-panel/CustomSectionSettings.tsx +150 -0
- package/components/builder/settings-panel/LayoutTab.tsx +11 -47
- package/components/builder/settings-panel/PageSettings.tsx +10 -4
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +6 -2
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +8 -3
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +11 -47
- package/components/builder/settings-panel/SectionV2Settings.tsx +6 -27
- package/components/builder/settings-panel/index.ts +6 -0
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +184 -0
- package/components/ui/Navbar.tsx +151 -30
- package/lib/builder/serializer/migrations.ts +107 -0
- package/lib/builder/serializer/normalizers.ts +278 -0
- package/lib/builder/serializer/serializers.ts +393 -0
- package/lib/builder/serializer/shared.ts +102 -0
- package/lib/builder/serializer.ts +11 -846
- package/lib/sanity/types.ts +22 -0
- package/package.json +13 -10
- package/styles/base.css +7 -3
|
@@ -1,271 +1,521 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { NavDesign, NavDesignResponsiveOverride } from "../../../lib/sanity/types";
|
|
5
|
+
import { getSiteConfig } from "../../../lib/config";
|
|
6
|
+
import ColorSwatchPicker, { usePaletteSwatches } from "../../builder/ColorSwatchPicker";
|
|
7
|
+
import {
|
|
8
|
+
Field,
|
|
9
|
+
TextInput,
|
|
10
|
+
SelectInput,
|
|
11
|
+
SegmentedControl,
|
|
12
|
+
Toggle,
|
|
13
|
+
RangeSlider,
|
|
14
|
+
CardSection,
|
|
15
|
+
Divider,
|
|
16
|
+
PositionIcon,
|
|
17
|
+
TypographyIcon,
|
|
18
|
+
AnimationIcon,
|
|
19
|
+
SpacingIcon,
|
|
20
|
+
BackgroundIcon,
|
|
21
|
+
ViewportSwitcher,
|
|
22
|
+
NavViewportBadge,
|
|
23
|
+
ResponsiveField,
|
|
24
|
+
type NavViewport,
|
|
25
|
+
} from "./NavSettingsFields";
|
|
26
|
+
|
|
27
|
+
// ── Responsive helpers ──
|
|
28
|
+
|
|
29
|
+
/** Read a responsive-overridable field value for the given viewport.
|
|
30
|
+
* Desktop always returns the top-level design value.
|
|
31
|
+
* Non-desktop returns the viewport override (or undefined if inherited). */
|
|
32
|
+
function getResponsiveValue<K extends keyof NavDesignResponsiveOverride>(
|
|
33
|
+
design: NavDesign,
|
|
34
|
+
viewport: NavViewport,
|
|
35
|
+
field: K,
|
|
36
|
+
): NavDesignResponsiveOverride[K] | undefined {
|
|
37
|
+
if (viewport === "desktop") return design[field as keyof NavDesign] as NavDesignResponsiveOverride[K];
|
|
38
|
+
return design.responsive?.[viewport]?.[field] ?? undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Write a responsive-overridable field value for the given viewport.
|
|
42
|
+
* Desktop writes to the top-level field. Non-desktop writes into the
|
|
43
|
+
* responsive.[viewport] object. Pass `undefined` to clear an override. */
|
|
44
|
+
function setResponsiveValue<K extends keyof NavDesignResponsiveOverride>(
|
|
45
|
+
design: NavDesign,
|
|
46
|
+
viewport: NavViewport,
|
|
47
|
+
field: K,
|
|
48
|
+
value: NavDesignResponsiveOverride[K] | undefined,
|
|
49
|
+
): NavDesign {
|
|
50
|
+
if (viewport === "desktop") return { ...design, [field]: value };
|
|
51
|
+
const vpOverrides = { ...(design.responsive?.[viewport] ?? {}) };
|
|
52
|
+
if (value === undefined || value === null) {
|
|
53
|
+
delete vpOverrides[field];
|
|
54
|
+
} else {
|
|
55
|
+
(vpOverrides as Record<string, unknown>)[field] = value;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...design,
|
|
59
|
+
responsive: { ...(design.responsive ?? {}), [viewport]: vpOverrides },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Check if a field is overridden (has a non-null value) for this viewport */
|
|
64
|
+
function isFieldOverridden<K extends keyof NavDesignResponsiveOverride>(
|
|
65
|
+
design: NavDesign,
|
|
66
|
+
viewport: NavViewport,
|
|
67
|
+
field: K,
|
|
68
|
+
): boolean {
|
|
69
|
+
if (viewport === "desktop") return false;
|
|
70
|
+
const val = design.responsive?.[viewport]?.[field];
|
|
71
|
+
return val !== undefined && val !== null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Alignment icon SVGs for segmented controls ──
|
|
75
|
+
|
|
76
|
+
const AlignLeftIcon = (
|
|
77
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
78
|
+
<line x1="3" y1="6" x2="15" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="17" y2="18" />
|
|
79
|
+
</svg>
|
|
80
|
+
);
|
|
81
|
+
const AlignCenterIcon = (
|
|
82
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
83
|
+
<line x1="5" y1="6" x2="19" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="4" y1="18" x2="20" y2="18" />
|
|
84
|
+
</svg>
|
|
85
|
+
);
|
|
86
|
+
const AlignRightIcon = (
|
|
87
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
88
|
+
<line x1="9" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="7" y1="18" x2="21" y2="18" />
|
|
89
|
+
</svg>
|
|
90
|
+
);
|
|
91
|
+
const VAlignTopIcon = (
|
|
92
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
93
|
+
<line x1="12" y1="3" x2="12" y2="15" /><polyline points="8 7 12 3 16 7" /><line x1="4" y1="21" x2="20" y2="21" />
|
|
94
|
+
</svg>
|
|
95
|
+
);
|
|
96
|
+
const VAlignMiddleIcon = (
|
|
97
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
98
|
+
<line x1="4" y1="12" x2="8" y2="12" /><line x1="16" y1="12" x2="20" y2="12" /><rect x="9" y="6" width="6" height="12" rx="1" />
|
|
99
|
+
</svg>
|
|
100
|
+
);
|
|
101
|
+
const VAlignBottomIcon = (
|
|
102
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
103
|
+
<line x1="12" y1="9" x2="12" y2="21" /><polyline points="16 17 12 21 8 17" /><line x1="4" y1="3" x2="20" y2="3" />
|
|
104
|
+
</svg>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
interface NavGeneralSettingsProps {
|
|
108
|
+
design: NavDesign;
|
|
109
|
+
activeTab: "settings" | "layout" | "animation";
|
|
110
|
+
onChange: (design: NavDesign) => void;
|
|
111
|
+
fonts: string[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default function NavGeneralSettings({
|
|
115
|
+
design,
|
|
116
|
+
activeTab,
|
|
117
|
+
onChange,
|
|
118
|
+
fonts,
|
|
119
|
+
}: NavGeneralSettingsProps) {
|
|
120
|
+
const swatches = usePaletteSwatches();
|
|
121
|
+
const [typoViewport, setTypoViewport] = useState<NavViewport>("desktop");
|
|
122
|
+
const [spacingViewport, setSpacingViewport] = useState<NavViewport>("desktop");
|
|
123
|
+
|
|
124
|
+
const update = (partial: Partial<NavDesign>) =>
|
|
125
|
+
onChange({ ...design, ...partial });
|
|
126
|
+
|
|
127
|
+
/** Update a responsive-overridable field respecting the current viewport */
|
|
128
|
+
const updateResponsive = (
|
|
129
|
+
viewport: NavViewport,
|
|
130
|
+
field: keyof NavDesignResponsiveOverride,
|
|
131
|
+
value: NavDesignResponsiveOverride[keyof NavDesignResponsiveOverride],
|
|
132
|
+
) => onChange(setResponsiveValue(design, viewport, field, value));
|
|
133
|
+
|
|
134
|
+
/** Reset a viewport override back to inherited */
|
|
135
|
+
const resetResponsive = (
|
|
136
|
+
viewport: NavViewport,
|
|
137
|
+
field: keyof NavDesignResponsiveOverride,
|
|
138
|
+
) => onChange(setResponsiveValue(design, viewport, field, undefined));
|
|
139
|
+
|
|
140
|
+
/** Get the display value: viewport override if set, else desktop fallback */
|
|
141
|
+
const getDisplay = <K extends keyof NavDesignResponsiveOverride>(
|
|
142
|
+
viewport: NavViewport,
|
|
143
|
+
field: K,
|
|
144
|
+
fallback: NonNullable<NavDesignResponsiveOverride[K]>,
|
|
145
|
+
): NonNullable<NavDesignResponsiveOverride[K]> => {
|
|
146
|
+
const vpVal = getResponsiveValue(design, viewport, field);
|
|
147
|
+
if (vpVal !== undefined && vpVal !== null) return vpVal as NonNullable<NavDesignResponsiveOverride[K]>;
|
|
148
|
+
// Inherit from desktop
|
|
149
|
+
if (viewport !== "desktop") {
|
|
150
|
+
const desktopVal = design[field as keyof NavDesign];
|
|
151
|
+
if (desktopVal !== undefined && desktopVal !== null) return desktopVal as NonNullable<NavDesignResponsiveOverride[K]>;
|
|
152
|
+
}
|
|
153
|
+
return fallback;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// ── Settings tab ── (2-col grid on wide screens, 1-col on narrow)
|
|
157
|
+
if (activeTab === "settings") {
|
|
158
|
+
return (
|
|
159
|
+
<div className="grid grid-cols-1 min-[640px]:grid-cols-2 gap-0">
|
|
160
|
+
{/* Left column group */}
|
|
161
|
+
<div>
|
|
162
|
+
{/* Position card */}
|
|
163
|
+
<CardSection title="Position" icon={<PositionIcon />} iconBg="#ede9fe">
|
|
164
|
+
<Field label="Mode">
|
|
165
|
+
<SegmentedControl
|
|
166
|
+
value={design.position || "fixed"}
|
|
167
|
+
onChange={(v) => update({ position: v as NavDesign["position"] })}
|
|
168
|
+
options={[
|
|
169
|
+
{ value: "fixed", label: "Fixed" },
|
|
170
|
+
{ value: "sticky", label: "Sticky" },
|
|
171
|
+
{ value: "static", label: "Static" },
|
|
172
|
+
]}
|
|
173
|
+
/>
|
|
174
|
+
</Field>
|
|
175
|
+
<Field label="Hide">
|
|
176
|
+
<div className="flex items-center gap-2">
|
|
177
|
+
<Toggle
|
|
178
|
+
value={design.hide_on_scroll ?? true}
|
|
179
|
+
onChange={(v) => update({ hide_on_scroll: v })}
|
|
180
|
+
/>
|
|
181
|
+
<span className="text-[10px] text-neutral-400">on scroll</span>
|
|
182
|
+
</div>
|
|
183
|
+
</Field>
|
|
184
|
+
</CardSection>
|
|
185
|
+
|
|
186
|
+
{/* Typography card — responsive per viewport */}
|
|
187
|
+
<CardSection title="Typography" icon={<TypographyIcon />} iconBg="#dbeafe">
|
|
188
|
+
<ViewportSwitcher value={typoViewport} onChange={setTypoViewport} />
|
|
189
|
+
<NavViewportBadge viewport={typoViewport} />
|
|
190
|
+
|
|
191
|
+
{/* Font family — desktop only (not in responsive overrides) */}
|
|
192
|
+
{typoViewport === "desktop" && (
|
|
193
|
+
<Field label="Font">
|
|
194
|
+
<SelectInput
|
|
195
|
+
value={design.font_family || ""}
|
|
196
|
+
onChange={(v) => update({ font_family: v })}
|
|
197
|
+
options={[
|
|
198
|
+
{ value: "", label: `Default (${getSiteConfig().typography.defaultFont})` },
|
|
199
|
+
...fonts.map((f) => ({ value: f, label: f })),
|
|
200
|
+
]}
|
|
201
|
+
/>
|
|
202
|
+
</Field>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
<ResponsiveField
|
|
206
|
+
label="Size"
|
|
207
|
+
viewport={typoViewport}
|
|
208
|
+
isOverridden={isFieldOverridden(design, typoViewport, "font_size") || isFieldOverridden(design, typoViewport, "font_weight")}
|
|
209
|
+
onReset={() => { resetResponsive(typoViewport, "font_size"); resetResponsive(typoViewport, "font_weight"); }}
|
|
210
|
+
>
|
|
211
|
+
<div className="grid grid-cols-2 gap-1.5">
|
|
212
|
+
<TextInput
|
|
213
|
+
value={getDisplay(typoViewport, "font_size", 14)}
|
|
214
|
+
onChange={(v) => updateResponsive(typoViewport, "font_size", Math.max(8, Math.min(48, parseInt(v) || 14)))}
|
|
215
|
+
type="number"
|
|
216
|
+
/>
|
|
217
|
+
<SelectInput
|
|
218
|
+
value={getDisplay(typoViewport, "font_weight", "400")}
|
|
219
|
+
onChange={(v) => updateResponsive(typoViewport, "font_weight", v)}
|
|
220
|
+
options={[
|
|
221
|
+
{ value: "100", label: "Thin" },
|
|
222
|
+
{ value: "200", label: "Extra Light" },
|
|
223
|
+
{ value: "300", label: "Light" },
|
|
224
|
+
{ value: "400", label: "Normal" },
|
|
225
|
+
{ value: "500", label: "Medium" },
|
|
226
|
+
{ value: "600", label: "Semi Bold" },
|
|
227
|
+
{ value: "700", label: "Bold" },
|
|
228
|
+
{ value: "800", label: "Extra Bold" },
|
|
229
|
+
{ value: "900", label: "Black" },
|
|
230
|
+
]}
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
</ResponsiveField>
|
|
234
|
+
|
|
235
|
+
<ResponsiveField
|
|
236
|
+
label="Case"
|
|
237
|
+
viewport={typoViewport}
|
|
238
|
+
isOverridden={isFieldOverridden(design, typoViewport, "text_transform")}
|
|
239
|
+
onReset={() => resetResponsive(typoViewport, "text_transform")}
|
|
240
|
+
>
|
|
241
|
+
<SegmentedControl
|
|
242
|
+
value={getDisplay(typoViewport, "text_transform", "uppercase")}
|
|
243
|
+
onChange={(v) => updateResponsive(typoViewport, "text_transform", v as NavDesignResponsiveOverride["text_transform"])}
|
|
244
|
+
options={[
|
|
245
|
+
{ value: "uppercase", label: "AA" },
|
|
246
|
+
{ value: "capitalize", label: "Aa" },
|
|
247
|
+
{ value: "lowercase", label: "aa" },
|
|
248
|
+
{ value: "none", label: "—" },
|
|
249
|
+
]}
|
|
250
|
+
/>
|
|
251
|
+
</ResponsiveField>
|
|
252
|
+
|
|
253
|
+
<Divider />
|
|
254
|
+
|
|
255
|
+
<ResponsiveField
|
|
256
|
+
label="Align"
|
|
257
|
+
viewport={typoViewport}
|
|
258
|
+
isOverridden={isFieldOverridden(design, typoViewport, "text_align")}
|
|
259
|
+
onReset={() => resetResponsive(typoViewport, "text_align")}
|
|
260
|
+
>
|
|
261
|
+
<SegmentedControl
|
|
262
|
+
value={getDisplay(typoViewport, "text_align", "left")}
|
|
263
|
+
onChange={(v) => updateResponsive(typoViewport, "text_align", v as NavDesignResponsiveOverride["text_align"])}
|
|
264
|
+
options={[
|
|
265
|
+
{ value: "left", label: AlignLeftIcon },
|
|
266
|
+
{ value: "center", label: AlignCenterIcon },
|
|
267
|
+
{ value: "right", label: AlignRightIcon },
|
|
268
|
+
]}
|
|
269
|
+
/>
|
|
270
|
+
</ResponsiveField>
|
|
271
|
+
|
|
272
|
+
<ResponsiveField
|
|
273
|
+
label="V. Align"
|
|
274
|
+
viewport={typoViewport}
|
|
275
|
+
isOverridden={isFieldOverridden(design, typoViewport, "vertical_align")}
|
|
276
|
+
onReset={() => resetResponsive(typoViewport, "vertical_align")}
|
|
277
|
+
>
|
|
278
|
+
<SegmentedControl
|
|
279
|
+
value={getDisplay(typoViewport, "vertical_align", "top")}
|
|
280
|
+
onChange={(v) => updateResponsive(typoViewport, "vertical_align", v as NavDesignResponsiveOverride["vertical_align"])}
|
|
281
|
+
options={[
|
|
282
|
+
{ value: "top", label: VAlignTopIcon },
|
|
283
|
+
{ value: "middle", label: VAlignMiddleIcon },
|
|
284
|
+
{ value: "bottom", label: VAlignBottomIcon },
|
|
285
|
+
]}
|
|
286
|
+
/>
|
|
287
|
+
</ResponsiveField>
|
|
288
|
+
|
|
289
|
+
<Divider />
|
|
290
|
+
|
|
291
|
+
{/* Color — desktop only (global, not responsive) */}
|
|
292
|
+
{typoViewport === "desktop" && (
|
|
293
|
+
<Field label="Color">
|
|
294
|
+
<ColorSwatchPicker
|
|
295
|
+
value={design.color || ""}
|
|
296
|
+
onChange={(v) => update({ color: typeof v === "string" ? v : "" })}
|
|
297
|
+
swatches={swatches}
|
|
298
|
+
/>
|
|
299
|
+
</Field>
|
|
300
|
+
)}
|
|
301
|
+
</CardSection>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{/* Right column group */}
|
|
305
|
+
<div>
|
|
306
|
+
{/* Spacing card — responsive per viewport */}
|
|
307
|
+
<CardSection title="Spacing" icon={<SpacingIcon />} iconBg="#fef3c7">
|
|
308
|
+
<ViewportSwitcher value={spacingViewport} onChange={setSpacingViewport} />
|
|
309
|
+
<NavViewportBadge viewport={spacingViewport} />
|
|
310
|
+
|
|
311
|
+
{/* Box model visual */}
|
|
312
|
+
<div className="relative border border-dashed border-neutral-200 rounded-lg p-4 mb-2">
|
|
313
|
+
<div className="absolute top-1 left-1/2 -translate-x-1/2 text-[8px] text-neutral-300 uppercase tracking-[1px]">
|
|
314
|
+
margin
|
|
315
|
+
</div>
|
|
316
|
+
<div className="border border-blue-100 rounded-md p-3 bg-blue-50/30 relative">
|
|
317
|
+
<div className="absolute top-0.5 left-1/2 -translate-x-1/2 text-[8px] text-blue-300 uppercase tracking-[1px]">
|
|
318
|
+
padding
|
|
319
|
+
</div>
|
|
320
|
+
<div className="bg-blue-200/30 rounded h-3 mt-1" />
|
|
321
|
+
<div className="grid grid-cols-2 gap-1.5 mt-2">
|
|
322
|
+
<div className="text-center">
|
|
323
|
+
<div className="text-[8px] text-blue-300 mb-0.5">H</div>
|
|
324
|
+
<ResponsiveField
|
|
325
|
+
label=""
|
|
326
|
+
viewport={spacingViewport}
|
|
327
|
+
isOverridden={isFieldOverridden(design, spacingViewport, "padding_h")}
|
|
328
|
+
onReset={() => resetResponsive(spacingViewport, "padding_h")}
|
|
329
|
+
>
|
|
330
|
+
<TextInput
|
|
331
|
+
value={getDisplay(spacingViewport, "padding_h", 24)}
|
|
332
|
+
onChange={(v) => updateResponsive(spacingViewport, "padding_h", Math.max(0, Math.min(120, parseInt(v) || 0)))}
|
|
333
|
+
type="number"
|
|
334
|
+
/>
|
|
335
|
+
</ResponsiveField>
|
|
336
|
+
</div>
|
|
337
|
+
<div className="text-center">
|
|
338
|
+
<div className="text-[8px] text-blue-300 mb-0.5">V</div>
|
|
339
|
+
<ResponsiveField
|
|
340
|
+
label=""
|
|
341
|
+
viewport={spacingViewport}
|
|
342
|
+
isOverridden={isFieldOverridden(design, spacingViewport, "padding_v")}
|
|
343
|
+
onReset={() => resetResponsive(spacingViewport, "padding_v")}
|
|
344
|
+
>
|
|
345
|
+
<TextInput
|
|
346
|
+
value={getDisplay(spacingViewport, "padding_v", 27)}
|
|
347
|
+
onChange={(v) => updateResponsive(spacingViewport, "padding_v", Math.max(0, Math.min(80, parseInt(v) || 0)))}
|
|
348
|
+
type="number"
|
|
349
|
+
/>
|
|
350
|
+
</ResponsiveField>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
<div className="grid grid-cols-2 gap-1.5 mt-2">
|
|
355
|
+
<div className="text-center">
|
|
356
|
+
<div className="text-[8px] text-neutral-300 mb-0.5">H</div>
|
|
357
|
+
<ResponsiveField
|
|
358
|
+
label=""
|
|
359
|
+
viewport={spacingViewport}
|
|
360
|
+
isOverridden={isFieldOverridden(design, spacingViewport, "margin_h")}
|
|
361
|
+
onReset={() => resetResponsive(spacingViewport, "margin_h")}
|
|
362
|
+
>
|
|
363
|
+
<TextInput
|
|
364
|
+
value={getDisplay(spacingViewport, "margin_h", 0)}
|
|
365
|
+
onChange={(v) => updateResponsive(spacingViewport, "margin_h", Math.max(0, Math.min(120, parseInt(v) || 0)))}
|
|
366
|
+
type="number"
|
|
367
|
+
/>
|
|
368
|
+
</ResponsiveField>
|
|
369
|
+
</div>
|
|
370
|
+
<div className="text-center">
|
|
371
|
+
<div className="text-[8px] text-neutral-300 mb-0.5">V</div>
|
|
372
|
+
<ResponsiveField
|
|
373
|
+
label=""
|
|
374
|
+
viewport={spacingViewport}
|
|
375
|
+
isOverridden={isFieldOverridden(design, spacingViewport, "margin_v")}
|
|
376
|
+
onReset={() => resetResponsive(spacingViewport, "margin_v")}
|
|
377
|
+
>
|
|
378
|
+
<TextInput
|
|
379
|
+
value={getDisplay(spacingViewport, "margin_v", 0)}
|
|
380
|
+
onChange={(v) => updateResponsive(spacingViewport, "margin_v", Math.max(0, Math.min(80, parseInt(v) || 0)))}
|
|
381
|
+
type="number"
|
|
382
|
+
/>
|
|
383
|
+
</ResponsiveField>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
<Divider />
|
|
388
|
+
<ResponsiveField
|
|
389
|
+
label="Gap"
|
|
390
|
+
viewport={spacingViewport}
|
|
391
|
+
isOverridden={isFieldOverridden(design, spacingViewport, "items_gap")}
|
|
392
|
+
onReset={() => resetResponsive(spacingViewport, "items_gap")}
|
|
393
|
+
>
|
|
394
|
+
<RangeSlider
|
|
395
|
+
value={getDisplay(spacingViewport, "items_gap", 32)}
|
|
396
|
+
onChange={(v) => updateResponsive(spacingViewport, "items_gap", v)}
|
|
397
|
+
min={4}
|
|
398
|
+
max={80}
|
|
399
|
+
suffix="px"
|
|
400
|
+
/>
|
|
401
|
+
</ResponsiveField>
|
|
402
|
+
</CardSection>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div className="h-2" />
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ── Animation tab ──
|
|
411
|
+
if (activeTab === "animation") {
|
|
412
|
+
return (
|
|
413
|
+
<>
|
|
414
|
+
<CardSection title="Entrance" icon={<AnimationIcon />} iconBg="#d1fae5">
|
|
415
|
+
<Field label="Effect">
|
|
416
|
+
<SelectInput
|
|
417
|
+
value={design.entrance_animation || ""}
|
|
418
|
+
onChange={(v) => update({ entrance_animation: v as NavDesign["entrance_animation"] })}
|
|
419
|
+
options={[
|
|
420
|
+
{ value: "", label: "None" },
|
|
421
|
+
{ value: "fade-in", label: "Fade In" },
|
|
422
|
+
{ value: "slide-down", label: "Slide Down" },
|
|
423
|
+
{ value: "blur-in", label: "Blur In" },
|
|
424
|
+
]}
|
|
425
|
+
/>
|
|
426
|
+
</Field>
|
|
427
|
+
{design.entrance_animation && (
|
|
428
|
+
<>
|
|
429
|
+
<Field label="Duration">
|
|
430
|
+
<RangeSlider
|
|
431
|
+
value={design.entrance_duration ?? 600}
|
|
432
|
+
onChange={(v) => update({ entrance_duration: v })}
|
|
433
|
+
min={200}
|
|
434
|
+
max={5000}
|
|
435
|
+
suffix="ms"
|
|
436
|
+
/>
|
|
437
|
+
</Field>
|
|
438
|
+
<Field label="Delay">
|
|
439
|
+
<RangeSlider
|
|
440
|
+
value={design.entrance_delay ?? 0}
|
|
441
|
+
onChange={(v) => update({ entrance_delay: v })}
|
|
442
|
+
min={0}
|
|
443
|
+
max={5000}
|
|
444
|
+
suffix="ms"
|
|
445
|
+
/>
|
|
446
|
+
</Field>
|
|
447
|
+
<Divider />
|
|
448
|
+
<Field label="Stagger">
|
|
449
|
+
<div className="flex items-center gap-2">
|
|
450
|
+
<Toggle
|
|
451
|
+
value={design.entrance_stagger ?? false}
|
|
452
|
+
onChange={(v) => update({ entrance_stagger: v })}
|
|
453
|
+
/>
|
|
454
|
+
<span className="text-[10px] text-neutral-400">per item</span>
|
|
455
|
+
</div>
|
|
456
|
+
</Field>
|
|
457
|
+
{design.entrance_stagger && (
|
|
458
|
+
<Field label="Interval">
|
|
459
|
+
<RangeSlider
|
|
460
|
+
value={design.entrance_stagger_delay ?? 80}
|
|
461
|
+
onChange={(v) => update({ entrance_stagger_delay: v })}
|
|
462
|
+
min={20}
|
|
463
|
+
max={300}
|
|
464
|
+
suffix="ms"
|
|
465
|
+
/>
|
|
466
|
+
</Field>
|
|
467
|
+
)}
|
|
468
|
+
</>
|
|
469
|
+
)}
|
|
470
|
+
</CardSection>
|
|
471
|
+
|
|
472
|
+
{!design.entrance_animation && (
|
|
473
|
+
<div className="mx-4 mt-2 px-3 py-4 rounded-lg bg-neutral-50 text-center">
|
|
474
|
+
<p className="text-[11px] text-neutral-400">
|
|
475
|
+
Select an entrance effect to configure animation timing.
|
|
476
|
+
</p>
|
|
477
|
+
</div>
|
|
478
|
+
)}
|
|
479
|
+
|
|
480
|
+
<div className="h-2" />
|
|
481
|
+
</>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ── Layout tab ──
|
|
486
|
+
return (
|
|
487
|
+
<>
|
|
488
|
+
<CardSection title="Background" icon={<BackgroundIcon />} iconBg="#e0e7ff">
|
|
489
|
+
<Field label="Color">
|
|
490
|
+
<ColorSwatchPicker
|
|
491
|
+
value={design.background_color || ""}
|
|
492
|
+
onChange={(v) => update({ background_color: typeof v === "string" ? v : "" })}
|
|
493
|
+
swatches={swatches}
|
|
494
|
+
/>
|
|
495
|
+
</Field>
|
|
496
|
+
{design.background_color && (
|
|
497
|
+
<Field label="Opacity">
|
|
498
|
+
<RangeSlider
|
|
499
|
+
value={design.background_opacity ?? 100}
|
|
500
|
+
onChange={(v) => update({ background_opacity: v })}
|
|
501
|
+
min={0}
|
|
502
|
+
max={100}
|
|
503
|
+
suffix="%"
|
|
504
|
+
/>
|
|
505
|
+
</Field>
|
|
506
|
+
)}
|
|
507
|
+
<Field label="Blur">
|
|
508
|
+
<div className="flex items-center gap-2">
|
|
509
|
+
<Toggle
|
|
510
|
+
value={design.backdrop_blur ?? false}
|
|
511
|
+
onChange={(v) => update({ backdrop_blur: v })}
|
|
512
|
+
/>
|
|
513
|
+
<span className="text-[10px] text-neutral-400">backdrop blur</span>
|
|
514
|
+
</div>
|
|
515
|
+
</Field>
|
|
516
|
+
</CardSection>
|
|
517
|
+
|
|
518
|
+
<div className="h-2" />
|
|
519
|
+
</>
|
|
520
|
+
);
|
|
521
|
+
}
|