@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.
Files changed (49) hide show
  1. package/README.md +3 -0
  2. package/components/admin/nav-builder/NavBuilder.tsx +90 -14
  3. package/components/admin/nav-builder/NavGeneralSettings.tsx +521 -271
  4. package/components/admin/nav-builder/NavItemSettings.tsx +331 -312
  5. package/components/admin/nav-builder/NavMobileSettings.tsx +159 -140
  6. package/components/admin/nav-builder/NavSettingsFields.tsx +287 -21
  7. package/components/admin/nav-builder/NavSettingsPanel.tsx +137 -127
  8. package/components/blocks/TextBlockRenderer.tsx +1 -1
  9. package/components/builder/SettingsPanel.tsx +29 -543
  10. package/components/builder/editors/ButtonBlockEditor.tsx +8 -3
  11. package/components/builder/editors/CoverBlockEditor.tsx +14 -6
  12. package/components/builder/editors/ImageBlockEditor.tsx +8 -3
  13. package/components/builder/editors/ImageGridBlockEditor.tsx +8 -3
  14. package/components/builder/editors/ProjectGridEditor.tsx +7 -46
  15. package/components/builder/editors/SpacerBlockEditor.tsx +4 -1
  16. package/components/builder/editors/StaggerSettings.tsx +2 -1
  17. package/components/builder/editors/TextBlockEditor.tsx +8 -3
  18. package/components/builder/editors/VideoBlockEditor.tsx +10 -4
  19. package/components/builder/editors/section-icons.tsx +492 -0
  20. package/components/builder/editors/shared.tsx +23 -4
  21. package/components/builder/live-preview/GhostCard.tsx +84 -0
  22. package/components/builder/live-preview/LiveProjectGridPreview.tsx +294 -1010
  23. package/components/builder/live-preview/LiveTextEditor.tsx +1 -1
  24. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -0
  25. package/components/builder/live-preview/drag-utils.tsx +89 -0
  26. package/components/builder/live-preview/useDragReorder.ts +370 -0
  27. package/components/builder/settings-panel/AnimationTab.tsx +152 -0
  28. package/components/builder/settings-panel/BlockLayoutTab.tsx +13 -58
  29. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -0
  30. package/components/builder/settings-panel/ColumnV2AnimationTab.tsx +32 -0
  31. package/components/builder/settings-panel/ColumnV2Settings.tsx +4 -1
  32. package/components/builder/settings-panel/CustomSectionSettings.tsx +150 -0
  33. package/components/builder/settings-panel/LayoutTab.tsx +11 -47
  34. package/components/builder/settings-panel/PageSettings.tsx +10 -4
  35. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +6 -2
  36. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +8 -3
  37. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +11 -47
  38. package/components/builder/settings-panel/SectionV2Settings.tsx +6 -27
  39. package/components/builder/settings-panel/index.ts +6 -0
  40. package/components/builder/settings-panel/useSettingsPanelSelection.ts +184 -0
  41. package/components/ui/Navbar.tsx +151 -30
  42. package/lib/builder/serializer/migrations.ts +107 -0
  43. package/lib/builder/serializer/normalizers.ts +278 -0
  44. package/lib/builder/serializer/serializers.ts +393 -0
  45. package/lib/builder/serializer/shared.ts +102 -0
  46. package/lib/builder/serializer.ts +11 -846
  47. package/lib/sanity/types.ts +22 -0
  48. package/package.json +13 -10
  49. 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
- // Matches admin page style: light theme, white cards, neutral borders
20
+ // v2: Card-based sections, colored icons, builder-aligned styling
7
21
 
8
- // ── Field wrapper ──
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-3 py-1.5">
13
- <label className="text-xs text-neutral-500 w-[72px] shrink-0 text-right">
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
- // ── Text input ──
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-1.5 text-xs bg-white border border-neutral-200 rounded-lg text-neutral-900 outline-none focus:border-[#076bff] focus:ring-2 focus:ring-[#076bff]/10 transition-colors font-[inherit] placeholder:text-neutral-400"
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
- // ── Select input ──
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 px-2.5 py-1.5 text-xs bg-white border border-neutral-200 rounded-lg text-neutral-900 outline-none appearance-none cursor-pointer pr-7 focus:border-[#076bff] focus:ring-2 focus:ring-[#076bff]/10 transition-colors font-[inherit]"
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
- // ── Segmented control ──
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-neutral-100 rounded-lg p-0.5">
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-1.5 text-[11px] font-medium rounded-md transition-all ${
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-white shadow-sm"
104
- : "text-neutral-500 hover:text-neutral-700"
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
- // ── Toggle switch ──
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
- // ── Range slider ──
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-500 w-8 text-right tabular-nums">
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
- // ── Collapsible section ──
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
- // ── Color input (hex) with swatch preview ──
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 &middot; 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 button ──
21
-
22
- function TabButton({
23
- active,
24
- label,
25
- onClick,
26
- }: {
27
- active: boolean;
28
- label: string;
29
- onClick: () => void;
30
- }) {
31
- return (
32
- <button
33
- onClick={onClick}
34
- className={`px-4 py-2 text-xs font-medium transition-colors border-b-2 -mb-px ${
35
- active
36
- ? "text-neutral-900 border-[#076bff]"
37
- : "text-neutral-400 border-transparent hover:text-neutral-600"
38
- }`}
39
- >
40
- {label}
41
- </button>
42
- );
43
- }
44
-
45
- // ── Main panel — renders below the grid as a horizontal block ──
46
-
47
- export default function NavSettingsPanel({
48
- selectedItem,
49
- items,
50
- design,
51
- onDesignChange,
52
- onUpdateItem,
53
- pages,
54
- fonts,
55
- }: NavSettingsPanelProps) {
56
- const [activeTab, setActiveTab] = useState<SettingsTab>("settings");
57
- const prevKeyRef = useRef(selectedItem?._key);
58
-
59
- // Reset to settings tab when selection changes
60
- useEffect(() => {
61
- if (selectedItem?._key !== prevKeyRef.current) {
62
- prevKeyRef.current = selectedItem?._key;
63
- setActiveTab("settings");
64
- }
65
- }, [selectedItem?._key]);
66
-
67
- const isItemMode = !!selectedItem;
68
- const panelTitle = isItemMode
69
- ? selectedItem.type === "logo"
70
- ? "Logo"
71
- : selectedItem.label || "Untitled"
72
- : "Navigation";
73
- const panelSubtitle = isItemMode
74
- ? `${selectedItem.type === "logo" ? "Logo" : "Menu Item"} · Col ${selectedItem.grid_column}${
75
- selectedItem.column_span > 1
76
- ? `–${selectedItem.grid_column + selectedItem.column_span - 1}`
77
- : ""
78
- }`
79
- : `${items.length} items · 12 columns`;
80
-
81
- return (
82
- <div className="bg-neutral-50/50">
83
- {/* Header + Tabs row */}
84
- <div className="flex items-center gap-6 px-5 border-b border-neutral-200">
85
- <div className="py-3 pr-4 border-r border-neutral-200">
86
- <div className="text-[13px] font-semibold text-neutral-900 truncate">
87
- {panelTitle}
88
- </div>
89
- <div className="text-[10px] text-neutral-400 mt-0.5">{panelSubtitle}</div>
90
- </div>
91
- <div className="flex gap-0.5">
92
- <TabButton
93
- active={activeTab === "settings"}
94
- label="Settings"
95
- onClick={() => setActiveTab("settings")}
96
- />
97
- <TabButton
98
- active={activeTab === "layout"}
99
- label="Layout"
100
- onClick={() => setActiveTab("layout")}
101
- />
102
- </div>
103
- </div>
104
-
105
- {/* Content — constrained width for readability */}
106
- <div className="px-5 py-4 max-w-xl">
107
- {isItemMode ? (
108
- <NavItemSettings
109
- item={selectedItem}
110
- items={items}
111
- activeTab={activeTab}
112
- onUpdate={onUpdateItem}
113
- pages={pages}
114
- fonts={fonts}
115
- />
116
- ) : (
117
- <NavGeneralSettings
118
- design={design}
119
- activeTab={activeTab}
120
- onChange={onDesignChange}
121
- fonts={fonts}
122
- />
123
- )}
124
- </div>
125
- </div>
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
+ }