@morphika/andami 0.5.0 → 0.5.2

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 (122) hide show
  1. package/README.md +151 -36
  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 +320 -327
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +6 -6
  8. package/app/admin/pages/page.tsx +11 -11
  9. package/app/admin/projects/page.tsx +14 -14
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/components/admin/MetadataEditor.tsx +6 -6
  13. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  14. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  15. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  16. package/components/admin/nav-builder/NavGridItem.tsx +4 -4
  17. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  18. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  19. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  20. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  21. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  22. package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
  23. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  24. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  25. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  26. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  27. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  28. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  29. package/components/admin/styles/ColorsEditor.tsx +2 -2
  30. package/components/admin/styles/FontsEditor.tsx +6 -6
  31. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  32. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  33. package/components/admin/styles/TypographyEditor.tsx +6 -6
  34. package/components/admin/styles/shared.tsx +68 -68
  35. package/components/blocks/AudioBlockRenderer.tsx +286 -0
  36. package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
  37. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  38. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  39. package/components/builder/BlockCardIcons.tsx +316 -227
  40. package/components/builder/BlockTypePicker.tsx +3 -1
  41. package/components/builder/BubbleIcons.tsx +90 -0
  42. package/components/builder/BuilderCanvas.tsx +2 -0
  43. package/components/builder/CanvasMinimap.tsx +2 -2
  44. package/components/builder/CoverSectionCanvas.tsx +363 -275
  45. package/components/builder/DeviceFrame.tsx +1 -1
  46. package/components/builder/DndWrapper.tsx +3 -3
  47. package/components/builder/InsertionLines.tsx +1 -1
  48. package/components/builder/SectionCardIcons.tsx +421 -320
  49. package/components/builder/SectionEditorBar.tsx +1 -1
  50. package/components/builder/SectionTypePicker.tsx +4 -4
  51. package/components/builder/SectionV2Canvas.tsx +20 -4
  52. package/components/builder/SectionV2Column.tsx +74 -68
  53. package/components/builder/SortableBlock.tsx +93 -73
  54. package/components/builder/SortableRow.tsx +27 -26
  55. package/components/builder/VirtualAssetGrid.tsx +2 -2
  56. package/components/builder/asset-browser/R2BrowserContent.tsx +34 -17
  57. package/components/builder/asset-browser/helpers.ts +4 -0
  58. package/components/builder/asset-browser/types.ts +2 -1
  59. package/components/builder/blockStyles.tsx +192 -173
  60. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  61. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  62. package/components/builder/color-picker/EyedropperButton.tsx +74 -74
  63. package/components/builder/color-picker/HueSlider.tsx +124 -124
  64. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  65. package/components/builder/color-picker/SwatchBar.tsx +93 -93
  66. package/components/builder/editors/AudioBlockEditor.tsx +242 -0
  67. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
  68. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  69. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  70. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  71. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  72. package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
  73. package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
  74. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  75. package/components/builder/editors/ProjectGridEditor.tsx +9 -9
  76. package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
  77. package/components/builder/editors/StaggerSettings.tsx +109 -109
  78. package/components/builder/editors/TextBlockEditor.tsx +3 -3
  79. package/components/builder/editors/TextStylePicker.tsx +1 -1
  80. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  81. package/components/builder/editors/index.ts +11 -10
  82. package/components/builder/editors/shared.tsx +7 -7
  83. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
  84. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
  85. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  86. package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
  87. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  88. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  89. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  90. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
  91. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  92. package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
  93. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  94. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  95. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  96. package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
  97. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  98. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  99. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  100. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  101. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  102. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  103. package/lib/animation/enter-types.ts +3 -0
  104. package/lib/animation/hover-effect-presets.ts +210 -210
  105. package/lib/animation/hover-effect-types.ts +3 -0
  106. package/lib/builder/block-registrations.ts +468 -335
  107. package/lib/builder/constants.ts +111 -111
  108. package/lib/builder/store-sections.ts +2 -2
  109. package/lib/builder/types-slices.ts +414 -414
  110. package/lib/builder/types.ts +6 -1
  111. package/lib/config/index.ts +27 -27
  112. package/lib/sanity/types.ts +156 -1
  113. package/lib/version.ts +1 -1
  114. package/package.json +1 -1
  115. package/sanity/schemas/blocks/audioBlock.ts +69 -0
  116. package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
  117. package/sanity/schemas/blocks/index.ts +12 -9
  118. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  119. package/sanity/schemas/index.ts +120 -111
  120. package/styles/admin.css +85 -85
  121. package/styles/animations.css +237 -237
  122. 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
+ }
@@ -23,7 +23,7 @@ import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup, isCoverSecti
23
23
  import SectionEditorBar from "../../../../components/builder/SectionEditorBar";
24
24
  import CustomSectionInstanceCard from "../../../../components/builder/CustomSectionInstanceCard";
25
25
  import { ColumnDragProvider } from "../../../../components/builder/ColumnDragContext";
26
- import type { BlockType } from "../../../../lib/builder/types";
26
+ import type { BlockType, SectionBlockType } from "../../../../lib/builder/types";
27
27
  import { findGaps } from "../../../../lib/builder/cascade";
28
28
  import { isSectionBlockType } from "../../../../lib/builder/types";
29
29
  import BlockLivePreview from "../../../../components/builder/BlockLivePreview";
@@ -415,10 +415,10 @@ export default function PageEditorPage() {
415
415
  [store]
416
416
  );
417
417
 
418
- // Handle add page section (project grid / project carousel)
418
+ // Handle add page section (project grid / project carousel / marquee)
419
419
  // afterRowKey=null → always appends to end of page (bottom "Add Section" button)
420
420
  const handleAddSection = useCallback(
421
- (blockType: "projectGridBlock" | "projectCarouselBlock") => {
421
+ (blockType: SectionBlockType) => {
422
422
  store.addSection(blockType, null);
423
423
  setShowSectionPicker(false);
424
424
  },
@@ -610,7 +610,7 @@ export default function PageEditorPage() {
610
610
  <button
611
611
  onClick={handleSave}
612
612
  disabled={store.isSaving || !store.isDirty}
613
- className="rounded-lg bg-[#076bff] px-5 py-1.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
613
+ className="rounded-lg bg-[#3580f9] px-5 py-1.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
614
614
  >
615
615
  {store.isSaving ? "Saving..." : "Save"}
616
616
  </button>
@@ -652,7 +652,7 @@ export default function PageEditorPage() {
652
652
  e.stopPropagation();
653
653
  setShowSectionPicker(true);
654
654
  }}
655
- className="rounded-lg bg-[#076bff] px-4 py-2 text-xs text-white hover:bg-[#0559d4] transition-colors"
655
+ className="rounded-lg bg-[#3580f9] px-4 py-2 text-xs text-white hover:bg-[#2d6dd4] transition-colors"
656
656
  >
657
657
  + Add First Section
658
658
  </button>
@@ -818,7 +818,7 @@ export default function PageEditorPage() {
818
818
  padding: "5px 16px",
819
819
  background: "#e0daff",
820
820
  color: "#7500d5",
821
- border: "1px dashed #7500d5",
821
+ border: "1.5px dashed #7500d5",
822
822
  }}
823
823
  >
824
824
  + Add Section
@@ -123,7 +123,7 @@ function CreatePageModal({
123
123
  value={title}
124
124
  onChange={(e) => setTitle(e.target.value)}
125
125
  placeholder="Page title"
126
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10"
126
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#3580f9] focus:outline-none focus:ring-2 focus:ring-[#3580f9]/10"
127
127
  autoFocus
128
128
  />
129
129
  </div>
@@ -136,7 +136,7 @@ function CreatePageModal({
136
136
  value={slug}
137
137
  onChange={(e) => { setSlug(e.target.value); setAutoSlug(false); }}
138
138
  placeholder="page-slug"
139
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10"
139
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#3580f9] focus:outline-none focus:ring-2 focus:ring-[#3580f9]/10"
140
140
  />
141
141
  <p className="text-xs text-neutral-400 mt-1">URL: /{slug || "page-slug"}</p>
142
142
  </div>
@@ -152,7 +152,7 @@ function CreatePageModal({
152
152
  <button
153
153
  type="submit"
154
154
  disabled={creating}
155
- className="rounded-lg bg-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-50"
155
+ className="rounded-lg bg-[#3580f9] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors disabled:opacity-50"
156
156
  >
157
157
  {creating ? "Creating..." : "Create"}
158
158
  </button>
@@ -297,7 +297,7 @@ function EditPageModal({
297
297
  type="text"
298
298
  value={title}
299
299
  onChange={(e) => setTitle(e.target.value)}
300
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10"
300
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#3580f9] focus:outline-none focus:ring-2 focus:ring-[#3580f9]/10"
301
301
  autoFocus
302
302
  />
303
303
  </div>
@@ -307,14 +307,14 @@ function EditPageModal({
307
307
  type="text"
308
308
  value={slug}
309
309
  onChange={(e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""))}
310
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10"
310
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#3580f9] focus:outline-none focus:ring-2 focus:ring-[#3580f9]/10"
311
311
  />
312
312
  <p className="text-xs text-neutral-400 mt-1">URL: /{slug || "page-slug"}</p>
313
313
  </div>
314
314
  {error && <p className="text-xs text-[var(--admin-error)]">{error}</p>}
315
315
  <div className="flex gap-3 justify-end pt-2">
316
316
  <button type="button" onClick={onClose} className="rounded-lg border border-neutral-200 px-5 py-2.5 text-sm text-neutral-500 hover:text-neutral-800 hover:border-neutral-300 transition-colors">Cancel</button>
317
- <button type="submit" disabled={saving} className="rounded-lg bg-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-50">{saving ? "Saving..." : "Save"}</button>
317
+ <button type="submit" disabled={saving} className="rounded-lg bg-[#3580f9] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors disabled:opacity-50">{saving ? "Saving..." : "Save"}</button>
318
318
  </div>
319
319
  </form>
320
320
  </div>
@@ -381,7 +381,7 @@ function HomeConfirmModal({
381
381
  {error && <p className="text-xs text-[var(--admin-error)] mb-4">{error}</p>}
382
382
  <div className="flex gap-3 justify-end">
383
383
  <button onClick={onClose} className="rounded-lg border border-neutral-200 px-5 py-2.5 text-sm text-neutral-500 hover:text-neutral-800 hover:border-neutral-300 transition-colors">Cancel</button>
384
- <button onClick={handleConfirm} disabled={setting} className="rounded-lg bg-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-50">{setting ? "Setting..." : "Set as Home"}</button>
384
+ <button onClick={handleConfirm} disabled={setting} className="rounded-lg bg-[#3580f9] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors disabled:opacity-50">{setting ? "Setting..." : "Set as Home"}</button>
385
385
  </div>
386
386
  </div>
387
387
  </div>
@@ -456,7 +456,7 @@ export default function AdminPagesPage() {
456
456
  </h1>
457
457
  <button
458
458
  onClick={() => setShowCreate(true)}
459
- className="rounded-lg bg-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors"
459
+ className="rounded-lg bg-[#3580f9] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors"
460
460
  >
461
461
  + New Page
462
462
  </button>
@@ -469,7 +469,7 @@ export default function AdminPagesPage() {
469
469
  value={search}
470
470
  onChange={(e) => setSearch(e.target.value)}
471
471
  placeholder="Search by title or slug..."
472
- className="w-full max-w-md rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#076bff] focus:outline-none focus:ring-2 focus:ring-[#076bff]/10 shadow-sm"
472
+ className="w-full max-w-md rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-[#3580f9] focus:outline-none focus:ring-2 focus:ring-[#3580f9]/10 shadow-sm"
473
473
  />
474
474
  </div>
475
475
 
@@ -484,7 +484,7 @@ export default function AdminPagesPage() {
484
484
  {pages.length === 0 ? "No pages yet" : "No pages match your search"}
485
485
  </p>
486
486
  {pages.length === 0 && (
487
- <button onClick={() => setShowCreate(true)} className="text-xs text-[#076bff] hover:underline">
487
+ <button onClick={() => setShowCreate(true)} className="text-xs text-[#3580f9] hover:underline">
488
488
  Create your first page
489
489
  </button>
490
490
  )}
@@ -507,7 +507,7 @@ export default function AdminPagesPage() {
507
507
  >
508
508
  {/* Title */}
509
509
  <div className="cursor-pointer" onClick={() => router.push(`/admin/pages/${page.slug.current}`)}>
510
- <p className="text-sm text-neutral-900 group-hover:text-[#076bff] transition-colors">
510
+ <p className="text-sm text-neutral-900 group-hover:text-[#3580f9] transition-colors">
511
511
  {page.title}
512
512
  </p>
513
513
  <p className="text-xs text-neutral-400">/{page.slug.current}</p>