@kyro-cms/admin 0.1.5 → 0.1.7

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 (164) hide show
  1. package/README.md +149 -51
  2. package/package.json +52 -5
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +136 -27
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +1417 -661
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +3 -3
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +199 -57
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +786 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +191 -53
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +149 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/UserManagement.tsx +204 -0
  26. package/src/components/VersionHistoryPanel.tsx +3 -3
  27. package/src/components/WebhookManager.tsx +608 -0
  28. package/src/components/blocks/AccordionBlock.tsx +97 -0
  29. package/src/components/blocks/ArrayBlock.tsx +75 -0
  30. package/src/components/blocks/BlockEditModal.MARKER +12 -0
  31. package/src/components/blocks/BlockEditModal.tsx +774 -0
  32. package/src/components/blocks/ButtonBlock.tsx +165 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +66 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +151 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +64 -0
  38. package/src/components/blocks/HeadingBlock.tsx +81 -0
  39. package/src/components/blocks/HeroBlock.tsx +157 -0
  40. package/src/components/blocks/ImageBlock.tsx +83 -0
  41. package/src/components/blocks/LinkBlock.tsx +71 -0
  42. package/src/components/blocks/ListBlock.tsx +39 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +279 -0
  45. package/src/components/blocks/VStackBlock.tsx +75 -0
  46. package/src/components/blocks/VideoBlock.tsx +45 -0
  47. package/src/components/blocks/index.ts +10 -0
  48. package/src/components/fields/BlocksField.tsx +323 -0
  49. package/src/components/fields/CheckboxField.tsx +15 -9
  50. package/src/components/fields/CodeField.tsx +234 -0
  51. package/src/components/fields/DateField.tsx +38 -11
  52. package/src/components/fields/EditorClient.tsx +271 -0
  53. package/src/components/fields/FileField.tsx +390 -0
  54. package/src/components/fields/HybridContentField.tsx +109 -0
  55. package/src/components/fields/ImageField.tsx +429 -0
  56. package/src/components/fields/JSONField.tsx +361 -0
  57. package/src/components/fields/MarkdownField.tsx +282 -0
  58. package/src/components/fields/NumberField.tsx +42 -12
  59. package/src/components/fields/PortableTextField.tsx +143 -0
  60. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  61. package/src/components/fields/RelationshipField.tsx +231 -59
  62. package/src/components/fields/SelectField.tsx +25 -15
  63. package/src/components/fields/TextField.tsx +45 -14
  64. package/src/components/fields/extensions/blockComponents.tsx +237 -0
  65. package/src/components/fields/extensions/blocksStore.ts +273 -0
  66. package/src/components/fields/index.ts +13 -0
  67. package/src/components/index.ts +1 -2
  68. package/src/components/layout/Header.tsx +2 -2
  69. package/src/components/layout/Layout.tsx +2 -2
  70. package/src/components/ui/Badge.tsx +9 -4
  71. package/src/components/ui/BlockDrawer.tsx +79 -0
  72. package/src/components/ui/Button.tsx +1 -1
  73. package/src/components/ui/CommandPalette.tsx +362 -0
  74. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  75. package/src/components/ui/Dropdown.tsx +1 -1
  76. package/src/components/ui/Modal.tsx +37 -12
  77. package/src/components/ui/PromptModal.tsx +94 -0
  78. package/src/components/ui/SlidePanel.tsx +43 -16
  79. package/src/components/ui/Toast.tsx +80 -14
  80. package/src/env.d.ts +16 -0
  81. package/src/env.ts +20 -0
  82. package/src/index.ts +0 -1
  83. package/src/layouts/AdminLayout.astro +164 -170
  84. package/src/layouts/AuthLayout.astro +50 -0
  85. package/src/lib/MediaService.ts +541 -0
  86. package/src/lib/auth/sqlite-adapter.ts +319 -0
  87. package/src/lib/config.ts +22 -6
  88. package/src/lib/dataStore.ts +132 -74
  89. package/src/lib/db/adapter.ts +54 -0
  90. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  91. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  92. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  93. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  94. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  95. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  96. package/src/lib/db/index.ts +449 -0
  97. package/src/lib/db/mongodb-adapter.ts +207 -0
  98. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  99. package/src/lib/db/schema/mysql-auth.ts +113 -0
  100. package/src/lib/db/schema/mysql-content.ts +20 -0
  101. package/src/lib/db/schema/postgres-auth.ts +116 -0
  102. package/src/lib/db/schema/postgres-content.ts +35 -0
  103. package/src/lib/db/schema/postgres-media.ts +52 -0
  104. package/src/lib/db/schema/postgres-settings.ts +11 -0
  105. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  106. package/src/lib/db/schema/sqlite-content.ts +20 -0
  107. package/src/lib/graphql/index.ts +1 -0
  108. package/src/lib/graphql/schema.ts +443 -0
  109. package/src/lib/rate-limit.ts +267 -0
  110. package/src/lib/storage.ts +374 -0
  111. package/src/lib/store.ts +85 -0
  112. package/src/middleware.ts +116 -28
  113. package/src/pages/[collection]/[id].astro +178 -122
  114. package/src/pages/[collection]/index.astro +24 -156
  115. package/src/pages/admin/api-explorer.astro +98 -0
  116. package/src/pages/admin/graphql-explorer.astro +40 -0
  117. package/src/pages/admin/graphql.astro +97 -0
  118. package/src/pages/admin/index.astro +286 -0
  119. package/src/pages/admin/keys.astro +8 -0
  120. package/src/pages/admin/rest-playground.astro +44 -0
  121. package/src/pages/admin/webhooks.astro +8 -0
  122. package/src/pages/api/[collection]/[id]/publish.ts +44 -0
  123. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  124. package/src/pages/api/[collection]/[id]/versions.ts +36 -0
  125. package/src/pages/api/[collection]/[id].ts +102 -159
  126. package/src/pages/api/[collection]/index.ts +151 -230
  127. package/src/pages/api/auth/[id].ts +48 -69
  128. package/src/pages/api/auth/audit-logs.ts +20 -43
  129. package/src/pages/api/auth/login.ts +159 -45
  130. package/src/pages/api/auth/logout.ts +50 -20
  131. package/src/pages/api/auth/refresh.ts +119 -0
  132. package/src/pages/api/auth/register.ts +110 -40
  133. package/src/pages/api/auth/users.ts +22 -97
  134. package/src/pages/api/collections.ts +59 -0
  135. package/src/pages/api/globals/[slug]/test.ts +172 -0
  136. package/src/pages/api/globals/[slug].ts +42 -0
  137. package/src/pages/api/graphql.ts +90 -0
  138. package/src/pages/api/health.ts +417 -40
  139. package/src/pages/api/keys/[id].ts +26 -0
  140. package/src/pages/api/keys/index.ts +75 -0
  141. package/src/pages/api/media/[id].ts +309 -0
  142. package/src/pages/api/media/folders.ts +609 -0
  143. package/src/pages/api/media/index.ts +146 -0
  144. package/src/pages/api/media/resize.ts +267 -0
  145. package/src/pages/api/search.ts +82 -0
  146. package/src/pages/api/slug-availability.ts +70 -0
  147. package/src/pages/api/storage-config.ts +20 -0
  148. package/src/pages/api/storage-status.ts +206 -0
  149. package/src/pages/api/upload.ts +334 -0
  150. package/src/pages/api/webhooks/index.ts +71 -0
  151. package/src/pages/audit/index.astro +2 -104
  152. package/src/pages/login.astro +82 -0
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +102 -0
  156. package/src/pages/roles/index.astro +21 -21
  157. package/src/pages/settings/[slug].astro +162 -0
  158. package/src/pages/settings/index.astro +9 -0
  159. package/src/pages/users/[id].astro +29 -21
  160. package/src/pages/users/index.astro +22 -17
  161. package/src/pages/users/new.astro +18 -17
  162. package/src/styles/main.css +553 -128
  163. package/src/components/layout/Sidebar.tsx +0 -497
  164. package/src/pages/index.astro +0 -225
@@ -1,6 +1,8 @@
1
1
  import { useState, useEffect } from "react";
2
2
  import type { CollectionConfig, KyroConfig } from "@kyro-cms/core";
3
3
  import { Spinner } from "./ui/Spinner";
4
+ import { ConfirmModal } from "./ui/Modal";
5
+ import { Search, Plus, Settings } from "lucide-react";
4
6
 
5
7
  interface ListViewProps {
6
8
  config: KyroConfig;
@@ -20,6 +22,9 @@ export function ListView({
20
22
  const [page, setPage] = useState(1);
21
23
  const [totalPages, setTotalPages] = useState(1);
22
24
  const [limit] = useState(25);
25
+ const [deleteId, setDeleteId] = useState<string | null>(null);
26
+ const [searchQuery, setSearchQuery] = useState("");
27
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
23
28
 
24
29
  const label = collection.label || collection.slug;
25
30
 
@@ -44,18 +49,29 @@ export function ListView({
44
49
  }
45
50
  };
46
51
 
52
+ const filteredDocs = docs.filter((d) =>
53
+ Object.values(d).some((v) =>
54
+ String(v).toLowerCase().includes(searchQuery.toLowerCase()),
55
+ ),
56
+ );
57
+
47
58
  const handleDelete = async (id: string) => {
48
- if (!confirm("Delete this document?")) return;
59
+ setDeleteId(id);
60
+ };
61
+
62
+ const confirmDelete = async () => {
63
+ if (!deleteId) return;
49
64
  try {
50
- const response = await fetch(`/api/${collection.slug}/${id}`, {
65
+ const response = await fetch(`/api/${collection.slug}/${deleteId}`, {
51
66
  method: "DELETE",
52
67
  });
53
68
  if (response.ok) {
54
- setDocs((prev) => prev.filter((d) => d.id !== id));
69
+ setDocs((prev) => prev.filter((d) => d.id !== deleteId));
55
70
  }
56
71
  } catch (error) {
57
72
  console.error("Failed to delete:", error);
58
73
  }
74
+ setDeleteId(null);
59
75
  };
60
76
 
61
77
  const columns =
@@ -63,25 +79,36 @@ export function ListView({
63
79
  Object.keys(collection.fields || {}).slice(0, 4);
64
80
 
65
81
  return (
66
- <div className="kyro-list">
67
- <div className="kyro-list-header">
68
- <h2 className="kyro-list-title">{label}</h2>
69
- <button
70
- className="kyro-btn kyro-btn-primary kyro-btn-md"
71
- onClick={onCreate}
72
- >
73
- <svg
74
- width="16"
75
- height="16"
76
- viewBox="0 0 24 24"
77
- fill="none"
78
- stroke="currentColor"
79
- strokeWidth="2"
82
+ <>
83
+ <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 mb-8 pt-4">
84
+ <div>
85
+ <h2 className="text-3xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
86
+ {label}
87
+ </h2>
88
+ <p className="text-[11px] font-bold uppercase tracking-widest opacity-40 mt-1">
89
+ Manage and explore your {label.toLowerCase()} entries
90
+ </p>
91
+ </div>
92
+
93
+ <div className="flex items-center gap-4 flex-1 max-w-2xl lg:justify-end">
94
+ <div className="relative flex-1 max-w-md group">
95
+ <Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-secondary)] opacity-40 group-focus-within:opacity-100 transition-opacity" />
96
+ <input
97
+ type="text"
98
+ placeholder={`Search ${label}...`}
99
+ value={searchQuery}
100
+ onChange={(e) => setSearchQuery(e.target.value)}
101
+ className="w-full pl-11 pr-4 py-2.5 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--kyro-primary)] focus:border-[var(--kyro-primary)] transition-all text-sm font-medium"
102
+ />
103
+ </div>
104
+ <button type="button"
105
+ className="flex items-center gap-2 px-6 py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-black text-xs shadow-lg active:scale-95 transition-all"
106
+ onClick={onCreate}
80
107
  >
81
- <path d="M12 5v14M5 12h14" />
82
- </svg>
83
- Create {label}
84
- </button>
108
+ <Plus className="w-4 h-4" />
109
+ Create New
110
+ </button>
111
+ </div>
85
112
  </div>
86
113
 
87
114
  {loading ? (
@@ -107,7 +134,7 @@ export function ListView({
107
134
  <p className="kyro-empty-text">
108
135
  Get started by creating your first one.
109
136
  </p>
110
- <button
137
+ <button type="button"
111
138
  className="kyro-btn kyro-btn-primary kyro-btn-md"
112
139
  style={{ marginTop: 16 }}
113
140
  onClick={onCreate}
@@ -117,43 +144,99 @@ export function ListView({
117
144
  </div>
118
145
  </div>
119
146
  ) : (
120
- <div className="kyro-card">
121
- <table className="kyro-table">
147
+ <div className="surface-tile overflow-hidden animate-in fade-in slide-in-from-bottom-4 duration-500">
148
+ <table className="w-full border-collapse">
122
149
  <thead>
123
- <tr>
124
- {columns.map((col) => (
125
- <th key={col}>{col}</th>
126
- ))}
127
- <th style={{ width: 100 }}>Actions</th>
150
+ <tr className="bg-[var(--kyro-bg-secondary)] text-[10px] font-black uppercase tracking-[0.2em] opacity-40 text-left">
151
+ <th className="px-6 py-4 w-12">
152
+ <input
153
+ type="checkbox"
154
+ className="accent-[var(--kyro-primary)]"
155
+ onChange={(e) => {
156
+ if (e.target.checked)
157
+ setSelectedIds(new Set(docs.map((d) => d.id)));
158
+ else setSelectedIds(new Set());
159
+ }}
160
+ />
161
+ </th>
162
+ <th className="px-6 py-4">Document</th>
163
+ {columns
164
+ .filter((c) => c !== "title" && c !== "name")
165
+ .map((col) => (
166
+ <th key={col} className="px-6 py-4">
167
+ {col}
168
+ </th>
169
+ ))}
170
+ <th key="status" className="px-6 py-4">
171
+ Status
172
+ </th>
173
+ <th className="px-6 py-4 text-right">Actions</th>
128
174
  </tr>
129
175
  </thead>
130
- <tbody>
131
- {docs.map((doc) => (
132
- <tr key={doc.id}>
133
- {columns.map((col) => (
134
- <td key={col}>{formatValue(doc[col])}</td>
135
- ))}
136
- <td>
137
- <div className="kyro-table-actions">
138
- <button
139
- className="kyro-table-action"
176
+ <tbody className="divide-y divide-[var(--kyro-border)]">
177
+ {filteredDocs.map((doc) => (
178
+ <tr
179
+ key={doc.id}
180
+ className={`group hover:bg-[var(--kyro-primary)] transition-all cursor-pointer ${selectedIds.has(doc.id) ? "bg-[var(--kyro-primary)]" : ""}`}
181
+ onClick={() => onEdit(doc.id)}
182
+ >
183
+ <td
184
+ className="px-6 py-5"
185
+ onClick={(e) => e.stopPropagation()}
186
+ >
187
+ <input
188
+ type="checkbox"
189
+ checked={selectedIds.has(doc.id)}
190
+ onChange={() => {
191
+ const next = new Set(selectedIds);
192
+ if (next.has(doc.id)) next.delete(doc.id);
193
+ else next.add(doc.id);
194
+ setSelectedIds(next);
195
+ }}
196
+ className="accent-[var(--kyro-primary)]"
197
+ />
198
+ </td>
199
+ <td className="px-6 py-5">
200
+ <div className="flex flex-col">
201
+ <span className="font-black text-sm group-hover:text-[var(--kyro-primary)] transition-colors">
202
+ {doc.title || doc.name || doc.id}
203
+ </span>
204
+ <span className="text-[10px] font-bold opacity-30 uppercase tracking-widest mt-1">
205
+ ID: {doc.id.slice(-8)}
206
+ </span>
207
+ </div>
208
+ </td>
209
+ {columns
210
+ .filter((c) => c !== "title" && c !== "name")
211
+ .map((col) => (
212
+ <td
213
+ key={col}
214
+ className="px-6 py-5 text-sm font-medium text-[var(--kyro-text-secondary)]"
215
+ >
216
+ {formatValue(doc[col])}
217
+ </td>
218
+ ))}
219
+ <td className="px-6 py-5">
220
+ <span
221
+ className={`inline-flex items-center px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest ${doc.status === "published" ? "bg-green-500/10 text-green-500" : "bg-amber-500/10 text-amber-500"}`}
222
+ >
223
+ {doc.status || "draft"}
224
+ </span>
225
+ </td>
226
+ <td
227
+ className="px-6 py-5 text-right"
228
+ onClick={(e) => e.stopPropagation()}
229
+ >
230
+ <div className="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
231
+ <button type="button"
232
+ className="p-2 text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-bg-secondary)] rounded-lg transition-all"
140
233
  onClick={() => onEdit(doc.id)}
141
234
  title="Edit"
142
235
  >
143
- <svg
144
- width="16"
145
- height="16"
146
- viewBox="0 0 24 24"
147
- fill="none"
148
- stroke="currentColor"
149
- strokeWidth="2"
150
- >
151
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
152
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
153
- </svg>
236
+ <Settings className="w-4 h-4" />
154
237
  </button>
155
- <button
156
- className="kyro-table-action danger"
238
+ <button type="button"
239
+ className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-all"
157
240
  onClick={() => handleDelete(doc.id)}
158
241
  title="Delete"
159
242
  >
@@ -176,7 +259,62 @@ export function ListView({
176
259
  </table>
177
260
  </div>
178
261
  )}
179
- </div>
262
+
263
+ {selectedIds.size > 0 && (
264
+ <div className="fixed bottom-12 left-1/2 -translate-x-1/2 z-[60] bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-3xl shadow-2xl p-4 flex items-center gap-8 animate-in slide-in-from-bottom-12 duration-500 ring-1 ring-white/10">
265
+ <div className="flex items-center gap-4 border-r border-[var(--kyro-border)] pr-8 ml-2">
266
+ <div className="w-10 h-10 rounded-2xl bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] flex items-center justify-center font-black">
267
+ {selectedIds.size}
268
+ </div>
269
+ <div>
270
+ <p className="text-xs font-black uppercase tracking-widest">
271
+ Docs Selected
272
+ </p>
273
+ <button type="button"
274
+ onClick={() => setSelectedIds(new Set())}
275
+ className="text-[10px] font-bold text-[var(--kyro-primary)] hover:underline"
276
+ >
277
+ Clear Selection
278
+ </button>
279
+ </div>
280
+ </div>
281
+ <div className="flex items-center gap-3 pr-2">
282
+ <button type="button" className="flex items-center gap-2 px-6 py-2.5 bg-green-500 text-white rounded-xl font-black text-[10px] uppercase tracking-widest shadow-lg hover:shadow-green-500/20 active:scale-95 transition-all">
283
+ Publish
284
+ </button>
285
+ <button type="button"
286
+ onClick={async () => {
287
+ if (
288
+ window.confirm(
289
+ `Are you sure you want to delete ${selectedIds.size} documents?`,
290
+ )
291
+ ) {
292
+ for (const id of Array.from(selectedIds)) {
293
+ await fetch(`/api/${collection.slug}/${id}`, {
294
+ method: "DELETE",
295
+ });
296
+ }
297
+ loadDocs();
298
+ setSelectedIds(new Set());
299
+ }
300
+ }}
301
+ className="flex items-center gap-2 px-6 py-2.5 bg-red-500 text-white rounded-xl font-black text-[10px] uppercase tracking-widest shadow-lg hover:shadow-red-500/20 active:scale-95 transition-all"
302
+ >
303
+ Delete
304
+ </button>
305
+ </div>
306
+ </div>
307
+ )}
308
+ <ConfirmModal
309
+ open={!!deleteId}
310
+ onClose={() => setDeleteId(null)}
311
+ onConfirm={confirmDelete}
312
+ title="Delete Document"
313
+ message="Are you sure you want to delete this document? This cannot be undone."
314
+ confirmLabel="Delete"
315
+ variant="danger"
316
+ />
317
+ </>
180
318
  );
181
319
  }
182
320