@kyro-cms/admin 0.11.3 → 0.11.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.
@@ -3,6 +3,7 @@ import AdminLayout from "../layouts/AdminLayout.astro";
3
3
  import { collections } from "../lib/config";
4
4
  const authCollections = ["users", "audit_logs"];
5
5
  import { adminPath, apiPath } from "../lib/paths";
6
+ import { DashboardMetrics } from "../components/DashboardMetrics";
6
7
 
7
8
  const authItems = authCollections.map((slug) => ({
8
9
  slug,
@@ -16,15 +17,8 @@ const authItems = authCollections.map((slug) => ({
16
17
  <!-- Header -->
17
18
  <div class="surface-tile p-6 flex items-center justify-between gap-8">
18
19
  <div class="relative flex-1 max-w-2xl">
19
- <div
20
- class="absolute inset-y-0 left-6 flex items-center pointer-events-none text-[var(--kyro-text-muted)]"
21
- >
22
- <svg
23
- class="w-6 h-6"
24
- fill="none"
25
- stroke="currentColor"
26
- viewBox="0 0 24 24"
27
- >
20
+ <div class="absolute inset-y-0 left-6 flex items-center pointer-events-none text-[var(--kyro-text-muted)]">
21
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
28
22
  <path
29
23
  stroke-linecap="round"
30
24
  stroke-linejoin="round"
@@ -55,29 +49,24 @@ const authItems = authCollections.map((slug) => ({
55
49
  </div>
56
50
  </div>
57
51
 
52
+ <!-- CMS Metrics -->
53
+ <DashboardMetrics client:load />
54
+
58
55
  <!-- Quick Links Section -->
59
- <div class="surface-tile overflow-hidden">
60
- <div
61
- class="flex items-center justify-between p-6 border-b border-[var(--kyro-border)]"
62
- >
56
+ <div class="surface-tile overflow-hidden mt-8">
57
+ <div class="flex items-center justify-between p-6 border-b border-[var(--kyro-border)]">
63
58
  <div class="flex-1">
64
- <h2
65
- class="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)] flex items-center gap-2"
66
- >
59
+ <h2 class="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)] flex items-center gap-2">
67
60
  Quick Links
68
61
  </h2>
69
- <p class="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">
70
- Create new collection documents
71
- </p>
62
+ <p class="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">Create new collection documents</p>
72
63
  </div>
73
64
  </div>
74
65
 
75
66
  <div class="grid grid-cols-1 md:grid-cols-3 gap-4 p-4">
76
67
  {
77
68
  Object.entries(collections)
78
- .filter(
79
- ([slug]) => !["users", "audit_logs", "media"].includes(slug),
80
- )
69
+ .filter(([slug]) => !["users", "audit_logs", "media"].includes(slug))
81
70
  .slice(0, 6)
82
71
  .map(([slug, config]: [string, any]) => (
83
72
  <a
@@ -86,23 +75,11 @@ const authItems = authCollections.map((slug) => ({
86
75
  >
87
76
  <div class="flex items-center gap-3">
88
77
  <div class="w-8 h-8 rounded-lg flex items-center justify-center bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] group-hover:bg-[var(--kyro-surface-accent)] group-hover:text-[var(--kyro-primary)] transition-all">
89
- <svg
90
- class="w-4 h-4"
91
- fill="none"
92
- stroke="currentColor"
93
- viewBox="0 0 24 24"
94
- >
95
- <path
96
- stroke-linecap="round"
97
- stroke-linejoin="round"
98
- stroke-width="2"
99
- d="M12 5v14M5 12h14"
100
- />
78
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
79
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14M5 12h14" />
101
80
  </svg>
102
81
  </div>
103
- <span class="text-sm font-medium text-[var(--kyro-text-primary)]">
104
- New {config.label || slug}
105
- </span>
82
+ <span class="text-sm font-medium text-[var(--kyro-text-primary)]">New {config.label || slug}</span>
106
83
  </div>
107
84
  </a>
108
85
  ))
@@ -113,11 +90,7 @@ const authItems = authCollections.map((slug) => ({
113
90
  <!-- Security Quick Actions -->
114
91
  <div class="surface-tile overflow-hidden">
115
92
  <div class="p-6 border-b border-[var(--kyro-border)]">
116
- <h2
117
- class="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]"
118
- >
119
- Security & Monitoring
120
- </h2>
93
+ <h2 class="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">Security & Monitoring</h2>
121
94
  <p class="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">
122
95
  Rate limiting, audit logs, and account lockout settings
123
96
  </p>
@@ -127,15 +100,8 @@ const authItems = authCollections.map((slug) => ({
127
100
  href={`${adminPath}/audit`}
128
101
  class="p-5 bg-[var(--kyro-surface-accent)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-all border border-transparent hover:border-[var(--kyro-border)] group"
129
102
  >
130
- <div
131
- class="w-10 h-10 rounded-lg bg-orange-500/10 text-orange-500 flex items-center justify-center mb-3"
132
- >
133
- <svg
134
- class="w-5 h-5"
135
- fill="none"
136
- stroke="currentColor"
137
- viewBox="0 0 24 24"
138
- >
103
+ <div class="w-10 h-10 rounded-lg bg-orange-500/10 text-orange-500 flex items-center justify-center mb-3">
104
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
139
105
  <path
140
106
  stroke-linecap="round"
141
107
  stroke-linejoin="round"
@@ -144,27 +110,16 @@ const authItems = authCollections.map((slug) => ({
144
110
  ></path>
145
111
  </svg>
146
112
  </div>
147
- <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">
148
- Audit Logs
149
- </h4>
150
- <p class="text-xs text-[var(--kyro-text-secondary)]">
151
- View last 30 days
152
- </p>
113
+ <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">Audit Logs</h4>
114
+ <p class="text-xs text-[var(--kyro-text-secondary)]">View last 30 days</p>
153
115
  </a>
154
116
 
155
117
  <a
156
118
  href={`${adminPath}/users?locked=true`}
157
119
  class="p-5 bg-[var(--kyro-surface-accent)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-all border border-transparent hover:border-[var(--kyro-border)] group"
158
120
  >
159
- <div
160
- class="w-10 h-10 rounded-lg bg-red-500/10 text-red-500 flex items-center justify-center mb-3"
161
- >
162
- <svg
163
- class="w-5 h-5"
164
- fill="none"
165
- stroke="currentColor"
166
- viewBox="0 0 24 24"
167
- >
121
+ <div class="w-10 h-10 rounded-lg bg-red-500/10 text-red-500 flex items-center justify-center mb-3">
122
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
168
123
  <path
169
124
  stroke-linecap="round"
170
125
  stroke-linejoin="round"
@@ -173,27 +128,16 @@ const authItems = authCollections.map((slug) => ({
173
128
  ></path>
174
129
  </svg>
175
130
  </div>
176
- <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">
177
- Locked Accounts
178
- </h4>
179
- <p class="text-xs text-[var(--kyro-text-secondary)]">
180
- Manage lockouts
181
- </p>
131
+ <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">Locked Accounts</h4>
132
+ <p class="text-xs text-[var(--kyro-text-secondary)]">Manage lockouts</p>
182
133
  </a>
183
134
 
184
135
  <a
185
136
  href={`${adminPath}/roles`}
186
137
  class="p-5 bg-[var(--kyro-surface-accent)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-all border border-transparent hover:border-[var(--kyro-border)] group"
187
138
  >
188
- <div
189
- class="w-10 h-10 rounded-lg bg-indigo-500/10 text-indigo-500 flex items-center justify-center mb-3"
190
- >
191
- <svg
192
- class="w-5 h-5"
193
- fill="none"
194
- stroke="currentColor"
195
- viewBox="0 0 24 24"
196
- >
139
+ <div class="w-10 h-10 rounded-lg bg-indigo-500/10 text-indigo-500 flex items-center justify-center mb-3">
140
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
197
141
  <path
198
142
  stroke-linecap="round"
199
143
  stroke-linejoin="round"
@@ -202,9 +146,7 @@ const authItems = authCollections.map((slug) => ({
202
146
  ></path>
203
147
  </svg>
204
148
  </div>
205
- <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">
206
- Permissions
207
- </h4>
149
+ <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">Permissions</h4>
208
150
  <p class="text-xs text-[var(--kyro-text-secondary)]">RBAC settings</p>
209
151
  </a>
210
152
 
@@ -213,15 +155,8 @@ const authItems = authCollections.map((slug) => ({
213
155
  target="_blank"
214
156
  class="p-5 bg-[var(--kyro-surface-accent)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-all border border-transparent hover:border-[var(--kyro-border)] group"
215
157
  >
216
- <div
217
- class="w-10 h-10 rounded-lg bg-green-500/10 text-green-500 flex items-center justify-center mb-3"
218
- >
219
- <svg
220
- class="w-5 h-5"
221
- fill="none"
222
- stroke="currentColor"
223
- viewBox="0 0 24 24"
224
- >
158
+ <div class="w-10 h-10 rounded-lg bg-green-500/10 text-green-500 flex items-center justify-center mb-3">
159
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
225
160
  <path
226
161
  stroke-linecap="round"
227
162
  stroke-linejoin="round"
@@ -229,9 +164,7 @@ const authItems = authCollections.map((slug) => ({
229
164
  d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
230
165
  </svg>
231
166
  </div>
232
- <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">
233
- API Health
234
- </h4>
167
+ <h4 class="font-medium text-[var(--kyro-text-primary)] mb-1">API Health</h4>
235
168
  <p class="text-xs text-[var(--kyro-text-secondary)]">System status</p>
236
169
  </a>
237
170
  </div>
@@ -248,8 +181,7 @@ const authItems = authCollections.map((slug) => ({
248
181
  function renderResults(results) {
249
182
  var html = "";
250
183
  if (!results || results.length === 0) {
251
- html =
252
- '<div class="p-4 text-center text-[var(--kyro-text-secondary)] opacity-60">No documents found</div>';
184
+ html = '<div class="p-4 text-center text-[var(--kyro-text-secondary)] opacity-60">No documents found</div>';
253
185
  } else {
254
186
  for (var i = 0; i < Math.min(results.length, 6); i++) {
255
187
  var r = results[i];
@@ -265,12 +197,8 @@ const authItems = authCollections.map((slug) => ({
265
197
  '<div class="flex flex-col"><span class="font-bold text-sm text-[var(--kyro-text-primary)]">' +
266
198
  (r.title || "Untitled") +
267
199
  "</span>";
268
- html +=
269
- '<span class="text-[10px] font-bold tracking-widest opacity-40">' +
270
- r.label +
271
- "</span></div>";
272
- html +=
273
- '<span class="text-xs text-[var(--kyro-text-muted)]">View</span></a>';
200
+ html += '<span class="text-[10px] font-bold tracking-widest opacity-40">' + r.label + "</span></div>";
201
+ html += '<span class="text-xs text-[var(--kyro-text-muted)]">View</span></a>';
274
202
  }
275
203
  }
276
204
  searchResults.innerHTML = html;
@@ -290,11 +218,7 @@ const authItems = authCollections.map((slug) => ({
290
218
  if (debounceTimer) clearTimeout(debounceTimer);
291
219
  debounceTimer = setTimeout(function () {
292
220
  var xhr = new XMLHttpRequest();
293
- xhr.open(
294
- "GET",
295
- apiPath + "/search?q=" + encodeURIComponent(query) + "&limit=10",
296
- true,
297
- );
221
+ xhr.open("GET", apiPath + "/search?q=" + encodeURIComponent(query) + "&limit=10", true);
298
222
  xhr.onload = function () {
299
223
  if (xhr.status === 200) {
300
224
  try {
@@ -314,10 +238,7 @@ const authItems = authCollections.map((slug) => ({
314
238
  });
315
239
 
316
240
  document.addEventListener("click", function (e) {
317
- if (
318
- !searchInput.contains(e.target) &&
319
- !searchResults.contains(e.target)
320
- ) {
241
+ if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
321
242
  hideResults();
322
243
  }
323
244
  });
@@ -7,7 +7,7 @@ import { adminPath, apiPath } from '../lib/paths';
7
7
  ---
8
8
 
9
9
  <AdminLayout title="Media Library">
10
- <div class="flex-1 overflow-hidden">
10
+ <div class="flex-1 flex flex-col h-full min-h-0 overflow-hidden -m-4 md:m-0">
11
11
  <MediaGallery client:load />
12
12
  </div>
13
13
  </AdminLayout>
@@ -102,7 +102,9 @@ const description =
102
102
 
103
103
  <script>
104
104
  console.log("[Astro] Settings page script running");
105
- document.getElementById("settings-save-btn")?.addEventListener("click", () => {
105
+ const saveBtn = document.getElementById("settings-save-btn");
106
+
107
+ saveBtn?.addEventListener("click", () => {
106
108
  console.log("[Astro] Header save clicked");
107
109
  const btn = document.getElementById("btn-save");
108
110
  if (btn) {
@@ -112,5 +114,21 @@ const description =
112
114
  console.error("[Astro] Hidden save button not found!");
113
115
  }
114
116
  });
117
+
118
+ window.addEventListener("kyro:global-save-start", () => {
119
+ if (saveBtn) {
120
+ saveBtn.disabled = true;
121
+ saveBtn.innerHTML = "Saving...";
122
+ saveBtn.classList.add("opacity-50", "cursor-wait");
123
+ }
124
+ });
125
+
126
+ window.addEventListener("kyro:global-save-end", () => {
127
+ if (saveBtn) {
128
+ saveBtn.disabled = false;
129
+ saveBtn.innerHTML = "Save Changes";
130
+ saveBtn.classList.remove("opacity-50", "cursor-wait");
131
+ }
132
+ });
115
133
  </script>
116
134
  </AdminLayout>
@@ -5,9 +5,10 @@
5
5
 
6
6
  :root {
7
7
  /* Monochrome Palette */
8
- --kyro-black: #0b1222;
9
- --kyro-black-light: #1a2332;
10
- --kyro-black-hover: #2a3342;
8
+ --kyro-black: #0a0a0a;
9
+ --kyro-black-light: #171717;
10
+ --kyro-black-hover: #262626;
11
+ --kyro-black-inverted: #ffffff;
11
12
 
12
13
  /* Neutral Palette */
13
14
  --kyro-gray-50: #f9fafb;
@@ -26,13 +27,13 @@
26
27
  --kyro-surface: #ffffff;
27
28
  --kyro-surface-accent: #f9fafb;
28
29
  --kyro-sidebar-bg: #ffffff;
29
- --kyro-sidebar-active: #0b1222;
30
+ --kyro-sidebar-active: var(--kyro-black);
30
31
  --kyro-sidebar-text: #64748b;
31
- --kyro-sidebar-text-active: #ffffff;
32
+ --kyro-sidebar-text-active: var(--kyro-black-inverted);
32
33
  --kyro-sidebar-border: rgba(0, 0, 0, 0.05);
33
34
 
34
35
  /* Semantic Typography */
35
- --kyro-text-primary: #0b1222;
36
+ --kyro-text-primary: #0a0a0a;
36
37
  --kyro-text-secondary: #64748b;
37
38
  --kyro-text-muted: #94a3b8;
38
39
 
@@ -48,8 +49,8 @@
48
49
  --kyro-shadow-tile: 0 10px 40px -10px rgba(0, 0, 0, 0.04);
49
50
 
50
51
  /* Primary Brand Color */
51
- --kyro-primary: #0b1222;
52
- --kyro-primary-hover: #1a2332;
52
+ --kyro-primary: #0a0a0a;
53
+ --kyro-primary-hover: #171717;
53
54
 
54
55
  /* Success Color */
55
56
  --kyro-success: #22c55e;
@@ -76,29 +77,30 @@
76
77
  }
77
78
 
78
79
  :root.dark {
79
- --kyro-bg: #050912;
80
- --kyro-surface: #0b1222;
81
- --kyro-surface-accent: #111a2e;
82
- --kyro-sidebar-bg: #0b1222;
83
- --kyro-sidebar-active: #ffffff;
84
- --kyro-sidebar-text: #94a3b8;
85
- --kyro-sidebar-text-active: #0b1222;
86
- --kyro-sidebar-border: rgba(255, 255, 255, 0.05);
87
-
88
- --kyro-text-primary: #ffffff;
89
- --kyro-text-secondary: #94a3b8;
90
- --kyro-text-muted: #64748b;
91
-
92
- --kyro-border: #1a2332;
93
- --kyro-input-bg: #111a2e;
94
- --kyro-input-border: #1e293b;
80
+ --kyro-bg: #0a0a0a;
81
+ --kyro-surface: #121212;
82
+ --kyro-surface-accent: #1c1c1c;
83
+ --kyro-sidebar-bg: #0a0a0a;
84
+ --kyro-sidebar-active: var(--kyro-black);
85
+ --kyro-sidebar-text: #a1a1aa;
86
+ --kyro-sidebar-text-active: var(--kyro-black-inverted);
87
+ --kyro-sidebar-border: rgba(255, 255, 255, 0.08);
88
+
89
+ --kyro-text-primary: #ededed;
90
+ --kyro-text-secondary: #a1a1aa;
91
+ --kyro-text-muted: #71717a;
92
+
93
+ --kyro-border: rgba(255, 255, 255, 0.08);
94
+ --kyro-input-bg: #1c1c1c;
95
+ --kyro-input-border: rgba(255, 255, 255, 0.1);
95
96
 
96
97
  --kyro-black: #ffffff;
97
98
  --kyro-black-light: #f3f4f6;
98
99
  --kyro-black-hover: #e5e7eb;
99
- --kyro-gray-50: #1a2332;
100
- --kyro-gray-100: #1e293b;
101
- --kyro-gray-200: #334155;
100
+ --kyro-black-inverted: #0a0a0a;
101
+ --kyro-gray-50: #171717;
102
+ --kyro-gray-100: #262626;
103
+ --kyro-gray-200: #404040;
102
104
 
103
105
  /* Primary Brand Color (dark) - Use sidebar accent for consistency */
104
106
  --kyro-primary: var(--kyro-sidebar-active);
@@ -121,7 +123,7 @@
121
123
  --kyro-border-active: #ffffff;
122
124
 
123
125
  /* Secondary Backgrounds (dark) */
124
- --kyro-bg-secondary: #111a2e;
126
+ --kyro-bg-secondary: #171717;
125
127
 
126
128
  color-scheme: dark;
127
129
  }
@@ -200,8 +202,8 @@
200
202
  }
201
203
 
202
204
  .dark .kyro-glass {
203
- background: rgba(11, 18, 34, 0.7);
204
- border: 1px solid rgba(255, 255, 255, 0.05);
205
+ background: rgba(18, 18, 18, 0.7);
206
+ border: 1px solid rgba(255, 255, 255, 0.08);
205
207
  }
206
208
 
207
209
  .font-bold {
@@ -293,6 +295,10 @@
293
295
  background: var(--kyro-gray-50);
294
296
  }
295
297
 
298
+ .kyro-sidebar-item:active {
299
+ transform: scale(0.97);
300
+ }
301
+
296
302
  .kyro-sidebar-item.active {
297
303
  color: var(--kyro-sidebar-text-active);
298
304
  background: var(--kyro-sidebar-active);
@@ -374,6 +380,11 @@
374
380
  cursor: not-allowed;
375
381
  }
376
382
 
383
+ /* Active states for all buttons */
384
+ .kyro-btn:active:not(:disabled) {
385
+ transform: scale(0.97);
386
+ }
387
+
377
388
  .kyro-btn-sm {
378
389
  padding: 6px 10px;
379
390
  font-size: 12px;
@@ -391,7 +402,7 @@
391
402
 
392
403
  .kyro-btn-primary {
393
404
  background: var(--kyro-black);
394
- color: var(--kyro-sidebar-text-active);
405
+ color: var(--kyro-black-inverted);
395
406
  border-color: var(--kyro-black);
396
407
  }
397
408
 
@@ -407,19 +418,20 @@
407
418
  }
408
419
 
409
420
  .kyro-btn-secondary:hover:not(:disabled) {
410
- background: var(--kyro-gray-50);
411
- border-color: var(--kyro-gray-300);
421
+ background: var(--kyro-bg-secondary);
422
+ border-color: var(--kyro-text-primary);
412
423
  }
413
424
 
414
425
  .kyro-btn-danger {
415
426
  background: var(--kyro-surface);
416
- color: var(--kyro-text-primary);
417
- border-color: var(--kyro-text-primary);
427
+ color: var(--kyro-danger);
428
+ border-color: var(--kyro-border);
418
429
  }
419
430
 
420
431
  .kyro-btn-danger:hover:not(:disabled) {
421
- background: var(--kyro-gray-900);
422
- color: var(--kyro-sidebar-text-active);
432
+ background: var(--kyro-danger);
433
+ color: #ffffff;
434
+ border-color: var(--kyro-danger);
423
435
  }
424
436
 
425
437
  .kyro-btn-ghost {
@@ -428,7 +440,7 @@
428
440
  }
429
441
 
430
442
  .kyro-btn-ghost:hover:not(:disabled) {
431
- background: var(--kyro-gray-100);
443
+ background: var(--kyro-bg-secondary);
432
444
  color: var(--kyro-text-primary);
433
445
  }
434
446