@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,24 +1,42 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, type ReactNode } from "react";
|
|
4
|
+
import {
|
|
5
|
+
PositionIcon,
|
|
6
|
+
TypographyIcon,
|
|
7
|
+
AnimationIcon,
|
|
8
|
+
SpacingIcon,
|
|
9
|
+
ContentIcon,
|
|
10
|
+
LinkIcon,
|
|
11
|
+
GridIcon,
|
|
12
|
+
StyleIcon,
|
|
13
|
+
BackgroundIcon,
|
|
14
|
+
} from "../../builder/editors/section-icons";
|
|
15
|
+
|
|
16
|
+
// ── Nav viewport type for responsive overrides ──
|
|
17
|
+
export type NavViewport = "desktop" | "tablet" | "phone";
|
|
4
18
|
|
|
5
19
|
// ── Shared field components for NavBuilder settings panel ──
|
|
6
|
-
//
|
|
20
|
+
// v2: Card-based sections, colored icons, builder-aligned styling
|
|
7
21
|
|
|
8
|
-
//
|
|
22
|
+
// ============================================
|
|
23
|
+
// Field wrapper
|
|
24
|
+
// ============================================
|
|
9
25
|
|
|
10
26
|
export function Field({ label, children }: { label: string; children: ReactNode }) {
|
|
11
27
|
return (
|
|
12
|
-
<div className="flex items-center gap-
|
|
13
|
-
<label className="text-
|
|
28
|
+
<div className="flex items-center gap-2.5 py-[5px]">
|
|
29
|
+
<label className="text-[11px] text-neutral-400 w-[56px] min-w-[56px] shrink-0">
|
|
14
30
|
{label}
|
|
15
31
|
</label>
|
|
16
|
-
<div className="flex-1">{children}</div>
|
|
32
|
+
<div className="flex-1 min-w-0">{children}</div>
|
|
17
33
|
</div>
|
|
18
34
|
);
|
|
19
35
|
}
|
|
20
36
|
|
|
21
|
-
//
|
|
37
|
+
// ============================================
|
|
38
|
+
// Text input
|
|
39
|
+
// ============================================
|
|
22
40
|
|
|
23
41
|
export function TextInput({
|
|
24
42
|
value,
|
|
@@ -37,12 +55,14 @@ export function TextInput({
|
|
|
37
55
|
value={value}
|
|
38
56
|
onChange={(e) => onChange(e.target.value)}
|
|
39
57
|
placeholder={placeholder}
|
|
40
|
-
className="w-full px-2.5 py-
|
|
58
|
+
className="w-full rounded-lg border border-transparent bg-white px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-neutral-50 focus:bg-white focus:border-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)] font-[inherit] placeholder:text-neutral-400"
|
|
41
59
|
/>
|
|
42
60
|
);
|
|
43
61
|
}
|
|
44
62
|
|
|
45
|
-
//
|
|
63
|
+
// ============================================
|
|
64
|
+
// Select input
|
|
65
|
+
// ============================================
|
|
46
66
|
|
|
47
67
|
export function SelectInput({
|
|
48
68
|
value,
|
|
@@ -58,7 +78,7 @@ export function SelectInput({
|
|
|
58
78
|
<select
|
|
59
79
|
value={value}
|
|
60
80
|
onChange={(e) => onChange(e.target.value)}
|
|
61
|
-
className="w-full
|
|
81
|
+
className="w-full rounded-lg border border-transparent bg-white px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none appearance-none cursor-pointer pr-7 transition-all hover:bg-neutral-50 focus:bg-white focus:border-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)] font-[inherit]"
|
|
62
82
|
>
|
|
63
83
|
{options.map((opt) => (
|
|
64
84
|
<option key={opt.value} value={opt.value}>
|
|
@@ -81,7 +101,9 @@ export function SelectInput({
|
|
|
81
101
|
);
|
|
82
102
|
}
|
|
83
103
|
|
|
84
|
-
//
|
|
104
|
+
// ============================================
|
|
105
|
+
// Segmented control
|
|
106
|
+
// ============================================
|
|
85
107
|
|
|
86
108
|
export function SegmentedControl({
|
|
87
109
|
value,
|
|
@@ -90,18 +112,18 @@ export function SegmentedControl({
|
|
|
90
112
|
}: {
|
|
91
113
|
value: string;
|
|
92
114
|
onChange: (value: string) => void;
|
|
93
|
-
options: { value: string; label: string }[];
|
|
115
|
+
options: { value: string; label: string | ReactNode }[];
|
|
94
116
|
}) {
|
|
95
117
|
return (
|
|
96
|
-
<div className="flex bg-
|
|
118
|
+
<div className="flex bg-white rounded-lg p-0.5 border border-[#f0f0f0]">
|
|
97
119
|
{options.map((opt) => (
|
|
98
120
|
<button
|
|
99
|
-
key={opt.value}
|
|
121
|
+
key={String(opt.value)}
|
|
100
122
|
onClick={() => onChange(opt.value)}
|
|
101
|
-
className={`flex-1 px-2 py-
|
|
123
|
+
className={`flex-1 px-2 py-[5px] text-[10px] font-medium rounded-md transition-all flex items-center justify-center ${
|
|
102
124
|
value === opt.value
|
|
103
|
-
? "text-neutral-900 bg-
|
|
104
|
-
: "text-neutral-
|
|
125
|
+
? "text-neutral-900 bg-neutral-100 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
|
|
126
|
+
: "text-neutral-400 hover:text-neutral-600"
|
|
105
127
|
}`}
|
|
106
128
|
>
|
|
107
129
|
{opt.label}
|
|
@@ -111,7 +133,9 @@ export function SegmentedControl({
|
|
|
111
133
|
);
|
|
112
134
|
}
|
|
113
135
|
|
|
114
|
-
//
|
|
136
|
+
// ============================================
|
|
137
|
+
// Toggle switch
|
|
138
|
+
// ============================================
|
|
115
139
|
|
|
116
140
|
export function Toggle({
|
|
117
141
|
value,
|
|
@@ -136,7 +160,9 @@ export function Toggle({
|
|
|
136
160
|
);
|
|
137
161
|
}
|
|
138
162
|
|
|
139
|
-
//
|
|
163
|
+
// ============================================
|
|
164
|
+
// Range slider
|
|
165
|
+
// ============================================
|
|
140
166
|
|
|
141
167
|
export function RangeSlider({
|
|
142
168
|
value,
|
|
@@ -162,14 +188,72 @@ export function RangeSlider({
|
|
|
162
188
|
className="flex-1"
|
|
163
189
|
style={{ accentColor: "#076bff" }}
|
|
164
190
|
/>
|
|
165
|
-
<span className="text-[10px] text-neutral-
|
|
191
|
+
<span className="text-[10px] text-neutral-400 w-9 text-right tabular-nums">
|
|
166
192
|
{value}{suffix || ""}
|
|
167
193
|
</span>
|
|
168
194
|
</div>
|
|
169
195
|
);
|
|
170
196
|
}
|
|
171
197
|
|
|
172
|
-
//
|
|
198
|
+
// ============================================
|
|
199
|
+
// Card section — collapsible with colored icon
|
|
200
|
+
// ============================================
|
|
201
|
+
|
|
202
|
+
export function CardSection({
|
|
203
|
+
title,
|
|
204
|
+
icon,
|
|
205
|
+
iconBg,
|
|
206
|
+
defaultOpen = true,
|
|
207
|
+
children,
|
|
208
|
+
}: {
|
|
209
|
+
title: string;
|
|
210
|
+
icon: ReactNode;
|
|
211
|
+
iconBg: string;
|
|
212
|
+
defaultOpen?: boolean;
|
|
213
|
+
children: ReactNode;
|
|
214
|
+
}) {
|
|
215
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div
|
|
219
|
+
className="mx-2.5 my-1 rounded-[10px] bg-[#fafafa] border border-transparent transition-colors hover:border-[#f0f0f0]"
|
|
220
|
+
>
|
|
221
|
+
<button
|
|
222
|
+
onClick={() => setOpen(!open)}
|
|
223
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 cursor-pointer select-none hover:bg-black/[0.01] rounded-[10px] transition-colors"
|
|
224
|
+
>
|
|
225
|
+
<div
|
|
226
|
+
className="w-[26px] h-[26px] rounded-[7px] flex items-center justify-center shrink-0"
|
|
227
|
+
style={{ background: iconBg }}
|
|
228
|
+
>
|
|
229
|
+
{icon}
|
|
230
|
+
</div>
|
|
231
|
+
<span className="text-[11px] font-semibold text-neutral-600 tracking-[0.2px]">
|
|
232
|
+
{title}
|
|
233
|
+
</span>
|
|
234
|
+
<span
|
|
235
|
+
className="ml-auto text-neutral-400 transition-transform"
|
|
236
|
+
style={{ transform: open ? "rotate(180deg)" : "rotate(0)" }}
|
|
237
|
+
>
|
|
238
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
239
|
+
<path
|
|
240
|
+
d="M3 4.5l3 3 3-3"
|
|
241
|
+
stroke="currentColor"
|
|
242
|
+
strokeWidth="1.5"
|
|
243
|
+
strokeLinecap="round"
|
|
244
|
+
strokeLinejoin="round"
|
|
245
|
+
/>
|
|
246
|
+
</svg>
|
|
247
|
+
</span>
|
|
248
|
+
</button>
|
|
249
|
+
{open && <div className="px-3 pb-3 pt-0.5">{children}</div>}
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ============================================
|
|
255
|
+
// Legacy Section — kept for NavItemSettings compatibility
|
|
256
|
+
// ============================================
|
|
173
257
|
|
|
174
258
|
export function Section({
|
|
175
259
|
title,
|
|
@@ -209,7 +293,17 @@ export function Section({
|
|
|
209
293
|
);
|
|
210
294
|
}
|
|
211
295
|
|
|
212
|
-
//
|
|
296
|
+
// ============================================
|
|
297
|
+
// Thin divider line
|
|
298
|
+
// ============================================
|
|
299
|
+
|
|
300
|
+
export function Divider() {
|
|
301
|
+
return <div className="h-px bg-[#f0f0f0] my-1.5" />;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ============================================
|
|
305
|
+
// Color input (hex) with swatch preview
|
|
306
|
+
// ============================================
|
|
213
307
|
|
|
214
308
|
export function ColorInput({
|
|
215
309
|
value,
|
|
@@ -246,3 +340,175 @@ export function ColorInput({
|
|
|
246
340
|
</div>
|
|
247
341
|
);
|
|
248
342
|
}
|
|
343
|
+
|
|
344
|
+
// ============================================
|
|
345
|
+
// Card section icon presets — re-exported from centralized section-icons.tsx
|
|
346
|
+
// Session 163: Unified icon system. All icons now live in section-icons.tsx.
|
|
347
|
+
// ============================================
|
|
348
|
+
export {
|
|
349
|
+
PositionIcon,
|
|
350
|
+
TypographyIcon,
|
|
351
|
+
AnimationIcon,
|
|
352
|
+
SpacingIcon,
|
|
353
|
+
ContentIcon,
|
|
354
|
+
LinkIcon,
|
|
355
|
+
GridIcon,
|
|
356
|
+
StyleIcon,
|
|
357
|
+
BackgroundIcon,
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// ============================================
|
|
361
|
+
// Viewport Switcher — segmented control for Desktop / Tablet / Phone
|
|
362
|
+
// ============================================
|
|
363
|
+
|
|
364
|
+
const viewportOptions: { value: NavViewport; icon: ReactNode; label: string }[] = [
|
|
365
|
+
{
|
|
366
|
+
value: "desktop",
|
|
367
|
+
label: "Desktop",
|
|
368
|
+
icon: (
|
|
369
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
370
|
+
<rect x="2" y="3" width="20" height="14" rx="2" /><line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" />
|
|
371
|
+
</svg>
|
|
372
|
+
),
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
value: "tablet",
|
|
376
|
+
label: "Tablet",
|
|
377
|
+
icon: (
|
|
378
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
379
|
+
<rect x="4" y="2" width="16" height="20" rx="2" /><line x1="12" y1="18" x2="12" y2="18" />
|
|
380
|
+
</svg>
|
|
381
|
+
),
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
value: "phone",
|
|
385
|
+
label: "Phone",
|
|
386
|
+
icon: (
|
|
387
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
388
|
+
<rect x="5" y="2" width="14" height="20" rx="2" /><line x1="12" y1="18" x2="12" y2="18" />
|
|
389
|
+
</svg>
|
|
390
|
+
),
|
|
391
|
+
},
|
|
392
|
+
];
|
|
393
|
+
|
|
394
|
+
export function ViewportSwitcher({
|
|
395
|
+
value,
|
|
396
|
+
onChange,
|
|
397
|
+
}: {
|
|
398
|
+
value: NavViewport;
|
|
399
|
+
onChange: (v: NavViewport) => void;
|
|
400
|
+
}) {
|
|
401
|
+
return (
|
|
402
|
+
<div className="flex bg-white rounded-lg p-0.5 border border-[#f0f0f0] mb-2">
|
|
403
|
+
{viewportOptions.map((opt) => {
|
|
404
|
+
const isActive = value === opt.value;
|
|
405
|
+
const isNonDesktop = opt.value !== "desktop";
|
|
406
|
+
return (
|
|
407
|
+
<button
|
|
408
|
+
key={opt.value}
|
|
409
|
+
onClick={() => onChange(opt.value)}
|
|
410
|
+
title={opt.label}
|
|
411
|
+
className={`flex-1 px-2 py-[5px] text-[10px] font-medium rounded-md transition-all flex items-center justify-center gap-1 ${
|
|
412
|
+
isActive
|
|
413
|
+
? isNonDesktop
|
|
414
|
+
? "text-[#076bff] bg-blue-50 shadow-[0_1px_2px_rgba(7,107,255,0.08)]"
|
|
415
|
+
: "text-neutral-900 bg-neutral-100 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
|
|
416
|
+
: "text-neutral-400 hover:text-neutral-600"
|
|
417
|
+
}`}
|
|
418
|
+
>
|
|
419
|
+
{opt.icon}
|
|
420
|
+
</button>
|
|
421
|
+
);
|
|
422
|
+
})}
|
|
423
|
+
</div>
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ============================================
|
|
428
|
+
// Viewport Badge — shows which viewport is being edited
|
|
429
|
+
// ============================================
|
|
430
|
+
|
|
431
|
+
export function NavViewportBadge({ viewport }: { viewport: NavViewport }) {
|
|
432
|
+
if (viewport === "desktop") return null;
|
|
433
|
+
|
|
434
|
+
const color = viewport === "tablet" ? "#076bff" : "#7c3aed";
|
|
435
|
+
const bgColor = viewport === "tablet" ? "rgba(7, 107, 255, 0.06)" : "rgba(124, 58, 237, 0.06)";
|
|
436
|
+
const label = viewport === "tablet" ? "Tablet" : "Phone";
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<div
|
|
440
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md mb-2 text-[10px]"
|
|
441
|
+
style={{ background: bgColor, color }}
|
|
442
|
+
>
|
|
443
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
444
|
+
<circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
|
|
445
|
+
</svg>
|
|
446
|
+
<span>
|
|
447
|
+
Editing <strong>{label}</strong> overrides · empty fields inherit desktop
|
|
448
|
+
</span>
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ============================================
|
|
454
|
+
// Responsive Field — wraps a field with inherited/overridden state
|
|
455
|
+
// ============================================
|
|
456
|
+
|
|
457
|
+
export function ResponsiveField({
|
|
458
|
+
label,
|
|
459
|
+
viewport,
|
|
460
|
+
isOverridden,
|
|
461
|
+
onReset,
|
|
462
|
+
children,
|
|
463
|
+
}: {
|
|
464
|
+
label: string;
|
|
465
|
+
viewport: NavViewport;
|
|
466
|
+
isOverridden: boolean;
|
|
467
|
+
onReset?: () => void;
|
|
468
|
+
children: ReactNode;
|
|
469
|
+
}) {
|
|
470
|
+
// Desktop mode — render as a normal field
|
|
471
|
+
if (viewport === "desktop") {
|
|
472
|
+
return <Field label={label}>{children}</Field>;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return (
|
|
476
|
+
<div className="flex items-center gap-2.5 py-[5px]">
|
|
477
|
+
<div className="w-[56px] min-w-[56px] shrink-0">
|
|
478
|
+
<label
|
|
479
|
+
className={`text-[11px] block ${
|
|
480
|
+
isOverridden ? "text-[#076bff] font-medium" : "text-neutral-400"
|
|
481
|
+
}`}
|
|
482
|
+
>
|
|
483
|
+
{label}
|
|
484
|
+
</label>
|
|
485
|
+
{isOverridden ? (
|
|
486
|
+
<span className="text-[8px] text-[#076bff]/60 uppercase tracking-[0.5px]">
|
|
487
|
+
override
|
|
488
|
+
</span>
|
|
489
|
+
) : (
|
|
490
|
+
<span className="text-[8px] text-neutral-300 uppercase tracking-[0.5px]">
|
|
491
|
+
inherited
|
|
492
|
+
</span>
|
|
493
|
+
)}
|
|
494
|
+
</div>
|
|
495
|
+
<div
|
|
496
|
+
className="flex-1 min-w-0 transition-opacity"
|
|
497
|
+
style={{ opacity: isOverridden ? 1 : 0.5 }}
|
|
498
|
+
>
|
|
499
|
+
{children}
|
|
500
|
+
</div>
|
|
501
|
+
{isOverridden && onReset && (
|
|
502
|
+
<button
|
|
503
|
+
onClick={onReset}
|
|
504
|
+
className="shrink-0 text-[#076bff]/60 hover:text-[#076bff] transition-colors"
|
|
505
|
+
title="Reset to desktop value"
|
|
506
|
+
>
|
|
507
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
508
|
+
<polyline points="1 4 1 10 7 10" /><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
|
509
|
+
</svg>
|
|
510
|
+
</button>
|
|
511
|
+
)}
|
|
512
|
+
</div>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
@@ -1,127 +1,137 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useRef, useEffect } from "react";
|
|
4
|
-
import type { NavItem, NavDesign, PageListItem } from "../../../lib/sanity/types";
|
|
5
|
-
import NavGeneralSettings from "./NavGeneralSettings";
|
|
6
|
-
import NavItemSettings from "./NavItemSettings";
|
|
7
|
-
|
|
8
|
-
type SettingsTab = "settings" | "layout";
|
|
9
|
-
|
|
10
|
-
interface NavSettingsPanelProps {
|
|
11
|
-
selectedItem: NavItem | null;
|
|
12
|
-
items: NavItem[];
|
|
13
|
-
design: NavDesign;
|
|
14
|
-
onDesignChange: (design: NavDesign) => void;
|
|
15
|
-
onUpdateItem: (updated: NavItem) => void;
|
|
16
|
-
pages: PageListItem[];
|
|
17
|
-
fonts: string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// ── Tab
|
|
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
|
-
const
|
|
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
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from "react";
|
|
4
|
+
import type { NavItem, NavDesign, PageListItem } from "../../../lib/sanity/types";
|
|
5
|
+
import NavGeneralSettings from "./NavGeneralSettings";
|
|
6
|
+
import NavItemSettings from "./NavItemSettings";
|
|
7
|
+
|
|
8
|
+
type SettingsTab = "settings" | "layout" | "animation";
|
|
9
|
+
|
|
10
|
+
interface NavSettingsPanelProps {
|
|
11
|
+
selectedItem: NavItem | null;
|
|
12
|
+
items: NavItem[];
|
|
13
|
+
design: NavDesign;
|
|
14
|
+
onDesignChange: (design: NavDesign) => void;
|
|
15
|
+
onUpdateItem: (updated: NavItem) => void;
|
|
16
|
+
pages: PageListItem[];
|
|
17
|
+
fonts: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── Tab definitions ──
|
|
21
|
+
|
|
22
|
+
const TAB_ICON_SETTINGS = (
|
|
23
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
24
|
+
<circle cx="12" cy="12" r="3" />
|
|
25
|
+
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const TAB_ICON_LAYOUT = (
|
|
30
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
31
|
+
<rect x="3" y="3" width="7" height="7" /><rect x="14" y="3" width="7" height="7" />
|
|
32
|
+
<rect x="14" y="14" width="7" height="7" /><rect x="3" y="14" width="7" height="7" />
|
|
33
|
+
</svg>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const TAB_ICON_ANIMATION = (
|
|
37
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
38
|
+
<path d="M5 12h14" /><path d="M12 5l7 7-7 7" />
|
|
39
|
+
</svg>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// ── Main panel ──
|
|
43
|
+
|
|
44
|
+
export default function NavSettingsPanel({
|
|
45
|
+
selectedItem,
|
|
46
|
+
items,
|
|
47
|
+
design,
|
|
48
|
+
onDesignChange,
|
|
49
|
+
onUpdateItem,
|
|
50
|
+
pages,
|
|
51
|
+
fonts,
|
|
52
|
+
}: NavSettingsPanelProps) {
|
|
53
|
+
const [activeTab, setActiveTab] = useState<SettingsTab>("settings");
|
|
54
|
+
const prevKeyRef = useRef(selectedItem?._key);
|
|
55
|
+
|
|
56
|
+
// Reset to settings tab when selection changes
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (selectedItem?._key !== prevKeyRef.current) {
|
|
59
|
+
prevKeyRef.current = selectedItem?._key;
|
|
60
|
+
setActiveTab("settings");
|
|
61
|
+
}
|
|
62
|
+
}, [selectedItem?._key]);
|
|
63
|
+
|
|
64
|
+
const isItemMode = !!selectedItem;
|
|
65
|
+
|
|
66
|
+
// For items, animation tab is not applicable — only Settings + Layout
|
|
67
|
+
const tabs: { id: SettingsTab; label: string; icon: React.ReactNode }[] = isItemMode
|
|
68
|
+
? [
|
|
69
|
+
{ id: "settings", label: "Settings", icon: TAB_ICON_SETTINGS },
|
|
70
|
+
{ id: "layout", label: "Layout", icon: TAB_ICON_LAYOUT },
|
|
71
|
+
]
|
|
72
|
+
: [
|
|
73
|
+
{ id: "settings", label: "Settings", icon: TAB_ICON_SETTINGS },
|
|
74
|
+
{ id: "layout", label: "Layout", icon: TAB_ICON_LAYOUT },
|
|
75
|
+
{ id: "animation", label: "Anim", icon: TAB_ICON_ANIMATION },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// Fall back to settings if the current tab doesn't exist in the current set
|
|
79
|
+
const validTab = tabs.some((t) => t.id === activeTab) ? activeTab : "settings";
|
|
80
|
+
|
|
81
|
+
const tabCount = tabs.length;
|
|
82
|
+
const activeIndex = tabs.findIndex((t) => t.id === validTab);
|
|
83
|
+
const pillIndex = activeIndex >= 0 ? activeIndex : 0;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="bg-white">
|
|
87
|
+
{/* ── Animated pill tabs ── */}
|
|
88
|
+
<div className="px-3 py-2 border-b border-[#f0f0f0] bg-[#fafafa]">
|
|
89
|
+
<div className="relative flex items-center bg-[#f0f0f0] rounded-lg p-[3px]">
|
|
90
|
+
{/* Sliding pill background */}
|
|
91
|
+
<div
|
|
92
|
+
className="absolute top-[3px] bottom-[3px] rounded-md bg-white shadow-sm border border-[#e5e5e5] transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]"
|
|
93
|
+
style={{
|
|
94
|
+
width: `calc((100% - 6px) / ${tabCount})`,
|
|
95
|
+
left: `calc(${pillIndex} * (100% - 6px) / ${tabCount} + 3px)`,
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
{tabs.map((tab) => (
|
|
99
|
+
<button
|
|
100
|
+
key={tab.id}
|
|
101
|
+
onClick={() => setActiveTab(tab.id)}
|
|
102
|
+
className={`relative z-10 flex-1 flex items-center justify-center gap-1 py-1.5 rounded-md transition-colors duration-200 text-[11px] font-medium ${
|
|
103
|
+
validTab === tab.id
|
|
104
|
+
? "text-neutral-900"
|
|
105
|
+
: "text-neutral-400 hover:text-neutral-500"
|
|
106
|
+
}`}
|
|
107
|
+
>
|
|
108
|
+
{tab.icon}
|
|
109
|
+
{tab.label}
|
|
110
|
+
</button>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* ── Panel body ── */}
|
|
116
|
+
<div className="py-1">
|
|
117
|
+
{isItemMode ? (
|
|
118
|
+
<NavItemSettings
|
|
119
|
+
item={selectedItem}
|
|
120
|
+
items={items}
|
|
121
|
+
activeTab={validTab === "animation" ? "settings" : validTab}
|
|
122
|
+
onUpdate={onUpdateItem}
|
|
123
|
+
pages={pages}
|
|
124
|
+
fonts={fonts}
|
|
125
|
+
/>
|
|
126
|
+
) : (
|
|
127
|
+
<NavGeneralSettings
|
|
128
|
+
design={design}
|
|
129
|
+
activeTab={validTab}
|
|
130
|
+
onChange={onDesignChange}
|
|
131
|
+
fonts={fonts}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|