@kyro-cms/admin 0.5.3 → 0.5.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 (38) hide show
  1. package/dist/{EditorClient-YLCGVDXY.cjs → EditorClient-Q23UXR37.cjs} +14 -14
  2. package/dist/{EditorClient-XEUOVAAC.js → EditorClient-T5PASFNR.js} +2 -2
  3. package/dist/chunk-3BGDYKTD.cjs +348 -0
  4. package/dist/chunk-3BGDYKTD.cjs.map +1 -0
  5. package/dist/chunk-EEFXLQVT.js +3 -0
  6. package/dist/chunk-EEFXLQVT.js.map +1 -0
  7. package/dist/index.cjs +462 -1020
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.css +13 -0
  10. package/dist/index.css.map +1 -1
  11. package/dist/index.js +271 -829
  12. package/dist/index.js.map +1 -1
  13. package/package.json +7 -3
  14. package/src/components/AuditLogsPage.tsx +4 -8
  15. package/src/components/Dashboard.tsx +2 -1
  16. package/src/components/DetailView.tsx +9 -2
  17. package/src/components/ListView.tsx +3 -2
  18. package/src/components/MediaGallery.tsx +13 -6
  19. package/src/components/Sidebar.astro +1 -1
  20. package/src/components/ui/Shimmer.tsx +28 -0
  21. package/src/components/users/UserDetail.tsx +1 -1
  22. package/src/components/users/UserForm.tsx +1 -1
  23. package/src/components/users/UsersList.tsx +1 -1
  24. package/src/hooks/useAutoFormState.ts +19 -3
  25. package/src/integration.ts +77 -25
  26. package/src/layouts/AdminLayout.astro +70 -48
  27. package/src/lib/config.ts +6 -1
  28. package/src/lib/globals.ts +56 -20
  29. package/src/pages/index.astro +1 -1
  30. package/src/pages/roles/index.astro +1 -1
  31. package/src/pages/users/[id].astro +2 -2
  32. package/src/styles/main.css +17 -0
  33. package/dist/chunk-7KPIUCGT.js +0 -384
  34. package/dist/chunk-7KPIUCGT.js.map +0 -1
  35. package/dist/chunk-GOACG6R7.cjs +0 -473
  36. package/dist/chunk-GOACG6R7.cjs.map +0 -1
  37. /package/dist/{EditorClient-XEUOVAAC.js.map → EditorClient-Q23UXR37.cjs.map} +0 -0
  38. /package/dist/{EditorClient-YLCGVDXY.cjs.map → EditorClient-T5PASFNR.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyro-cms/admin",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
@@ -89,30 +89,34 @@
89
89
  "@uiw/codemirror-theme-github": "^4.25.9",
90
90
  "@uiw/react-codemirror": "^4.25.9",
91
91
  "astro": "^6.3.1",
92
+ "astro-loading-indicator": "^0.8.0",
93
+ "dotenv": "^17.4.2",
92
94
  "graphiql": "^5.2.2",
93
95
  "idb-keyval": "^6.2.2",
94
96
  "lucide-react": "^0.475.0",
95
97
  "react": "^19.0.0",
96
98
  "react-dom": "^19.0.0",
99
+ "react-icons": "^5.0.0",
97
100
  "react-image-crop": "^11.0.10",
98
101
  "slate": "^0.124.1",
99
102
  "slate-history": "^0.113.1",
100
103
  "slate-react": "^0.124.0",
104
+ "swup": "^4.9.0",
101
105
  "tailwindcss": "^4.0.0",
106
+ "unstorage": "^1.17.5",
102
107
  "zustand": "^5.0.3"
103
108
  },
104
109
  "devDependencies": {
105
110
  "@astrojs/check": "^0.9.9",
106
111
  "@types/react": "^19.0.0",
107
112
  "@types/react-dom": "^19.0.0",
108
- "dotenv": "^17.4.2",
109
113
  "dotenv-cli": "^11.0.0",
110
114
  "tsup": "^6.0.0",
111
115
  "typescript": "^6.0.3",
112
116
  "vitest": "^4.1.4"
113
117
  },
114
118
  "peerDependencies": {
115
- "@kyro-cms/core": "^0.5.3",
119
+ "@kyro-cms/core": "^0.5.4",
116
120
  "react": "^18.0.0",
117
121
  "react-dom": "^18.0.0"
118
122
  },
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect, useCallback } from "react";
2
2
  import { fetchWithAuth } from "../lib/api";
3
3
  import { Modal } from "./ui/Modal";
4
+ import { Shimmer } from "./ui/Shimmer";
4
5
 
5
6
  interface AuditLog {
6
7
  id: string;
@@ -206,7 +207,7 @@ export function AuditLogsPage() {
206
207
  };
207
208
 
208
209
  return (
209
- <div className="flex-1 overflow-y-auto pr-12 space-y-6">
210
+ <div className="flex-1 overflow-y-auto space-y-6">
210
211
  {/* Header */}
211
212
  <div className="surface-tile p-6 flex items-center justify-between gap-8">
212
213
  <div>
@@ -332,13 +333,8 @@ export function AuditLogsPage() {
332
333
  {/* Table */}
333
334
  <div className="surface-tile overflow-hidden">
334
335
  {loading ? (
335
- <div className="divide-y divide-[var(--kyro-border)]">
336
- {Array.from({ length: 5 }).map((_, i) => (
337
- <div
338
- key={i}
339
- className="h-16 animate-pulse bg-[var(--kyro-surface-accent)]"
340
- />
341
- ))}
336
+ <div className="space-y-2 p-4">
337
+ <Shimmer variant="table-row" count={5} />
342
338
  </div>
343
339
  ) : logs.length === 0 ? (
344
340
  <div className="px-8 py-20 text-center">
@@ -3,6 +3,7 @@ import { LayoutDashboard, FileText, Image as ImageIcon, Users, Plus, ArrowUpRigh
3
3
  import { useAuthStore } from "../lib/stores";
4
4
  import { authCollectionSlugs } from "../lib/config";
5
5
  import { PageHeader } from "./ui/PageHeader";
6
+ import { Shimmer } from "./ui/Shimmer";
6
7
 
7
8
 
8
9
  interface DashboardProps {
@@ -124,7 +125,7 @@ export function Dashboard({ collections, onNavigate, user }: DashboardProps) {
124
125
  {stat.label}
125
126
  </p>
126
127
  <h3 className="text-3xl font-bold tracking-tighter">
127
- {loading ? "..." : stat.value}
128
+ {loading ? <Shimmer variant="text" className="w-16" /> : stat.value}
128
129
  </h3>
129
130
  </div>
130
131
  <div
@@ -8,6 +8,7 @@ import type {
8
8
  import { AutoForm } from "./AutoForm";
9
9
  import { ActionBar, type DocumentStatus, type SaveStatus } from "./ActionBar";
10
10
  import { Spinner } from "./ui/Spinner";
11
+ import { Shimmer } from "./ui/Shimmer";
11
12
  import { useToast } from "./ui/Toast";
12
13
  import { useUIStore } from "../lib/stores";
13
14
  import { PageHeader } from "./ui/PageHeader";
@@ -233,8 +234,14 @@ export function DetailView({
233
234
  if (loading) {
234
235
  return (
235
236
  <div className="kyro-detail">
236
- <div className="kyro-loading">
237
- <Spinner />
237
+ <div className="space-y-6 p-4">
238
+ <div className="space-y-2">
239
+ <Shimmer variant="text" className="w-1/3" />
240
+ <Shimmer variant="text" className="w-2/3" />
241
+ </div>
242
+ <div className="space-y-4">
243
+ <Shimmer variant="rect" count={4} />
244
+ </div>
238
245
  </div>
239
246
  </div>
240
247
  );
@@ -1,5 +1,6 @@
1
1
  import { useState, useEffect, useMemo, useCallback, useRef } from "react";
2
2
  import { Spinner } from "./ui/Spinner";
3
+ import { Shimmer } from "./ui/Shimmer";
3
4
  import { Plus } from "./ui/icons";
4
5
  import { apiGet, apiDelete, withCacheBust } from "../lib/api";
5
6
 
@@ -615,8 +616,8 @@ export function ListView({
615
616
  {/* Data Table */}
616
617
  <div className="surface-tile overflow-hidden">
617
618
  {loading ? (
618
- <div className="flex items-center justify-center py-20">
619
- <Spinner />
619
+ <div className="space-y-2 p-4">
620
+ <Shimmer variant="table-row" count={8} />
620
621
  </div>
621
622
  ) : docs.length === 0 ? (
622
623
  <div className="flex flex-col items-center justify-center py-16 px-8">
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { Spinner } from "./ui/Spinner";
4
+ import { Shimmer } from "./ui/Shimmer";
4
5
  import { SlidePanel } from "./ui/SlidePanel";
5
6
  import { Badge } from "./ui/Badge";
6
7
  import { Folder } from "./ui/icons";
@@ -139,9 +140,8 @@ export function MediaGallery({
139
140
  {},
140
141
  );
141
142
  const [showNewFolderModal, setShowNewFolderModal] = useState(false);
142
- const [storageConfigured, setStorageConfigured] = useState<boolean | null>(
143
- null,
144
- );
143
+ const [storageConfigured, setStorageConfigured] = useState<boolean | null>(null);
144
+ const [storageChecked, setStorageChecked] = useState(false);
145
145
  const [showStorageConfigModal, setShowStorageConfigModal] = useState(false);
146
146
  const [page, setPage] = useState(1);
147
147
  const [total, setTotal] = useState(0);
@@ -204,6 +204,13 @@ export function MediaGallery({
204
204
  checkStorage();
205
205
  }, [checkStorage]);
206
206
 
207
+ useEffect(() => {
208
+ if (storageConfigured === false && !storageChecked) {
209
+ setStorageChecked(true);
210
+ setShowStorageConfigModal(true);
211
+ }
212
+ }, [storageConfigured, storageChecked]);
213
+
207
214
  useEffect(() => {
208
215
  loadMedia();
209
216
  }, [loadMedia]);
@@ -569,8 +576,8 @@ export function MediaGallery({
569
576
  <div className="flex-1 flex flex-col min-h-0 bg-[var(--kyro-bg)]">
570
577
  <div className="flex-1 overflow-y-auto py-8 px-4 custom-scrollbar">
571
578
  {loading ? (
572
- <div className="flex items-center justify-center h-64">
573
- <Spinner />
579
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
580
+ <Shimmer variant="media-card" count={12} />
574
581
  </div>
575
582
  ) : items.length === 0 ? (
576
583
  <div className="flex flex-col items-center justify-center py-32 text-center">
@@ -847,7 +854,7 @@ export function MediaGallery({
847
854
  {/* Selection Footer */}
848
855
  {selectedIds.size > 0 && (
849
856
  <div className="fixed bottom-12 left-1/2 -translate-x-1/2 z-[60] bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-full shadow-2xl px-2 py-2 flex items-center gap-12 animate-in slide-in-from-bottom-12 duration-700 ring-1 ring-white/10 backdrop-blur-xl">
850
- <div className="flex items-center gap-5 border-r border-[var(--kyro-border)] pr-12">
857
+ <div className="flex items-center gap-5 border-r border-[var(--kyro-border)] ">
851
858
  <div className="w-12 h-12 rounded-full bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] flex items-center justify-center text-lg font-bold shadow-inner">
852
859
  {selectedIds.size}
853
860
  </div>
@@ -30,7 +30,7 @@ interface NavItem {
30
30
  icon: string;
31
31
  }
32
32
 
33
- const siteSettings = await getSiteSettings();
33
+ const siteSettings = await getSiteSettings({ request: Astro.request });
34
34
  const siteName = siteSettings?.siteName || "KYRO.";
35
35
  const siteLogo = siteSettings?.siteLogo;
36
36
  const logoWidth = siteSettings?.logo?.width;
@@ -0,0 +1,28 @@
1
+ interface ShimmerProps {
2
+ variant: "text" | "circle" | "rect" | "card" | "table-row" | "media-card" | "stat-card";
3
+ count?: number;
4
+ className?: string;
5
+ }
6
+
7
+ export function Shimmer({ variant, count = 1, className = "" }: ShimmerProps) {
8
+ const variants = {
9
+ text: "h-3 rounded-md",
10
+ circle: "size-10 rounded-full",
11
+ rect: "h-10 rounded-xl",
12
+ card: "h-32 rounded-2xl",
13
+ "table-row": "h-14 rounded-xl",
14
+ "media-card": "aspect-square rounded-2xl",
15
+ "stat-card": "h-24 rounded-2xl",
16
+ };
17
+
18
+ return (
19
+ <>
20
+ {Array.from({ length: count }).map((_, i) => (
21
+ <div
22
+ key={i}
23
+ className={`kyro-shimmer ${variants[variant]} ${className}`}
24
+ />
25
+ ))}
26
+ </>
27
+ );
28
+ }
@@ -136,7 +136,7 @@ export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
136
136
  };
137
137
 
138
138
  return (
139
- <div className="flex-1 overflow-y-auto pr-12 space-y-8">
139
+ <div className="flex-1 overflow-y-auto space-y-8">
140
140
  <div className="surface-tile p-6 flex items-center justify-between">
141
141
  <div className="flex items-center gap-4">
142
142
  <div className="w-14 h-14 rounded-full bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] flex items-center justify-center font-bold text-xl">
@@ -85,7 +85,7 @@ export function UserForm({ mode, apiPath, adminPath, user }: UserFormProps) {
85
85
  };
86
86
 
87
87
  return (
88
- <div className="flex-1 overflow-y-auto pr-12 space-y-8">
88
+ <div className="flex-1 overflow-y-auto space-y-8">
89
89
  <div className="surface-tile p-6 flex items-center justify-between">
90
90
  <div>
91
91
  <h1 className="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
@@ -81,7 +81,7 @@ export function UsersList({
81
81
  };
82
82
 
83
83
  return (
84
- <div className="flex-1 overflow-y-auto pr-12 space-y-8">
84
+ <div className="flex-1 overflow-y-auto space-y-8">
85
85
  <div className="surface-tile p-6 flex items-center justify-between">
86
86
  <div>
87
87
  <h1 className="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
@@ -389,11 +389,27 @@ const drafts = [browserDraft, serverDraft].filter(Boolean) as Array<{
389
389
  setFormData,
390
390
  ]);
391
391
 
392
+ // Recursively find a field by name inside tabs/group/collapsible
393
+ function findFieldDeep(fields: Record<string, any>[], name: string): Record<string, any> | undefined {
394
+ for (const f of fields) {
395
+ if (f.name === name && f.admin?.autoGenerate === "title") return f;
396
+ if (f.type === "tabs" && "tabs" in f) {
397
+ for (const tab of f.tabs) {
398
+ const found = findFieldDeep(tab.fields, name);
399
+ if (found) return found;
400
+ }
401
+ }
402
+ if ((f.type === "group" || f.type === "collapsible") && "fields" in f) {
403
+ const found = findFieldDeep(f.fields, name);
404
+ if (found) return found;
405
+ }
406
+ }
407
+ return undefined;
408
+ }
409
+
392
410
  // Auto-generate metaTitle
393
411
  useEffect(() => {
394
- const metaTitleField = config.fields.find(
395
- (f: Record<string, unknown>) => f.name === "metaTitle" && f.admin?.autoGenerate === "title",
396
- );
412
+ const metaTitleField = findFieldDeep(config.fields, "metaTitle");
397
413
  if (!metaTitleField) return;
398
414
 
399
415
  let titleValue = "";
@@ -1,6 +1,10 @@
1
1
  import type { AstroIntegration } from "astro";
2
2
  import path from "path";
3
3
  import fs from "fs";
4
+ import { execSync } from "child_process";
5
+ import { pathToFileURL } from "url";
6
+ import { config as loadDotEnv } from "dotenv";
7
+ import { transform } from "esbuild";
4
8
 
5
9
  export interface KyroAdminOptions {
6
10
  basePath?: string;
@@ -18,7 +22,7 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
18
22
  return {
19
23
  name: "@kyro-cms/admin",
20
24
  hooks: {
21
- "astro:config:setup": ({ config, updateConfig, injectRoute, logger }) => {
25
+ "astro:config:setup": async ({ config, updateConfig, injectRoute, logger }) => {
22
26
  logger.info(`Kyro Admin mounted at ${basePath} (API: ${apiPath})`);
23
27
 
24
28
  const fallbackConfig = path.resolve(
@@ -42,19 +46,81 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
42
46
  logger.warn(`Config file not found. Using defaults.`);
43
47
  }
44
48
 
45
- // Inject API Routes
46
- if (apiPath) {
47
- const apiHandlerPath = path.resolve(
48
- config.root.pathname,
49
- "..",
50
- "src/api-handler.ts",
51
- );
52
- injectRoute({
53
- pattern: `${apiPath}/[...path]`,
54
- entrypoint: apiHandlerPath,
49
+ // Load the user's config and expose it globally so that
50
+ // admin lib modules (config.ts, globals.ts) can access it
51
+ // without needing the kyro:config Vite alias during config loading.
52
+ // Load .env first since Vite hasn't processed it yet at this point
53
+ // in the lifecycle, and the config module evaluates eagerly.
54
+ // Use esbuild to transpile TS to ESM, then evaluate in a child
55
+ // process to completely bypass Vite's module runner interception.
56
+ let tmpFile = "";
57
+ try {
58
+ const envPath = path.join(path.dirname(resolvedConfig), ".env");
59
+ if (fs.existsSync(envPath)) {
60
+ loadDotEnv({ path: envPath });
61
+ }
62
+ const configContent = fs.readFileSync(resolvedConfig, "utf8");
63
+ const result = await transform(configContent, {
64
+ loader: "ts",
65
+ format: "esm",
66
+ target: "es2022",
67
+ sourcemap: false,
55
68
  });
69
+ // Write transpiled config alongside original so Node.js can
70
+ // resolve @kyro-cms/core from the project's node_modules
71
+ tmpFile = resolvedConfig.replace(/\.ts$/, ".admin.mjs");
72
+ fs.writeFileSync(tmpFile, result.code, "utf8");
73
+ // Evaluate in a child process to bypass Vite's module runner.
74
+ // Write a wrapper entrypoint that imports the config and prints JSON,
75
+ // then execute it with tsx (handles .ts resolution from .js imports).
76
+ const entryFile = tmpFile.replace(/\.admin\.mjs$/, ".admin-entry.mjs");
77
+ const resultFile = tmpFile.replace(/\.admin\.mjs$/, ".admin-result.json");
78
+ fs.writeFileSync(entryFile, `
79
+ import cfg from './${path.basename(tmpFile)}';
80
+ import fs from 'fs';
81
+ const data = { collections: cfg.default?.collections || cfg?.collections || [], globals: cfg.default?.globals || cfg?.globals || [] };
82
+ fs.writeFileSync('${path.basename(resultFile)}', JSON.stringify(data));
83
+ `, "utf8");
84
+ execSync(
85
+ `npx tsx "${entryFile}"`,
86
+ { cwd: path.dirname(resolvedConfig), encoding: "utf8", timeout: 15000, stdio: "pipe" },
87
+ );
88
+ const resultContent = fs.readFileSync(resultFile, "utf8");
89
+ const configModule = JSON.parse(resultContent);
90
+ try { fs.unlinkSync(resultFile); } catch {}
91
+ if (configModule.error) {
92
+ throw new Error(configModule.error);
93
+ }
94
+ (globalThis as any).__KYRO_ADMIN_PROJECT_CONFIG__ = {
95
+ collections: configModule.collections,
96
+ globals: configModule.globals,
97
+ adapter: configModule.adapter || null,
98
+ };
99
+ logger.info("Project config loaded for admin");
100
+ } catch (e: any) {
101
+ logger.warn(`Could not load project config: ${e.message}`);
102
+ } finally {
103
+ for (const suffix of [".admin.mjs", ".admin-entry.mjs", ".admin-result.json"]) {
104
+ const f = resolvedConfig.replace(/\.ts$/, suffix);
105
+ if (fs.existsSync(f)) { try { fs.unlinkSync(f); } catch { /* ignore */ } }
106
+ }
56
107
  }
57
108
 
109
+ // Set up Vite aliases and defines for runtime use
110
+ updateConfig({
111
+ vite: {
112
+ resolve: {
113
+ alias: {
114
+ "kyro:config": resolvedConfig,
115
+ },
116
+ },
117
+ define: {
118
+ __KYRO_ADMIN_PATH__: JSON.stringify(basePath),
119
+ __KYRO_API_PATH__: JSON.stringify(apiPath),
120
+ },
121
+ },
122
+ });
123
+
58
124
  // Inject Admin UI Routes
59
125
  const pages = [
60
126
  { pattern: "", entrypoint: "./pages/index.astro" },
@@ -104,20 +170,6 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
104
170
  ),
105
171
  });
106
172
  }
107
-
108
- updateConfig({
109
- vite: {
110
- resolve: {
111
- alias: {
112
- "kyro:config": resolvedConfig,
113
- },
114
- },
115
- define: {
116
- __KYRO_ADMIN_PATH__: JSON.stringify(basePath),
117
- __KYRO_API_PATH__: JSON.stringify(apiPath),
118
- },
119
- },
120
- });
121
173
  },
122
174
  "astro:build:done": ({ logger }) => {
123
175
  logger.info("Kyro Admin build complete");
@@ -14,7 +14,7 @@ interface Props {
14
14
  }
15
15
 
16
16
  const { title } = Astro.props;
17
- const siteSettings = await getSiteSettings();
17
+ const siteSettings = await getSiteSettings({ request: Astro.request });
18
18
  const siteName = siteSettings?.siteName || "Kyro CMS";
19
19
  const siteFavicon = siteSettings?.siteFavicon;
20
20
  ---
@@ -25,38 +25,42 @@ const siteFavicon = siteSettings?.siteFavicon;
25
25
  <meta charset="UTF-8" />
26
26
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
27
27
  <title>{title} - {siteName}</title>
28
- <link rel="icon" type={siteFavicon?.mimeType || "image/svg+xml"} href={siteFavicon?.url || "/favicon.svg"} />
29
- <link rel="preconnect" href="https://fonts.googleapis.com" />
30
28
  <link
31
- rel="preconnect"
32
- href="https://fonts.gstatic.com"
33
- crossorigin
29
+ rel="icon"
30
+ type={siteFavicon?.mimeType || "image/svg+xml"}
31
+ href={siteFavicon?.url || "/favicon.svg"}
34
32
  />
33
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
34
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
35
35
  <link
36
36
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap"
37
37
  rel="stylesheet"
38
38
  />
39
39
  <script is:inline define:vars={{ adminPath, apiPath }}>
40
40
  // Simple in-memory auth state (alternative to Zustand for SSR)
41
- window.__kyroAuth = window.__kyroAuth || { user: null, permissions: null, verified: false };
41
+ window.__kyroAuth = window.__kyroAuth || {
42
+ user: null,
43
+ permissions: null,
44
+ verified: false,
45
+ };
42
46
 
43
47
  // Verify auth - redirect to login if not authenticated
44
48
  (async () => {
45
49
  try {
46
50
  // Fetch user and permissions using cookies
47
51
  const [meRes, accessRes] = await Promise.all([
48
- fetch(apiPath + '/auth/me', { credentials: 'include' }),
49
- fetch(apiPath + '/auth/access', { credentials: 'include' })
52
+ fetch(apiPath + "/auth/me", { credentials: "include" }),
53
+ fetch(apiPath + "/auth/access", { credentials: "include" }),
50
54
  ]);
51
55
 
52
56
  if (!meRes.ok) {
53
57
  window.location.href = adminPath + "/login";
54
58
  return;
55
59
  }
56
-
60
+
57
61
  const meData = await meRes.json();
58
62
  if (!meData.user) {
59
- console.log('[AdminLayout] No user in data, redirecting to login');
63
+ console.log("[AdminLayout] No user in data, redirecting to login");
60
64
  window.location.href = adminPath + "/login";
61
65
  return;
62
66
  }
@@ -67,52 +71,70 @@ const siteFavicon = siteSettings?.siteFavicon;
67
71
  window.__kyroAuth.user = meData.user;
68
72
  window.__kyroAuth.permissions = permissions;
69
73
  window.__kyroAuth.verified = true;
70
-
74
+
71
75
  // Dispatch event for components to update
72
- window.dispatchEvent(new CustomEvent('kyro:auth-ready', {
73
- detail: {
74
- user: meData.user,
75
- permissions
76
- }
77
- }));
76
+ window.dispatchEvent(
77
+ new CustomEvent("kyro:auth-ready", {
78
+ detail: {
79
+ user: meData.user,
80
+ permissions,
81
+ },
82
+ }),
83
+ );
78
84
 
79
85
  // Navigation Guard - Check if user has read access to current page
80
86
  if (permissions) {
81
87
  const path = window.location.pathname;
82
- const relativePath = path.replace(adminPath, '');
83
-
88
+ const relativePath = path.replace(adminPath, "");
89
+
84
90
  // Extract slug and type
85
- let slug = '';
86
- let type = '';
87
-
88
- if (relativePath.startsWith('/users')) { slug = 'users'; type = 'collection'; }
89
- else if (relativePath.startsWith('/audit')) { slug = 'audit_logs'; type = 'collection'; }
90
- else if (relativePath.startsWith('/media')) { slug = 'media'; type = 'collection'; }
91
- else if (relativePath.startsWith('/settings/')) { slug = relativePath.split('/')[2]; type = 'global'; }
92
- else if (relativePath.includes('/') && !relativePath.startsWith('/login') && !relativePath.startsWith('/403')) {
93
- // Dynamic collections: /[adminPath]/[collectionSlug]/...
94
- slug = relativePath.split('/')[1];
95
- type = 'collection';
91
+ let slug = "";
92
+ let type = "";
93
+
94
+ if (relativePath.startsWith("/users")) {
95
+ slug = "users";
96
+ type = "collection";
97
+ } else if (relativePath.startsWith("/audit")) {
98
+ slug = "audit_logs";
99
+ type = "collection";
100
+ } else if (relativePath.startsWith("/media")) {
101
+ slug = "media";
102
+ type = "collection";
103
+ } else if (relativePath.startsWith("/settings/")) {
104
+ slug = relativePath.split("/")[2];
105
+ type = "global";
106
+ } else if (
107
+ relativePath.includes("/") &&
108
+ !relativePath.startsWith("/login") &&
109
+ !relativePath.startsWith("/403")
110
+ ) {
111
+ // Dynamic collections: /[adminPath]/[collectionSlug]/...
112
+ slug = relativePath.split("/")[1];
113
+ type = "collection";
96
114
  }
97
115
 
98
116
  if (slug && type) {
99
117
  let hasAccess = true;
100
- if (type === 'collection' && permissions.collections) {
118
+ if (type === "collection" && permissions.collections) {
101
119
  const p = permissions.collections[slug];
102
120
  if (p && p.read === false) hasAccess = false;
103
- } else if (type === 'global' && permissions.globals) {
121
+ } else if (type === "global" && permissions.globals) {
104
122
  const p = permissions.globals[slug];
105
123
  if (p && p.read === false) hasAccess = false;
106
124
  }
107
-
125
+
108
126
  if (!hasAccess) {
109
- console.log('[AdminLayout] Access denied for', slug, 'redirecting to 403');
127
+ console.log(
128
+ "[AdminLayout] Access denied for",
129
+ slug,
130
+ "redirecting to 403",
131
+ );
110
132
  window.location.href = adminPath + "/403";
111
133
  }
112
134
  }
113
135
  }
114
136
  } catch (err) {
115
- console.error('[AdminLayout] Auth check error:', err);
137
+ console.error("[AdminLayout] Auth check error:", err);
116
138
  window.location.href = adminPath + "/login";
117
139
  }
118
140
  })();
@@ -291,18 +313,18 @@ const siteFavicon = siteSettings?.siteFavicon;
291
313
 
292
314
  logoutBackdrop?.addEventListener("click", closeLogoutModal);
293
315
  logoutCancel?.addEventListener("click", closeLogoutModal);
294
- logoutConfirm?.addEventListener("click", async () => {
295
- try {
296
- await fetch(apiPath + '/auth/logout', {
297
- method: 'POST',
298
- credentials: 'include'
299
- });
300
- } finally {
301
- // Clear auth state
302
- window.__kyroAuth = { user: null, verified: false };
303
- window.location.href = adminPath + "/login";
304
- }
305
- });
316
+ logoutConfirm?.addEventListener("click", async () => {
317
+ try {
318
+ await fetch(apiPath + "/auth/logout", {
319
+ method: "POST",
320
+ credentials: "include",
321
+ });
322
+ } finally {
323
+ // Clear auth state
324
+ window.__kyroAuth = { user: null, verified: false };
325
+ window.location.href = adminPath + "/login";
326
+ }
327
+ });
306
328
  </script>
307
329
  </body>
308
330
  </html>
package/src/lib/config.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core/client";
2
- import projectConfig from "kyro:config";
3
2
  import {
4
3
  blogCollections,
5
4
  ecommerceCollections,
@@ -122,6 +121,12 @@ function createProjectAdminConfig(config: {
122
121
  };
123
122
  }
124
123
 
124
+ // Default to kitchen-sink during module evaluation (config loading phase).
125
+ // The real config is set later by the admin integration via globalThis.
126
+ const global = globalThis as any;
127
+ const projectConfig: { collections?: ConfigCollectionInput; globals?: ConfigGlobalInput } =
128
+ global.__KYRO_ADMIN_PROJECT_CONFIG__ || {};
129
+
125
130
  export const adminConfig = createProjectAdminConfig(projectConfig);
126
131
  export const collections = adminConfig.collections;
127
132
  export const globals = adminConfig.globals;