@kyro-cms/admin 0.1.2

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 (102) hide show
  1. package/.astro/content.d.ts +154 -0
  2. package/.astro/settings.json +5 -0
  3. package/.astro/types.d.ts +2 -0
  4. package/astro.config.mjs +28 -0
  5. package/bun.lock +1374 -0
  6. package/dist/client/_astro/AdminLayout.DkDpng53.css +1 -0
  7. package/dist/client/_astro/AutoForm.3eJCmCJp.js +1 -0
  8. package/dist/client/_astro/client.DyczpTbx.js +9 -0
  9. package/dist/client/_astro/index.B02hbnpo.js +1 -0
  10. package/dist/client/fonts/Serotiva-Black.woff2 +0 -0
  11. package/dist/client/fonts/Serotiva-Bold.woff2 +0 -0
  12. package/dist/client/fonts/Serotiva-Medium.woff2 +0 -0
  13. package/dist/client/fonts/Serotiva-Regular.woff2 +0 -0
  14. package/dist/client/fonts/Serotiva-SemiBold.woff2 +0 -0
  15. package/dist/server/chunks/AdminLayout_D-_JeUqC.mjs +26 -0
  16. package/dist/server/chunks/_id__BzI_o0qT.mjs +50 -0
  17. package/dist/server/chunks/_id__Cd-jOuY3.mjs +238 -0
  18. package/dist/server/chunks/_id__DvbD--iR.mjs +992 -0
  19. package/dist/server/chunks/_id__vpVaEo16.mjs +128 -0
  20. package/dist/server/chunks/_virtual_astro_server-island-manifest_CQQ1F5PF.mjs +7 -0
  21. package/dist/server/chunks/_virtual_astro_session-driver_Bk3Q189E.mjs +4 -0
  22. package/dist/server/chunks/astro-component_Dbx3T2Nh.mjs +37 -0
  23. package/dist/server/chunks/audit-logs_DrnUMRvY.mjs +74 -0
  24. package/dist/server/chunks/config_CPXslElD.mjs +4221 -0
  25. package/dist/server/chunks/dataStore_Dl7cA2Qp.mjs +89 -0
  26. package/dist/server/chunks/index_CVqOkerS.mjs +2960 -0
  27. package/dist/server/chunks/index_CX8SQ4BF.mjs +55 -0
  28. package/dist/server/chunks/index_CYofDU51.mjs +58 -0
  29. package/dist/server/chunks/index_DdNRhuaM.mjs +55 -0
  30. package/dist/server/chunks/index_DupPvtIF.mjs +42 -0
  31. package/dist/server/chunks/index_YTS_M-B9.mjs +263 -0
  32. package/dist/server/chunks/index_YeCzuVps.mjs +53 -0
  33. package/dist/server/chunks/login_DLyqMRO8.mjs +93 -0
  34. package/dist/server/chunks/logout_CSbt5wea.mjs +50 -0
  35. package/dist/server/chunks/me_C04jlYhH.mjs +41 -0
  36. package/dist/server/chunks/new_BbQ9b55M.mjs +92 -0
  37. package/dist/server/chunks/node_9bvTewss.mjs +1014 -0
  38. package/dist/server/chunks/noop-entrypoint_BOlrdqWF.mjs +3 -0
  39. package/dist/server/chunks/sequence_9cl7AJy-.mjs +2503 -0
  40. package/dist/server/chunks/server_peBx9VXG.mjs +8117 -0
  41. package/dist/server/chunks/sharp_pmJ7nHES.mjs +142 -0
  42. package/dist/server/chunks/users_Dzddy_YR.mjs +137 -0
  43. package/dist/server/entry.mjs +5 -0
  44. package/dist/server/virtual_astro_middleware.mjs +48 -0
  45. package/package.json +33 -0
  46. package/public/fonts/Serotiva-Black.woff2 +0 -0
  47. package/public/fonts/Serotiva-Bold.woff2 +0 -0
  48. package/public/fonts/Serotiva-Medium.woff2 +0 -0
  49. package/public/fonts/Serotiva-Regular.woff2 +0 -0
  50. package/public/fonts/Serotiva-SemiBold.woff2 +0 -0
  51. package/src/collections/auth/index.ts +155 -0
  52. package/src/components/ActionBar.tsx +215 -0
  53. package/src/components/Admin.tsx +214 -0
  54. package/src/components/AutoForm.tsx +1123 -0
  55. package/src/components/BulkActionsBar.tsx +80 -0
  56. package/src/components/CreateView.tsx +99 -0
  57. package/src/components/DetailView.tsx +329 -0
  58. package/src/components/Icons.tsx +23 -0
  59. package/src/components/ListView.tsx +192 -0
  60. package/src/components/StatusBadge.tsx +76 -0
  61. package/src/components/ThemeProvider.tsx +155 -0
  62. package/src/components/VersionHistoryPanel.tsx +205 -0
  63. package/src/components/fields/CheckboxField.tsx +37 -0
  64. package/src/components/fields/DateField.tsx +42 -0
  65. package/src/components/fields/NumberField.tsx +44 -0
  66. package/src/components/fields/RelationshipField.tsx +87 -0
  67. package/src/components/fields/SelectField.tsx +56 -0
  68. package/src/components/fields/TextField.tsx +49 -0
  69. package/src/components/index.ts +30 -0
  70. package/src/components/layout/Breadcrumbs.tsx +36 -0
  71. package/src/components/layout/Header.tsx +37 -0
  72. package/src/components/layout/Layout.tsx +25 -0
  73. package/src/components/layout/Sidebar.tsx +462 -0
  74. package/src/components/ui/Badge.tsx +14 -0
  75. package/src/components/ui/Button.tsx +41 -0
  76. package/src/components/ui/Dropdown.tsx +82 -0
  77. package/src/components/ui/Modal.tsx +135 -0
  78. package/src/components/ui/SlidePanel.tsx +73 -0
  79. package/src/components/ui/Spinner.tsx +24 -0
  80. package/src/components/ui/Toast.tsx +78 -0
  81. package/src/layouts/AdminLayout.astro +197 -0
  82. package/src/lib/config.ts +68 -0
  83. package/src/lib/dataStore.ts +111 -0
  84. package/src/middleware.ts +48 -0
  85. package/src/pages/[collection]/[id].astro +176 -0
  86. package/src/pages/[collection]/index.astro +180 -0
  87. package/src/pages/api/[collection]/[id].ts +258 -0
  88. package/src/pages/api/[collection]/index.ts +289 -0
  89. package/src/pages/api/auth/[id].ts +142 -0
  90. package/src/pages/api/auth/audit-logs.ts +80 -0
  91. package/src/pages/api/auth/login.ts +101 -0
  92. package/src/pages/api/auth/logout.ts +48 -0
  93. package/src/pages/api/auth/me.ts +36 -0
  94. package/src/pages/api/auth/users.ts +150 -0
  95. package/src/pages/audit/index.astro +110 -0
  96. package/src/pages/index.astro +225 -0
  97. package/src/pages/roles/index.astro +114 -0
  98. package/src/pages/users/[id].astro +174 -0
  99. package/src/pages/users/index.astro +142 -0
  100. package/src/pages/users/new.astro +91 -0
  101. package/src/styles/main.css +1449 -0
  102. package/tsconfig.json +12 -0
@@ -0,0 +1,135 @@
1
+ import React, { useEffect, type ReactNode } from "react";
2
+
3
+ interface ModalProps {
4
+ open: boolean;
5
+ onClose: () => void;
6
+ title: string;
7
+ children: ReactNode;
8
+ footer?: ReactNode;
9
+ size?: "sm" | "md" | "lg";
10
+ }
11
+
12
+ export function Modal({
13
+ open,
14
+ onClose,
15
+ title,
16
+ children,
17
+ footer,
18
+ size = "md",
19
+ }: ModalProps) {
20
+ useEffect(() => {
21
+ const handleEscape = (e: KeyboardEvent) => {
22
+ if (e.key === "Escape") onClose();
23
+ };
24
+
25
+ if (open) {
26
+ document.addEventListener("keydown", handleEscape);
27
+ document.body.style.overflow = "hidden";
28
+ }
29
+
30
+ return () => {
31
+ document.removeEventListener("keydown", handleEscape);
32
+ document.body.style.overflow = "";
33
+ };
34
+ }, [open, onClose]);
35
+
36
+ if (!open) return null;
37
+
38
+ const sizeClasses = {
39
+ sm: "max-w-sm",
40
+ md: "max-w-md",
41
+ lg: "max-w-2xl",
42
+ };
43
+
44
+ return (
45
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
46
+ <div
47
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
48
+ onClick={onClose}
49
+ />
50
+ <div
51
+ className={`relative w-full ${sizeClasses[size]} mx-4 bg-white rounded-xl shadow-2xl animate-in fade-in zoom-in-95 duration-200`}
52
+ >
53
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
54
+ <h2 className="text-lg font-semibold text-gray-900">{title}</h2>
55
+ <button
56
+ onClick={onClose}
57
+ className="p-1 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors"
58
+ >
59
+ <svg
60
+ width="20"
61
+ height="20"
62
+ viewBox="0 0 24 24"
63
+ fill="none"
64
+ stroke="currentColor"
65
+ strokeWidth="2"
66
+ >
67
+ <path d="M18 6L6 18M6 6l12 12" />
68
+ </svg>
69
+ </button>
70
+ </div>
71
+ <div className="px-6 py-4">{children}</div>
72
+ {footer && (
73
+ <div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200 bg-gray-50 rounded-b-xl">
74
+ {footer}
75
+ </div>
76
+ )}
77
+ </div>
78
+ </div>
79
+ );
80
+ }
81
+
82
+ interface ConfirmModalProps {
83
+ open: boolean;
84
+ onClose: () => void;
85
+ onConfirm: () => void;
86
+ title: string;
87
+ message: string;
88
+ confirmLabel?: string;
89
+ cancelLabel?: string;
90
+ variant?: "default" | "danger";
91
+ loading?: boolean;
92
+ }
93
+
94
+ export function ConfirmModal({
95
+ open,
96
+ onClose,
97
+ onConfirm,
98
+ title,
99
+ message,
100
+ confirmLabel = "Confirm",
101
+ cancelLabel = "Cancel",
102
+ variant = "default",
103
+ loading = false,
104
+ }: ConfirmModalProps) {
105
+ return (
106
+ <Modal
107
+ open={open}
108
+ onClose={onClose}
109
+ title={title}
110
+ size="sm"
111
+ footer={
112
+ <>
113
+ <button
114
+ onClick={onClose}
115
+ disabled={loading}
116
+ className="kyro-btn kyro-btn-secondary kyro-btn-md"
117
+ >
118
+ {cancelLabel}
119
+ </button>
120
+ <button
121
+ onClick={onConfirm}
122
+ disabled={loading}
123
+ className={`kyro-btn kyro-btn-md ${
124
+ variant === "danger" ? "kyro-btn-danger" : "kyro-btn-primary"
125
+ }`}
126
+ >
127
+ {loading ? "Loading..." : confirmLabel}
128
+ </button>
129
+ </>
130
+ }
131
+ >
132
+ <p className="text-gray-600">{message}</p>
133
+ </Modal>
134
+ );
135
+ }
@@ -0,0 +1,73 @@
1
+ import React, { useEffect, useRef, type ReactNode } from "react";
2
+
3
+ interface SlidePanelProps {
4
+ open: boolean;
5
+ onClose: () => void;
6
+ title: string;
7
+ children: ReactNode;
8
+ width?: "sm" | "md" | "lg";
9
+ }
10
+
11
+ export function SlidePanel({
12
+ open,
13
+ onClose,
14
+ title,
15
+ children,
16
+ width = "md",
17
+ }: SlidePanelProps) {
18
+ const panelRef = useRef<HTMLDivElement>(null);
19
+
20
+ useEffect(() => {
21
+ const handleEscape = (e: KeyboardEvent) => {
22
+ if (e.key === "Escape") onClose();
23
+ };
24
+
25
+ if (open) {
26
+ document.addEventListener("keydown", handleEscape);
27
+ document.body.style.overflow = "hidden";
28
+ }
29
+
30
+ return () => {
31
+ document.removeEventListener("keydown", handleEscape);
32
+ document.body.style.overflow = "";
33
+ };
34
+ }, [open, onClose]);
35
+
36
+ const widthClasses = {
37
+ sm: "w-[320px]",
38
+ md: "w-[480px]",
39
+ lg: "w-[640px]",
40
+ };
41
+
42
+ if (!open) return null;
43
+
44
+ return (
45
+ <div className="fixed inset-0 z-50">
46
+ <div className="absolute inset-0 bg-black/30" onClick={onClose} />
47
+ <div
48
+ ref={panelRef}
49
+ className={`absolute right-0 top-0 h-full ${widthClasses[width]} bg-white shadow-2xl flex flex-col animate-in slide-in-from-right duration-300`}
50
+ >
51
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
52
+ <h2 className="text-lg font-semibold text-gray-900">{title}</h2>
53
+ <button
54
+ onClick={onClose}
55
+ className="p-1 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors"
56
+ >
57
+ <svg
58
+ width="20"
59
+ height="20"
60
+ viewBox="0 0 24 24"
61
+ fill="none"
62
+ stroke="currentColor"
63
+ strokeWidth="2"
64
+ >
65
+ <path d="M18 6L6 18M6 6l12 12" />
66
+ </svg>
67
+ </button>
68
+ </div>
69
+ <div className="flex-1 overflow-y-auto p-6">{children}</div>
70
+ </div>
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,24 @@
1
+ interface SpinnerProps {
2
+ size?: "sm" | "md" | "lg";
3
+ className?: string;
4
+ }
5
+
6
+ export function Spinner({ size = "md", className = "" }: SpinnerProps) {
7
+ return (
8
+ <svg
9
+ className={`kyro-spinner kyro-spinner-${size} ${className}`}
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ >
13
+ <circle
14
+ cx="12"
15
+ cy="12"
16
+ r="10"
17
+ stroke="currentColor"
18
+ strokeWidth="3"
19
+ strokeLinecap="round"
20
+ strokeDasharray="40 20"
21
+ />
22
+ </svg>
23
+ );
24
+ }
@@ -0,0 +1,78 @@
1
+ import { createContext, useContext, type ReactNode } from 'react';
2
+
3
+ interface Toast {
4
+ id: string;
5
+ type: 'success' | 'error' | 'info' | 'warning';
6
+ message: string;
7
+ }
8
+
9
+ interface ToastContextType {
10
+ toasts: Toast[];
11
+ addToast: (type: Toast['type'], message: string) => void;
12
+ removeToast: (id: string) => void;
13
+ }
14
+
15
+ const ToastContext = createContext<ToastContextType | null>(null);
16
+
17
+ interface ToastProps {
18
+ type: Toast['type'];
19
+ message: string;
20
+ onClose: () => void;
21
+ }
22
+
23
+ export function Toast({ type, message, onClose }: ToastProps) {
24
+ const icons = {
25
+ success: (
26
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
27
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
28
+ <path d="M22 4L12 14.01l-3-3" />
29
+ </svg>
30
+ ),
31
+ error: (
32
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
33
+ <circle cx="12" cy="12" r="10" />
34
+ <path d="M15 9l-6 6M9 9l6 6" />
35
+ </svg>
36
+ ),
37
+ warning: (
38
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
39
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
40
+ <path d="M12 9v4M12 17h.01" />
41
+ </svg>
42
+ ),
43
+ info: (
44
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
45
+ <circle cx="12" cy="12" r="10" />
46
+ <path d="M12 16v-4M12 8h.01" />
47
+ </svg>
48
+ )
49
+ };
50
+
51
+ return (
52
+ <div className={`kyro-toast kyro-toast-${type}`}>
53
+ <span className="kyro-toast-icon">{icons[type]}</span>
54
+ <span className="kyro-toast-message">{message}</span>
55
+ <button className="kyro-toast-close" onClick={onClose}>
56
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
57
+ <path d="M18 6L6 18M6 6l12 12" />
58
+ </svg>
59
+ </button>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ export function ToastProvider({ children }: { children: ReactNode }) {
65
+ return (
66
+ <ToastContext.Provider value={{ toasts: [], addToast: () => {}, removeToast: () => {} }}>
67
+ {children}
68
+ </ToastContext.Provider>
69
+ );
70
+ }
71
+
72
+ export function useToast() {
73
+ const context = useContext(ToastContext);
74
+ if (!context) {
75
+ throw new Error('useToast must be used within a ToastProvider');
76
+ }
77
+ return context;
78
+ }
@@ -0,0 +1,197 @@
1
+ ---
2
+ import "../styles/main.css";
3
+ import { getRegistry } from "@kyro-cms/core";
4
+
5
+ const registry = getRegistry();
6
+ const collections = registry.getCollections();
7
+
8
+ interface Props {
9
+ title: string;
10
+ }
11
+
12
+ const { title } = Astro.props;
13
+ ---
14
+
15
+ <!doctype html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="UTF-8" />
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
+ <title>{title} - Kyro CMS</title>
21
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
22
+ </head>
23
+ <body class="bg-[#eaeff2] antialiased text-[#0b1222]">
24
+ <div class="flex h-screen p-6 gap-6 overflow-hidden">
25
+ <!-- Sidebar Tile -->
26
+ <aside
27
+ class="surface-tile w-[320px] flex flex-col flex-shrink-0 overflow-hidden"
28
+ >
29
+ <!-- Logo Section -->
30
+ <div class="px-4 py-8">
31
+ <div class="flex items-center gap-4">
32
+ <span class="text-3xl font-black tracking-tighter text-[#0b1222]"
33
+ >KYRO.</span
34
+ >
35
+ </div>
36
+ </div>
37
+
38
+ <!-- Navigation -->
39
+ <nav class="flex-1 px-4 overflow-y-auto">
40
+ <div class="space-y-2 mt-4">
41
+ <a
42
+ href="/"
43
+ class={`flex items-center gap-4 px-6 py-4 rounded-2xl transition-all font-bold ${
44
+ title === "Dashboard"
45
+ ? "bg-[#0b1222] text-white shadow-lg"
46
+ : "text-[#64748b] hover:bg-gray-50 hover:text-[#0b1222]"
47
+ }`}
48
+ >
49
+ <svg
50
+ class="w-5 h-5"
51
+ fill="none"
52
+ stroke="currentColor"
53
+ viewBox="0 0 24 24"
54
+ ><path
55
+ stroke-linecap="round"
56
+ stroke-linejoin="round"
57
+ stroke-width="2.5"
58
+ d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
59
+ ></path></svg
60
+ >
61
+ <span>Dashboard</span>
62
+ </a>
63
+
64
+ <div class="pt-8 pb-4">
65
+ <p
66
+ class="px-6 text-[10px] font-black text-[#64748b] uppercase tracking-[0.3em] opacity-50"
67
+ >
68
+ Authentication
69
+ </p>
70
+ </div>
71
+
72
+ <a
73
+ href="/users"
74
+ class={`flex items-center gap-4 px-6 py-4 rounded-2xl transition-all font-bold ${
75
+ title === "Users"
76
+ ? "bg-[#0b1222] text-white shadow-lg"
77
+ : "text-[#64748b] hover:bg-gray-50 hover:text-[#0b1222]"
78
+ }`}
79
+ >
80
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
81
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
82
+ </svg>
83
+ <span>Users</span>
84
+ </a>
85
+
86
+ <a
87
+ href="/roles"
88
+ class={`flex items-center gap-4 px-6 py-4 rounded-2xl transition-all font-bold ${
89
+ title === "Roles"
90
+ ? "bg-[#0b1222] text-white shadow-lg"
91
+ : "text-[#64748b] hover:bg-gray-50 hover:text-[#0b1222]"
92
+ }`}
93
+ >
94
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
95
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
96
+ </svg>
97
+ <span>Roles</span>
98
+ </a>
99
+
100
+ <a
101
+ href="/audit"
102
+ class={`flex items-center gap-4 px-6 py-4 rounded-2xl transition-all font-bold ${
103
+ title === "Audit Logs"
104
+ ? "bg-[#0b1222] text-white shadow-lg"
105
+ : "text-[#64748b] hover:bg-gray-50 hover:text-[#0b1222]"
106
+ }`}
107
+ >
108
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
109
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
110
+ </svg>
111
+ <span>Audit Logs</span>
112
+ </a>
113
+
114
+ <div class="pt-8 pb-4">
115
+ <p
116
+ class="px-6 text-[10px] font-black text-[#64748b] uppercase tracking-[0.3em] opacity-50"
117
+ >
118
+ Collections
119
+ </p>
120
+ </div>
121
+
122
+ {
123
+ collections.map((col) => (
124
+ <a
125
+ href={`/${col.slug}`}
126
+ class="flex items-center gap-4 px-6 py-4 rounded-2xl text-[#64748b] font-bold transition-all hover:bg-gray-50 hover:text-[#0b1222] group"
127
+ >
128
+ <div class="w-2 h-2 rounded-full bg-gray-200 group-hover:bg-[#0b1222] transition-colors" />
129
+ <span>{col.label}</span>
130
+ </a>
131
+ ))
132
+ }
133
+
134
+ <div class="pt-8 pb-4">
135
+ <p
136
+ class="px-6 text-[10px] font-black text-[#64748b] uppercase tracking-[0.3em] opacity-50"
137
+ >
138
+ System
139
+ </p>
140
+ </div>
141
+
142
+ <a
143
+ href="/admin"
144
+ class="flex items-center gap-4 px-6 py-4 rounded-2xl text-[#64748b] font-bold transition-all hover:bg-gray-50 hover:text-[#0b1222]"
145
+ >
146
+ <svg
147
+ class="w-5 h-5"
148
+ fill="none"
149
+ stroke="currentColor"
150
+ viewBox="0 0 24 24"
151
+ ><path
152
+ stroke-linecap="round"
153
+ stroke-linejoin="round"
154
+ stroke-width="2.5"
155
+ d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
156
+ ></path><path
157
+ stroke-linecap="round"
158
+ stroke-linejoin="round"
159
+ stroke-width="2.5"
160
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg
161
+ >
162
+ <span>Settings</span>
163
+ </a>
164
+ </div>
165
+ </nav>
166
+
167
+ <!-- User Section Tile-Adjacent -->
168
+ <div class="p-10 mt-auto bg-gray-50/50">
169
+ <div class="flex flex-col items-center text-center">
170
+ <div class="relative mb-5">
171
+ <img
172
+ src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?q=80&w=150&auto=format&fit=crop"
173
+ class="w-20 h-20 rounded-full object-cover border-4 border-white shadow-md"
174
+ alt="Emily Jonson"
175
+ />
176
+ <div
177
+ class="absolute bottom-1 right-1 w-5 h-5 bg-orange-400 border-4 border-white rounded-full"
178
+ >
179
+ </div>
180
+ </div>
181
+ <h3 class="font-black text-lg text-[#0b1222] tracking-tight">
182
+ Emily Jonson
183
+ </h3>
184
+ <p class="text-xs text-[#64748b] mt-1 uppercase tracking-widest">
185
+ jonson@bress.com
186
+ </p>
187
+ </div>
188
+ </div>
189
+ </aside>
190
+
191
+ <!-- Main Content Column -->
192
+ <main class="flex-1 flex flex-col gap-6 overflow-hidden">
193
+ <slot />
194
+ </main>
195
+ </div>
196
+ </body>
197
+ </html>
@@ -0,0 +1,68 @@
1
+ import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core";
2
+ import { blogCollections } from "../../../src/index";
3
+ import { ecommerceCollections } from "../../../src/index";
4
+ import { minimalCollections } from "../../../src/index";
5
+ import { kitchenSinkCollections } from "../../../src/index";
6
+ import { mediaCollections } from "../../../src/templates/media";
7
+ import {
8
+ allSettingsGlobals,
9
+ coreSettingsGlobals,
10
+ ecommerceSettingsGlobals,
11
+ } from "../../../src/templates/settings";
12
+ import { authCollections } from "../collections/auth";
13
+
14
+ export type AdminTemplate = "minimal" | "blog" | "ecommerce" | "kitchen-sink";
15
+
16
+ export function getAdminConfig(template: AdminTemplate = "blog") {
17
+ const collections: CollectionConfig[] = [];
18
+ const globals: GlobalConfig[] = [];
19
+
20
+ collections.push(...Object.values(mediaCollections));
21
+ collections.push(...Object.values(authCollections));
22
+
23
+ switch (template) {
24
+ case "minimal":
25
+ collections.push(...Object.values(minimalCollections));
26
+ globals.push(...coreSettingsGlobals);
27
+ break;
28
+ case "blog":
29
+ collections.push(...Object.values(blogCollections));
30
+ globals.push(...coreSettingsGlobals);
31
+ break;
32
+ case "ecommerce":
33
+ collections.push(...Object.values(ecommerceCollections));
34
+ globals.push(...coreSettingsGlobals, ...ecommerceSettingsGlobals);
35
+ break;
36
+ case "kitchen-sink":
37
+ collections.push(
38
+ ...Object.values(minimalCollections),
39
+ ...Object.values(blogCollections),
40
+ ...Object.values(ecommerceCollections),
41
+ ...Object.values(kitchenSinkCollections),
42
+ );
43
+ globals.push(...allSettingsGlobals);
44
+ break;
45
+ }
46
+
47
+ const collectionsMap = collections.reduce(
48
+ (acc, c) => {
49
+ if (c.slug) acc[c.slug] = c;
50
+ return acc;
51
+ },
52
+ {} as Record<string, CollectionConfig>,
53
+ );
54
+
55
+ const globalsMap = globals.reduce(
56
+ (acc, g) => {
57
+ if (g.slug) acc[g.slug] = g;
58
+ return acc;
59
+ },
60
+ {} as Record<string, GlobalConfig>,
61
+ );
62
+
63
+ return { collections: collectionsMap, globals: globalsMap };
64
+ }
65
+
66
+ export const adminConfig = getAdminConfig("blog");
67
+ export const collections = adminConfig.collections;
68
+ export const globals = adminConfig.globals;
@@ -0,0 +1,111 @@
1
+ import type { CollectionConfig } from "@kyro-cms/core";
2
+
3
+ interface Document {
4
+ id: string;
5
+ [key: string]: any;
6
+ }
7
+
8
+ class DataStore {
9
+ private data: Map<string, Document[]> = new Map();
10
+ private metadata: Map<string, { createdAt: string; updatedAt: string }> =
11
+ new Map();
12
+ private idCounters: Map<string, number> = new Map();
13
+
14
+ initialize(collections: Record<string, CollectionConfig>) {
15
+ for (const [slug, config] of Object.entries(collections)) {
16
+ if (!this.data.has(slug)) {
17
+ this.data.set(slug, []);
18
+ this.idCounters.set(slug, 1);
19
+ }
20
+ }
21
+ }
22
+
23
+ private generateId(slug: string): string {
24
+ const counter = this.idCounters.get(slug) || 1;
25
+ this.idCounters.set(slug, counter + 1);
26
+ return `${slug}-${counter}`;
27
+ }
28
+
29
+ private getTimestamp(): string {
30
+ return new Date().toISOString();
31
+ }
32
+
33
+ find<T = any>(
34
+ slug: string,
35
+ options: { page?: number; limit?: number } = {},
36
+ ): { docs: T[]; totalDocs: number; totalPages: number; page: number } {
37
+ const docs = this.data.get(slug) || [];
38
+ const page = options.page || 1;
39
+ const limit = options.limit || 25;
40
+ const start = (page - 1) * limit;
41
+ const end = start + limit;
42
+
43
+ return {
44
+ docs: docs.slice(start, end) as T[],
45
+ totalDocs: docs.length,
46
+ totalPages: Math.ceil(docs.length / limit),
47
+ page,
48
+ };
49
+ }
50
+
51
+ findById<T = any>(slug: string, id: string): T | null {
52
+ const docs = this.data.get(slug) || [];
53
+ return (docs.find((d) => d.id === id) as T) || null;
54
+ }
55
+
56
+ create<T = any>(slug: string, data: Partial<T>): T {
57
+ const docs = this.data.get(slug) || [];
58
+ const now = this.getTimestamp();
59
+ const newDoc = {
60
+ id: this.generateId(slug),
61
+ ...data,
62
+ createdAt: now,
63
+ updatedAt: now,
64
+ } as T;
65
+ docs.push(newDoc as any);
66
+ this.data.set(slug, docs);
67
+ this.metadata.set(`${slug}:${(newDoc as any).id}`, {
68
+ createdAt: now,
69
+ updatedAt: now,
70
+ });
71
+ return newDoc;
72
+ }
73
+
74
+ update<T = any>(slug: string, id: string, data: Partial<T>): T | null {
75
+ const docs = this.data.get(slug) || [];
76
+ const index = docs.findIndex((d) => d.id === id);
77
+ if (index === -1) return null;
78
+
79
+ const now = this.getTimestamp();
80
+ const updated = {
81
+ ...docs[index],
82
+ ...data,
83
+ id,
84
+ updatedAt: now,
85
+ };
86
+ docs[index] = updated;
87
+ this.data.set(slug, docs);
88
+ this.metadata.set(`${slug}:${id}`, {
89
+ createdAt: docs[index].createdAt,
90
+ updatedAt: now,
91
+ });
92
+ return updated as T;
93
+ }
94
+
95
+ delete(slug: string, id: string): boolean {
96
+ const docs = this.data.get(slug) || [];
97
+ const index = docs.findIndex((d) => d.id === id);
98
+ if (index === -1) return false;
99
+
100
+ docs.splice(index, 1);
101
+ this.data.set(slug, docs);
102
+ this.metadata.delete(`${slug}:${id}`);
103
+ return true;
104
+ }
105
+
106
+ count(slug: string): number {
107
+ return (this.data.get(slug) || []).length;
108
+ }
109
+ }
110
+
111
+ export const dataStore = new DataStore();