@morphika/andami 0.5.1 → 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 (117) hide show
  1. package/app/admin/assets/page.tsx +6 -6
  2. package/app/admin/database/page.tsx +302 -302
  3. package/app/admin/error.tsx +53 -53
  4. package/app/admin/layout.tsx +320 -320
  5. package/app/admin/navigation/page.tsx +255 -255
  6. package/app/admin/pages/[slug]/page.tsx +6 -6
  7. package/app/admin/pages/page.tsx +11 -11
  8. package/app/admin/projects/page.tsx +14 -14
  9. package/app/admin/setup/page.tsx +1 -1
  10. package/app/admin/styles/page.tsx +1 -1
  11. package/components/admin/MetadataEditor.tsx +6 -6
  12. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  13. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  14. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  15. package/components/admin/nav-builder/NavGridItem.tsx +4 -4
  16. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  17. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  18. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  19. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  20. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  21. package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
  22. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  23. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  24. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  25. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  26. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  27. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  28. package/components/admin/styles/ColorsEditor.tsx +2 -2
  29. package/components/admin/styles/FontsEditor.tsx +6 -6
  30. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  31. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  32. package/components/admin/styles/TypographyEditor.tsx +6 -6
  33. package/components/admin/styles/shared.tsx +68 -68
  34. package/components/blocks/AudioBlockRenderer.tsx +286 -286
  35. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  36. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  37. package/components/builder/BlockCardIcons.tsx +316 -316
  38. package/components/builder/BlockTypePicker.tsx +1 -1
  39. package/components/builder/BubbleIcons.tsx +90 -0
  40. package/components/builder/BuilderCanvas.tsx +2 -0
  41. package/components/builder/CanvasMinimap.tsx +2 -2
  42. package/components/builder/CoverSectionCanvas.tsx +363 -363
  43. package/components/builder/DeviceFrame.tsx +1 -1
  44. package/components/builder/DndWrapper.tsx +3 -3
  45. package/components/builder/InsertionLines.tsx +1 -1
  46. package/components/builder/SectionCardIcons.tsx +421 -320
  47. package/components/builder/SectionEditorBar.tsx +1 -1
  48. package/components/builder/SectionTypePicker.tsx +4 -4
  49. package/components/builder/SectionV2Canvas.tsx +1 -1
  50. package/components/builder/SectionV2Column.tsx +69 -67
  51. package/components/builder/SortableBlock.tsx +93 -73
  52. package/components/builder/SortableRow.tsx +27 -26
  53. package/components/builder/VirtualAssetGrid.tsx +2 -2
  54. package/components/builder/asset-browser/R2BrowserContent.tsx +11 -11
  55. package/components/builder/blockStyles.tsx +192 -185
  56. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  57. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  58. package/components/builder/color-picker/EyedropperButton.tsx +74 -74
  59. package/components/builder/color-picker/HueSlider.tsx +124 -124
  60. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  61. package/components/builder/color-picker/SwatchBar.tsx +93 -93
  62. package/components/builder/editors/AudioBlockEditor.tsx +242 -242
  63. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
  64. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  65. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  66. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  67. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  68. package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
  69. package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
  70. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  71. package/components/builder/editors/ProjectGridEditor.tsx +9 -9
  72. package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
  73. package/components/builder/editors/StaggerSettings.tsx +109 -109
  74. package/components/builder/editors/TextBlockEditor.tsx +3 -3
  75. package/components/builder/editors/TextStylePicker.tsx +1 -1
  76. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  77. package/components/builder/editors/index.ts +11 -10
  78. package/components/builder/editors/shared.tsx +6 -6
  79. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
  80. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
  81. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  82. package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
  83. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  84. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  85. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  86. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
  87. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  88. package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
  89. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  90. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  91. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  92. package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
  93. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  94. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  95. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  96. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  97. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  98. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  99. package/lib/animation/enter-types.ts +1 -0
  100. package/lib/animation/hover-effect-presets.ts +210 -210
  101. package/lib/animation/hover-effect-types.ts +1 -0
  102. package/lib/builder/block-registrations.ts +468 -417
  103. package/lib/builder/constants.ts +111 -111
  104. package/lib/builder/store-sections.ts +2 -2
  105. package/lib/builder/types-slices.ts +414 -414
  106. package/lib/builder/types.ts +4 -1
  107. package/lib/config/index.ts +27 -27
  108. package/lib/sanity/types.ts +98 -1
  109. package/lib/version.ts +1 -1
  110. package/package.json +1 -1
  111. package/sanity/schemas/blocks/audioBlock.ts +69 -69
  112. package/sanity/schemas/blocks/index.ts +12 -11
  113. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  114. package/sanity/schemas/index.ts +120 -117
  115. package/styles/admin.css +85 -85
  116. package/styles/animations.css +237 -237
  117. package/styles/base.css +114 -114
@@ -1,320 +1,320 @@
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="#076BFF"
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-[#076bff] 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
- }
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
+ }