@morphika/andami 0.2.9 → 0.2.11
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.
- package/README.md +8 -3
- package/admin/backups.ts +7 -0
- package/app/admin/backups/page.tsx +1 -0
- package/app/admin/layout.tsx +327 -274
- package/app/api/admin/backups/export/route.ts +76 -0
- package/app/api/admin/backups/prepare-export/route.ts +56 -0
- package/app/api/admin/backups/restore/route.ts +131 -0
- package/app/api/admin/backups/restore-data/route.ts +424 -0
- package/app/api/admin/backups/status/route.ts +35 -0
- package/app/api/custom-sections/[id]/route.ts +9 -5
- package/components/admin/backups/BackupsPage.tsx +1581 -0
- package/components/builder/CustomSectionInstanceCard.tsx +7 -3
- package/components/builder/ReadOnlyFrame.tsx +4 -2
- package/components/builder/settings-panel/CustomSectionSettings.tsx +5 -3
- package/lib/backup/export.ts +377 -0
- package/lib/backup/manifest.ts +121 -0
- package/lib/backup/r2-helpers.ts +294 -0
- package/lib/backup/restore.ts +266 -0
- package/lib/backup/sanity-ops.ts +194 -0
- package/lib/builder/serializer/normalizers.ts +1 -0
- package/lib/builder/store-canvas.ts +4 -0
- package/lib/builder/store.ts +1 -0
- package/lib/builder/types.ts +4 -0
- package/lib/security.ts +30 -0
- package/lib/security.ts.new +27 -0
- package/lib/storage/types.ts +33 -1
- package/lib/version.ts +7 -4
- package/package.json +17 -1
package/app/admin/layout.tsx
CHANGED
|
@@ -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 —
|
|
11
|
-
// ============================================
|
|
12
|
-
|
|
13
|
-
const
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
</svg>
|
|
52
|
-
);
|
|
53
|
-
case "palette":
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<circle cx="
|
|
58
|
-
<circle cx="8
|
|
59
|
-
<circle cx="
|
|
60
|
-
<
|
|
61
|
-
</svg>
|
|
62
|
-
);
|
|
63
|
-
case "nav":
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<line x1="
|
|
68
|
-
<line x1="
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
</
|
|
241
|
-
</div>
|
|
242
|
-
|
|
243
|
-
{/*
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
{/* 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
|
+
}
|