@kyro-cms/admin 0.12.3 → 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.
Files changed (110) hide show
  1. package/dist/index.cjs +64 -26
  2. package/dist/index.d.cts +2 -1
  3. package/dist/index.d.ts +2 -1
  4. package/dist/index.js +64 -26
  5. package/package.json +8 -2
  6. package/src/components/ActionBar.tsx +16 -16
  7. package/src/components/ApiHealth.tsx +522 -0
  8. package/src/components/ApiKeysManager.tsx +6 -4
  9. package/src/components/AuditLogsPage.tsx +4 -2
  10. package/src/components/AutoForm.tsx +10 -7
  11. package/src/components/BrandingHub.tsx +3 -1
  12. package/src/components/Dashboard.tsx +44 -37
  13. package/src/components/DashboardMetrics.tsx +168 -58
  14. package/src/components/DetailView.tsx +125 -50
  15. package/src/components/DeveloperCenter.tsx +5 -3
  16. package/src/components/GraphQLPlayground.tsx +10 -8
  17. package/src/components/ListView.tsx +66 -45
  18. package/src/components/LoginPage.tsx +21 -19
  19. package/src/components/MarketplaceManager.tsx +3 -1
  20. package/src/components/MediaGallery.tsx +12 -9
  21. package/src/components/PluginsManager.tsx +6 -4
  22. package/src/components/RestPlayground.tsx +8 -6
  23. package/src/components/SessionsManager.tsx +3 -1
  24. package/src/components/Sidebar.astro +180 -92
  25. package/src/components/UserManagement.tsx +8 -6
  26. package/src/components/UserMenu.tsx +19 -17
  27. package/src/components/VersionHistoryPanel.tsx +6 -4
  28. package/src/components/WebhookManager.tsx +143 -15
  29. package/src/components/autoform/AutoFormApiView.tsx +3 -1
  30. package/src/components/autoform/AutoFormEditView.tsx +5 -3
  31. package/src/components/autoform/AutoFormHeader.tsx +6 -6
  32. package/src/components/blocks/AccordionBlock.tsx +5 -3
  33. package/src/components/blocks/ArrayBlock.tsx +5 -3
  34. package/src/components/blocks/BlockWrapper.tsx +5 -3
  35. package/src/components/blocks/CodeBlock.tsx +4 -2
  36. package/src/components/blocks/FileBlock.tsx +5 -3
  37. package/src/components/blocks/HeroBlock.tsx +5 -3
  38. package/src/components/blocks/ImageBlock.tsx +6 -4
  39. package/src/components/blocks/ListBlock.tsx +5 -3
  40. package/src/components/blocks/ParagraphBlock.tsx +3 -1
  41. package/src/components/blocks/RelationshipBlock.tsx +5 -3
  42. package/src/components/blocks/RichTextBlock.tsx +5 -3
  43. package/src/components/blocks/VideoBlock.tsx +5 -3
  44. package/src/components/fields/AccordionField.tsx +10 -51
  45. package/src/components/fields/ArrayField.tsx +9 -7
  46. package/src/components/fields/ArrayLayout.tsx +3 -1
  47. package/src/components/fields/BlocksField.tsx +24 -75
  48. package/src/components/fields/ButtonField.tsx +3 -1
  49. package/src/components/fields/CardField.tsx +6 -4
  50. package/src/components/fields/GroupLayout.tsx +2 -49
  51. package/src/components/fields/HeadingField.tsx +3 -1
  52. package/src/components/fields/HeadingSubheadingField.tsx +4 -2
  53. package/src/components/fields/HeroField.tsx +8 -6
  54. package/src/components/fields/JSONField.tsx +9 -10
  55. package/src/components/fields/LinkField.tsx +4 -2
  56. package/src/components/fields/ListField.tsx +3 -1
  57. package/src/components/fields/MarkdownField.tsx +3 -17
  58. package/src/components/fields/RelationshipBlockField.tsx +7 -5
  59. package/src/components/fields/RelationshipField.tsx +3 -1
  60. package/src/components/fields/RichTextField.tsx +19 -16
  61. package/src/components/fields/SecretField.tsx +9 -69
  62. package/src/components/fields/SelectField.tsx +11 -6
  63. package/src/components/fields/TabsLayout.tsx +2 -48
  64. package/src/components/fields/TextField.tsx +9 -7
  65. package/src/components/fields/UploadField.tsx +3 -1
  66. package/src/components/fields/VideoField.tsx +6 -4
  67. package/src/components/ui/BlockDrawer.tsx +3 -1
  68. package/src/components/ui/CommandPalette.tsx +9 -7
  69. package/src/components/ui/CommandPaletteWrapper.tsx +3 -1
  70. package/src/components/ui/IconPickerModal.tsx +4 -2
  71. package/src/components/ui/PageHeader.tsx +48 -20
  72. package/src/components/users/UserDetail.tsx +3 -1
  73. package/src/components/users/UserForm.tsx +5 -3
  74. package/src/fields/examples/sample-field-2.tsx +3 -1
  75. package/src/fields/examples/sample-field.tsx +3 -1
  76. package/src/hooks/useAutoFormState.ts +19 -11
  77. package/src/integration.ts +21 -0
  78. package/src/layouts/AdminLayout.astro +252 -189
  79. package/src/layouts/AuthLayout.astro +52 -31
  80. package/src/lib/api.ts +7 -7
  81. package/src/lib/autoform-store.ts +4 -0
  82. package/src/lib/deep-equal.ts +3 -3
  83. package/src/lib/i18n.ts +44 -0
  84. package/src/lib/vite-shim-plugin.ts +3 -0
  85. package/src/locales/de.json +163 -0
  86. package/src/locales/en.json +163 -0
  87. package/src/locales/es.json +163 -0
  88. package/src/locales/fr.json +163 -0
  89. package/src/locales/pt.json +163 -0
  90. package/src/pages/403.astro +2 -1
  91. package/src/pages/[collection]/[id].astro +23 -10
  92. package/src/pages/[collection]/index.astro +48 -34
  93. package/src/pages/audit/index.astro +2 -1
  94. package/src/pages/graphql-explorer.astro +2 -1
  95. package/src/pages/graphql.astro +2 -1
  96. package/src/pages/health.astro +9 -0
  97. package/src/pages/index.astro +6 -5
  98. package/src/pages/keys.astro +2 -1
  99. package/src/pages/marketplace.astro +2 -1
  100. package/src/pages/media.astro +2 -1
  101. package/src/pages/plugins.astro +2 -1
  102. package/src/pages/rest-playground.astro +2 -1
  103. package/src/pages/roles/index.astro +16 -52
  104. package/src/pages/sessions.astro +2 -1
  105. package/src/pages/settings/[slug].astro +45 -30
  106. package/src/pages/users/[id].astro +1 -0
  107. package/src/pages/users/index.astro +2 -1
  108. package/src/pages/users/new.astro +2 -1
  109. package/src/pages/webhooks.astro +2 -1
  110. package/src/pages/index_ALT.astro +0 -3
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import "../styles/main.css";
3
- import { nonAuthCollections, collections, globals } from "../lib/config";
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="en">
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
- (async () => {
74
+ const runAuthCheck = async () => {
69
75
  try {
70
- // Fetch user and permissions using cookies
71
- const [meRes, accessRes] = await Promise.all([
72
- fetch(apiPath + "/auth/me", { credentials: "include" }),
73
- fetch(apiPath + "/auth/access", { credentials: "include" }),
74
- ]);
75
-
76
- if (!meRes.ok) {
77
- window.location.href = adminPath + "/login";
78
- return;
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
- const meData = await meRes.json();
82
- if (!meData.user) {
83
- console.log("[AdminLayout] No user in data, redirecting to login");
84
- window.location.href = adminPath + "/login";
85
- return;
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
- const permissions = accessRes.ok ? await accessRes.json() : null;
98
+ permissions = accessRes.ok ? await accessRes.json() : null;
99
+ user = meData.user;
89
100
 
90
- // Store user in memory (not localStorage)
91
- window.__kyroAuth.user = meData.user;
92
- window.__kyroAuth.permissions = permissions;
93
- window.__kyroAuth.verified = true;
101
+ // Store user in memory
102
+ window.__kyroAuth.user = user;
103
+ window.__kyroAuth.permissions = permissions;
104
+ window.__kyroAuth.verified = true;
94
105
 
95
- // Dispatch event for components to update
96
- window.dispatchEvent(
97
- new CustomEvent("kyro:auth-ready", {
98
- detail: {
99
- user: meData.user,
100
- permissions,
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 id="mobile-sidebar-backdrop" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-40 hidden md:hidden transition-opacity opacity-0 duration-300"></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 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">
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 id="mobile-menu-btn" class="p-2 -ml-2 rounded-lg text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors">
195
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
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 menuBtn = document.getElementById("mobile-menu-btn");
210
- const closeBtn = document.getElementById("mobile-close-btn");
211
- const backdrop = document.getElementById("mobile-sidebar-backdrop");
212
- const sidebar = document.getElementById("kyro-sidebar");
213
-
214
- const toggleSidebar = () => {
215
- if (!sidebar) return;
216
- const isOpen = sidebar.classList.contains("translate-x-0");
217
- const fab = document.getElementById("kyro-fab");
218
- if (isOpen) {
219
- sidebar.classList.remove("translate-x-0");
220
- sidebar.classList.add("-translate-x-full");
221
- backdrop?.classList.remove("opacity-100");
222
- setTimeout(() => backdrop?.classList.add("hidden"), 300);
223
- fab?.classList.remove("fab-hidden");
224
- } else {
225
- sidebar.classList.remove("-translate-x-full");
226
- sidebar.classList.add("translate-x-0");
227
- backdrop?.classList.remove("hidden");
228
- setTimeout(() => backdrop?.classList.add("opacity-100"), 10);
229
- fab?.classList.add("fab-hidden");
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
- menuBtn?.addEventListener("click", toggleSidebar);
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="flex items-center justify-between px-6 py-4 border-b border-[var(--kyro-border)]"
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 xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
305
- <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" />
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
- const lightBtn = document.getElementById("theme-light-btn");
312
- const darkBtn = document.getElementById("theme-dark-btn");
313
-
314
- const updateUI = (theme) => {
315
- const isDark = theme === "dark";
316
-
317
- // Update Buttons
318
- if (isDark) {
319
- darkBtn?.classList.add(
320
- "bg-[var(--kyro-sidebar-active)]",
321
- "text-[var(--kyro-sidebar-text-active)]",
322
- "shadow-lg",
323
- );
324
- darkBtn?.classList.remove("text-[var(--kyro-text-secondary)]");
325
- lightBtn?.classList.remove(
326
- "bg-[var(--kyro-sidebar-active)]",
327
- "text-[var(--kyro-sidebar-text-active)]",
328
- "shadow-lg",
329
- );
330
- lightBtn?.classList.add("text-[var(--kyro-text-secondary)]");
331
- } else {
332
- lightBtn?.classList.add(
333
- "bg-[var(--kyro-sidebar-active)]",
334
- "text-[var(--kyro-sidebar-text-active)]",
335
- "shadow-lg",
336
- );
337
- lightBtn?.classList.remove("text-[var(--kyro-text-secondary)]");
338
- darkBtn?.classList.remove(
339
- "bg-[var(--kyro-sidebar-active)]",
340
- "text-[var(--kyro-sidebar-text-active)]",
341
- "shadow-lg",
342
- );
343
- darkBtn?.classList.add("text-[var(--kyro-text-secondary)]");
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
- // Update Document
347
- if (isDark) {
348
- document.documentElement.classList.add("dark");
349
- } else {
350
- document.documentElement.classList.remove("dark");
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
- // Initial Sync
355
- const currentTheme =
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
- darkBtn?.addEventListener("click", () => {
369
- localStorage.setItem("theme", "dark");
370
- updateUI("dark");
371
- });
399
+ // Listeners
400
+ lightBtn?.addEventListener("click", () => {
401
+ localStorage.setItem("theme", "light");
402
+ updateUI("light");
403
+ });
372
404
 
373
- // System Preference Listener
374
- window
375
- .matchMedia("(prefers-color-scheme: dark)")
376
- .addEventListener("change", (e) => {
377
- if (!localStorage.getItem("theme")) {
378
- updateUI(e.matches ? "dark" : "light");
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
- // Logout Modal
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
- document.getElementById("logout-btn")?.addEventListener("click", () => {
389
- logoutModal?.classList.remove("hidden");
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
- const closeLogoutModal = () => logoutModal?.classList.add("hidden");
393
-
394
- logoutBackdrop?.addEventListener("click", closeLogoutModal);
395
- logoutCancel?.addEventListener("click", closeLogoutModal);
396
- logoutConfirm?.addEventListener("click", async () => {
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>