@morphika/andami 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +27 -2
  2. package/app/admin/assets/page.tsx +6 -6
  3. package/app/admin/database/page.tsx +302 -302
  4. package/app/admin/error.tsx +53 -53
  5. package/app/admin/layout.tsx +332 -320
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +44 -27
  8. package/app/admin/pages/page.tsx +24 -19
  9. package/app/admin/projects/page.tsx +30 -21
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/app/api/admin/assets/register/route.ts +51 -14
  13. package/app/api/admin/assets/registry/route.ts +4 -1
  14. package/app/api/admin/assets/relink/confirm/route.ts +4 -1
  15. package/app/api/admin/assets/relink/route.ts +4 -1
  16. package/app/api/admin/assets/scan/route.ts +4 -1
  17. package/app/api/admin/backups/restore-data/route.ts +4 -1
  18. package/app/api/admin/r2/connect/route.ts +4 -1
  19. package/app/api/admin/r2/delete/route.ts +4 -1
  20. package/app/api/admin/r2/rename/route.ts +4 -1
  21. package/app/api/admin/r2/upload-url/route.ts +4 -1
  22. package/app/api/admin/revalidate/route.ts +4 -1
  23. package/app/api/admin/storage/switch/route.ts +4 -1
  24. package/app/api/custom-sections/[id]/route.ts +5 -6
  25. package/components/admin/MetadataEditor.tsx +6 -6
  26. package/components/admin/PublishToggle.tsx +2 -2
  27. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  28. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  29. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  30. package/components/admin/nav-builder/NavGridItem.tsx +8 -6
  31. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  32. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  33. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  34. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  35. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  36. package/components/admin/nav-builder/NavSettingsFields.tsx +518 -514
  37. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  38. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  39. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  40. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  41. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  42. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  43. package/components/admin/styles/ColorsEditor.tsx +9 -8
  44. package/components/admin/styles/FontsEditor.tsx +9 -7
  45. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  46. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  47. package/components/admin/styles/TypographyEditor.tsx +6 -6
  48. package/components/admin/styles/shared.tsx +68 -68
  49. package/components/blocks/AudioBlockRenderer.tsx +286 -286
  50. package/components/blocks/CoverSectionRenderer.tsx +7 -1
  51. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  52. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  53. package/components/blocks/SectionV2Renderer.tsx +8 -1
  54. package/components/builder/BlockCardIcons.tsx +316 -316
  55. package/components/builder/BlockTypePicker.tsx +1 -1
  56. package/components/builder/BubbleIcons.tsx +104 -0
  57. package/components/builder/BuilderCanvas.tsx +2 -0
  58. package/components/builder/CanvasMinimap.tsx +66 -49
  59. package/components/builder/CanvasToolbar.tsx +31 -41
  60. package/components/builder/CoverSectionCanvas.tsx +363 -363
  61. package/components/builder/DeviceFrame.tsx +1 -1
  62. package/components/builder/DndWrapper.tsx +3 -3
  63. package/components/builder/InsertionLines.tsx +1 -1
  64. package/components/builder/SectionCardIcons.tsx +421 -320
  65. package/components/builder/SectionEditorBar.tsx +5 -3
  66. package/components/builder/SectionTypePicker.tsx +7 -5
  67. package/components/builder/SectionV2Canvas.tsx +1 -1
  68. package/components/builder/SectionV2Column.tsx +82 -68
  69. package/components/builder/SettingsPanel.tsx +21 -17
  70. package/components/builder/SortableBlock.tsx +93 -73
  71. package/components/builder/SortableRow.tsx +33 -35
  72. package/components/builder/VirtualAssetGrid.tsx +10 -4
  73. package/components/builder/asset-browser/R2BrowserContent.tsx +18 -14
  74. package/components/builder/blockStyles.tsx +192 -185
  75. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  76. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  77. package/components/builder/color-picker/EyedropperButton.tsx +75 -74
  78. package/components/builder/color-picker/HueSlider.tsx +124 -124
  79. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  80. package/components/builder/color-picker/SwatchBar.tsx +98 -93
  81. package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
  82. package/components/builder/editors/AudioBlockEditor.tsx +242 -242
  83. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
  84. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  85. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  86. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  87. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  88. package/components/builder/editors/ImageGridBlockEditor.tsx +8 -6
  89. package/components/builder/editors/MarqueeBlockEditor.tsx +622 -0
  90. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  91. package/components/builder/editors/ProjectGridEditor.tsx +21 -16
  92. package/components/builder/editors/SpacerBlockEditor.tsx +29 -27
  93. package/components/builder/editors/StaggerSettings.tsx +109 -109
  94. package/components/builder/editors/TextBlockEditor.tsx +22 -17
  95. package/components/builder/editors/TextStylePicker.tsx +1 -1
  96. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  97. package/components/builder/editors/index.ts +11 -10
  98. package/components/builder/editors/shared.tsx +10 -8
  99. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
  100. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
  101. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  102. package/components/builder/live-preview/LiveImagePreview.tsx +4 -2
  103. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  104. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  105. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  106. package/components/builder/live-preview/ProjectCardWrapper.tsx +293 -291
  107. package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
  108. package/components/builder/live-preview/shared.tsx +5 -2
  109. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  110. package/components/builder/settings-panel/BlockLayoutTab.tsx +11 -9
  111. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  112. package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
  113. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  114. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  115. package/components/builder/settings-panel/CoverSectionSettings.tsx +337 -335
  116. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  117. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  118. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  119. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  120. package/components/builder/settings-panel/SectionV2Settings.tsx +25 -20
  121. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  122. package/components/builder/settings-panel/index.ts +1 -0
  123. package/lib/animation/enter-types.ts +1 -0
  124. package/lib/animation/hover-effect-presets.ts +210 -210
  125. package/lib/animation/hover-effect-types.ts +1 -0
  126. package/lib/builder/block-registrations.ts +468 -417
  127. package/lib/builder/constants.ts +111 -111
  128. package/lib/builder/serializer/normalizers.ts +14 -0
  129. package/lib/builder/serializer/serializers.ts +27 -0
  130. package/lib/builder/store-sections.ts +23 -2
  131. package/lib/builder/types-slices.ts +428 -414
  132. package/lib/builder/types.ts +4 -1
  133. package/lib/config/index.ts +27 -27
  134. package/lib/sanity/queries.ts +48 -0
  135. package/lib/sanity/types.ts +112 -1
  136. package/lib/version.ts +1 -1
  137. package/package.json +7 -5
  138. package/sanity/schemas/blocks/audioBlock.ts +69 -69
  139. package/sanity/schemas/blocks/index.ts +12 -11
  140. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  141. package/sanity/schemas/index.ts +120 -117
  142. package/sanity/schemas/objects/coverSection.ts +32 -0
  143. package/sanity/schemas/objects/parallaxSlide.ts +32 -0
  144. package/sanity/schemas/pageSectionV2.ts +32 -0
  145. package/styles/admin.css +85 -85
  146. package/styles/animations.css +237 -237
  147. package/styles/base.css +114 -114
@@ -1,320 +1,332 @@
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
+ // Tabler file-code (outline). Bumped to 22px — the file silhouette
100
+ // has empty space around the `< >` symbol, so it reads smaller than
101
+ // the other icons at the default 20px. Outline keeps it consistent
102
+ // with the rest of the sidebar.
103
+ return (
104
+ <svg width={22} height={22} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
105
+ <path d="M14 3v4a1 1 0 0 0 1 1h4" />
106
+ <path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" />
107
+ <path d="M10 13l-1 2l1 2" />
108
+ <path d="M14 13l1 2l-1 2" />
109
+ </svg>
110
+ );
111
+ case "film":
112
+ // Tray / archive — stack of items going into a container.
113
+ return (
114
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none">
115
+ <path d="M6.75 3.75H17.25" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
116
+ <path d="M4.75 7.25H19.25" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
117
+ <path d="M19.1556 10.75H4.84441C3.53306 10.75 2.57653 11.9908 2.91026 13.259L4.16144 18.0135C4.50827 19.3314 5.69986 20.25 7.06267 20.25H16.9373C18.3001 20.25 19.4917 19.3314 19.8386 18.0135L21.0897 13.259C21.4235 11.9908 20.4669 10.75 19.1556 10.75Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
118
+ </svg>
119
+ );
120
+ case "palette":
121
+ // Tabler color-swatch — geometric, matches the outline style of
122
+ // the rest of the sidebar.
123
+ return (
124
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
125
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
126
+ <path d="M19 3h-4a2 2 0 0 0 -2 2v12a4 4 0 0 0 8 0v-12a2 2 0 0 0 -2 -2" />
127
+ <path d="M13 7.35l-2 -2a2 2 0 0 0 -2.828 0l-2.828 2.828a2 2 0 0 0 0 2.828l9 9" />
128
+ <path d="M7.3 13h-2.3a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h12" />
129
+ <path d="M17 17l0 .01" />
130
+ </svg>
131
+ );
132
+ case "nav":
133
+ return (
134
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
135
+ <line x1="4" y1="7" x2="20" y2="7" />
136
+ <line x1="4" y1="12" x2="20" y2="12" />
137
+ <line x1="4" y1="17" x2="14" y2="17" />
138
+ </svg>
139
+ );
140
+ case "database":
141
+ return (
142
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
143
+ <ellipse cx="12" cy="6" rx="8" ry="3" />
144
+ <path d="M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6" />
145
+ <path d="M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6" />
146
+ </svg>
147
+ );
148
+ case "harddisk":
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
+ <path d="M22 12H2" />
152
+ <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" />
153
+ <line x1="6" y1="16" x2="6.01" y2="16" strokeWidth="2" />
154
+ <line x1="10" y1="16" x2="10.01" y2="16" strokeWidth="2" />
155
+ </svg>
156
+ );
157
+ case "code":
158
+ // Tag reads as "title, description, OG image, SEO", which is
159
+ // what this page actually edits (the old chevrons looked like
160
+ // "developer tool").
161
+ return (
162
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
163
+ <path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
164
+ <circle cx="7" cy="7" r="1" fill="currentColor" stroke="none" />
165
+ </svg>
166
+ );
167
+ case "cloud-download":
168
+ return (
169
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
170
+ <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" />
171
+ <path d="M12 12v8" />
172
+ <path d="M8.5 16.5L12 20l3.5-3.5" />
173
+ </svg>
174
+ );
175
+ case "setup":
176
+ return (
177
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
178
+ <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" />
179
+ </svg>
180
+ );
181
+ case "view":
182
+ return (
183
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
184
+ <path d="M15 3h6v6" />
185
+ <path d="M10 14L21 3" />
186
+ <path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" />
187
+ </svg>
188
+ );
189
+ case "logout":
190
+ return (
191
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
192
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
193
+ <polyline points="16 17 21 12 16 7" />
194
+ <line x1="21" y1="12" x2="9" y2="12" />
195
+ </svg>
196
+ );
197
+ default:
198
+ return null;
199
+ }
200
+ }
201
+
202
+ // ============================================
203
+ // Layout Component — collapsed-only sidebar
204
+ // ============================================
205
+
206
+ export default function AdminLayout({
207
+ children,
208
+ }: {
209
+ children: React.ReactNode;
210
+ }) {
211
+ const pathname = usePathname();
212
+ const router = useRouter();
213
+
214
+ const isPageBuilder =
215
+ /^\/admin\/pages\/[^/]+$/.test(pathname) ||
216
+ /^\/admin\/projects\/[^/]+$/.test(pathname);
217
+
218
+ // Don't show admin shell on login or setup pages
219
+ if (pathname === "/admin/login" || pathname === "/admin/setup") {
220
+ return <>{children}</>;
221
+ }
222
+
223
+ const handleLogout = async () => {
224
+ await fetch("/api/admin/auth", { method: "DELETE" });
225
+ router.push("/admin/login");
226
+ router.refresh();
227
+ };
228
+
229
+ const isLinkActive = (href: string) =>
230
+ pathname === href || pathname.startsWith(href + "/");
231
+
232
+ const tileBase = `group relative flex items-center justify-center ${TILE_SIZE} ${TILE_RADIUS} transition-colors`;
233
+
234
+ const renderNavLink = (link: { href: string; label: string; icon: string }) => {
235
+ const isActive = isLinkActive(link.href);
236
+ return (
237
+ <Link
238
+ key={link.href}
239
+ href={link.href}
240
+ className={`${tileBase} ${
241
+ isActive
242
+ ? "bg-[#3580f9] text-white"
243
+ : "text-white/70 hover:bg-white/[0.06] hover:text-white"
244
+ }`}
245
+ aria-label={link.label}
246
+ >
247
+ <NavIcon icon={link.icon} />
248
+ <Tooltip>{link.label}</Tooltip>
249
+ </Link>
250
+ );
251
+ };
252
+
253
+ return (
254
+ <div
255
+ data-admin
256
+ className="flex h-screen bg-[#f8f8f8]"
257
+ style={{ fontFamily: "Inter, system-ui, sans-serif" }}
258
+ >
259
+ {/* Sidebar — collapsed-only (64px) */}
260
+ <aside className="flex w-16 flex-col items-center bg-gradient-to-b from-[#1e2025] to-[#1a1c20] border-r border-white/[0.07]">
261
+ {/* Logo — white tile with the Andami mark, same square as nav items */}
262
+ <div className="pt-3 pb-2">
263
+ <div
264
+ className={`${tileBase} bg-white`}
265
+ aria-label={`Morphika Andami v${ANDAMI_VERSION}`}
266
+ >
267
+ <AndamiMark size={30} />
268
+ <Tooltip>
269
+ Morphika Andami
270
+ <span className="ml-1.5 text-white/55">v{ANDAMI_VERSION}</span>
271
+ </Tooltip>
272
+ </div>
273
+ </div>
274
+
275
+ {/* Workspace nav */}
276
+ <nav className="flex flex-1 flex-col items-center gap-1.5 pt-2 pb-2">
277
+ {workspaceLinks.map(renderNavLink)}
278
+
279
+ <div className="my-2 h-px w-6 bg-white/10" aria-hidden />
280
+
281
+ {systemLinks.map(renderNavLink)}
282
+ </nav>
283
+
284
+ {/* Utility footer */}
285
+ <div className="flex flex-col items-center gap-1.5 pb-3">
286
+ <div className="mb-2 h-px w-6 bg-white/10" aria-hidden />
287
+
288
+ <Link
289
+ href="/admin/setup"
290
+ className={`${tileBase} text-white/55 hover:bg-white/[0.06] hover:text-white`}
291
+ aria-label="Setup Wizard"
292
+ >
293
+ <NavIcon icon="setup" />
294
+ <Tooltip>Setup Wizard</Tooltip>
295
+ </Link>
296
+
297
+ <Link
298
+ href="/"
299
+ target="_blank"
300
+ className={`${tileBase} text-white/55 hover:bg-white/[0.06] hover:text-white`}
301
+ aria-label="View Site"
302
+ >
303
+ <NavIcon icon="view" />
304
+ <Tooltip>View Site</Tooltip>
305
+ </Link>
306
+
307
+ <button
308
+ onClick={handleLogout}
309
+ className={`${tileBase} text-white/55 hover:bg-red-500/[0.1] hover:text-red-300`}
310
+ aria-label="Log out"
311
+ >
312
+ <NavIcon icon="logout" />
313
+ <Tooltip>Log out</Tooltip>
314
+ </button>
315
+ </div>
316
+ </aside>
317
+
318
+ {/* Main content area — no top header bar, pages have their own titles */}
319
+ <div className="flex flex-1 flex-col overflow-hidden">
320
+ <main
321
+ className={`flex-1 ${
322
+ isPageBuilder
323
+ ? "overflow-hidden"
324
+ : "overflow-y-auto p-8 bg-[#f8f8f8]"
325
+ }`}
326
+ >
327
+ {children}
328
+ </main>
329
+ </div>
330
+ </div>
331
+ );
332
+ }