@morphika/andami 0.2.8 → 0.2.10

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.
@@ -1,274 +1,327 @@
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 — flat list, no sections
11
- // ============================================
12
-
13
- const navLinks = [
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
- { href: "/admin/storage", label: "Storage", icon: "harddisk" },
19
- { href: "/admin/database", label: "Database", icon: "database" },
20
- { href: "/admin/settings", label: "Metadata", icon: "code" },
21
- ];
22
-
23
- // ============================================
24
- // Icon Component white stroke for dark sidebar
25
- // ============================================
26
-
27
- function NavIcon({ icon, active }: { icon: string; active?: boolean }) {
28
- const size = 18;
29
- const color = active ? "currentColor" : "currentColor";
30
- void active; // active state handled by parent text color
31
- void color;
32
- switch (icon) {
33
- case "file":
34
- return (
35
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
36
- <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" />
37
- <polyline points="14 2 14 8 20 8" />
38
- </svg>
39
- );
40
- case "film":
41
- return (
42
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
43
- <rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18" />
44
- <line x1="7" y1="2" x2="7" y2="22" />
45
- <line x1="17" y1="2" x2="17" y2="22" />
46
- <line x1="2" y1="12" x2="22" y2="12" />
47
- <line x1="2" y1="7" x2="7" y2="7" />
48
- <line x1="2" y1="17" x2="7" y2="17" />
49
- <line x1="17" y1="7" x2="22" y2="7" />
50
- <line x1="17" y1="17" x2="22" y2="17" />
51
- </svg>
52
- );
53
- case "palette":
54
- return (
55
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
56
- <circle cx="13.5" cy="6.5" r="0.5" fill="currentColor" />
57
- <circle cx="17.5" cy="10.5" r="0.5" fill="currentColor" />
58
- <circle cx="8.5" cy="7.5" r="0.5" fill="currentColor" />
59
- <circle cx="6.5" cy="12" r="0.5" fill="currentColor" />
60
- <path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2Z" />
61
- </svg>
62
- );
63
- case "nav":
64
- return (
65
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
66
- <line x1="3" y1="6" x2="21" y2="6" />
67
- <line x1="3" y1="12" x2="15" y2="12" />
68
- <line x1="3" y1="18" x2="18" y2="18" />
69
- </svg>
70
- );
71
- case "database":
72
- return (
73
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
74
- <ellipse cx="12" cy="5" rx="9" ry="3" />
75
- <path d="M21 12c0 1.66-4.03 3-9 3s-9-1.34-9-3" />
76
- <path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5" />
77
- </svg>
78
- );
79
- case "harddisk":
80
- return (
81
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
82
- <path d="M22 12H2" />
83
- <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" />
84
- <line x1="6" y1="16" x2="6.01" y2="16" strokeWidth="2" />
85
- <line x1="10" y1="16" x2="10.01" y2="16" strokeWidth="2" />
86
- </svg>
87
- );
88
- case "code":
89
- return (
90
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
91
- <polyline points="16 18 22 12 16 6" />
92
- <polyline points="8 6 2 12 8 18" />
93
- </svg>
94
- );
95
- default:
96
- return null;
97
- }
98
- }
99
-
100
- // ============================================
101
- // Layout Component
102
- // ============================================
103
-
104
- export default function AdminLayout({
105
- children,
106
- }: {
107
- children: React.ReactNode;
108
- }) {
109
- const pathname = usePathname();
110
- const router = useRouter();
111
-
112
- // Detect if we're inside the page builder (editing a specific page or project)
113
- const isPageBuilder =
114
- /^\/admin\/pages\/[^/]+$/.test(pathname) ||
115
- /^\/admin\/projects\/[^/]+$/.test(pathname);
116
-
117
- // Start collapsed if loading directly into the builder
118
- const [sidebarOpen, setSidebarOpen] = useState(!isPageBuilder);
119
- const prevPathname = useRef(pathname);
120
-
121
- // Auto-collapse sidebar when entering the page builder,
122
- // auto-expand when leaving it
123
- useEffect(() => {
124
- if (pathname === prevPathname.current) return;
125
- const wasInBuilder =
126
- /^\/admin\/pages\/[^/]+$/.test(prevPathname.current) ||
127
- /^\/admin\/projects\/[^/]+$/.test(prevPathname.current);
128
- prevPathname.current = pathname;
129
-
130
- if (isPageBuilder && !wasInBuilder) {
131
- setSidebarOpen(false);
132
- } else if (!isPageBuilder && wasInBuilder) {
133
- setSidebarOpen(true);
134
- }
135
- }, [pathname, isPageBuilder]);
136
-
137
- // Don't show admin shell on login or setup pages
138
- if (pathname === "/admin/login" || pathname === "/admin/setup") {
139
- return <>{children}</>;
140
- }
141
-
142
- const handleLogout = async () => {
143
- await fetch("/api/admin/auth", { method: "DELETE" });
144
- router.push("/admin/login");
145
- router.refresh();
146
- };
147
-
148
- // Check if a link is active
149
- const isLinkActive = (href: string) => {
150
- return pathname === href || pathname.startsWith(href + "/");
151
- };
152
-
153
- return (
154
- <div data-admin className="flex h-screen bg-[#f8f8f8]" style={{ fontFamily: "Inter, system-ui, sans-serif" }}>
155
- {/* Sidebar Dark */}
156
- <aside
157
- className={`flex flex-col bg-[#141414] transition-all duration-200 ${
158
- sidebarOpen ? "w-56" : "w-16"
159
- }`}
160
- >
161
- {/* Logo */}
162
- <div className="flex h-14 items-center justify-between px-4 border-b border-white/[0.06]">
163
- {sidebarOpen && (
164
- <span className="text-[10px] font-semibold tracking-widest uppercase text-white/90 leading-tight">
165
- Morphika Andami <span className="text-white/40 font-normal">v{ANDAMI_VERSION}</span><br />
166
- <span className="text-white/50 font-normal">{getSiteConfig().name}</span>
167
- </span>
168
- )}
169
- <button
170
- onClick={() => setSidebarOpen(!sidebarOpen)}
171
- className="text-white/40 hover:text-white/70 transition-colors p-1 rounded-md hover:bg-white/[0.06]"
172
- aria-label={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
173
- >
174
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
175
- {sidebarOpen ? (
176
- <polyline points="11 17 6 12 11 7" />
177
- ) : (
178
- <polyline points="13 7 18 12 13 17" />
179
- )}
180
- </svg>
181
- </button>
182
- </div>
183
-
184
- {/* Nav links — flat list */}
185
- <nav className="flex-1 py-3 px-2 overflow-y-auto">
186
- {navLinks.map((link) => {
187
- const isActive = isLinkActive(link.href);
188
- return (
189
- <Link
190
- key={link.href}
191
- href={link.href}
192
- className={`flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-all mb-0.5 ${
193
- isActive
194
- ? "bg-white/[0.08] text-white font-medium"
195
- : "text-white/60 hover:bg-white/[0.06] hover:text-white/90"
196
- } ${!sidebarOpen ? "justify-center px-0" : ""}`}
197
- title={!sidebarOpen ? link.label : undefined}
198
- >
199
- <NavIcon icon={link.icon} active={isActive} />
200
- {sidebarOpen && (
201
- <span className="text-[13px]">{link.label}</span>
202
- )}
203
- </Link>
204
- );
205
- })}
206
- </nav>
207
-
208
- {/* Setup Wizard */}
209
- <div className="px-2 mb-0.5">
210
- <Link
211
- href="/admin/setup"
212
- className={`flex items-center gap-3 text-white/40 hover:text-white/80 transition-colors px-3 py-2 rounded-lg hover:bg-white/[0.06] w-full text-sm ${
213
- !sidebarOpen ? "justify-center px-0" : ""
214
- }`}
215
- title="Setup Wizard"
216
- >
217
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
218
- <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76Z" />
219
- </svg>
220
- {sidebarOpen && <span className="text-[13px]">Setup Wizard</span>}
221
- </Link>
222
- </div>
223
-
224
- {/* View Site */}
225
- <div className="px-2 mb-1">
226
- <Link
227
- href="/"
228
- target="_blank"
229
- className={`flex items-center gap-3 text-white/40 hover:text-white/80 transition-colors px-3 py-2 rounded-lg hover:bg-white/[0.06] w-full text-sm ${
230
- !sidebarOpen ? "justify-center px-0" : ""
231
- }`}
232
- title="View Site"
233
- >
234
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
235
- <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
236
- <polyline points="15 3 21 3 21 9" />
237
- <line x1="10" y1="14" x2="21" y2="3" />
238
- </svg>
239
- {sidebarOpen && <span className="text-[13px]">View Site</span>}
240
- </Link>
241
- </div>
242
-
243
- {/* Logout */}
244
- <div className="border-t border-white/[0.06] p-2">
245
- <button
246
- onClick={handleLogout}
247
- className={`flex items-center gap-3 text-white/40 hover:text-red-400 transition-colors px-3 py-2 rounded-lg hover:bg-red-500/10 w-full text-sm ${
248
- !sidebarOpen ? "justify-center px-0" : ""
249
- }`}
250
- title="Logout"
251
- >
252
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
253
- <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
254
- <polyline points="16 17 21 12 16 7" />
255
- <line x1="21" y1="12" x2="9" y2="12" />
256
- </svg>
257
- {sidebarOpen && <span className="text-[13px]">Logout</span>}
258
- </button>
259
- </div>
260
- </aside>
261
-
262
- {/* Main content area no top header bar, pages have their own titles */}
263
- <div className="flex flex-1 flex-col overflow-hidden">
264
- <main className={`flex-1 ${
265
- isPageBuilder
266
- ? "overflow-hidden"
267
- : "overflow-y-auto p-8 bg-[#f8f8f8]"
268
- }`}>
269
- {children}
270
- </main>
271
- </div>
272
- </div>
273
- );
274
- }
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
+ {/* FooterUtility 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
+ }