@kyro-cms/admin 0.12.4 → 0.12.5
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/dist/index.cjs +64 -26
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +64 -26
- package/package.json +9 -3
- package/src/components/ActionBar.tsx +16 -16
- package/src/components/ApiHealth.tsx +522 -0
- package/src/components/ApiKeysManager.tsx +6 -4
- package/src/components/AuditLogsPage.tsx +4 -2
- package/src/components/AutoForm.tsx +10 -7
- package/src/components/BrandingHub.tsx +3 -1
- package/src/components/Dashboard.tsx +44 -37
- package/src/components/DashboardMetrics.tsx +168 -58
- package/src/components/DetailView.tsx +41 -38
- package/src/components/DeveloperCenter.tsx +5 -3
- package/src/components/GraphQLPlayground.tsx +10 -8
- package/src/components/ListView.tsx +66 -45
- package/src/components/LoginPage.tsx +21 -19
- package/src/components/MarketplaceManager.tsx +3 -1
- package/src/components/MediaGallery.tsx +12 -9
- package/src/components/PluginsManager.tsx +6 -4
- package/src/components/RestPlayground.tsx +8 -6
- package/src/components/SessionsManager.tsx +3 -1
- package/src/components/Sidebar.astro +180 -92
- package/src/components/UserManagement.tsx +8 -6
- package/src/components/UserMenu.tsx +19 -17
- package/src/components/VersionHistoryPanel.tsx +6 -4
- package/src/components/WebhookManager.tsx +11 -10
- package/src/components/autoform/AutoFormApiView.tsx +3 -1
- package/src/components/autoform/AutoFormEditView.tsx +3 -1
- package/src/components/autoform/AutoFormHeader.tsx +6 -6
- package/src/components/blocks/AccordionBlock.tsx +5 -3
- package/src/components/blocks/ArrayBlock.tsx +5 -3
- package/src/components/blocks/BlockWrapper.tsx +5 -3
- package/src/components/blocks/CodeBlock.tsx +4 -2
- package/src/components/blocks/FileBlock.tsx +5 -3
- package/src/components/blocks/HeroBlock.tsx +5 -3
- package/src/components/blocks/ImageBlock.tsx +6 -4
- package/src/components/blocks/ListBlock.tsx +5 -3
- package/src/components/blocks/ParagraphBlock.tsx +3 -1
- package/src/components/blocks/RelationshipBlock.tsx +5 -3
- package/src/components/blocks/RichTextBlock.tsx +5 -3
- package/src/components/blocks/VideoBlock.tsx +5 -3
- package/src/components/fields/AccordionField.tsx +10 -51
- package/src/components/fields/ArrayField.tsx +9 -7
- package/src/components/fields/ArrayLayout.tsx +3 -1
- package/src/components/fields/BlocksField.tsx +24 -75
- package/src/components/fields/ButtonField.tsx +3 -1
- package/src/components/fields/CardField.tsx +6 -4
- package/src/components/fields/GroupLayout.tsx +2 -49
- package/src/components/fields/HeadingField.tsx +3 -1
- package/src/components/fields/HeadingSubheadingField.tsx +4 -2
- package/src/components/fields/HeroField.tsx +8 -6
- package/src/components/fields/JSONField.tsx +9 -10
- package/src/components/fields/LinkField.tsx +4 -2
- package/src/components/fields/ListField.tsx +3 -1
- package/src/components/fields/MarkdownField.tsx +3 -17
- package/src/components/fields/RelationshipBlockField.tsx +7 -5
- package/src/components/fields/RelationshipField.tsx +3 -1
- package/src/components/fields/RichTextField.tsx +19 -16
- package/src/components/fields/SecretField.tsx +9 -69
- package/src/components/fields/SelectField.tsx +11 -6
- package/src/components/fields/TabsLayout.tsx +2 -48
- package/src/components/fields/TextField.tsx +9 -7
- package/src/components/fields/UploadField.tsx +3 -1
- package/src/components/fields/VideoField.tsx +6 -4
- package/src/components/ui/BlockDrawer.tsx +3 -1
- package/src/components/ui/CommandPalette.tsx +9 -7
- package/src/components/ui/CommandPaletteWrapper.tsx +3 -1
- package/src/components/ui/IconPickerModal.tsx +4 -2
- package/src/components/ui/PageHeader.tsx +48 -19
- package/src/components/users/UserDetail.tsx +3 -1
- package/src/components/users/UserForm.tsx +5 -3
- package/src/fields/examples/sample-field-2.tsx +3 -1
- package/src/fields/examples/sample-field.tsx +3 -1
- package/src/hooks/useAutoFormState.ts +19 -11
- package/src/integration.ts +21 -0
- package/src/layouts/AdminLayout.astro +252 -189
- package/src/layouts/AuthLayout.astro +52 -31
- package/src/lib/api.ts +7 -7
- package/src/lib/deep-equal.ts +3 -3
- package/src/lib/i18n.ts +44 -0
- package/src/lib/vite-shim-plugin.ts +3 -0
- package/src/locales/de.json +163 -0
- package/src/locales/en.json +163 -0
- package/src/locales/es.json +163 -0
- package/src/locales/fr.json +163 -0
- package/src/locales/pt.json +163 -0
- package/src/pages/403.astro +2 -1
- package/src/pages/[collection]/[id].astro +23 -10
- package/src/pages/[collection]/index.astro +48 -34
- package/src/pages/audit/index.astro +2 -1
- package/src/pages/graphql-explorer.astro +2 -1
- package/src/pages/graphql.astro +2 -1
- package/src/pages/health.astro +9 -0
- package/src/pages/index.astro +6 -5
- package/src/pages/keys.astro +2 -1
- package/src/pages/marketplace.astro +2 -1
- package/src/pages/media.astro +2 -1
- package/src/pages/plugins.astro +2 -1
- package/src/pages/rest-playground.astro +2 -1
- package/src/pages/roles/index.astro +16 -52
- package/src/pages/sessions.astro +2 -1
- package/src/pages/settings/[slug].astro +45 -30
- package/src/pages/users/[id].astro +1 -0
- package/src/pages/users/index.astro +2 -1
- package/src/pages/users/new.astro +2 -1
- package/src/pages/webhooks.astro +2 -1
- package/src/pages/index_ALT.astro +0 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import "../styles/main.css";
|
|
3
|
-
import {
|
|
3
|
+
import { collections, globals } from "../lib/config";
|
|
4
4
|
import { CommandPaletteWrapper } from "../components/ui/CommandPaletteWrapper";
|
|
5
5
|
import Sidebar from "../components/Sidebar.astro";
|
|
6
6
|
import { adminPath, apiPath } from "../lib/paths";
|
|
@@ -8,6 +8,8 @@ import { AuthBridge } from "../components/AuthBridge";
|
|
|
8
8
|
import { GlobalModal } from "../components/ui/GlobalModal";
|
|
9
9
|
import { Toaster } from "../components/ui/Toaster";
|
|
10
10
|
import { getSiteSettings, getGlobal } from "../lib/globals";
|
|
11
|
+
import { ClientRouter } from 'astro:transitions';
|
|
12
|
+
import LoadingIndicator from 'astro-loading-indicator/component';
|
|
11
13
|
|
|
12
14
|
interface Props {
|
|
13
15
|
title: string;
|
|
@@ -34,22 +36,24 @@ let displayTitle = title === "Dashboard" && defaultTitle !== "Dashboard" ? defau
|
|
|
34
36
|
if (includeSiteName) {
|
|
35
37
|
displayTitle = `${displayTitle}${titleSeparator}${siteName}`;
|
|
36
38
|
}
|
|
39
|
+
const defaultLang = siteSettings?.defaultLanguage || "en";
|
|
37
40
|
---
|
|
38
41
|
|
|
39
42
|
<!doctype html>
|
|
40
|
-
<html lang=
|
|
43
|
+
<html lang={defaultLang}>
|
|
41
44
|
<head>
|
|
45
|
+
<ClientRouter />
|
|
46
|
+
<LoadingIndicator color="var(--kyro-primary)" height="3px" />
|
|
47
|
+
<script is:inline define:vars={{ lang: defaultLang }}>
|
|
48
|
+
window.KYRO_LANG = lang;
|
|
49
|
+
</script>
|
|
42
50
|
<meta charset="UTF-8" />
|
|
43
51
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
44
52
|
<title>{displayTitle}</title>
|
|
45
53
|
{defaultDescription && <meta name="description" content={defaultDescription} />}
|
|
46
54
|
{robots && <meta name="robots" content={robots} />}
|
|
47
55
|
{canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
|
|
48
|
-
<link
|
|
49
|
-
rel="icon"
|
|
50
|
-
type={siteFavicon?.mimeType || "image/svg+xml"}
|
|
51
|
-
href={siteFavicon?.url || "/favicon.svg"}
|
|
52
|
-
/>
|
|
56
|
+
<link rel="icon" type={siteFavicon?.mimeType || "image/svg+xml"} href={siteFavicon?.url || "/favicon.svg"} />
|
|
53
57
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
54
58
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
55
59
|
<link
|
|
@@ -64,43 +68,48 @@ if (includeSiteName) {
|
|
|
64
68
|
verified: false,
|
|
65
69
|
};
|
|
66
70
|
|
|
71
|
+
let initialAuthDone = false;
|
|
72
|
+
|
|
67
73
|
// Verify auth - redirect to login if not authenticated
|
|
68
|
-
|
|
74
|
+
const runAuthCheck = async () => {
|
|
69
75
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
let permissions = window.__kyroAuth?.permissions;
|
|
77
|
+
let user = window.__kyroAuth?.user;
|
|
78
|
+
|
|
79
|
+
// Fetch user and permissions using cookies if not verified
|
|
80
|
+
if (!window.__kyroAuth?.verified) {
|
|
81
|
+
const [meRes, accessRes] = await Promise.all([
|
|
82
|
+
fetch(apiPath + "/auth/me", { credentials: "include" }),
|
|
83
|
+
fetch(apiPath + "/auth/access", { credentials: "include" }),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
if (!meRes.ok) {
|
|
87
|
+
window.location.href = adminPath + "/login";
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
80
90
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
const meData = await meRes.json();
|
|
92
|
+
if (!meData.user) {
|
|
93
|
+
console.log("[AdminLayout] No user in data, redirecting to login");
|
|
94
|
+
window.location.href = adminPath + "/login";
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
87
97
|
|
|
88
|
-
|
|
98
|
+
permissions = accessRes.ok ? await accessRes.json() : null;
|
|
99
|
+
user = meData.user;
|
|
89
100
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
// Store user in memory
|
|
102
|
+
window.__kyroAuth.user = user;
|
|
103
|
+
window.__kyroAuth.permissions = permissions;
|
|
104
|
+
window.__kyroAuth.verified = true;
|
|
94
105
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}),
|
|
103
|
-
);
|
|
106
|
+
// Dispatch event for components to update
|
|
107
|
+
window.dispatchEvent(
|
|
108
|
+
new CustomEvent("kyro:auth-ready", {
|
|
109
|
+
detail: { user, permissions },
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
}
|
|
104
113
|
|
|
105
114
|
// Navigation Guard - Check if user has read access to current page
|
|
106
115
|
if (permissions) {
|
|
@@ -144,11 +153,7 @@ if (includeSiteName) {
|
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
if (!hasAccess) {
|
|
147
|
-
console.log(
|
|
148
|
-
"[AdminLayout] Access denied for",
|
|
149
|
-
slug,
|
|
150
|
-
"redirecting to 403",
|
|
151
|
-
);
|
|
156
|
+
console.log("[AdminLayout] Access denied for", slug, "redirecting to 403");
|
|
152
157
|
window.location.href = adminPath + "/403";
|
|
153
158
|
}
|
|
154
159
|
}
|
|
@@ -157,16 +162,19 @@ if (includeSiteName) {
|
|
|
157
162
|
console.error("[AdminLayout] Auth check error:", err);
|
|
158
163
|
window.location.href = adminPath + "/login";
|
|
159
164
|
}
|
|
160
|
-
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
runAuthCheck().then(() => { initialAuthDone = true; });
|
|
168
|
+
document.addEventListener("astro:page-load", () => {
|
|
169
|
+
if (initialAuthDone) runAuthCheck();
|
|
170
|
+
});
|
|
161
171
|
|
|
162
172
|
// Theme init (only theme persisted)
|
|
163
|
-
(
|
|
173
|
+
const initTheme = () => {
|
|
164
174
|
const getTheme = () => {
|
|
165
175
|
const stored = localStorage.getItem("theme");
|
|
166
176
|
if (stored) return stored;
|
|
167
|
-
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
168
|
-
? "dark"
|
|
169
|
-
: "light";
|
|
177
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
170
178
|
};
|
|
171
179
|
const theme = getTheme();
|
|
172
180
|
if (theme === "dark") {
|
|
@@ -174,14 +182,21 @@ if (includeSiteName) {
|
|
|
174
182
|
} else {
|
|
175
183
|
document.documentElement.classList.remove("dark");
|
|
176
184
|
}
|
|
177
|
-
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
initTheme();
|
|
188
|
+
document.addEventListener("astro:page-load", initTheme);
|
|
178
189
|
</script>
|
|
179
190
|
</head>
|
|
180
191
|
<body class="bg-[var(--kyro-bg)] antialiased text-[var(--kyro-text-primary)] overflow-x-hidden overflow-y-auto">
|
|
181
192
|
<div id="kyro-user-data" data-user=""></div>
|
|
182
|
-
|
|
193
|
+
|
|
183
194
|
<!-- Mobile Sidebar Backdrop -->
|
|
184
|
-
<div
|
|
195
|
+
<div
|
|
196
|
+
id="mobile-sidebar-backdrop"
|
|
197
|
+
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-40 hidden md:hidden transition-opacity opacity-0 duration-300"
|
|
198
|
+
>
|
|
199
|
+
</div>
|
|
185
200
|
|
|
186
201
|
<div class="flex h-[100dvh] md:h-screen p-0 md:p-6 gap-0 md:gap-6 overflow-hidden w-full relative">
|
|
187
202
|
<Sidebar title={title} />
|
|
@@ -189,10 +204,31 @@ if (includeSiteName) {
|
|
|
189
204
|
<!-- Main Content Column -->
|
|
190
205
|
<main class="flex-1 flex flex-col gap-0 md:gap-6 overflow-hidden relative w-full h-full max-w-full">
|
|
191
206
|
<!-- Mobile Header -->
|
|
192
|
-
<header
|
|
207
|
+
<header
|
|
208
|
+
class="md:hidden flex items-center justify-between p-4 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)] shrink-0 z-30 shadow-sm"
|
|
209
|
+
>
|
|
193
210
|
<div class="flex items-center gap-3">
|
|
194
|
-
<button
|
|
195
|
-
|
|
211
|
+
<button
|
|
212
|
+
id="mobile-menu-btn"
|
|
213
|
+
class="p-2 -ml-2 rounded-lg text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
|
|
214
|
+
>
|
|
215
|
+
<svg
|
|
216
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
217
|
+
width="24"
|
|
218
|
+
height="24"
|
|
219
|
+
viewBox="0 0 24 24"
|
|
220
|
+
fill="none"
|
|
221
|
+
stroke="currentColor"
|
|
222
|
+
stroke-width="2"
|
|
223
|
+
stroke-linecap="round"
|
|
224
|
+
stroke-linejoin="round"
|
|
225
|
+
class="lucide lucide-menu"
|
|
226
|
+
><line x1="4" x2="20" y1="12" y2="12"></line><line x1="4" x2="20" y1="6" y2="6"></line><line
|
|
227
|
+
x1="4"
|
|
228
|
+
x2="20"
|
|
229
|
+
y1="18"
|
|
230
|
+
y2="18"></line></svg
|
|
231
|
+
>
|
|
196
232
|
</button>
|
|
197
233
|
<span class="font-bold text-lg">{title}</span>
|
|
198
234
|
</div>
|
|
@@ -206,59 +242,50 @@ if (includeSiteName) {
|
|
|
206
242
|
|
|
207
243
|
<!-- Mobile Sidebar Logic -->
|
|
208
244
|
<script is:inline>
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
245
|
+
const initMobileSidebar = () => {
|
|
246
|
+
const menuBtn = document.getElementById("mobile-menu-btn");
|
|
247
|
+
const closeBtn = document.getElementById("mobile-close-btn");
|
|
248
|
+
const backdrop = document.getElementById("mobile-sidebar-backdrop");
|
|
249
|
+
const sidebar = document.getElementById("kyro-sidebar");
|
|
250
|
+
|
|
251
|
+
const toggleSidebar = () => {
|
|
252
|
+
if (!sidebar) return;
|
|
253
|
+
const isOpen = sidebar.classList.contains("translate-x-0");
|
|
254
|
+
const fab = document.getElementById("kyro-fab");
|
|
255
|
+
if (isOpen) {
|
|
256
|
+
sidebar.classList.remove("translate-x-0");
|
|
257
|
+
sidebar.classList.add("-translate-x-full");
|
|
258
|
+
backdrop?.classList.remove("opacity-100");
|
|
259
|
+
setTimeout(() => backdrop?.classList.add("hidden"), 300);
|
|
260
|
+
fab?.classList.remove("fab-hidden");
|
|
261
|
+
} else {
|
|
262
|
+
sidebar.classList.remove("-translate-x-full");
|
|
263
|
+
sidebar.classList.add("translate-x-0");
|
|
264
|
+
backdrop?.classList.remove("hidden");
|
|
265
|
+
setTimeout(() => backdrop?.classList.add("opacity-100"), 10);
|
|
266
|
+
fab?.classList.add("fab-hidden");
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
menuBtn?.addEventListener("click", toggleSidebar);
|
|
271
|
+
closeBtn?.addEventListener("click", toggleSidebar);
|
|
272
|
+
backdrop?.addEventListener("click", toggleSidebar);
|
|
231
273
|
};
|
|
232
274
|
|
|
233
|
-
|
|
234
|
-
closeBtn?.addEventListener("click", toggleSidebar);
|
|
235
|
-
backdrop?.addEventListener("click", toggleSidebar);
|
|
275
|
+
document.addEventListener("astro:page-load", initMobileSidebar);
|
|
236
276
|
</script>
|
|
237
277
|
|
|
238
278
|
<!-- Logout Confirmation Modal -->
|
|
239
|
-
<div
|
|
240
|
-
id="logout-modal"
|
|
241
|
-
class="fixed inset-0 z-50 hidden flex items-center justify-center"
|
|
242
|
-
>
|
|
243
|
-
<div
|
|
244
|
-
class="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
245
|
-
id="logout-modal-backdrop"
|
|
246
|
-
>
|
|
247
|
-
</div>
|
|
279
|
+
<div id="logout-modal" class="fixed inset-0 z-50 hidden flex items-center justify-center">
|
|
280
|
+
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" id="logout-modal-backdrop"></div>
|
|
248
281
|
<div
|
|
249
282
|
class="relative w-full max-w-sm mx-4 bg-[var(--kyro-surface)] rounded-lg shadow-2xl border border-[var(--kyro-border)]"
|
|
250
283
|
>
|
|
251
|
-
<div
|
|
252
|
-
class="
|
|
253
|
-
>
|
|
254
|
-
<h2 class="text-lg font-semibold text-[var(--kyro-text-primary)]">
|
|
255
|
-
Sign Out
|
|
256
|
-
</h2>
|
|
284
|
+
<div class="flex items-center justify-between px-6 py-4 border-b border-[var(--kyro-border)]">
|
|
285
|
+
<h2 class="text-lg font-semibold text-[var(--kyro-text-primary)]">Sign Out</h2>
|
|
257
286
|
</div>
|
|
258
287
|
<div class="px-6 py-4">
|
|
259
|
-
<p class="text-[var(--kyro-text-secondary)]">
|
|
260
|
-
Are you sure you want to sign out?
|
|
261
|
-
</p>
|
|
288
|
+
<p class="text-[var(--kyro-text-secondary)]">Are you sure you want to sign out?</p>
|
|
262
289
|
</div>
|
|
263
290
|
<div
|
|
264
291
|
class="flex items-center justify-end gap-3 px-6 py-4 border-t border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] rounded-b-lg"
|
|
@@ -281,11 +308,7 @@ if (includeSiteName) {
|
|
|
281
308
|
<AuthBridge client:load />
|
|
282
309
|
|
|
283
310
|
<!-- Command Palette (React) -->
|
|
284
|
-
<CommandPaletteWrapper
|
|
285
|
-
client:only="react"
|
|
286
|
-
collections={collections}
|
|
287
|
-
globals={globals}
|
|
288
|
-
/>
|
|
311
|
+
<CommandPaletteWrapper client:only="react" collections={collections} globals={globals} />
|
|
289
312
|
|
|
290
313
|
<!-- Global Modals (React) -->
|
|
291
314
|
<GlobalModal client:only="react" />
|
|
@@ -301,108 +324,148 @@ if (includeSiteName) {
|
|
|
301
324
|
class="fixed bottom-6 right-6 z-50 w-12 h-12 flex md:hidden items-center justify-center rounded-2xl bg-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)] shadow-lg shadow-[var(--kyro-primary)]/30 hover:shadow-xl hover:shadow-[var(--kyro-primary)]/40 hover:scale-110 active:scale-95 transition-all duration-200"
|
|
302
325
|
title="Open command palette (Cmd+K)"
|
|
303
326
|
>
|
|
304
|
-
<svg
|
|
305
|
-
|
|
327
|
+
<svg
|
|
328
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
329
|
+
width="20"
|
|
330
|
+
height="20"
|
|
331
|
+
viewBox="0 0 24 24"
|
|
332
|
+
fill="none"
|
|
333
|
+
stroke="currentColor"
|
|
334
|
+
stroke-width="2.5"
|
|
335
|
+
stroke-linecap="round"
|
|
336
|
+
stroke-linejoin="round"
|
|
337
|
+
>
|
|
338
|
+
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"></path>
|
|
306
339
|
</svg>
|
|
307
340
|
</button>
|
|
308
341
|
|
|
309
342
|
<!-- Theme UI Logic -->
|
|
310
343
|
<script is:inline define:vars={{ adminPath, apiPath }}>
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
"
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
344
|
+
// Run immediately to prevent FOUC
|
|
345
|
+
const currentTheme = localStorage.getItem("theme") || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
346
|
+
if (currentTheme === "dark") {
|
|
347
|
+
document.documentElement.classList.add("dark");
|
|
348
|
+
} else {
|
|
349
|
+
document.documentElement.classList.remove("dark");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const initThemeUI = () => {
|
|
353
|
+
const lightBtn = document.getElementById("theme-light-btn");
|
|
354
|
+
const darkBtn = document.getElementById("theme-dark-btn");
|
|
355
|
+
|
|
356
|
+
const updateUI = (theme) => {
|
|
357
|
+
const isDark = theme === "dark";
|
|
358
|
+
|
|
359
|
+
// Update Buttons
|
|
360
|
+
if (isDark) {
|
|
361
|
+
darkBtn?.classList.add(
|
|
362
|
+
"bg-[var(--kyro-sidebar-active)]",
|
|
363
|
+
"text-[var(--kyro-sidebar-text-active)]",
|
|
364
|
+
"shadow-lg",
|
|
365
|
+
);
|
|
366
|
+
darkBtn?.classList.remove("text-[var(--kyro-text-secondary)]");
|
|
367
|
+
lightBtn?.classList.remove(
|
|
368
|
+
"bg-[var(--kyro-sidebar-active)]",
|
|
369
|
+
"text-[var(--kyro-sidebar-text-active)]",
|
|
370
|
+
"shadow-lg",
|
|
371
|
+
);
|
|
372
|
+
lightBtn?.classList.add("text-[var(--kyro-text-secondary)]");
|
|
373
|
+
} else {
|
|
374
|
+
lightBtn?.classList.add(
|
|
375
|
+
"bg-[var(--kyro-sidebar-active)]",
|
|
376
|
+
"text-[var(--kyro-sidebar-text-active)]",
|
|
377
|
+
"shadow-lg",
|
|
378
|
+
);
|
|
379
|
+
lightBtn?.classList.remove("text-[var(--kyro-text-secondary)]");
|
|
380
|
+
darkBtn?.classList.remove(
|
|
381
|
+
"bg-[var(--kyro-sidebar-active)]",
|
|
382
|
+
"text-[var(--kyro-sidebar-text-active)]",
|
|
383
|
+
"shadow-lg",
|
|
384
|
+
);
|
|
385
|
+
darkBtn?.classList.add("text-[var(--kyro-text-secondary)]");
|
|
386
|
+
}
|
|
345
387
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
388
|
+
// Update Document
|
|
389
|
+
if (isDark) {
|
|
390
|
+
document.documentElement.classList.add("dark");
|
|
391
|
+
} else {
|
|
392
|
+
document.documentElement.classList.remove("dark");
|
|
393
|
+
}
|
|
394
|
+
};
|
|
353
395
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
localStorage.getItem("theme") ||
|
|
357
|
-
(window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
358
|
-
? "dark"
|
|
359
|
-
: "light");
|
|
360
|
-
updateUI(currentTheme);
|
|
361
|
-
|
|
362
|
-
// Listeners
|
|
363
|
-
lightBtn?.addEventListener("click", () => {
|
|
364
|
-
localStorage.setItem("theme", "light");
|
|
365
|
-
updateUI("light");
|
|
366
|
-
});
|
|
396
|
+
// Sync buttons on page load
|
|
397
|
+
updateUI(localStorage.getItem("theme") || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"));
|
|
367
398
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
399
|
+
// Listeners
|
|
400
|
+
lightBtn?.addEventListener("click", () => {
|
|
401
|
+
localStorage.setItem("theme", "light");
|
|
402
|
+
updateUI("light");
|
|
403
|
+
});
|
|
372
404
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
405
|
+
darkBtn?.addEventListener("click", () => {
|
|
406
|
+
localStorage.setItem("theme", "dark");
|
|
407
|
+
updateUI("dark");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Logout Modal
|
|
411
|
+
const logoutModal = document.getElementById("logout-modal");
|
|
412
|
+
const logoutBackdrop = document.getElementById("logout-modal-backdrop");
|
|
413
|
+
const logoutCancel = document.getElementById("logout-modal-cancel");
|
|
414
|
+
const logoutConfirm = document.getElementById("logout-modal-confirm");
|
|
415
|
+
|
|
416
|
+
document.getElementById("logout-btn")?.addEventListener("click", () => {
|
|
417
|
+
logoutModal?.classList.remove("hidden");
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const closeLogoutModal = () => logoutModal?.classList.add("hidden");
|
|
421
|
+
|
|
422
|
+
logoutBackdrop?.addEventListener("click", closeLogoutModal);
|
|
423
|
+
logoutCancel?.addEventListener("click", closeLogoutModal);
|
|
424
|
+
logoutConfirm?.addEventListener("click", async () => {
|
|
425
|
+
try {
|
|
426
|
+
await fetch(apiPath + "/auth/logout", {
|
|
427
|
+
method: "POST",
|
|
428
|
+
credentials: "include",
|
|
429
|
+
});
|
|
430
|
+
} finally {
|
|
431
|
+
// Clear auth state
|
|
432
|
+
window.__kyroAuth = { user: null, verified: false };
|
|
433
|
+
window.location.href = adminPath + "/login";
|
|
379
434
|
}
|
|
380
435
|
});
|
|
436
|
+
};
|
|
381
437
|
|
|
382
|
-
|
|
383
|
-
const logoutModal = document.getElementById("logout-modal");
|
|
384
|
-
const logoutBackdrop = document.getElementById("logout-modal-backdrop");
|
|
385
|
-
const logoutCancel = document.getElementById("logout-modal-cancel");
|
|
386
|
-
const logoutConfirm = document.getElementById("logout-modal-confirm");
|
|
438
|
+
document.addEventListener("astro:page-load", initThemeUI);
|
|
387
439
|
|
|
388
|
-
|
|
389
|
-
|
|
440
|
+
// System Preference Listener (attached once globally)
|
|
441
|
+
if (!window.__kyroThemeListenerAttached) {
|
|
442
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
|
443
|
+
if (!localStorage.getItem("theme")) {
|
|
444
|
+
const isDark = e.matches;
|
|
445
|
+
if (isDark) {
|
|
446
|
+
document.documentElement.classList.add("dark");
|
|
447
|
+
} else {
|
|
448
|
+
document.documentElement.classList.remove("dark");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
window.__kyroThemeListenerAttached = true;
|
|
453
|
+
}
|
|
454
|
+
</script>
|
|
455
|
+
<script>
|
|
456
|
+
import { navigate } from 'astro:transitions/client';
|
|
457
|
+
|
|
458
|
+
window.addEventListener('kyro:soft-reload', () => {
|
|
459
|
+
const url = new URL(window.location.href);
|
|
460
|
+
url.searchParams.set('_r', Date.now().toString());
|
|
461
|
+
navigate(url.toString(), { history: 'replace' });
|
|
390
462
|
});
|
|
391
463
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
await fetch(apiPath + "/auth/logout", {
|
|
399
|
-
method: "POST",
|
|
400
|
-
credentials: "include",
|
|
401
|
-
});
|
|
402
|
-
} finally {
|
|
403
|
-
// Clear auth state
|
|
404
|
-
window.__kyroAuth = { user: null, verified: false };
|
|
405
|
-
window.location.href = adminPath + "/login";
|
|
464
|
+
document.addEventListener('astro:page-load', () => {
|
|
465
|
+
const url = new URL(window.location.href);
|
|
466
|
+
if (url.searchParams.has('_r')) {
|
|
467
|
+
url.searchParams.delete('_r');
|
|
468
|
+
window.history.replaceState(null, '', url.toString() === url.origin + url.pathname ? url.pathname : url.toString());
|
|
406
469
|
}
|
|
407
470
|
});
|
|
408
471
|
</script>
|