@morphika/andami 0.1.9 → 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/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/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/LiveTextEditor.tsx +1 -1
- package/components/builder/settings-panel/BlockLayoutTab.tsx +13 -58
- package/components/builder/settings-panel/ColumnV2Settings.tsx +4 -1
- 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/ui/Navbar.tsx +151 -30
- package/lib/sanity/types.ts +22 -0
- package/package.json +5 -2
- package/styles/base.css +7 -3
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
import { useBuilderStore } from "../../../lib/builder/store";
|
|
18
18
|
import type { PageSectionV2, SectionColumn } from "../../../lib/sanity/types";
|
|
19
|
+
import {
|
|
20
|
+
ColumnSizeIcon,
|
|
21
|
+
} from "../editors/section-icons";
|
|
19
22
|
import {
|
|
20
23
|
SettingsField,
|
|
21
24
|
SettingsSection,
|
|
@@ -90,7 +93,7 @@ export default function ColumnV2Settings({
|
|
|
90
93
|
</div>
|
|
91
94
|
)}
|
|
92
95
|
|
|
93
|
-
<SettingsSection title="Column Size" defaultOpen>
|
|
96
|
+
<SettingsSection title="Column Size" defaultOpen icon={<ColumnSizeIcon />}>
|
|
94
97
|
<SettingsField label={
|
|
95
98
|
<span>
|
|
96
99
|
Span
|
|
@@ -29,49 +29,13 @@ import {
|
|
|
29
29
|
} from "./responsive-helpers";
|
|
30
30
|
import { TRBLInputs } from "./TRBLInputs";
|
|
31
31
|
|
|
32
|
-
// ── Section title icons (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<path d="M7 10.5 L7 13" stroke="currentColor" strokeWidth="0.8" opacity="0.7" />
|
|
40
|
-
<path d="M1 7 L3.5 7" stroke="currentColor" strokeWidth="0.8" opacity="0.7" />
|
|
41
|
-
<path d="M10.5 7 L13 7" stroke="currentColor" strokeWidth="0.8" opacity="0.7" />
|
|
42
|
-
</svg>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function OffsetSectionIcon() {
|
|
47
|
-
return (
|
|
48
|
-
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
49
|
-
<rect x="3" y="3" width="8" height="8" rx="1" stroke="currentColor" strokeWidth="0.8" strokeDasharray="2 1" fill="none" opacity="0.35" />
|
|
50
|
-
<rect x="5" y="5" width="6" height="6" rx="1" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.7" />
|
|
51
|
-
<path d="M4 4 L5 5" stroke="currentColor" strokeWidth="0.6" opacity="0.5" />
|
|
52
|
-
</svg>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function BackgroundSectionIcon() {
|
|
57
|
-
return (
|
|
58
|
-
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
59
|
-
<rect x="1.5" y="1.5" width="11" height="11" rx="2" fill="currentColor" opacity="0.15" />
|
|
60
|
-
<rect x="1.5" y="1.5" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="0.8" opacity="0.5" fill="none" />
|
|
61
|
-
<circle cx="5" cy="5" r="1.5" fill="currentColor" opacity="0.5" />
|
|
62
|
-
<path d="M1.5 10 L5 7 L8 9 L10.5 6.5 L12.5 8.5 L12.5 11 C12.5 11.8 11.8 12.5 11 12.5 L3 12.5 C2.2 12.5 1.5 11.8 1.5 11 Z" fill="currentColor" opacity="0.3" />
|
|
63
|
-
</svg>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function BorderSectionIcon() {
|
|
68
|
-
return (
|
|
69
|
-
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
70
|
-
<rect x="2" y="2" width="10" height="10" rx="2" stroke="currentColor" strokeWidth="1.2" fill="none" opacity="0.6" />
|
|
71
|
-
<rect x="2" y="2" width="10" height="1.2" rx="0.5" fill="currentColor" opacity="0.7" />
|
|
72
|
-
</svg>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
32
|
+
// ── Section title icons (centralized colored icons — Session 163) ──
|
|
33
|
+
import {
|
|
34
|
+
SpacingIcon,
|
|
35
|
+
OffsetIcon,
|
|
36
|
+
BackgroundIcon,
|
|
37
|
+
BorderIcon,
|
|
38
|
+
} from "../editors/section-icons";
|
|
75
39
|
|
|
76
40
|
/**
|
|
77
41
|
* BUG-007 fix: LayoutTab now handles PageSection styling (spacing, background, border).
|
|
@@ -141,7 +105,7 @@ export function LayoutTab({ section, sectionKey }: { section: PageSection; secti
|
|
|
141
105
|
)}
|
|
142
106
|
|
|
143
107
|
{/* Spacing (Padding) */}
|
|
144
|
-
<SettingsSection title="Spacing" defaultOpen icon={<
|
|
108
|
+
<SettingsSection title="Spacing" defaultOpen icon={<SpacingIcon />}>
|
|
145
109
|
<TRBLInputs
|
|
146
110
|
top={effectiveSpacingTop}
|
|
147
111
|
right={effectiveSpacingRight}
|
|
@@ -192,7 +156,7 @@ export function LayoutTab({ section, sectionKey }: { section: PageSection; secti
|
|
|
192
156
|
</SettingsSection>
|
|
193
157
|
|
|
194
158
|
{/* Offset (Margin) */}
|
|
195
|
-
<SettingsSection title="Offset" icon={<
|
|
159
|
+
<SettingsSection title="Offset" icon={<OffsetIcon />}>
|
|
196
160
|
<TRBLInputs
|
|
197
161
|
top={getRowSettingValue<string>(section, activeViewport, "offset_top", "0")}
|
|
198
162
|
right={getRowSettingValue<string>(section, activeViewport, "offset_right", "0")}
|
|
@@ -205,7 +169,7 @@ export function LayoutTab({ section, sectionKey }: { section: PageSection; secti
|
|
|
205
169
|
</SettingsSection>
|
|
206
170
|
|
|
207
171
|
{/* Background */}
|
|
208
|
-
<SettingsSection title="Background" defaultOpen icon={<
|
|
172
|
+
<SettingsSection title="Background" defaultOpen icon={<BackgroundIcon />}>
|
|
209
173
|
<SettingsField label="Color">
|
|
210
174
|
<ColorSwatchPicker
|
|
211
175
|
value={parseColorField(getRowSettingValue<string>(section, activeViewport, "background_color", ""))}
|
|
@@ -305,7 +269,7 @@ export function LayoutTab({ section, sectionKey }: { section: PageSection; secti
|
|
|
305
269
|
</SettingsSection>
|
|
306
270
|
|
|
307
271
|
{/* Border */}
|
|
308
|
-
<SettingsSection title="Border" icon={<
|
|
272
|
+
<SettingsSection title="Border" icon={<BorderIcon />}>
|
|
309
273
|
<SettingsField label="Color">
|
|
310
274
|
<ColorSwatchPicker
|
|
311
275
|
value={parseColorField(getRowSettingValue<string>(section, activeViewport, "border_color", ""))}
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
|
|
12
12
|
import { useState } from "react";
|
|
13
13
|
import { useBuilderStore } from "../../../lib/builder/store";
|
|
14
|
+
import {
|
|
15
|
+
GeneralIcon,
|
|
16
|
+
AppearanceIcon,
|
|
17
|
+
NavigationIcon,
|
|
18
|
+
SEOIcon,
|
|
19
|
+
} from "../editors/section-icons";
|
|
14
20
|
import {
|
|
15
21
|
SettingsField,
|
|
16
22
|
SettingsSection,
|
|
@@ -39,7 +45,7 @@ export default function PageSettings() {
|
|
|
39
45
|
|
|
40
46
|
return (
|
|
41
47
|
<>
|
|
42
|
-
<SettingsSection title="General" defaultOpen>
|
|
48
|
+
<SettingsSection title="General" defaultOpen icon={<GeneralIcon />}>
|
|
43
49
|
<SettingsField label="Title">
|
|
44
50
|
<input
|
|
45
51
|
type="text"
|
|
@@ -73,7 +79,7 @@ export default function PageSettings() {
|
|
|
73
79
|
</SettingsField>
|
|
74
80
|
</SettingsSection>
|
|
75
81
|
|
|
76
|
-
<SettingsSection title="Appearance" defaultOpen>
|
|
82
|
+
<SettingsSection title="Appearance" defaultOpen icon={<AppearanceIcon />}>
|
|
77
83
|
<SettingsField label="Background">
|
|
78
84
|
<ColorSwatchPicker
|
|
79
85
|
value={parseColorField(store.pageSettings.background_color || "")}
|
|
@@ -91,7 +97,7 @@ export default function PageSettings() {
|
|
|
91
97
|
</SettingsField>
|
|
92
98
|
</SettingsSection>
|
|
93
99
|
|
|
94
|
-
<SettingsSection title="Navigation">
|
|
100
|
+
<SettingsSection title="Navigation" icon={<NavigationIcon />}>
|
|
95
101
|
<SettingsField label="Nav Color">
|
|
96
102
|
<ColorSwatchPicker
|
|
97
103
|
value={store.pageSettings.nav_color || ""}
|
|
@@ -164,7 +170,7 @@ export function PageSeoSettings() {
|
|
|
164
170
|
|
|
165
171
|
return (
|
|
166
172
|
<>
|
|
167
|
-
<SettingsSection title="SEO" defaultOpen>
|
|
173
|
+
<SettingsSection title="SEO" defaultOpen icon={<SEOIcon />}>
|
|
168
174
|
<SettingsField label="SEO Title">
|
|
169
175
|
<input
|
|
170
176
|
type="text"
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
import { useBuilderStore } from "../../../lib/builder/store";
|
|
16
16
|
import type { ParallaxGroup } from "../../../lib/sanity/types";
|
|
17
|
+
import {
|
|
18
|
+
TransitionIcon,
|
|
19
|
+
InfoIcon,
|
|
20
|
+
} from "../editors/section-icons";
|
|
17
21
|
import {
|
|
18
22
|
SettingsSection,
|
|
19
23
|
} from "../editors/shared";
|
|
@@ -56,7 +60,7 @@ export default function ParallaxGroupSettings({
|
|
|
56
60
|
|
|
57
61
|
return (
|
|
58
62
|
<>
|
|
59
|
-
<SettingsSection title="Transition Effect" defaultOpen>
|
|
63
|
+
<SettingsSection title="Transition Effect" defaultOpen icon={<TransitionIcon />}>
|
|
60
64
|
<div className="space-y-1.5">
|
|
61
65
|
{TRANSITION_EFFECTS.map((effect) => {
|
|
62
66
|
const isActive = activeEffect === effect.value;
|
|
@@ -103,7 +107,7 @@ export default function ParallaxGroupSettings({
|
|
|
103
107
|
</SettingsSection>
|
|
104
108
|
|
|
105
109
|
{/* Group info */}
|
|
106
|
-
<SettingsSection title="Info">
|
|
110
|
+
<SettingsSection title="Info" icon={<InfoIcon />}>
|
|
107
111
|
<div className="text-[11px] text-neutral-500 space-y-1">
|
|
108
112
|
<p>
|
|
109
113
|
{group.slides.length} slide{group.slides.length !== 1 ? "s" : ""}
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
|
|
20
20
|
import { useBuilderStore } from "../../../lib/builder/store";
|
|
21
21
|
import type { ParallaxSlideV2, ParallaxGroup, PageSectionV2 } from "../../../lib/sanity/types";
|
|
22
|
+
import {
|
|
23
|
+
BackgroundIcon,
|
|
24
|
+
NavbarColorIcon,
|
|
25
|
+
OverlayIcon,
|
|
26
|
+
} from "../editors/section-icons";
|
|
22
27
|
import {
|
|
23
28
|
SettingsField,
|
|
24
29
|
SettingsSection,
|
|
@@ -70,7 +75,7 @@ export default function ParallaxSlideSettings({
|
|
|
70
75
|
return (
|
|
71
76
|
<>
|
|
72
77
|
{/* Background Type */}
|
|
73
|
-
<SettingsSection title="Background" defaultOpen>
|
|
78
|
+
<SettingsSection title="Background" defaultOpen icon={<BackgroundIcon />}>
|
|
74
79
|
{/* Segmented control: Image / Video */}
|
|
75
80
|
<SettingsField label="Type">
|
|
76
81
|
<div className="flex rounded-lg bg-[#f0f0f0] p-[3px]">
|
|
@@ -123,7 +128,7 @@ export default function ParallaxSlideSettings({
|
|
|
123
128
|
</SettingsSection>
|
|
124
129
|
|
|
125
130
|
{/* Navbar Color Override */}
|
|
126
|
-
<SettingsSection title="Navbar Color" defaultOpen={false}>
|
|
131
|
+
<SettingsSection title="Navbar Color" defaultOpen={false} icon={<NavbarColorIcon />}>
|
|
127
132
|
<SettingsField label="Color">
|
|
128
133
|
<div className="flex items-center gap-2">
|
|
129
134
|
<ColorSwatchPicker
|
|
@@ -147,7 +152,7 @@ export default function ParallaxSlideSettings({
|
|
|
147
152
|
</SettingsSection>
|
|
148
153
|
|
|
149
154
|
{/* Overlay */}
|
|
150
|
-
<SettingsSection title="Overlay" defaultOpen>
|
|
155
|
+
<SettingsSection title="Overlay" defaultOpen icon={<OverlayIcon />}>
|
|
151
156
|
<SettingsField label="Color">
|
|
152
157
|
<ColorSwatchPicker
|
|
153
158
|
value={overlayColor}
|
|
@@ -25,49 +25,13 @@ import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
|
|
|
25
25
|
import { serializeColorField, parseColorField, isGradient } from "../../../lib/color-utils";
|
|
26
26
|
import { TRBLInputs } from "./TRBLInputs";
|
|
27
27
|
|
|
28
|
-
// ── Section title icons (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<path d="M7 10.5 L7 13" stroke="currentColor" strokeWidth="0.8" opacity="0.7" />
|
|
36
|
-
<path d="M1 7 L3.5 7" stroke="currentColor" strokeWidth="0.8" opacity="0.7" />
|
|
37
|
-
<path d="M10.5 7 L13 7" stroke="currentColor" strokeWidth="0.8" opacity="0.7" />
|
|
38
|
-
</svg>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function BackgroundSectionIcon() {
|
|
43
|
-
return (
|
|
44
|
-
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
45
|
-
<rect x="1.5" y="1.5" width="11" height="11" rx="2" fill="currentColor" opacity="0.15" />
|
|
46
|
-
<rect x="1.5" y="1.5" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="0.8" opacity="0.5" fill="none" />
|
|
47
|
-
<circle cx="5" cy="5" r="1.5" fill="currentColor" opacity="0.5" />
|
|
48
|
-
<path d="M1.5 10 L5 7 L8 9 L10.5 6.5 L12.5 8.5 L12.5 11 C12.5 11.8 11.8 12.5 11 12.5 L3 12.5 C2.2 12.5 1.5 11.8 1.5 11 Z" fill="currentColor" opacity="0.3" />
|
|
49
|
-
</svg>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function OffsetSectionIcon() {
|
|
54
|
-
return (
|
|
55
|
-
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
56
|
-
<rect x="3" y="3" width="8" height="8" rx="1" stroke="currentColor" strokeWidth="0.8" strokeDasharray="2 1" fill="none" opacity="0.35" />
|
|
57
|
-
<rect x="5" y="5" width="6" height="6" rx="1" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.7" />
|
|
58
|
-
<path d="M4 4 L5 5" stroke="currentColor" strokeWidth="0.6" opacity="0.5" />
|
|
59
|
-
</svg>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function BorderSectionIcon() {
|
|
64
|
-
return (
|
|
65
|
-
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
66
|
-
<rect x="2" y="2" width="10" height="10" rx="2" stroke="currentColor" strokeWidth="1.2" fill="none" opacity="0.6" />
|
|
67
|
-
<rect x="2" y="2" width="10" height="1.2" rx="0.5" fill="currentColor" opacity="0.7" />
|
|
68
|
-
</svg>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
28
|
+
// ── Section title icons (centralized colored icons — Session 163) ──
|
|
29
|
+
import {
|
|
30
|
+
SpacingIcon,
|
|
31
|
+
OffsetIcon,
|
|
32
|
+
BackgroundIcon,
|
|
33
|
+
BorderIcon,
|
|
34
|
+
} from "../editors/section-icons";
|
|
71
35
|
|
|
72
36
|
// ── Override indicator badge (matching BlockLayoutTab pattern) ──
|
|
73
37
|
|
|
@@ -179,7 +143,7 @@ export function SectionV2LayoutTab({ section }: { section: PageSectionV2 }) {
|
|
|
179
143
|
)}
|
|
180
144
|
|
|
181
145
|
{/* Spacing (Padding) */}
|
|
182
|
-
<SettingsSection title="Spacing" defaultOpen icon={<
|
|
146
|
+
<SettingsSection title="Spacing" defaultOpen icon={<SpacingIcon />}>
|
|
183
147
|
<TRBLInputs
|
|
184
148
|
top={getSettingValue<string>("spacing_top", "0")}
|
|
185
149
|
right={getSettingValue<string>("spacing_right", "0")}
|
|
@@ -197,7 +161,7 @@ export function SectionV2LayoutTab({ section }: { section: PageSectionV2 }) {
|
|
|
197
161
|
</SettingsSection>
|
|
198
162
|
|
|
199
163
|
{/* Offset (Margin) */}
|
|
200
|
-
<SettingsSection title="Offset" icon={<
|
|
164
|
+
<SettingsSection title="Offset" icon={<OffsetIcon />}>
|
|
201
165
|
<TRBLInputs
|
|
202
166
|
top={getSettingValue<string>("offset_top", "0")}
|
|
203
167
|
right={getSettingValue<string>("offset_right", "0")}
|
|
@@ -215,7 +179,7 @@ export function SectionV2LayoutTab({ section }: { section: PageSectionV2 }) {
|
|
|
215
179
|
</SettingsSection>
|
|
216
180
|
|
|
217
181
|
{/* Background */}
|
|
218
|
-
<SettingsSection title="Background" defaultOpen icon={<
|
|
182
|
+
<SettingsSection title="Background" defaultOpen icon={<BackgroundIcon />}>
|
|
219
183
|
<SettingsField label="Color">
|
|
220
184
|
<ColorSwatchPicker
|
|
221
185
|
value={parseColorField(getSettingValue<string>("background_color", ""))}
|
|
@@ -309,7 +273,7 @@ export function SectionV2LayoutTab({ section }: { section: PageSectionV2 }) {
|
|
|
309
273
|
</SettingsSection>
|
|
310
274
|
|
|
311
275
|
{/* Border */}
|
|
312
|
-
<SettingsSection title="Border" icon={<
|
|
276
|
+
<SettingsSection title="Border" icon={<BorderIcon />}>
|
|
313
277
|
<SettingsField label="Color">
|
|
314
278
|
<ColorSwatchPicker
|
|
315
279
|
value={parseColorField(getSettingValue<string>("border_color", ""))}
|
|
@@ -50,32 +50,11 @@ const PRESETS: PresetOption[] = [
|
|
|
50
50
|
|
|
51
51
|
const CUSTOM_PRESET: PresetOption = { id: "custom", label: "Custom", cols: [], readonly: true };
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
60
|
-
<rect x="1.5" y="1.5" width="11" height="11" rx="1.5" stroke="currentColor" strokeWidth="0.8" fill="none" opacity="0.4" />
|
|
61
|
-
<line x1="6" y1="1.5" x2="6" y2="12.5" stroke="currentColor" strokeWidth="0.8" opacity="0.6" />
|
|
62
|
-
<rect x="1.5" y="1.5" width="4.5" height="11" rx="0" fill="currentColor" opacity="0.12" />
|
|
63
|
-
</svg>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function GridGapIcon() {
|
|
68
|
-
return (
|
|
69
|
-
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
70
|
-
<rect x="1" y="1" width="5" height="5" rx="1" fill="currentColor" opacity="0.25" />
|
|
71
|
-
<rect x="8" y="1" width="5" height="5" rx="1" fill="currentColor" opacity="0.25" />
|
|
72
|
-
<rect x="1" y="8" width="5" height="5" rx="1" fill="currentColor" opacity="0.25" />
|
|
73
|
-
<rect x="8" y="8" width="5" height="5" rx="1" fill="currentColor" opacity="0.25" />
|
|
74
|
-
<line x1="7" y1="1" x2="7" y2="13" stroke="currentColor" strokeWidth="0.8" strokeDasharray="1.5 1" opacity="0.5" />
|
|
75
|
-
<line x1="1" y1="7" x2="13" y2="7" stroke="currentColor" strokeWidth="0.8" strokeDasharray="1.5 1" opacity="0.5" />
|
|
76
|
-
</svg>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
53
|
+
// ── Section title icons (centralized colored icons — Session 163) ──
|
|
54
|
+
import {
|
|
55
|
+
LayoutPresetIcon,
|
|
56
|
+
GridGapsIcon,
|
|
57
|
+
} from "../editors/section-icons";
|
|
79
58
|
|
|
80
59
|
// ============================================
|
|
81
60
|
// Preset Grid Component
|
|
@@ -278,7 +257,7 @@ export default function SectionV2Settings({ section }: { section: PageSectionV2
|
|
|
278
257
|
)}
|
|
279
258
|
|
|
280
259
|
{/* Gaps */}
|
|
281
|
-
<SettingsSection title="Grid Gaps" defaultOpen icon={<
|
|
260
|
+
<SettingsSection title="Grid Gaps" defaultOpen icon={<GridGapsIcon />}>
|
|
282
261
|
<SettingsField label={
|
|
283
262
|
<span>
|
|
284
263
|
Col Gap
|
package/components/ui/Navbar.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback, useRef } from "react";
|
|
3
|
+
import { useState, useEffect, useCallback, useRef, useId, useMemo } from "react";
|
|
4
4
|
import { usePathname } from "next/navigation";
|
|
5
5
|
import Link from "next/link";
|
|
6
|
-
import type { NavItem, NavDesign, NavEntrancePreset, MobileNavDesign } from "../../lib/sanity/types";
|
|
6
|
+
import type { NavItem, NavDesign, NavEntrancePreset, NavDesignResponsiveOverride, MobileNavDesign } from "../../lib/sanity/types";
|
|
7
7
|
import { useNavColor } from "../../lib/contexts/NavColorContext";
|
|
8
8
|
import { useNavAnimation } from "../../lib/contexts/NavAnimationContext";
|
|
9
9
|
import { usePageExit } from "../../lib/contexts/PageExitContext";
|
|
@@ -23,6 +23,79 @@ const colorMap: Record<string, string> = {
|
|
|
23
23
|
white: "text-brand-text",
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
// ============================================
|
|
27
|
+
// Responsive CSS custom properties generator
|
|
28
|
+
// ============================================
|
|
29
|
+
|
|
30
|
+
/** Breakpoints matching the builder's responsive system */
|
|
31
|
+
const NAV_BREAKPOINTS = { tablet: 1024, phone: 640 } as const;
|
|
32
|
+
|
|
33
|
+
/** Generates a <style> block with CSS custom properties for responsive nav overrides.
|
|
34
|
+
* Desktop values are set on the scoping selector; tablet/phone overrides via media queries.
|
|
35
|
+
* Returns empty string if no responsive overrides exist. */
|
|
36
|
+
function buildResponsiveNavCSS(design: NavDesign | undefined, scopeSelector: string): string {
|
|
37
|
+
if (!design) return "";
|
|
38
|
+
|
|
39
|
+
// Desktop (base) custom properties
|
|
40
|
+
const vars: Record<string, string> = {
|
|
41
|
+
"--nav-font-size": `${design.font_size ?? 14}px`,
|
|
42
|
+
"--nav-font-weight": design.font_weight || "400",
|
|
43
|
+
"--nav-text-transform": design.text_transform || "uppercase",
|
|
44
|
+
"--nav-text-align": design.text_align || "left",
|
|
45
|
+
"--nav-vertical-align": (() => {
|
|
46
|
+
const v = design.vertical_align || "top";
|
|
47
|
+
return v === "bottom" ? "end" : v === "middle" ? "center" : "start";
|
|
48
|
+
})(),
|
|
49
|
+
"--nav-items-justify": (() => {
|
|
50
|
+
const a = design.text_align || "left";
|
|
51
|
+
return a === "center" ? "center" : a === "right" ? "flex-end" : "flex-start";
|
|
52
|
+
})(),
|
|
53
|
+
"--nav-padding-h": `${design.padding_h ?? 24}px`,
|
|
54
|
+
"--nav-padding-v": `${design.padding_v ?? 27}px`,
|
|
55
|
+
"--nav-margin-h": `${design.margin_h ?? 0}px`,
|
|
56
|
+
"--nav-margin-v": `${design.margin_v ?? 0}px`,
|
|
57
|
+
"--nav-items-gap": `${design.items_gap ?? 32}px`,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const responsive = design.responsive;
|
|
61
|
+
if (!responsive) {
|
|
62
|
+
// No responsive overrides — still emit base vars for consistency
|
|
63
|
+
const baseLines = Object.entries(vars).map(([k, v]) => `${k}:${v}`).join(";");
|
|
64
|
+
return `${scopeSelector}{${baseLines}}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Build media query blocks for tablet and phone
|
|
68
|
+
const mqBlocks: string[] = [];
|
|
69
|
+
for (const [viewport, bp] of [["tablet", NAV_BREAKPOINTS.tablet], ["phone", NAV_BREAKPOINTS.phone]] as const) {
|
|
70
|
+
const overrides = responsive[viewport] as NavDesignResponsiveOverride | undefined;
|
|
71
|
+
if (!overrides) continue;
|
|
72
|
+
const ovLines: string[] = [];
|
|
73
|
+
if (overrides.font_size != null) ovLines.push(`--nav-font-size:${overrides.font_size}px`);
|
|
74
|
+
if (overrides.font_weight != null) ovLines.push(`--nav-font-weight:${overrides.font_weight}`);
|
|
75
|
+
if (overrides.text_transform != null) ovLines.push(`--nav-text-transform:${overrides.text_transform}`);
|
|
76
|
+
if (overrides.text_align != null) {
|
|
77
|
+
ovLines.push(`--nav-text-align:${overrides.text_align}`);
|
|
78
|
+
const j = overrides.text_align === "center" ? "center" : overrides.text_align === "right" ? "flex-end" : "flex-start";
|
|
79
|
+
ovLines.push(`--nav-items-justify:${j}`);
|
|
80
|
+
}
|
|
81
|
+
if (overrides.vertical_align != null) {
|
|
82
|
+
const v = overrides.vertical_align === "bottom" ? "end" : overrides.vertical_align === "middle" ? "center" : "start";
|
|
83
|
+
ovLines.push(`--nav-vertical-align:${v}`);
|
|
84
|
+
}
|
|
85
|
+
if (overrides.padding_h != null) ovLines.push(`--nav-padding-h:${overrides.padding_h}px`);
|
|
86
|
+
if (overrides.padding_v != null) ovLines.push(`--nav-padding-v:${overrides.padding_v}px`);
|
|
87
|
+
if (overrides.margin_h != null) ovLines.push(`--nav-margin-h:${overrides.margin_h}px`);
|
|
88
|
+
if (overrides.margin_v != null) ovLines.push(`--nav-margin-v:${overrides.margin_v}px`);
|
|
89
|
+
if (overrides.items_gap != null) ovLines.push(`--nav-items-gap:${overrides.items_gap}px`);
|
|
90
|
+
if (ovLines.length > 0) {
|
|
91
|
+
mqBlocks.push(`@media(max-width:${bp}px){${scopeSelector}{${ovLines.join(";")}}}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const baseLines = Object.entries(vars).map(([k, v]) => `${k}:${v}`).join(";");
|
|
96
|
+
return `${scopeSelector}{${baseLines}}${mqBlocks.join("")}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
26
99
|
// ============================================
|
|
27
100
|
// NavLink — shared link component (eliminates duplication)
|
|
28
101
|
// ============================================
|
|
@@ -181,6 +254,18 @@ export default function Navbar({
|
|
|
181
254
|
// Map vertical_align to CSS align-items value for the grid container
|
|
182
255
|
const gridAlignItems = verticalAlign === "bottom" ? "end" : verticalAlign === "middle" ? "center" : "start";
|
|
183
256
|
|
|
257
|
+
// ── Responsive CSS custom properties (Session 164) ──
|
|
258
|
+
// Uses a stable ID scoped to the nav element. CSS vars + media queries
|
|
259
|
+
// ensure zero-flash SSR: the correct values apply before hydration.
|
|
260
|
+
const hasResponsive = !!(design?.responsive?.tablet || design?.responsive?.phone);
|
|
261
|
+
const reactId = useId();
|
|
262
|
+
const navScopeId = `nav-${reactId.replace(/:/g, "")}`;
|
|
263
|
+
const responsiveCSS = useMemo(
|
|
264
|
+
() => hasResponsive ? buildResponsiveNavCSS(design, `[data-nav-scope="${navScopeId}"]`) : "",
|
|
265
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
266
|
+
[hasResponsive, design?.responsive, navScopeId, fontSize, fontWeight, textTransformVal, textAlignVal, paddingH, paddingV, marginH, marginV, itemsGap, verticalAlign],
|
|
267
|
+
);
|
|
268
|
+
|
|
184
269
|
// ── Mobile menu resolved values (Session 158) ──
|
|
185
270
|
// Mobile styles are independent from page-level NavColorContext overrides.
|
|
186
271
|
// They fall back to desktop design values when not explicitly set.
|
|
@@ -374,13 +459,23 @@ export default function Navbar({
|
|
|
374
459
|
// When using hex color, we must set `color: inherit` on links because browser
|
|
375
460
|
// user-agent stylesheet sets `a { color: ... }` which overrides CSS inheritance.
|
|
376
461
|
const linkClassName = `tracking-normal ${textColorClass} transition-colors duration-200 hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
462
|
+
// When responsive overrides exist, link styles reference CSS custom properties
|
|
463
|
+
// so they respond to media queries without JS. Otherwise use direct values.
|
|
464
|
+
const linkStyle: React.CSSProperties = hasResponsive
|
|
465
|
+
? {
|
|
466
|
+
fontSize: "var(--nav-font-size)",
|
|
467
|
+
fontWeight: "var(--nav-font-weight)" as React.CSSProperties["fontWeight"],
|
|
468
|
+
textTransform: "var(--nav-text-transform)" as React.CSSProperties["textTransform"],
|
|
469
|
+
fontFamily: fontFamily || "var(--font-sans, Inter, system-ui, sans-serif)",
|
|
470
|
+
...(isHexColor ? { color: "inherit" } : {}),
|
|
471
|
+
}
|
|
472
|
+
: {
|
|
473
|
+
fontSize: `${fontSize}px`,
|
|
474
|
+
fontWeight: fontWeight as React.CSSProperties["fontWeight"],
|
|
475
|
+
textTransform: textTransformVal as React.CSSProperties["textTransform"],
|
|
476
|
+
fontFamily: fontFamily || "var(--font-sans, Inter, system-ui, sans-serif)",
|
|
477
|
+
...(isHexColor ? { color: "inherit" } : {}),
|
|
478
|
+
};
|
|
384
479
|
|
|
385
480
|
return (
|
|
386
481
|
<>
|
|
@@ -388,6 +483,7 @@ export default function Navbar({
|
|
|
388
483
|
<nav
|
|
389
484
|
role="navigation"
|
|
390
485
|
aria-label="Main navigation"
|
|
486
|
+
data-nav-scope={navScopeId}
|
|
391
487
|
className={`${positionClass} top-0 left-0 right-0 z-50 transition-transform duration-300 ease-in-out ${
|
|
392
488
|
shouldHide ? "-translate-y-full" : "translate-y-0"
|
|
393
489
|
}`}
|
|
@@ -397,14 +493,21 @@ export default function Navbar({
|
|
|
397
493
|
style={{
|
|
398
494
|
...navBgStyle,
|
|
399
495
|
...textColorStyle,
|
|
400
|
-
...(
|
|
496
|
+
...(hasResponsive
|
|
401
497
|
? {
|
|
402
|
-
left:
|
|
403
|
-
right:
|
|
404
|
-
top:
|
|
405
|
-
borderRadius: "8px",
|
|
498
|
+
left: "var(--nav-margin-h)",
|
|
499
|
+
right: "var(--nav-margin-h)",
|
|
500
|
+
top: "var(--nav-margin-v)",
|
|
501
|
+
...(marginH > 0 || marginV > 0 ? { borderRadius: "8px" } : {}),
|
|
406
502
|
}
|
|
407
|
-
:
|
|
503
|
+
: marginH > 0 || marginV > 0
|
|
504
|
+
? {
|
|
505
|
+
left: `${marginH}px`,
|
|
506
|
+
right: `${marginH}px`,
|
|
507
|
+
top: `${marginV}px`,
|
|
508
|
+
borderRadius: "8px",
|
|
509
|
+
}
|
|
510
|
+
: {}),
|
|
408
511
|
// CSS custom properties for animation duration
|
|
409
512
|
...(entrancePreset && !entranceStagger ? {
|
|
410
513
|
"--nav-entrance-duration": `${entranceDuration}ms`,
|
|
@@ -412,21 +515,39 @@ export default function Navbar({
|
|
|
412
515
|
} as React.CSSProperties : {}),
|
|
413
516
|
}}
|
|
414
517
|
>
|
|
518
|
+
{/* Responsive nav CSS custom properties + media queries */}
|
|
519
|
+
{responsiveCSS && (
|
|
520
|
+
<style dangerouslySetInnerHTML={{ __html: responsiveCSS }} />
|
|
521
|
+
)}
|
|
415
522
|
{/* Desktop: 12-column grid constrained to --grid-width */}
|
|
416
523
|
<div
|
|
417
524
|
className="hidden lg:grid"
|
|
418
|
-
style={
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
525
|
+
style={hasResponsive
|
|
526
|
+
? {
|
|
527
|
+
gridTemplateColumns: "repeat(12, 1fr)",
|
|
528
|
+
maxWidth: "var(--grid-width, 1445px)",
|
|
529
|
+
marginLeft: "auto",
|
|
530
|
+
marginRight: "auto",
|
|
531
|
+
paddingLeft: "var(--nav-padding-h)",
|
|
532
|
+
paddingRight: "var(--nav-padding-h)",
|
|
533
|
+
paddingTop: "var(--nav-padding-v)",
|
|
534
|
+
paddingBottom: "var(--nav-padding-v)",
|
|
535
|
+
alignItems: "var(--nav-vertical-align)" as React.CSSProperties["alignItems"],
|
|
536
|
+
columnGap: "var(--nav-items-gap)",
|
|
537
|
+
}
|
|
538
|
+
: {
|
|
539
|
+
gridTemplateColumns: "repeat(12, 1fr)",
|
|
540
|
+
maxWidth: "var(--grid-width, 1445px)",
|
|
541
|
+
marginLeft: "auto",
|
|
542
|
+
marginRight: "auto",
|
|
543
|
+
paddingLeft: `${paddingH}px`,
|
|
544
|
+
paddingRight: `${paddingH}px`,
|
|
545
|
+
paddingTop: `${paddingV}px`,
|
|
546
|
+
paddingBottom: `${paddingV}px`,
|
|
547
|
+
alignItems: gridAlignItems,
|
|
548
|
+
columnGap: `${itemsGap}px`,
|
|
549
|
+
}
|
|
550
|
+
}
|
|
430
551
|
>
|
|
431
552
|
{/* Logo */}
|
|
432
553
|
<div
|
|
@@ -480,7 +601,7 @@ export default function Navbar({
|
|
|
480
601
|
gridColumn: `${item.grid_column} / span ${item.column_span || 1}`,
|
|
481
602
|
gridRow: 1,
|
|
482
603
|
display: "flex",
|
|
483
|
-
justifyContent: itemsJustify,
|
|
604
|
+
justifyContent: hasResponsive ? "var(--nav-items-justify)" as React.CSSProperties["justifyContent"] : itemsJustify,
|
|
484
605
|
alignItems: "center",
|
|
485
606
|
minWidth: 0,
|
|
486
607
|
overflow: "hidden",
|
|
@@ -518,9 +639,9 @@ export default function Navbar({
|
|
|
518
639
|
style={{
|
|
519
640
|
gridColumn: `${logoCols + 1} / -1`,
|
|
520
641
|
display: "flex",
|
|
521
|
-
justifyContent: itemsJustify,
|
|
642
|
+
justifyContent: hasResponsive ? "var(--nav-items-justify)" as React.CSSProperties["justifyContent"] : itemsJustify,
|
|
522
643
|
alignItems: "center",
|
|
523
|
-
gap: `${design?.items_gap ?? 32}px`,
|
|
644
|
+
gap: hasResponsive ? "var(--nav-items-gap)" : `${design?.items_gap ?? 32}px`,
|
|
524
645
|
}}
|
|
525
646
|
>
|
|
526
647
|
{unplacedItems.map((item) => (
|
package/lib/sanity/types.ts
CHANGED
|
@@ -704,6 +704,28 @@ export interface NavDesign {
|
|
|
704
704
|
entrance_delay?: number; // ms, default 0
|
|
705
705
|
entrance_stagger?: boolean; // stagger items, default false
|
|
706
706
|
entrance_stagger_delay?: number; // ms between items, default 80
|
|
707
|
+
|
|
708
|
+
/** Per-viewport overrides for responsive nav settings.
|
|
709
|
+
* Only typography + spacing fields are overridable.
|
|
710
|
+
* Missing fields = inherit from desktop. */
|
|
711
|
+
responsive?: {
|
|
712
|
+
tablet?: NavDesignResponsiveOverride;
|
|
713
|
+
phone?: NavDesignResponsiveOverride;
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Subset of NavDesign fields that can be overridden per viewport */
|
|
718
|
+
export interface NavDesignResponsiveOverride {
|
|
719
|
+
font_size?: number;
|
|
720
|
+
font_weight?: string;
|
|
721
|
+
text_align?: "left" | "center" | "right";
|
|
722
|
+
vertical_align?: "top" | "middle" | "bottom";
|
|
723
|
+
text_transform?: "none" | "uppercase" | "lowercase" | "capitalize";
|
|
724
|
+
padding_h?: number;
|
|
725
|
+
padding_v?: number;
|
|
726
|
+
margin_h?: number;
|
|
727
|
+
margin_v?: number;
|
|
728
|
+
items_gap?: number;
|
|
707
729
|
}
|
|
708
730
|
|
|
709
731
|
// ============================================
|