@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,327 +1,320 @@
1
- "use client";
2
-
3
- import { usePathname, useRouter } from "next/navigation";
4
- import Link from "next/link";
5
- import { useState, useEffect, useRef } from "react";
6
- import { getSiteConfig } from "../../lib/config";
7
- import { ANDAMI_VERSION } from "../../lib/version";
8
-
9
- // ============================================
10
- // Navigation Configuration — grouped by section
11
- // ============================================
12
-
13
- const workspaceLinks = [
14
- { href: "/admin/pages", label: "Pages", icon: "file" },
15
- { href: "/admin/projects", label: "Projects", icon: "film" },
16
- { href: "/admin/styles", label: "Customize", icon: "palette" },
17
- { href: "/admin/navigation", label: "Navigation", icon: "nav" },
18
- ];
19
-
20
- const systemLinks = [
21
- { href: "/admin/storage", label: "Storage", icon: "harddisk" },
22
- { href: "/admin/database", label: "Database", icon: "database" },
23
- { href: "/admin/settings", label: "Metadata", icon: "code" },
24
- { href: "/admin/backups", label: "Backups", icon: "cloud-download" },
25
- ];
26
-
27
- // ============================================
28
- // Icon Component Mockup A set (Storage kept from original)
29
- // ============================================
30
-
31
- function NavIcon({ icon, active }: { icon: string; active?: boolean }) {
32
- const size = 18;
33
- void active; // active state handled by parent text color
34
- switch (icon) {
35
- case "file":
36
- // Mockup A document with folded corner
37
- return (
38
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
39
- <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
40
- <path d="M14 2v6h6" />
41
- </svg>
42
- );
43
- case "film":
44
- // Mockup A — 2x2 rounded grid (Projects)
45
- return (
46
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
47
- <rect x="3" y="3" width="7" height="7" rx="1.5" />
48
- <rect x="14" y="3" width="7" height="7" rx="1.5" />
49
- <rect x="3" y="14" width="7" height="7" rx="1.5" />
50
- <rect x="14" y="14" width="7" height="7" rx="1.5" />
51
- </svg>
52
- );
53
- case "palette":
54
- // Mockup A — minimal palette face (Customize)
55
- return (
56
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
57
- <circle cx="12" cy="12" r="9" />
58
- <circle cx="8" cy="10" r="1" />
59
- <circle cx="16" cy="10" r="1" />
60
- <circle cx="12" cy="15" r="1" />
61
- </svg>
62
- );
63
- case "nav":
64
- // Mockup A — 3 horizontal lines (Navigation)
65
- return (
66
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
67
- <line x1="4" y1="7" x2="20" y2="7" />
68
- <line x1="4" y1="12" x2="20" y2="12" />
69
- <line x1="4" y1="17" x2="14" y2="17" />
70
- </svg>
71
- );
72
- case "database":
73
- // Mockup A — 3-tier cylinder (Database)
74
- return (
75
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
76
- <ellipse cx="12" cy="6" rx="8" ry="3" />
77
- <path d="M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6" />
78
- <path d="M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6" />
79
- </svg>
80
- );
81
- case "harddisk":
82
- // Kept as-is per user request (Storage)
83
- return (
84
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
85
- <path d="M22 12H2" />
86
- <path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11Z" />
87
- <line x1="6" y1="16" x2="6.01" y2="16" strokeWidth="2" />
88
- <line x1="10" y1="16" x2="10.01" y2="16" strokeWidth="2" />
89
- </svg>
90
- );
91
- case "code":
92
- // Mockup A < > brackets (Metadata)
93
- return (
94
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
95
- <polyline points="8 6 3 12 8 18" />
96
- <polyline points="16 6 21 12 16 18" />
97
- </svg>
98
- );
99
- case "cloud-download":
100
- // Mockup A cloud with down arrow (Backups)
101
- return (
102
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
103
- <path d="M20 16.5A4.5 4.5 0 0 0 17 8.5 7 7 0 0 0 4 9a5 5 0 0 0 1 9.9" />
104
- <path d="M12 12v8" />
105
- <path d="M8.5 16.5L12 20l3.5-3.5" />
106
- </svg>
107
- );
108
- default:
109
- return null;
110
- }
111
- }
112
-
113
- // ============================================
114
- // Layout Component
115
- // ============================================
116
-
117
- export default function AdminLayout({
118
- children,
119
- }: {
120
- children: React.ReactNode;
121
- }) {
122
- const pathname = usePathname();
123
- const router = useRouter();
124
-
125
- // Detect if we're inside the page builder (editing a specific page or project)
126
- const isPageBuilder =
127
- /^\/admin\/pages\/[^/]+$/.test(pathname) ||
128
- /^\/admin\/projects\/[^/]+$/.test(pathname);
129
-
130
- // Start collapsed if loading directly into the builder
131
- const [sidebarOpen, setSidebarOpen] = useState(!isPageBuilder);
132
- const prevPathname = useRef(pathname);
133
-
134
- // Auto-collapse sidebar when entering the page builder,
135
- // auto-expand when leaving it
136
- useEffect(() => {
137
- if (pathname === prevPathname.current) return;
138
- const wasInBuilder =
139
- /^\/admin\/pages\/[^/]+$/.test(prevPathname.current) ||
140
- /^\/admin\/projects\/[^/]+$/.test(prevPathname.current);
141
- prevPathname.current = pathname;
142
-
143
- if (isPageBuilder && !wasInBuilder) {
144
- setSidebarOpen(false);
145
- } else if (!isPageBuilder && wasInBuilder) {
146
- setSidebarOpen(true);
147
- }
148
- }, [pathname, isPageBuilder]);
149
-
150
- // Don't show admin shell on login or setup pages
151
- if (pathname === "/admin/login" || pathname === "/admin/setup") {
152
- return <>{children}</>;
153
- }
154
-
155
- const handleLogout = async () => {
156
- await fetch("/api/admin/auth", { method: "DELETE" });
157
- router.push("/admin/login");
158
- router.refresh();
159
- };
160
-
161
- // Check if a link is active
162
- const isLinkActive = (href: string) => {
163
- return pathname === href || pathname.startsWith(href + "/");
164
- };
165
-
166
- // Reusable nav link renderer for workspace/system sections
167
- const renderNavLink = (link: { href: string; label: string; icon: string }) => {
168
- const isActive = isLinkActive(link.href);
169
- return (
170
- <Link
171
- key={link.href}
172
- href={link.href}
173
- className={`relative flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors mb-0.5 ${
174
- isActive
175
- ? "bg-gradient-to-r from-[rgba(7,107,255,0.14)] to-[rgba(7,107,255,0.02)] text-white shadow-[inset_0_0_0_1px_rgba(7,107,255,0.18)]"
176
- : "text-white/65 hover:bg-white/[0.035] hover:text-white"
177
- } ${!sidebarOpen ? "justify-center px-0" : ""}`}
178
- title={!sidebarOpen ? link.label : undefined}
179
- >
180
- {isActive && (
181
- <span
182
- aria-hidden
183
- className="pointer-events-none absolute left-[-8px] top-2 bottom-2 w-[3px] rounded-r-full bg-[#076bff] shadow-[0_0_12px_rgba(7,107,255,0.45)]"
184
- />
185
- )}
186
- <span className={`shrink-0 transition-colors ${isActive ? "text-[#076bff]" : ""}`}>
187
- <NavIcon icon={link.icon} active={isActive} />
188
- </span>
189
- {sidebarOpen && <span className="text-[13px]">{link.label}</span>}
190
- </Link>
191
- );
192
- };
193
-
194
- // Shared class for non-primary utility links (Setup Wizard, View Site, Log out)
195
- const utilityItemBase = `flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors mb-0.5 ${
196
- !sidebarOpen ? "justify-center px-0" : ""
197
- }`;
198
-
199
- return (
200
- <div data-admin className="flex h-screen bg-[#f8f8f8]" style={{ fontFamily: "Inter, system-ui, sans-serif" }}>
201
- {/* Sidebar — Dark (refined) */}
202
- <aside
203
- className={`flex flex-col bg-gradient-to-b from-[#1e2025] to-[#1a1c20] border-r border-white/[0.07] transition-all duration-200 ${
204
- sidebarOpen ? "w-56" : "w-16"
205
- }`}
206
- >
207
- {/* Workspace header */}
208
- <div
209
- className={`flex h-14 items-center border-b border-white/[0.06] ${
210
- sidebarOpen ? "justify-between px-4" : "justify-center px-0"
211
- }`}
212
- >
213
- {sidebarOpen && (
214
- <div className="flex min-w-0 flex-col leading-tight">
215
- <div className="flex items-center gap-1.5">
216
- <span className="whitespace-nowrap text-[11.5px] font-semibold tracking-wide text-white">
217
- Morphika Andami
218
- </span>
219
- <span className="rounded-full border border-[rgba(7,107,255,0.2)] bg-[rgba(7,107,255,0.12)] px-1.5 text-[9px] font-semibold leading-[15px] tracking-[0.08em] text-[#9cc2ff]">
220
- v{ANDAMI_VERSION}
221
- </span>
222
- </div>
223
- <span className="mt-0.5 truncate text-[10.5px] text-white/45">
224
- {getSiteConfig().name}
225
- </span>
226
- </div>
227
- )}
228
- <button
229
- onClick={() => setSidebarOpen(!sidebarOpen)}
230
- className="shrink-0 rounded-md p-1 text-white/40 transition-colors hover:bg-white/[0.05] hover:text-white/75"
231
- aria-label={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
232
- >
233
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
234
- {sidebarOpen ? (
235
- <polyline points="11 17 6 12 11 7" />
236
- ) : (
237
- <polyline points="13 7 18 12 13 17" />
238
- )}
239
- </svg>
240
- </button>
241
- </div>
242
-
243
- {/* Nav — grouped into Workspace + System */}
244
- <nav className="flex-1 overflow-y-auto px-2 py-3">
245
- {sidebarOpen && (
246
- <div className="px-3 pt-1 pb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-white/35">
247
- Workspace
248
- </div>
249
- )}
250
- {workspaceLinks.map(renderNavLink)}
251
-
252
- {sidebarOpen ? (
253
- <div className="px-3 pt-4 pb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-white/35">
254
- System
255
- </div>
256
- ) : (
257
- <div className="mx-3 my-2 h-px bg-white/[0.06]" aria-hidden />
258
- )}
259
- {systemLinks.map(renderNavLink)}
260
- </nav>
261
-
262
- {/* Footer — Utility group */}
263
- <div className="px-2 pb-2">
264
- {sidebarOpen ? (
265
- <div className="px-3 pt-1 pb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-white/35">
266
- Utility
267
- </div>
268
- ) : (
269
- <div className="mx-3 my-2 h-px bg-white/[0.06]" aria-hidden />
270
- )}
271
-
272
- {/* Setup Wizard */}
273
- <Link
274
- href="/admin/setup"
275
- className={`${utilityItemBase} text-white/50 hover:bg-white/[0.035] hover:text-white`}
276
- title="Setup Wizard"
277
- >
278
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
279
- <path d="M14.7 6.3a4 4 0 0 0-5.6 5.6l-6 6a1.5 1.5 0 0 0 2.1 2.1l6-6a4 4 0 0 0 5.6-5.6l-2.2 2.2-2.1-2.1z" />
280
- </svg>
281
- {sidebarOpen && <span className="text-[13px]">Setup Wizard</span>}
282
- </Link>
283
-
284
- {/* View Site */}
285
- <Link
286
- href="/"
287
- target="_blank"
288
- className={`${utilityItemBase} text-white/50 hover:bg-white/[0.035] hover:text-white`}
289
- title="View Site"
290
- >
291
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
292
- <path d="M15 3h6v6" />
293
- <path d="M10 14L21 3" />
294
- <path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" />
295
- </svg>
296
- {sidebarOpen && <span className="text-[13px]">View Site</span>}
297
- </Link>
298
-
299
- {/* Log out */}
300
- <button
301
- onClick={handleLogout}
302
- className={`${utilityItemBase} w-full text-white/50 hover:bg-red-500/[0.08] hover:text-red-300`}
303
- title="Log out"
304
- >
305
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
306
- <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
307
- <polyline points="16 17 21 12 16 7" />
308
- <line x1="21" y1="12" x2="9" y2="12" />
309
- </svg>
310
- {sidebarOpen && <span className="text-[13px]">Log out</span>}
311
- </button>
312
- </div>
313
- </aside>
314
-
315
- {/* Main content area — no top header bar, pages have their own titles */}
316
- <div className="flex flex-1 flex-col overflow-hidden">
317
- <main className={`flex-1 ${
318
- isPageBuilder
319
- ? "overflow-hidden"
320
- : "overflow-y-auto p-8 bg-[#f8f8f8]"
321
- }`}>
322
- {children}
323
- </main>
324
- </div>
325
- </div>
326
- );
327
- }
1
+ "use client";
2
+
3
+ import { usePathname, useRouter } from "next/navigation";
4
+ import Link from "next/link";
5
+ import { getSiteConfig } from "../../lib/config";
6
+ import { ANDAMI_VERSION } from "../../lib/version";
7
+
8
+ // ============================================
9
+ // Navigation Configuration — grouped by section
10
+ // ============================================
11
+
12
+ const workspaceLinks = [
13
+ { href: "/admin/pages", label: "Pages", icon: "file" },
14
+ { href: "/admin/projects", label: "Projects", icon: "film" },
15
+ { href: "/admin/styles", label: "Customize", icon: "palette" },
16
+ { href: "/admin/navigation", label: "Navigation", icon: "nav" },
17
+ ];
18
+
19
+ const systemLinks = [
20
+ { href: "/admin/storage", label: "Storage", icon: "harddisk" },
21
+ { href: "/admin/database", label: "Database", icon: "database" },
22
+ { href: "/admin/settings", label: "Metadata", icon: "code" },
23
+ { href: "/admin/backups", label: "Backups", icon: "cloud-download" },
24
+ ];
25
+
26
+ // Shared tile shape — logo square, active item, hover bg all share this
27
+ // so every piece lines up pixel-perfect.
28
+ const TILE_SIZE = "w-10 h-10";
29
+ const TILE_RADIUS = "rounded-lg";
30
+
31
+ // ============================================
32
+ // Tooltip CSS-only (group-hover), no JS state, 120ms fade + slide-in
33
+ // ============================================
34
+ // Rendered to the right of its parent tile. The parent must have
35
+ // `group relative` classes (already baked into `tileBase`). Uses
36
+ // `pointer-events-none` so it never blocks clicks or creates its own
37
+ // hover state.
38
+
39
+ function Tooltip({ children }: { children: React.ReactNode }) {
40
+ return (
41
+ <span
42
+ role="tooltip"
43
+ className="pointer-events-none absolute left-full top-1/2 ml-1.5 z-50 whitespace-nowrap rounded-md border border-white/10 bg-[#2a2d33] px-2.5 py-1.5 text-[11px] font-medium text-white shadow-lg opacity-0 transition-[opacity,margin] duration-150 ease-out group-hover:opacity-100 group-hover:ml-3"
44
+ style={{ transform: "translateY(-50%)" }}
45
+ >
46
+ {children}
47
+ </span>
48
+ );
49
+ }
50
+
51
+ // ============================================
52
+ // Andami Mark — inlined from docs/andami_mark.svg
53
+ // ============================================
54
+
55
+ function AndamiMark({ size = 30 }: { size?: number }) {
56
+ // viewBox cropped to the actual content bbox (the original 0 0 500 500
57
+ // had ~36% empty padding that made the mark render tiny). Centered on the
58
+ // shape's true centroid (249.75, 240.2) with a 280px padded square.
59
+ return (
60
+ <svg
61
+ width={size}
62
+ height={size}
63
+ viewBox="110 100 280 280"
64
+ xmlns="http://www.w3.org/2000/svg"
65
+ aria-hidden
66
+ >
67
+ <path
68
+ fill="#3580f9"
69
+ d="M228.8,166.3h39.6c3.1,0,5.7-2.5,5.7-5.6V121c0-3.1-2.6-5.7-5.7-5.7h-39.6c-3.1,0-5.7,2.6-5.7,5.7v39.6 C223.1,163.8,225.7,166.3,228.8,166.3z"
70
+ />
71
+ <path
72
+ fill="#1E2025"
73
+ d="M227.2,185.8h-39.6c-3.1,0-5.7,2.6-5.7,5.7v39.6c0,3.1,2.6,5.7,5.7,5.7h39.6c3.1,0,5.7-2.6,5.7-5.7v-39.6 C232.9,188.4,230.3,185.8,227.2,185.8z"
74
+ />
75
+ <path
76
+ fill="#1E2025"
77
+ d="M311.4,231.1v-39.6c0-3.1-2.6-5.7-5.7-5.7h-39.6c-3.1,0-5.7,2.6-5.7,5.7v39.6c0,3.1,2.6,5.7,5.7,5.7h39.6 C308.8,236.8,311.4,234.3,311.4,231.1z"
78
+ />
79
+ <path
80
+ fill="#1E2025"
81
+ d="M332.7,258.4h-41.3c-3.3,0-6,2.7-6,6v94.7c0,3.3,2.7,6,6,6h41.3c3.3,0,6-2.7,6-6v-94.7 C338.7,261.1,336,258.4,332.7,258.4z"
82
+ />
83
+ <path
84
+ fill="#1E2025"
85
+ d="M208.1,258.4h-41.3c-3.3,0-6,2.7-6,6v94.7c0,3.3,2.7,6,6,6h41.3c3.3,0,6-2.7,6-6v-94.7 C214.2,261.1,211.5,258.4,208.1,258.4z"
86
+ />
87
+ </svg>
88
+ );
89
+ }
90
+
91
+ // ============================================
92
+ // Icon Component — Mockup A set (Storage kept from original)
93
+ // ============================================
94
+
95
+ function NavIcon({ icon }: { icon: string }) {
96
+ const size = 20;
97
+ switch (icon) {
98
+ case "file":
99
+ return (
100
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
101
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
102
+ <path d="M14 2v6h6" />
103
+ </svg>
104
+ );
105
+ case "film":
106
+ return (
107
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
108
+ <rect x="3" y="3" width="7" height="7" rx="1.5" />
109
+ <rect x="14" y="3" width="7" height="7" rx="1.5" />
110
+ <rect x="3" y="14" width="7" height="7" rx="1.5" />
111
+ <rect x="14" y="14" width="7" height="7" rx="1.5" />
112
+ </svg>
113
+ );
114
+ case "palette":
115
+ return (
116
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
117
+ <circle cx="12" cy="12" r="9" />
118
+ <circle cx="8" cy="10" r="1" />
119
+ <circle cx="16" cy="10" r="1" />
120
+ <circle cx="12" cy="15" r="1" />
121
+ </svg>
122
+ );
123
+ case "nav":
124
+ return (
125
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
126
+ <line x1="4" y1="7" x2="20" y2="7" />
127
+ <line x1="4" y1="12" x2="20" y2="12" />
128
+ <line x1="4" y1="17" x2="14" y2="17" />
129
+ </svg>
130
+ );
131
+ case "database":
132
+ return (
133
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
134
+ <ellipse cx="12" cy="6" rx="8" ry="3" />
135
+ <path d="M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6" />
136
+ <path d="M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6" />
137
+ </svg>
138
+ );
139
+ case "harddisk":
140
+ return (
141
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
142
+ <path d="M22 12H2" />
143
+ <path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11Z" />
144
+ <line x1="6" y1="16" x2="6.01" y2="16" strokeWidth="2" />
145
+ <line x1="10" y1="16" x2="10.01" y2="16" strokeWidth="2" />
146
+ </svg>
147
+ );
148
+ case "code":
149
+ return (
150
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
151
+ <polyline points="8 6 3 12 8 18" />
152
+ <polyline points="16 6 21 12 16 18" />
153
+ </svg>
154
+ );
155
+ case "cloud-download":
156
+ return (
157
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
158
+ <path d="M20 16.5A4.5 4.5 0 0 0 17 8.5 7 7 0 0 0 4 9a5 5 0 0 0 1 9.9" />
159
+ <path d="M12 12v8" />
160
+ <path d="M8.5 16.5L12 20l3.5-3.5" />
161
+ </svg>
162
+ );
163
+ case "setup":
164
+ return (
165
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
166
+ <path d="M14.7 6.3a4 4 0 0 0-5.6 5.6l-6 6a1.5 1.5 0 0 0 2.1 2.1l6-6a4 4 0 0 0 5.6-5.6l-2.2 2.2-2.1-2.1z" />
167
+ </svg>
168
+ );
169
+ case "view":
170
+ return (
171
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
172
+ <path d="M15 3h6v6" />
173
+ <path d="M10 14L21 3" />
174
+ <path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" />
175
+ </svg>
176
+ );
177
+ case "logout":
178
+ return (
179
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
180
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
181
+ <polyline points="16 17 21 12 16 7" />
182
+ <line x1="21" y1="12" x2="9" y2="12" />
183
+ </svg>
184
+ );
185
+ default:
186
+ return null;
187
+ }
188
+ }
189
+
190
+ // ============================================
191
+ // Layout Component — collapsed-only sidebar
192
+ // ============================================
193
+
194
+ export default function AdminLayout({
195
+ children,
196
+ }: {
197
+ children: React.ReactNode;
198
+ }) {
199
+ const pathname = usePathname();
200
+ const router = useRouter();
201
+
202
+ const isPageBuilder =
203
+ /^\/admin\/pages\/[^/]+$/.test(pathname) ||
204
+ /^\/admin\/projects\/[^/]+$/.test(pathname);
205
+
206
+ // Don't show admin shell on login or setup pages
207
+ if (pathname === "/admin/login" || pathname === "/admin/setup") {
208
+ return <>{children}</>;
209
+ }
210
+
211
+ const handleLogout = async () => {
212
+ await fetch("/api/admin/auth", { method: "DELETE" });
213
+ router.push("/admin/login");
214
+ router.refresh();
215
+ };
216
+
217
+ const isLinkActive = (href: string) =>
218
+ pathname === href || pathname.startsWith(href + "/");
219
+
220
+ const tileBase = `group relative flex items-center justify-center ${TILE_SIZE} ${TILE_RADIUS} transition-colors`;
221
+
222
+ const renderNavLink = (link: { href: string; label: string; icon: string }) => {
223
+ const isActive = isLinkActive(link.href);
224
+ return (
225
+ <Link
226
+ key={link.href}
227
+ href={link.href}
228
+ className={`${tileBase} ${
229
+ isActive
230
+ ? "bg-[#3580f9] text-white"
231
+ : "text-white/70 hover:bg-white/[0.06] hover:text-white"
232
+ }`}
233
+ aria-label={link.label}
234
+ >
235
+ <NavIcon icon={link.icon} />
236
+ <Tooltip>{link.label}</Tooltip>
237
+ </Link>
238
+ );
239
+ };
240
+
241
+ return (
242
+ <div
243
+ data-admin
244
+ className="flex h-screen bg-[#f8f8f8]"
245
+ style={{ fontFamily: "Inter, system-ui, sans-serif" }}
246
+ >
247
+ {/* Sidebar — collapsed-only (64px) */}
248
+ <aside className="flex w-16 flex-col items-center bg-gradient-to-b from-[#1e2025] to-[#1a1c20] border-r border-white/[0.07]">
249
+ {/* Logo — white tile with the Andami mark, same square as nav items */}
250
+ <div className="pt-3 pb-2">
251
+ <div
252
+ className={`${tileBase} bg-white`}
253
+ aria-label={`Morphika Andami v${ANDAMI_VERSION}`}
254
+ >
255
+ <AndamiMark size={30} />
256
+ <Tooltip>
257
+ Morphika Andami
258
+ <span className="ml-1.5 text-white/55">v{ANDAMI_VERSION}</span>
259
+ </Tooltip>
260
+ </div>
261
+ </div>
262
+
263
+ {/* Workspace nav */}
264
+ <nav className="flex flex-1 flex-col items-center gap-1.5 pt-2 pb-2">
265
+ {workspaceLinks.map(renderNavLink)}
266
+
267
+ <div className="my-2 h-px w-6 bg-white/10" aria-hidden />
268
+
269
+ {systemLinks.map(renderNavLink)}
270
+ </nav>
271
+
272
+ {/* Utility footer */}
273
+ <div className="flex flex-col items-center gap-1.5 pb-3">
274
+ <div className="mb-2 h-px w-6 bg-white/10" aria-hidden />
275
+
276
+ <Link
277
+ href="/admin/setup"
278
+ className={`${tileBase} text-white/55 hover:bg-white/[0.06] hover:text-white`}
279
+ aria-label="Setup Wizard"
280
+ >
281
+ <NavIcon icon="setup" />
282
+ <Tooltip>Setup Wizard</Tooltip>
283
+ </Link>
284
+
285
+ <Link
286
+ href="/"
287
+ target="_blank"
288
+ className={`${tileBase} text-white/55 hover:bg-white/[0.06] hover:text-white`}
289
+ aria-label="View Site"
290
+ >
291
+ <NavIcon icon="view" />
292
+ <Tooltip>View Site</Tooltip>
293
+ </Link>
294
+
295
+ <button
296
+ onClick={handleLogout}
297
+ className={`${tileBase} text-white/55 hover:bg-red-500/[0.1] hover:text-red-300`}
298
+ aria-label="Log out"
299
+ >
300
+ <NavIcon icon="logout" />
301
+ <Tooltip>Log out</Tooltip>
302
+ </button>
303
+ </div>
304
+ </aside>
305
+
306
+ {/* Main content area no top header bar, pages have their own titles */}
307
+ <div className="flex flex-1 flex-col overflow-hidden">
308
+ <main
309
+ className={`flex-1 ${
310
+ isPageBuilder
311
+ ? "overflow-hidden"
312
+ : "overflow-y-auto p-8 bg-[#f8f8f8]"
313
+ }`}
314
+ >
315
+ {children}
316
+ </main>
317
+ </div>
318
+ </div>
319
+ );
320
+ }