@morphika/andami 0.5.1 → 0.5.3

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 (147) hide show
  1. package/README.md +27 -2
  2. package/app/admin/assets/page.tsx +6 -6
  3. package/app/admin/database/page.tsx +302 -302
  4. package/app/admin/error.tsx +53 -53
  5. package/app/admin/layout.tsx +332 -320
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +44 -27
  8. package/app/admin/pages/page.tsx +24 -19
  9. package/app/admin/projects/page.tsx +30 -21
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/app/api/admin/assets/register/route.ts +51 -14
  13. package/app/api/admin/assets/registry/route.ts +4 -1
  14. package/app/api/admin/assets/relink/confirm/route.ts +4 -1
  15. package/app/api/admin/assets/relink/route.ts +4 -1
  16. package/app/api/admin/assets/scan/route.ts +4 -1
  17. package/app/api/admin/backups/restore-data/route.ts +4 -1
  18. package/app/api/admin/r2/connect/route.ts +4 -1
  19. package/app/api/admin/r2/delete/route.ts +4 -1
  20. package/app/api/admin/r2/rename/route.ts +4 -1
  21. package/app/api/admin/r2/upload-url/route.ts +4 -1
  22. package/app/api/admin/revalidate/route.ts +4 -1
  23. package/app/api/admin/storage/switch/route.ts +4 -1
  24. package/app/api/custom-sections/[id]/route.ts +5 -6
  25. package/components/admin/MetadataEditor.tsx +6 -6
  26. package/components/admin/PublishToggle.tsx +2 -2
  27. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  28. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  29. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  30. package/components/admin/nav-builder/NavGridItem.tsx +8 -6
  31. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  32. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  33. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  34. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  35. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  36. package/components/admin/nav-builder/NavSettingsFields.tsx +518 -514
  37. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  38. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  39. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  40. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  41. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  42. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  43. package/components/admin/styles/ColorsEditor.tsx +9 -8
  44. package/components/admin/styles/FontsEditor.tsx +9 -7
  45. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  46. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  47. package/components/admin/styles/TypographyEditor.tsx +6 -6
  48. package/components/admin/styles/shared.tsx +68 -68
  49. package/components/blocks/AudioBlockRenderer.tsx +286 -286
  50. package/components/blocks/CoverSectionRenderer.tsx +7 -1
  51. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  52. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  53. package/components/blocks/SectionV2Renderer.tsx +8 -1
  54. package/components/builder/BlockCardIcons.tsx +316 -316
  55. package/components/builder/BlockTypePicker.tsx +1 -1
  56. package/components/builder/BubbleIcons.tsx +104 -0
  57. package/components/builder/BuilderCanvas.tsx +2 -0
  58. package/components/builder/CanvasMinimap.tsx +66 -49
  59. package/components/builder/CanvasToolbar.tsx +31 -41
  60. package/components/builder/CoverSectionCanvas.tsx +363 -363
  61. package/components/builder/DeviceFrame.tsx +1 -1
  62. package/components/builder/DndWrapper.tsx +3 -3
  63. package/components/builder/InsertionLines.tsx +1 -1
  64. package/components/builder/SectionCardIcons.tsx +421 -320
  65. package/components/builder/SectionEditorBar.tsx +5 -3
  66. package/components/builder/SectionTypePicker.tsx +7 -5
  67. package/components/builder/SectionV2Canvas.tsx +1 -1
  68. package/components/builder/SectionV2Column.tsx +82 -68
  69. package/components/builder/SettingsPanel.tsx +21 -17
  70. package/components/builder/SortableBlock.tsx +93 -73
  71. package/components/builder/SortableRow.tsx +33 -35
  72. package/components/builder/VirtualAssetGrid.tsx +10 -4
  73. package/components/builder/asset-browser/R2BrowserContent.tsx +18 -14
  74. package/components/builder/blockStyles.tsx +192 -185
  75. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  76. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  77. package/components/builder/color-picker/EyedropperButton.tsx +75 -74
  78. package/components/builder/color-picker/HueSlider.tsx +124 -124
  79. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  80. package/components/builder/color-picker/SwatchBar.tsx +98 -93
  81. package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
  82. package/components/builder/editors/AudioBlockEditor.tsx +242 -242
  83. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
  84. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  85. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  86. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  87. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  88. package/components/builder/editors/ImageGridBlockEditor.tsx +8 -6
  89. package/components/builder/editors/MarqueeBlockEditor.tsx +622 -0
  90. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  91. package/components/builder/editors/ProjectGridEditor.tsx +21 -16
  92. package/components/builder/editors/SpacerBlockEditor.tsx +29 -27
  93. package/components/builder/editors/StaggerSettings.tsx +109 -109
  94. package/components/builder/editors/TextBlockEditor.tsx +22 -17
  95. package/components/builder/editors/TextStylePicker.tsx +1 -1
  96. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  97. package/components/builder/editors/index.ts +11 -10
  98. package/components/builder/editors/shared.tsx +10 -8
  99. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
  100. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
  101. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  102. package/components/builder/live-preview/LiveImagePreview.tsx +4 -2
  103. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  104. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  105. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  106. package/components/builder/live-preview/ProjectCardWrapper.tsx +293 -291
  107. package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
  108. package/components/builder/live-preview/shared.tsx +5 -2
  109. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  110. package/components/builder/settings-panel/BlockLayoutTab.tsx +11 -9
  111. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  112. package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
  113. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  114. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  115. package/components/builder/settings-panel/CoverSectionSettings.tsx +337 -335
  116. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  117. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  118. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  119. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  120. package/components/builder/settings-panel/SectionV2Settings.tsx +25 -20
  121. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  122. package/components/builder/settings-panel/index.ts +1 -0
  123. package/lib/animation/enter-types.ts +1 -0
  124. package/lib/animation/hover-effect-presets.ts +210 -210
  125. package/lib/animation/hover-effect-types.ts +1 -0
  126. package/lib/builder/block-registrations.ts +468 -417
  127. package/lib/builder/constants.ts +111 -111
  128. package/lib/builder/serializer/normalizers.ts +14 -0
  129. package/lib/builder/serializer/serializers.ts +27 -0
  130. package/lib/builder/store-sections.ts +23 -2
  131. package/lib/builder/types-slices.ts +428 -414
  132. package/lib/builder/types.ts +4 -1
  133. package/lib/config/index.ts +27 -27
  134. package/lib/sanity/queries.ts +48 -0
  135. package/lib/sanity/types.ts +112 -1
  136. package/lib/version.ts +1 -1
  137. package/package.json +7 -5
  138. package/sanity/schemas/blocks/audioBlock.ts +69 -69
  139. package/sanity/schemas/blocks/index.ts +12 -11
  140. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  141. package/sanity/schemas/index.ts +120 -117
  142. package/sanity/schemas/objects/coverSection.ts +32 -0
  143. package/sanity/schemas/objects/parallaxSlide.ts +32 -0
  144. package/sanity/schemas/pageSectionV2.ts +32 -0
  145. package/styles/admin.css +85 -85
  146. package/styles/animations.css +237 -237
  147. package/styles/base.css +114 -114
@@ -1,255 +1,255 @@
1
- "use client";
2
-
3
- import { useState, useEffect, useCallback } from "react";
4
- import { csrfHeaders } from "../../../lib/csrf-client";
5
- import { NavBuilder } from "../../../components/admin/nav-builder";
6
- import NavMobileSettings from "../../../components/admin/nav-builder/NavMobileSettings";
7
- import { revalidateSite } from "../../../lib/revalidate";
8
- import type { NavItem, NavDesign, MobileNavDesign, SiteSettings } from "../../../lib/sanity/types";
9
-
10
- /**
11
- * /admin/navigation — Navigation editing.
12
- * Session 129: Footer tab removed — footers are now custom sections placed freely by the user.
13
- * Session 158: Added "Mobile Menu" tab for independent mobile menu styles.
14
- */
15
-
16
- type NavTabId = "desktop" | "mobile";
17
-
18
- const TABS: { id: NavTabId; label: string }[] = [
19
- { id: "desktop", label: "Desktop" },
20
- { id: "mobile", label: "Mobile Menu" },
21
- ];
22
-
23
- export default function AdminNavigationPage() {
24
- const [settings, setSettings] = useState<SiteSettings | null>(null);
25
- const [loading, setLoading] = useState(true);
26
- const [activeTab, setActiveTab] = useState<NavTabId>("desktop");
27
- const [savingNav, setSavingNav] = useState(false);
28
- const [savingMobile, setSavingMobile] = useState(false);
29
- const [mobileDesign, setMobileDesign] = useState<MobileNavDesign>({});
30
- const [mobileHasChanges, setMobileHasChanges] = useState(false);
31
- const [message, setMessage] = useState<{
32
- type: "success" | "error";
33
- text: string;
34
- } | null>(null);
35
-
36
- // ── Load settings ──
37
- const fetchSettings = useCallback(async () => {
38
- try {
39
- const res = await fetch("/api/admin/settings");
40
- if (res.ok) {
41
- const data = await res.json();
42
- setSettings(data.settings || null);
43
- }
44
- } catch {
45
- setMessage({ type: "error", text: "Failed to load navigation" });
46
- } finally {
47
- setLoading(false);
48
- }
49
- }, []);
50
-
51
- useEffect(() => {
52
- fetchSettings();
53
- }, [fetchSettings]);
54
-
55
- // Sync mobile design from loaded settings
56
- useEffect(() => {
57
- if (settings?.nav_mobile_design) {
58
- setMobileDesign(settings.nav_mobile_design);
59
- }
60
- }, [settings]);
61
-
62
- // ── Auto-dismiss messages ──
63
- useEffect(() => {
64
- if (!message) return;
65
- const timer = setTimeout(() => setMessage(null), 5000);
66
- return () => clearTimeout(timer);
67
- }, [message]);
68
-
69
- // ── Save handlers ──
70
- const handleSaveNavBuilder = async (navItems: NavItem[], navDesign: NavDesign) => {
71
- setSavingNav(true);
72
- setMessage(null);
73
- try {
74
- // Sanitize items for Sanity
75
- const sanitizedItems = navItems.map((item) => ({
76
- _key: item._key,
77
- type: item.type || "menu-item",
78
- label: item.label,
79
- ...(item.logo_image ? { logo_image: item.logo_image } : {}),
80
- link_type: item.link_type,
81
- ...(item.link_type === "internal" && item.internal_page
82
- ? { internal_page: { _ref: item.internal_page._id } }
83
- : {}),
84
- ...(item.link_type === "external"
85
- ? { external_url: item.external_url }
86
- : {}),
87
- ...(item.link_type === "content" ? {
88
- ...(item.content_type ? { content_type: item.content_type } : {}),
89
- ...(item.content_asset ? { content_asset: item.content_asset } : {}),
90
- ...(item.content_url ? { content_url: item.content_url } : {}),
91
- } : {}),
92
- visible: item.visible,
93
- grid_column: item.grid_column,
94
- column_span: item.column_span || 1,
95
- ...(item.style_overrides && Object.keys(item.style_overrides).length > 0
96
- ? { style_overrides: item.style_overrides }
97
- : {}),
98
- }));
99
-
100
- // Save nav items
101
- const navRes = await fetch("/api/admin/settings", {
102
- method: "POST",
103
- headers: { "Content-Type": "application/json", ...csrfHeaders() },
104
- body: JSON.stringify({ section: "navigation", data: { nav_items: sanitizedItems } }),
105
- });
106
- if (!navRes.ok) {
107
- const errData = await navRes.json();
108
- throw new Error(errData.error || "Save items failed");
109
- }
110
-
111
- // Save nav design
112
- const designRes = await fetch("/api/admin/settings", {
113
- method: "POST",
114
- headers: { "Content-Type": "application/json", ...csrfHeaders() },
115
- body: JSON.stringify({ section: "nav_design", data: { nav_design: navDesign } }),
116
- });
117
- if (!designRes.ok) {
118
- const errData = await designRes.json();
119
- throw new Error(errData.error || "Save design failed");
120
- }
121
-
122
- setMessage({ type: "success", text: "Navigation saved" });
123
-
124
- // Refresh settings
125
- const settingsRes = await fetch("/api/admin/settings");
126
- if (settingsRes.ok) {
127
- const settingsData = await settingsRes.json();
128
- setSettings(settingsData.settings || null);
129
- }
130
- revalidateSite();
131
- } catch (err) {
132
- setMessage({
133
- type: "error",
134
- text: err instanceof Error ? err.message : "Save failed",
135
- });
136
- } finally {
137
- setSavingNav(false);
138
- }
139
- };
140
-
141
- // ── Mobile menu design handlers ──
142
- const handleMobileDesignChange = useCallback((newDesign: MobileNavDesign) => {
143
- setMobileDesign(newDesign);
144
- setMobileHasChanges(true);
145
- }, []);
146
-
147
- const handleSaveMobileDesign = useCallback(async () => {
148
- setSavingMobile(true);
149
- setMessage(null);
150
- try {
151
- const res = await fetch("/api/admin/settings", {
152
- method: "POST",
153
- headers: { "Content-Type": "application/json", ...csrfHeaders() },
154
- body: JSON.stringify({
155
- section: "nav_mobile_design",
156
- data: { nav_mobile_design: mobileDesign },
157
- }),
158
- });
159
- if (!res.ok) {
160
- const errData = await res.json();
161
- throw new Error(errData.error || "Save mobile design failed");
162
- }
163
- setMobileHasChanges(false);
164
- setMessage({ type: "success", text: "Mobile menu saved" });
165
-
166
- // Refresh settings
167
- const settingsRes = await fetch("/api/admin/settings");
168
- if (settingsRes.ok) {
169
- const settingsData = await settingsRes.json();
170
- setSettings(settingsData.settings || null);
171
- }
172
- revalidateSite();
173
- } catch (err) {
174
- setMessage({
175
- type: "error",
176
- text: err instanceof Error ? err.message : "Save failed",
177
- });
178
- } finally {
179
- setSavingMobile(false);
180
- }
181
- }, [mobileDesign]);
182
-
183
- if (loading) {
184
- return (
185
- <div className="flex items-center justify-center py-20">
186
- <span className="text-sm text-neutral-400 animate-pulse">
187
- Loading navigation...
188
- </span>
189
- </div>
190
- );
191
- }
192
-
193
- return (
194
- <div className="space-y-6">
195
- <h1 className="text-2xl font-semibold text-neutral-900">
196
- Navigation
197
- </h1>
198
-
199
- {/* Tab navigation — matches admin Customize style */}
200
- <div className="flex items-center gap-1 border-b border-neutral-200">
201
- {TABS.map((tab) => {
202
- const isActive = activeTab === tab.id;
203
- return (
204
- <button
205
- key={tab.id}
206
- onClick={() => setActiveTab(tab.id)}
207
- className={`flex items-center gap-1.5 px-4 py-2.5 text-[13px] font-medium transition-colors border-b-2 -mb-px ${
208
- isActive
209
- ? "text-neutral-900 border-[#076bff]"
210
- : "text-neutral-400 border-transparent hover:text-neutral-600"
211
- }`}
212
- >
213
- {tab.label}
214
- </button>
215
- );
216
- })}
217
- </div>
218
-
219
- {/* Toast message */}
220
- {message && (
221
- <div
222
- className={`p-3 rounded-xl border transition-opacity ${
223
- message.type === "success"
224
- ? "border-green-200 bg-green-50 text-green-700"
225
- : "border-red-200 bg-red-50 text-red-700"
226
- }`}
227
- >
228
- <p className="text-sm">{message.text}</p>
229
- </div>
230
- )}
231
-
232
- {/* Tab content */}
233
- {activeTab === "desktop" && (
234
- <NavBuilder
235
- initialItems={settings?.nav_items || []}
236
- initialDesign={settings?.nav_design || {}}
237
- onSave={handleSaveNavBuilder}
238
- saving={savingNav}
239
- />
240
- )}
241
-
242
- {activeTab === "mobile" && (
243
- <NavMobileSettings
244
- design={mobileDesign}
245
- desktopDesign={settings?.nav_design || {}}
246
- items={settings?.nav_items || []}
247
- onChange={handleMobileDesignChange}
248
- onSave={handleSaveMobileDesign}
249
- saving={savingMobile}
250
- hasChanges={mobileHasChanges}
251
- />
252
- )}
253
- </div>
254
- );
255
- }
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { csrfHeaders } from "../../../lib/csrf-client";
5
+ import { NavBuilder } from "../../../components/admin/nav-builder";
6
+ import NavMobileSettings from "../../../components/admin/nav-builder/NavMobileSettings";
7
+ import { revalidateSite } from "../../../lib/revalidate";
8
+ import type { NavItem, NavDesign, MobileNavDesign, SiteSettings } from "../../../lib/sanity/types";
9
+
10
+ /**
11
+ * /admin/navigation — Navigation editing.
12
+ * Session 129: Footer tab removed — footers are now custom sections placed freely by the user.
13
+ * Session 158: Added "Mobile Menu" tab for independent mobile menu styles.
14
+ */
15
+
16
+ type NavTabId = "desktop" | "mobile";
17
+
18
+ const TABS: { id: NavTabId; label: string }[] = [
19
+ { id: "desktop", label: "Desktop" },
20
+ { id: "mobile", label: "Mobile Menu" },
21
+ ];
22
+
23
+ export default function AdminNavigationPage() {
24
+ const [settings, setSettings] = useState<SiteSettings | null>(null);
25
+ const [loading, setLoading] = useState(true);
26
+ const [activeTab, setActiveTab] = useState<NavTabId>("desktop");
27
+ const [savingNav, setSavingNav] = useState(false);
28
+ const [savingMobile, setSavingMobile] = useState(false);
29
+ const [mobileDesign, setMobileDesign] = useState<MobileNavDesign>({});
30
+ const [mobileHasChanges, setMobileHasChanges] = useState(false);
31
+ const [message, setMessage] = useState<{
32
+ type: "success" | "error";
33
+ text: string;
34
+ } | null>(null);
35
+
36
+ // ── Load settings ──
37
+ const fetchSettings = useCallback(async () => {
38
+ try {
39
+ const res = await fetch("/api/admin/settings");
40
+ if (res.ok) {
41
+ const data = await res.json();
42
+ setSettings(data.settings || null);
43
+ }
44
+ } catch {
45
+ setMessage({ type: "error", text: "Failed to load navigation" });
46
+ } finally {
47
+ setLoading(false);
48
+ }
49
+ }, []);
50
+
51
+ useEffect(() => {
52
+ fetchSettings();
53
+ }, [fetchSettings]);
54
+
55
+ // Sync mobile design from loaded settings
56
+ useEffect(() => {
57
+ if (settings?.nav_mobile_design) {
58
+ setMobileDesign(settings.nav_mobile_design);
59
+ }
60
+ }, [settings]);
61
+
62
+ // ── Auto-dismiss messages ──
63
+ useEffect(() => {
64
+ if (!message) return;
65
+ const timer = setTimeout(() => setMessage(null), 5000);
66
+ return () => clearTimeout(timer);
67
+ }, [message]);
68
+
69
+ // ── Save handlers ──
70
+ const handleSaveNavBuilder = async (navItems: NavItem[], navDesign: NavDesign) => {
71
+ setSavingNav(true);
72
+ setMessage(null);
73
+ try {
74
+ // Sanitize items for Sanity
75
+ const sanitizedItems = navItems.map((item) => ({
76
+ _key: item._key,
77
+ type: item.type || "menu-item",
78
+ label: item.label,
79
+ ...(item.logo_image ? { logo_image: item.logo_image } : {}),
80
+ link_type: item.link_type,
81
+ ...(item.link_type === "internal" && item.internal_page
82
+ ? { internal_page: { _ref: item.internal_page._id } }
83
+ : {}),
84
+ ...(item.link_type === "external"
85
+ ? { external_url: item.external_url }
86
+ : {}),
87
+ ...(item.link_type === "content" ? {
88
+ ...(item.content_type ? { content_type: item.content_type } : {}),
89
+ ...(item.content_asset ? { content_asset: item.content_asset } : {}),
90
+ ...(item.content_url ? { content_url: item.content_url } : {}),
91
+ } : {}),
92
+ visible: item.visible,
93
+ grid_column: item.grid_column,
94
+ column_span: item.column_span || 1,
95
+ ...(item.style_overrides && Object.keys(item.style_overrides).length > 0
96
+ ? { style_overrides: item.style_overrides }
97
+ : {}),
98
+ }));
99
+
100
+ // Save nav items
101
+ const navRes = await fetch("/api/admin/settings", {
102
+ method: "POST",
103
+ headers: { "Content-Type": "application/json", ...csrfHeaders() },
104
+ body: JSON.stringify({ section: "navigation", data: { nav_items: sanitizedItems } }),
105
+ });
106
+ if (!navRes.ok) {
107
+ const errData = await navRes.json();
108
+ throw new Error(errData.error || "Save items failed");
109
+ }
110
+
111
+ // Save nav design
112
+ const designRes = await fetch("/api/admin/settings", {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json", ...csrfHeaders() },
115
+ body: JSON.stringify({ section: "nav_design", data: { nav_design: navDesign } }),
116
+ });
117
+ if (!designRes.ok) {
118
+ const errData = await designRes.json();
119
+ throw new Error(errData.error || "Save design failed");
120
+ }
121
+
122
+ setMessage({ type: "success", text: "Navigation saved" });
123
+
124
+ // Refresh settings
125
+ const settingsRes = await fetch("/api/admin/settings");
126
+ if (settingsRes.ok) {
127
+ const settingsData = await settingsRes.json();
128
+ setSettings(settingsData.settings || null);
129
+ }
130
+ revalidateSite();
131
+ } catch (err) {
132
+ setMessage({
133
+ type: "error",
134
+ text: err instanceof Error ? err.message : "Save failed",
135
+ });
136
+ } finally {
137
+ setSavingNav(false);
138
+ }
139
+ };
140
+
141
+ // ── Mobile menu design handlers ──
142
+ const handleMobileDesignChange = useCallback((newDesign: MobileNavDesign) => {
143
+ setMobileDesign(newDesign);
144
+ setMobileHasChanges(true);
145
+ }, []);
146
+
147
+ const handleSaveMobileDesign = useCallback(async () => {
148
+ setSavingMobile(true);
149
+ setMessage(null);
150
+ try {
151
+ const res = await fetch("/api/admin/settings", {
152
+ method: "POST",
153
+ headers: { "Content-Type": "application/json", ...csrfHeaders() },
154
+ body: JSON.stringify({
155
+ section: "nav_mobile_design",
156
+ data: { nav_mobile_design: mobileDesign },
157
+ }),
158
+ });
159
+ if (!res.ok) {
160
+ const errData = await res.json();
161
+ throw new Error(errData.error || "Save mobile design failed");
162
+ }
163
+ setMobileHasChanges(false);
164
+ setMessage({ type: "success", text: "Mobile menu saved" });
165
+
166
+ // Refresh settings
167
+ const settingsRes = await fetch("/api/admin/settings");
168
+ if (settingsRes.ok) {
169
+ const settingsData = await settingsRes.json();
170
+ setSettings(settingsData.settings || null);
171
+ }
172
+ revalidateSite();
173
+ } catch (err) {
174
+ setMessage({
175
+ type: "error",
176
+ text: err instanceof Error ? err.message : "Save failed",
177
+ });
178
+ } finally {
179
+ setSavingMobile(false);
180
+ }
181
+ }, [mobileDesign]);
182
+
183
+ if (loading) {
184
+ return (
185
+ <div className="flex items-center justify-center py-20">
186
+ <span className="text-sm text-neutral-400 animate-pulse">
187
+ Loading navigation...
188
+ </span>
189
+ </div>
190
+ );
191
+ }
192
+
193
+ return (
194
+ <div className="space-y-6">
195
+ <h1 className="text-2xl font-semibold text-neutral-900">
196
+ Navigation
197
+ </h1>
198
+
199
+ {/* Tab navigation — matches admin Customize style */}
200
+ <div className="flex items-center gap-1 border-b border-neutral-200">
201
+ {TABS.map((tab) => {
202
+ const isActive = activeTab === tab.id;
203
+ return (
204
+ <button
205
+ key={tab.id}
206
+ onClick={() => setActiveTab(tab.id)}
207
+ className={`flex items-center gap-1.5 px-4 py-2.5 text-[13px] font-medium transition-colors border-b-2 -mb-px ${
208
+ isActive
209
+ ? "text-neutral-900 border-[#3580f9]"
210
+ : "text-neutral-400 border-transparent hover:text-neutral-600"
211
+ }`}
212
+ >
213
+ {tab.label}
214
+ </button>
215
+ );
216
+ })}
217
+ </div>
218
+
219
+ {/* Toast message */}
220
+ {message && (
221
+ <div
222
+ className={`p-3 rounded-xl border transition-opacity ${
223
+ message.type === "success"
224
+ ? "border-green-200 bg-green-50 text-green-700"
225
+ : "border-red-200 bg-red-50 text-red-700"
226
+ }`}
227
+ >
228
+ <p className="text-sm">{message.text}</p>
229
+ </div>
230
+ )}
231
+
232
+ {/* Tab content */}
233
+ {activeTab === "desktop" && (
234
+ <NavBuilder
235
+ initialItems={settings?.nav_items || []}
236
+ initialDesign={settings?.nav_design || {}}
237
+ onSave={handleSaveNavBuilder}
238
+ saving={savingNav}
239
+ />
240
+ )}
241
+
242
+ {activeTab === "mobile" && (
243
+ <NavMobileSettings
244
+ design={mobileDesign}
245
+ desktopDesign={settings?.nav_design || {}}
246
+ items={settings?.nav_items || []}
247
+ onChange={handleMobileDesignChange}
248
+ onSave={handleSaveMobileDesign}
249
+ saving={savingMobile}
250
+ hasChanges={mobileHasChanges}
251
+ />
252
+ )}
253
+ </div>
254
+ );
255
+ }