@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,215 @@
1
+ import React, { useState, useEffect, useCallback } from "react";
2
+ import { Dropdown, DropdownItem, DropdownSeparator } from "./ui/Dropdown";
3
+ import { Spinner } from "./ui/Spinner";
4
+
5
+ export type DocumentStatus = "draft" | "published" | "scheduled" | "archived";
6
+ export type SaveStatus = "idle" | "saving" | "saved" | "error";
7
+
8
+ export interface ActionBarProps {
9
+ status: DocumentStatus;
10
+ saveStatus: SaveStatus;
11
+ hasChanges: boolean;
12
+ onSave: () => void;
13
+ onPublish?: () => void;
14
+ onUnpublish?: () => void;
15
+ onDuplicate?: () => void;
16
+ onViewHistory?: () => void;
17
+ onPreview?: () => void;
18
+ onDelete?: () => void;
19
+ publishedAt?: string | null;
20
+ updatedAt?: string | null;
21
+ }
22
+
23
+ export function ActionBar({
24
+ status,
25
+ saveStatus,
26
+ hasChanges,
27
+ onSave,
28
+ onPublish,
29
+ onUnpublish,
30
+ onDuplicate,
31
+ onViewHistory,
32
+ onPreview,
33
+ onDelete,
34
+ publishedAt,
35
+ updatedAt,
36
+ }: ActionBarProps) {
37
+ const getSaveStatusText = () => {
38
+ if (saveStatus === "saving") return "Saving...";
39
+ if (saveStatus === "saved") return "Saved";
40
+ if (saveStatus === "error") return "Error saving";
41
+ if (hasChanges) return "Unsaved changes";
42
+ return null;
43
+ };
44
+
45
+ const getStatusBadge = () => {
46
+ const statusConfig = {
47
+ draft: { label: "Draft", class: "bg-gray-100 text-gray-600" },
48
+ published: { label: "Published", class: "bg-green-100 text-green-700" },
49
+ scheduled: { label: "Scheduled", class: "bg-blue-100 text-blue-700" },
50
+ archived: { label: "Archived", class: "bg-yellow-100 text-yellow-700" },
51
+ };
52
+ const config = statusConfig[status];
53
+ return (
54
+ <span
55
+ className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.class}`}
56
+ >
57
+ {config.label}
58
+ </span>
59
+ );
60
+ };
61
+
62
+ const formatDate = (dateStr: string | null | undefined) => {
63
+ if (!dateStr) return "Never";
64
+ return new Date(dateStr).toLocaleString();
65
+ };
66
+
67
+ return (
68
+ <div className="flex items-center justify-between py-3 px-1">
69
+ <div className="flex items-center gap-4">
70
+ <div className="flex items-center gap-2">
71
+ {getStatusBadge()}
72
+ {getSaveStatusText() && (
73
+ <span
74
+ className={`text-sm ${saveStatus === "error" ? "text-red-500" : "text-gray-500"}`}
75
+ >
76
+ {saveStatus === "saving" ? (
77
+ <Spinner size="sm" className="inline mr-1" />
78
+ ) : null}
79
+ {getSaveStatusText()}
80
+ </span>
81
+ )}
82
+ </div>
83
+ <div className="text-xs text-gray-400 space-y-0.5">
84
+ {updatedAt && <div>Updated: {formatDate(updatedAt)}</div>}
85
+ {publishedAt && status === "published" && (
86
+ <div>Published: {formatDate(publishedAt)}</div>
87
+ )}
88
+ </div>
89
+ </div>
90
+
91
+ <div className="flex items-center gap-2">
92
+ {status === "draft" && onPublish && (
93
+ <button
94
+ onClick={onPublish}
95
+ disabled={saveStatus === "saving"}
96
+ className="kyro-btn kyro-btn-primary kyro-btn-md"
97
+ >
98
+ Publish
99
+ </button>
100
+ )}
101
+ {status === "published" && onUnpublish && (
102
+ <button
103
+ onClick={onUnpublish}
104
+ disabled={saveStatus === "saving"}
105
+ className="kyro-btn kyro-btn-secondary kyro-btn-md"
106
+ >
107
+ Unpublish
108
+ </button>
109
+ )}
110
+ <button
111
+ onClick={onSave}
112
+ disabled={
113
+ saveStatus === "saving" || (!hasChanges && saveStatus !== "error")
114
+ }
115
+ className="kyro-btn kyro-btn-secondary kyro-btn-md"
116
+ >
117
+ {saveStatus === "saving" ? "Saving..." : "Save"}
118
+ </button>
119
+
120
+ <Dropdown
121
+ trigger={
122
+ <button className="kyro-btn kyro-btn-ghost kyro-btn-md p-2">
123
+ <svg
124
+ width="16"
125
+ height="16"
126
+ viewBox="0 0 24 24"
127
+ fill="none"
128
+ stroke="currentColor"
129
+ strokeWidth="2"
130
+ >
131
+ <circle cx="12" cy="12" r="1" />
132
+ <circle cx="12" cy="5" r="1" />
133
+ <circle cx="12" cy="19" r="1" />
134
+ </svg>
135
+ </button>
136
+ }
137
+ >
138
+ {onDuplicate && (
139
+ <DropdownItem
140
+ onClick={onDuplicate}
141
+ icon={
142
+ <svg
143
+ viewBox="0 0 24 24"
144
+ fill="none"
145
+ stroke="currentColor"
146
+ strokeWidth="2"
147
+ >
148
+ <rect x="9" y="9" width="13" height="13" rx="2" />
149
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
150
+ </svg>
151
+ }
152
+ >
153
+ Duplicate
154
+ </DropdownItem>
155
+ )}
156
+ {onViewHistory && (
157
+ <DropdownItem
158
+ onClick={onViewHistory}
159
+ icon={
160
+ <svg
161
+ viewBox="0 0 24 24"
162
+ fill="none"
163
+ stroke="currentColor"
164
+ strokeWidth="2"
165
+ >
166
+ <circle cx="12" cy="12" r="10" />
167
+ <polyline points="12,6 12,12 16,14" />
168
+ </svg>
169
+ }
170
+ >
171
+ View History
172
+ </DropdownItem>
173
+ )}
174
+ {onPreview && (
175
+ <DropdownItem
176
+ onClick={onPreview}
177
+ icon={
178
+ <svg
179
+ viewBox="0 0 24 24"
180
+ fill="none"
181
+ stroke="currentColor"
182
+ strokeWidth="2"
183
+ >
184
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
185
+ <circle cx="12" cy="12" r="3" />
186
+ </svg>
187
+ }
188
+ >
189
+ Preview
190
+ </DropdownItem>
191
+ )}
192
+ {(onDuplicate || onViewHistory || onPreview) && <DropdownSeparator />}
193
+ {onDelete && (
194
+ <DropdownItem
195
+ onClick={onDelete}
196
+ danger
197
+ icon={
198
+ <svg
199
+ viewBox="0 0 24 24"
200
+ fill="none"
201
+ stroke="currentColor"
202
+ strokeWidth="2"
203
+ >
204
+ <path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
205
+ </svg>
206
+ }
207
+ >
208
+ Delete
209
+ </DropdownItem>
210
+ )}
211
+ </Dropdown>
212
+ </div>
213
+ </div>
214
+ );
215
+ }
@@ -0,0 +1,214 @@
1
+ import { useState, useEffect } from "react";
2
+ import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core";
3
+ import { Sidebar } from "./layout/Sidebar";
4
+ import { ListView } from "./ListView";
5
+ import { DetailView } from "./DetailView";
6
+ import { CreateView } from "./CreateView";
7
+ import { Toast, ToastProvider } from "./ui/Toast";
8
+ import { ThemeProvider, type ThemeMode } from "./ThemeProvider";
9
+ import "../styles/main.css";
10
+
11
+ type View = "list" | "detail" | "create" | "settings";
12
+
13
+ export interface KyroAdminConfig {
14
+ collections?: CollectionConfig[] | Record<string, CollectionConfig>;
15
+ globals?: GlobalConfig[] | Record<string, GlobalConfig>;
16
+ adapter?: unknown;
17
+ name?: string;
18
+ }
19
+
20
+ interface AdminProps {
21
+ config: KyroAdminConfig;
22
+ theme?: ThemeMode;
23
+ onThemeChange?: (mode: ThemeMode) => void;
24
+ }
25
+
26
+ interface ToastMessage {
27
+ id: string;
28
+ type: "success" | "error" | "info" | "warning";
29
+ message: string;
30
+ }
31
+
32
+ function normalizeCollections(
33
+ input?: CollectionConfig[] | Record<string, CollectionConfig>,
34
+ ): Record<string, CollectionConfig> {
35
+ if (!input) return {};
36
+
37
+ if (Array.isArray(input)) {
38
+ return input.reduce(
39
+ (acc, c) => {
40
+ if (c.slug) acc[c.slug] = c;
41
+ return acc;
42
+ },
43
+ {} as Record<string, CollectionConfig>,
44
+ );
45
+ }
46
+
47
+ return input as Record<string, CollectionConfig>;
48
+ }
49
+
50
+ function normalizeGlobals(
51
+ input?: GlobalConfig[] | Record<string, GlobalConfig>,
52
+ ): Record<string, GlobalConfig> {
53
+ if (!input) return {};
54
+
55
+ if (Array.isArray(input)) {
56
+ return input.reduce(
57
+ (acc, g) => {
58
+ if (g.slug) acc[g.slug] = g;
59
+ return acc;
60
+ },
61
+ {} as Record<string, GlobalConfig>,
62
+ );
63
+ }
64
+
65
+ return input as Record<string, GlobalConfig>;
66
+ }
67
+
68
+ export function Admin({ config, theme = "light", onThemeChange }: AdminProps) {
69
+ const [activeCollection, setActiveCollection] = useState<string | null>(null);
70
+ const [activeGlobal, setActiveGlobal] = useState<string | null>(null);
71
+ const [currentView, setCurrentView] = useState<View>("list");
72
+ const [selectedId, setSelectedId] = useState<string | null>(null);
73
+ const [toasts, setToasts] = useState<ToastMessage[]>([]);
74
+
75
+ const collections = normalizeCollections(config.collections);
76
+ const globals = normalizeGlobals(config.globals);
77
+
78
+ useEffect(() => {
79
+ const collectionKeys = Object.keys(collections);
80
+ if (collectionKeys.length > 0 && !activeCollection) {
81
+ setActiveCollection(collectionKeys[0]);
82
+ }
83
+ }, []);
84
+
85
+ const addToast = (type: ToastMessage["type"], message: string) => {
86
+ const id = Math.random().toString(36).substring(7);
87
+ setToasts((prev) => [...prev, { id, type, message }]);
88
+ setTimeout(() => {
89
+ setToasts((prev) => prev.filter((t) => t.id !== id));
90
+ }, 5000);
91
+ };
92
+
93
+ const handleCollectionChange = (collectionName: string) => {
94
+ setActiveCollection(collectionName);
95
+ setActiveGlobal(null);
96
+ setCurrentView("list");
97
+ setSelectedId(null);
98
+ };
99
+
100
+ const handleGlobalChange = (globalName: string) => {
101
+ setActiveGlobal(globalName);
102
+ setActiveCollection(null);
103
+ setCurrentView("settings");
104
+ setSelectedId(null);
105
+ };
106
+
107
+ const handleNavigate = (view: View, id?: string) => {
108
+ setCurrentView(view);
109
+ setSelectedId(id || null);
110
+ };
111
+
112
+ const handleActionComplete = (action: string) => {
113
+ const messages: Record<string, string> = {
114
+ create: "Created successfully",
115
+ update: "Updated successfully",
116
+ delete: "Deleted successfully",
117
+ publish: "Published successfully",
118
+ };
119
+ addToast("success", messages[action] || "Action completed");
120
+ if (action !== "delete") {
121
+ setCurrentView("list");
122
+ }
123
+ };
124
+
125
+ const handleError = (message: string) => {
126
+ addToast("error", message);
127
+ };
128
+
129
+ const renderContent = () => {
130
+ if (currentView === "settings" && activeGlobal) {
131
+ const global = globals[activeGlobal];
132
+ if (!global) return null;
133
+ return (
134
+ <DetailView
135
+ config={{} as any}
136
+ global={global}
137
+ onBack={() => setCurrentView("list")}
138
+ onSave={() => handleActionComplete("update")}
139
+ onError={handleError}
140
+ mode="global"
141
+ />
142
+ );
143
+ }
144
+
145
+ if (!activeCollection) return null;
146
+
147
+ const collection = collections[activeCollection];
148
+ if (!collection) return null;
149
+
150
+ switch (currentView) {
151
+ case "create":
152
+ return (
153
+ <CreateView
154
+ config={{} as any}
155
+ collection={collection}
156
+ onCancel={() => setCurrentView("list")}
157
+ onSuccess={() => handleActionComplete("create")}
158
+ onError={handleError}
159
+ />
160
+ );
161
+ case "detail":
162
+ return (
163
+ <DetailView
164
+ config={{} as any}
165
+ collection={collection}
166
+ documentId={selectedId || undefined}
167
+ onBack={() => setCurrentView("list")}
168
+ onSave={() => handleActionComplete("update")}
169
+ onDelete={() => handleActionComplete("delete")}
170
+ onError={handleError}
171
+ />
172
+ );
173
+ default:
174
+ return (
175
+ <ListView
176
+ config={{} as any}
177
+ collection={collection}
178
+ onCreate={() => setCurrentView("create")}
179
+ onEdit={(id) => handleNavigate("detail", id)}
180
+ />
181
+ );
182
+ }
183
+ };
184
+
185
+ return (
186
+ <ThemeProvider defaultMode={theme}>
187
+ <ToastProvider>
188
+ <div className="kyro-admin">
189
+ <Sidebar
190
+ collections={collections}
191
+ globals={globals}
192
+ activeCollection={activeCollection}
193
+ activeGlobal={activeGlobal}
194
+ onCollectionClick={handleCollectionChange}
195
+ onGlobalClick={handleGlobalChange}
196
+ />
197
+ <div className="kyro-main">
198
+ <div className="kyro-content">{renderContent()}</div>
199
+ </div>
200
+ {toasts.map((toast) => (
201
+ <Toast
202
+ key={toast.id}
203
+ type={toast.type}
204
+ message={toast.message}
205
+ onClose={() =>
206
+ setToasts((prev) => prev.filter((t) => t.id !== toast.id))
207
+ }
208
+ />
209
+ ))}
210
+ </div>
211
+ </ToastProvider>
212
+ </ThemeProvider>
213
+ );
214
+ }