@kyro-cms/admin 0.1.8 → 0.2.0

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.
@@ -28,6 +28,14 @@ import ReactCrop, {
28
28
  convertToPixelCrop,
29
29
  } from "react-image-crop";
30
30
  import "react-image-crop/dist/ReactCrop.css";
31
+ import {
32
+ apiGet,
33
+ apiPost,
34
+ apiDelete,
35
+ apiPatch,
36
+ withCacheBust,
37
+ apiUpload,
38
+ } from "@kyro-cms/utils/lib/api";
31
39
 
32
40
  interface MediaItem {
33
41
  id: string;
@@ -176,10 +184,7 @@ export function MediaGallery() {
176
184
 
177
185
  const loadFolders = useCallback(async () => {
178
186
  try {
179
- const resp = await fetch("/api/media/folders", {
180
- credentials: "include",
181
- });
182
- const data = await resp.json();
187
+ const data = await apiGet("/api/media/folders");
183
188
  setAvailableFolders((data.folders || []).sort());
184
189
  } catch (e) {
185
190
  console.error("loadFolders error:", e);
@@ -199,11 +204,11 @@ export function MediaGallery() {
199
204
  ? `&folder=${encodeURIComponent(currentFolder)}`
200
205
  : "";
201
206
 
202
- const response = await fetch(
203
- `/api/media?limit=30&page=${currentPage}&sortBy=${sortBy}&sortDir=${sortDir}${typeParam}${searchParam}${folderParam}&t=${Date.now()}`,
207
+ const result = await apiGet(
208
+ withCacheBust(
209
+ `/api/media?limit=30&page=${currentPage}&sortBy=${sortBy}&sortDir=${sortDir}${typeParam}${searchParam}${folderParam}`,
210
+ ),
204
211
  );
205
- if (!response.ok) throw new Error("Failed to load media");
206
- const result = await response.json();
207
212
 
208
213
  if (reset) {
209
214
  setItems(result.docs || []);
@@ -230,8 +235,7 @@ export function MediaGallery() {
230
235
 
231
236
  useEffect(() => {
232
237
  // Check if storage settings are configured
233
- fetch("/api/storage-status")
234
- .then((res) => res.json())
238
+ apiGet("/api/storage-status")
235
239
  .then((status) => {
236
240
  // If not configured, show modal to configure
237
241
  if (!status.configured) {
@@ -293,15 +297,7 @@ export function MediaGallery() {
293
297
  formData.append("file", file);
294
298
  if (uploadFolder) formData.append("folder", uploadFolder);
295
299
 
296
- const response = await fetch("/api/upload", {
297
- method: "POST",
298
- body: formData,
299
- signal: ac.signal,
300
- credentials: "include",
301
- });
302
- if (!response.ok) continue;
303
-
304
- const result = await response.json();
300
+ const result = await apiUpload("/api/upload", formData);
305
301
  const newItem: MediaItem = {
306
302
  ...result,
307
303
  type: getFileType(result.mimeType || file.type) as MediaItem["type"],
@@ -332,7 +328,7 @@ export function MediaGallery() {
332
328
  if (!ids?.length) return;
333
329
  try {
334
330
  for (const id of ids) {
335
- await fetch(`/api/media/${id}`, { method: "DELETE" });
331
+ await apiDelete(`/api/media/${id}`);
336
332
  }
337
333
  loadMedia(true);
338
334
  setSelectedItems(new Set());
@@ -397,11 +393,7 @@ export function MediaGallery() {
397
393
  setLoading(true);
398
394
  const ids = Array.from(selectedItems);
399
395
  for (const id of ids) {
400
- await fetch(`/api/media/${id}`, {
401
- method: "PATCH",
402
- headers: { "Content-Type": "application/json" },
403
- body: JSON.stringify({ folder: targetFolder }),
404
- });
396
+ await apiPatch(`/api/media/${id}`, { folder: targetFolder });
405
397
  }
406
398
  setSelectedItems(new Set());
407
399
  loadMedia(true);
@@ -416,18 +408,14 @@ export function MediaGallery() {
416
408
  const createFolder = async (name: string) => {
417
409
  if (!name) return;
418
410
  try {
419
- const resp = await fetch("/api/media/folders", {
420
- method: "POST",
421
- headers: { "Content-Type": "application/json" },
422
- body: JSON.stringify({ name, parentPath: currentFolder || "" }),
423
- credentials: "include",
411
+ await apiPost("/api/media/folders", {
412
+ name,
413
+ parentPath: currentFolder || "",
424
414
  });
425
- if (resp.ok) {
426
- const newPath = currentFolder ? `${currentFolder}/${name}` : name;
427
- setCurrentFolder(newPath);
428
- setUploadFolder(newPath);
429
- loadFolders();
430
- }
415
+ const newPath = currentFolder ? `${currentFolder}/${name}` : name;
416
+ setCurrentFolder(newPath);
417
+ setUploadFolder(newPath);
418
+ loadFolders();
431
419
  } catch (e) {
432
420
  console.error("Failed to create folder:", e);
433
421
  }
@@ -442,27 +430,19 @@ export function MediaGallery() {
442
430
  const deleteFolder = async () => {
443
431
  if (!folderToDelete) return;
444
432
  try {
445
- const resp = await fetch(
433
+ const result = await apiDelete(
446
434
  `/api/media/folders?path=${encodeURIComponent(folderToDelete)}`,
447
- {
448
- method: "DELETE",
449
- credentials: "include",
450
- },
451
435
  );
452
- console.log("[deleteFolder] Response status:", resp.status);
453
- const result = await resp.json();
454
436
  console.log("[deleteFolder] Response:", result);
455
- if (resp.ok) {
456
- // Clear items first, then reload
457
- setItems([]);
458
- if (currentFolder === folderToDelete) {
459
- setCurrentFolder("");
460
- }
461
- loadFolders();
462
- // Force fresh load by resetting page
463
- setPage(1);
464
- loadMedia(true);
437
+ // Clear items first, then reload
438
+ setItems([]);
439
+ if (currentFolder === folderToDelete) {
440
+ setCurrentFolder("");
465
441
  }
442
+ loadFolders();
443
+ // Force fresh load by resetting page
444
+ setPage(1);
445
+ loadMedia(true);
466
446
  } catch (e) {
467
447
  console.error("Failed to delete folder:", e);
468
448
  }
@@ -473,15 +453,12 @@ export function MediaGallery() {
473
453
  const savePanelMetadata = async () => {
474
454
  if (!panelItem) return;
475
455
  try {
476
- const resp = await fetch(`/api/media/${panelItem.id}`, {
477
- method: "PATCH",
478
- headers: { "Content-Type": "application/json" },
479
- body: JSON.stringify({ title: panelName, alt: panelAlt }),
456
+ await apiPatch(`/api/media/${panelItem.id}`, {
457
+ title: panelName,
458
+ alt: panelAlt,
480
459
  });
481
- if (resp.ok) {
482
- setPanelItem(null);
483
- loadMedia(true);
484
- }
460
+ setPanelItem(null);
461
+ loadMedia(true);
485
462
  } catch (e) {
486
463
  console.error("Failed to save metadata:", e);
487
464
  }
@@ -531,11 +508,7 @@ export function MediaGallery() {
531
508
 
532
509
  setUploading(true);
533
510
  try {
534
- const res = await fetch("/api/upload", {
535
- method: "POST",
536
- body: formData,
537
- credentials: "include",
538
- });
511
+ const res = await apiUpload("/api/upload", formData);
539
512
  if (res.ok) {
540
513
  setShowCrop(false);
541
514
  setPanelItem(null);
@@ -561,7 +534,8 @@ export function MediaGallery() {
561
534
  type: FilterType;
562
535
  label: string;
563
536
  }) => (
564
- <button type="button"
537
+ <button
538
+ type="button"
565
539
  onClick={() => setFilter(type)}
566
540
  className={`px-4 py-2 text-sm font-bold rounded-lg transition-colors ${
567
541
  filter === type
@@ -589,14 +563,16 @@ export function MediaGallery() {
589
563
  </div>
590
564
 
591
565
  <div className="flex items-center gap-4 px-4">
592
- <button type="button"
566
+ <button
567
+ type="button"
593
568
  onClick={handleBulkDelete}
594
569
  className="p-3 bg-[var(--kyro-danger-bg)] text-[var(--kyro-danger)] hover:bg-[var(--kyro-danger)] hover:text-white rounded-full transition-all"
595
570
  title="Delete Selected"
596
571
  >
597
572
  <Trash2 className="w-5 h-5" />
598
573
  </button>
599
- <button type="button"
574
+ <button
575
+ type="button"
600
576
  className="p-3 bg-white/10 text-white hover:bg-white/20 rounded-full transition-all"
601
577
  title="Download Collection"
602
578
  >
@@ -604,7 +580,8 @@ export function MediaGallery() {
604
580
  </button>
605
581
  </div>
606
582
 
607
- <button type="button"
583
+ <button
584
+ type="button"
608
585
  onClick={() => setSelectedItems(new Set())}
609
586
  className="p-2 hover:bg-white/10 rounded-full transition-all"
610
587
  >
@@ -625,7 +602,8 @@ export function MediaGallery() {
625
602
  </p>
626
603
  </div>
627
604
  <div className="flex items-center gap-3">
628
- <button type="button"
605
+ <button
606
+ type="button"
629
607
  onClick={() => setViewMode("grid")}
630
608
  className={`p-2 rounded-lg ${viewMode === "grid" ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"}`}
631
609
  >
@@ -643,7 +621,8 @@ export function MediaGallery() {
643
621
  />
644
622
  </svg>
645
623
  </button>
646
- <button type="button"
624
+ <button
625
+ type="button"
647
626
  onClick={() => setViewMode("list")}
648
627
  className={`p-2 rounded-lg ${viewMode === "list" ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"}`}
649
628
  >
@@ -665,7 +644,8 @@ export function MediaGallery() {
665
644
  </div>
666
645
 
667
646
  <div className="flex flex-col sm:flex-row gap-4 mb-4 items-start sm:items-center w-full">
668
- <button type="button"
647
+ <button
648
+ type="button"
669
649
  onClick={() => fileInputRef.current?.click()}
670
650
  disabled={uploading}
671
651
  className="flex items-center gap-2 px-3 py-1.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-bold text-xs hover:opacity-90 transition-colors"
@@ -691,7 +671,8 @@ export function MediaGallery() {
691
671
  </button>
692
672
  {availableFolders.length > 0 && (
693
673
  <div className="flex bg-[var(--kyro-surface-accent)] rounded-full p-1 gap-1 overflow-x-auto items-center">
694
- <button type="button"
674
+ <button
675
+ type="button"
695
676
  onClick={() => setCurrentFolder("")}
696
677
  className={`flex-shrink-0 px-3 py-1 rounded-full text-xs font-bold transition-colors ${!currentFolder ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-sm" : "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)]"}`}
697
678
  >
@@ -699,13 +680,15 @@ export function MediaGallery() {
699
680
  </button>
700
681
  {availableFolders.map((folder) => (
701
682
  <div key={folder} className="flex items-center group">
702
- <button type="button"
683
+ <button
684
+ type="button"
703
685
  onClick={() => setCurrentFolder(folder)}
704
686
  className={`flex-shrink-0 px-3 py-1 rounded-full text-xs font-bold transition-colors ${currentFolder === folder ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-sm" : "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)]"}`}
705
687
  >
706
688
  {folder}
707
689
  </button>
708
- <button type="button"
690
+ <button
691
+ type="button"
709
692
  onClick={() => confirmDeleteFolder(folder)}
710
693
  className="flex-shrink-0 p-1 rounded-full text-[var(--kyro-text-muted)] hover:text-[var(--kyro-danger)] opacity-0 group-hover:opacity-100 transition-opacity ml-1"
711
694
  title="Delete folder"
@@ -728,7 +711,8 @@ export function MediaGallery() {
728
711
  ))}
729
712
  </div>
730
713
  )}
731
- <button type="button"
714
+ <button
715
+ type="button"
732
716
  onClick={() => setShowNewFolderModal(true)}
733
717
  className="flex items-center gap-2 px-3 py-1.5 border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] rounded-full font-bold text-xs hover:bg-[var(--kyro-surface-accent)] transition-colors"
734
718
  >
@@ -865,7 +849,8 @@ export function MediaGallery() {
865
849
  <span className="text-[10px] font-black uppercase tracking-widest opacity-40 mr-2">
866
850
  {selectedItems.size} Selected
867
851
  </span>
868
- <button type="button"
852
+ <button
853
+ type="button"
869
854
  onClick={() => setSelectedItems(new Set())}
870
855
  className="text-[10px] font-black uppercase tracking-widest text-[var(--kyro-danger)] hover:underline"
871
856
  >
@@ -950,7 +935,8 @@ export function MediaGallery() {
950
935
  </div>
951
936
  )}
952
937
  </div>
953
- <button type="button"
938
+ <button
939
+ type="button"
954
940
  onClick={(e) => {
955
941
  e.stopPropagation();
956
942
  toggleSelect(item.id);
@@ -973,7 +959,8 @@ export function MediaGallery() {
973
959
  </svg>
974
960
  )}
975
961
  </button>
976
- <button type="button"
962
+ <button
963
+ type="button"
977
964
  onClick={(e) => {
978
965
  e.stopPropagation();
979
966
  openMetadataPanel(item);
@@ -995,7 +982,8 @@ export function MediaGallery() {
995
982
  />
996
983
  </svg>
997
984
  </button>
998
- <button type="button"
985
+ <button
986
+ type="button"
999
987
  onClick={(e) => {
1000
988
  e.stopPropagation();
1001
989
  handleDelete(item.id);
@@ -1031,7 +1019,8 @@ export function MediaGallery() {
1031
1019
  ) : (
1032
1020
  <div className="space-y-2">
1033
1021
  <div className="flex items-center gap-4 px-4 py-2 text-xs font-bold text-[var(--kyro-text-muted)] uppercase">
1034
- <button type="button"
1022
+ <button
1023
+ type="button"
1035
1024
  onClick={selectAll}
1036
1025
  className="w-6 h-6 rounded border border-[var(--kyro-border)] flex items-center justify-center hover:bg-[var(--kyro-surface-accent)]"
1037
1026
  >
@@ -1067,7 +1056,8 @@ export function MediaGallery() {
1067
1056
  }}
1068
1057
  className={`flex items-center gap-4 px-4 py-3 rounded-lg transition-colors cursor-pointer ${selectedItems.has(item.id) ? "bg-[var(--kyro-surface-accent)]" : "hover:bg-[var(--kyro-surface-accent)]"}`}
1069
1058
  >
1070
- <button type="button"
1059
+ <button
1060
+ type="button"
1071
1061
  onClick={() => toggleSelect(item.id)}
1072
1062
  className={`w-6 h-6 rounded border flex items-center justify-center transition-colors ${selectedItems.has(item.id) ? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)]" : "border-[var(--kyro-border)] hover:border-[var(--kyro-border-active)]"}`}
1073
1063
  >
@@ -1132,7 +1122,8 @@ export function MediaGallery() {
1132
1122
  <span className="w-32 text-sm text-[var(--kyro-text-muted)]">
1133
1123
  {formatDate(item.createdAt)}
1134
1124
  </span>
1135
- <button type="button"
1125
+ <button
1126
+ type="button"
1136
1127
  onClick={(e) => {
1137
1128
  e.stopPropagation();
1138
1129
  openMetadataPanel(item);
@@ -1154,7 +1145,8 @@ export function MediaGallery() {
1154
1145
  />
1155
1146
  </svg>
1156
1147
  </button>
1157
- <button type="button"
1148
+ <button
1149
+ type="button"
1158
1150
  onClick={() => handleDelete(item.id)}
1159
1151
  className="w-10 h-10 rounded-lg hover:bg-[var(--kyro-danger-bg)] text-[var(--kyro-text-muted)] hover:text-[var(--kyro-danger)] flex items-center justify-center transition-colors"
1160
1152
  >
@@ -1194,7 +1186,8 @@ export function MediaGallery() {
1194
1186
  className="fixed inset-0 z-[9999] bg-black/95 flex items-center justify-center p-8 animate-in fade-in duration-200"
1195
1187
  onClick={() => setLightboxItem(null)}
1196
1188
  >
1197
- <button type="button"
1189
+ <button
1190
+ type="button"
1198
1191
  onClick={() => setLightboxItem(null)}
1199
1192
  className="absolute top-4 right-4 w-12 h-12 rounded-full bg-white/10 text-white flex items-center justify-center hover:bg-white/20 transition-colors"
1200
1193
  >
@@ -1235,7 +1228,8 @@ export function MediaGallery() {
1235
1228
  >
1236
1229
  Open in new tab
1237
1230
  </a>
1238
- <button type="button"
1231
+ <button
1232
+ type="button"
1239
1233
  onClick={(e) => {
1240
1234
  e.stopPropagation();
1241
1235
  navigator.clipboard.writeText(
@@ -1264,7 +1258,8 @@ export function MediaGallery() {
1264
1258
  <h2 className="text-lg font-bold text-[var(--kyro-text-primary)]">
1265
1259
  Media Details
1266
1260
  </h2>
1267
- <button type="button"
1261
+ <button
1262
+ type="button"
1268
1263
  onClick={() => setPanelItem(null)}
1269
1264
  className="p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)]"
1270
1265
  >
@@ -1355,7 +1350,8 @@ export function MediaGallery() {
1355
1350
  value={getAbsoluteUrl(panelItem.url)}
1356
1351
  className="flex-1 px-3 py-2 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg text-xs text-[var(--kyro-text-muted)] focus:outline-none truncate"
1357
1352
  />
1358
- <button type="button"
1353
+ <button
1354
+ type="button"
1359
1355
  onClick={() => {
1360
1356
  navigator.clipboard.writeText(
1361
1357
  getAbsoluteUrl(panelItem.url),
@@ -1389,7 +1385,8 @@ export function MediaGallery() {
1389
1385
  </p>
1390
1386
  </div>
1391
1387
  {panelItem.type === "image" && (
1392
- <button type="button"
1388
+ <button
1389
+ type="button"
1393
1390
  onClick={() => setShowCrop(true)}
1394
1391
  className="flex items-center gap-2 px-3 py-1.5 bg-[var(--kyro-surface-accent)] text-[var(--kyro-primary)] rounded-lg font-bold text-xs hover:opacity-80 transition-colors mt-2 w-full"
1395
1392
  >
@@ -1409,13 +1406,15 @@ export function MediaGallery() {
1409
1406
  >
1410
1407
  <Download className="w-5 h-5 text-[var(--kyro-text-secondary)]" />
1411
1408
  </a>
1412
- <button type="button"
1409
+ <button
1410
+ type="button"
1413
1411
  onClick={savePanelMetadata}
1414
1412
  className="flex-1 py-2 px-4 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-lg font-bold text-sm hover:opacity-90"
1415
1413
  >
1416
1414
  Save
1417
1415
  </button>
1418
- <button type="button"
1416
+ <button
1417
+ type="button"
1419
1418
  onClick={() => setPanelItem(null)}
1420
1419
  className="py-2 px-4 border border-[var(--kyro-border)] rounded-lg font-bold text-sm text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
1421
1420
  >
@@ -1433,13 +1432,15 @@ export function MediaGallery() {
1433
1432
  <div className="flex w-full justify-between items-center mb-4 text-white">
1434
1433
  <h3 className="text-xl font-bold">Crop Image</h3>
1435
1434
  <div className="flex gap-3">
1436
- <button type="button"
1435
+ <button
1436
+ type="button"
1437
1437
  onClick={() => setShowCrop(false)}
1438
1438
  className="px-4 py-2 border border-white/20 text-white/80 hover:bg-white/10 rounded-lg font-bold text-sm transition-colors"
1439
1439
  >
1440
1440
  Cancel
1441
1441
  </button>
1442
- <button type="button"
1442
+ <button
1443
+ type="button"
1443
1444
  disabled={uploading}
1444
1445
  onClick={onCropComplete}
1445
1446
  className="px-4 py-2 bg-[var(--kyro-sidebar-active)] hover:opacity-90 text-[var(--kyro-sidebar-text-active)] rounded-lg font-bold text-sm transition-colors"
@@ -1513,19 +1514,16 @@ export function MediaGallery() {
1513
1514
  >
1514
1515
  Configure Storage
1515
1516
  </a>
1516
- <button type="button"
1517
+ <button
1518
+ type="button"
1517
1519
  onClick={() => {
1518
1520
  // Set default storage config programmatically
1519
- fetch("/api/globals/storage-settings", {
1520
- method: "PATCH",
1521
- headers: { "Content-Type": "application/json" },
1522
- body: JSON.stringify({
1523
- provider: "local",
1524
- local: {
1525
- uploadDir: "./public/uploads",
1526
- baseUrl: "/uploads",
1527
- },
1528
- }),
1521
+ apiPatch("/api/globals/storage-settings", {
1522
+ provider: "local",
1523
+ local: {
1524
+ uploadDir: "./public/uploads",
1525
+ baseUrl: "/uploads",
1526
+ },
1529
1527
  }).then(() => {
1530
1528
  setShowStorageConfigModal(false);
1531
1529
  setStorageConfigured(true);
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect } from "react";
2
+ import { apiGet, apiPatch } from "@kyro-cms/utils/lib/api";
2
3
  import {
3
4
  Users,
4
5
  UserPlus,
@@ -34,8 +35,7 @@ export function UserManagement() {
34
35
  const loadUsers = async () => {
35
36
  try {
36
37
  setLoading(true);
37
- const response = await fetch("/api/auth/users");
38
- const result = await response.json();
38
+ const result = await apiGet("/api/auth/users");
39
39
  setUsers(result.docs || []);
40
40
  } catch (error) {
41
41
  console.error("Failed to load users:", error);
@@ -46,16 +46,10 @@ export function UserManagement() {
46
46
 
47
47
  const handleToggleLock = async (user: User) => {
48
48
  try {
49
- const response = await fetch(`/api/auth/${user.id}`, {
50
- method: "PATCH",
51
- headers: { "Content-Type": "application/json" },
52
- body: JSON.stringify({ locked: !user.locked }),
53
- });
54
- if (response.ok) {
55
- setUsers((prev) =>
56
- prev.map((u) => (u.id === user.id ? { ...u, locked: !u.locked } : u)),
57
- );
58
- }
49
+ await apiPatch(`/api/auth/${user.id}`, { locked: !user.locked });
50
+ setUsers((prev) =>
51
+ prev.map((u) => (u.id === user.id ? { ...u, locked: !u.locked } : u)),
52
+ );
59
53
  } catch (error) {
60
54
  console.error("Failed to toggle user lock:", error);
61
55
  }
@@ -80,7 +74,10 @@ export function UserManagement() {
80
74
  </p>
81
75
  </div>
82
76
  <div className="flex items-center gap-3">
83
- <button type="button" className="flex items-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-2xl font-black text-sm shadow-xl active:scale-95 transition-all">
77
+ <button
78
+ type="button"
79
+ className="flex items-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-2xl font-black text-sm shadow-xl active:scale-95 transition-all"
80
+ >
84
81
  <UserPlus className="w-4 h-4" />
85
82
  Invite Member
86
83
  </button>
@@ -100,13 +97,22 @@ export function UserManagement() {
100
97
  />
101
98
  </div>
102
99
  <div className="flex items-center gap-3 bg-[var(--kyro-bg-secondary)] p-1 rounded-2xl border border-[var(--kyro-border)]">
103
- <button type="button" className="px-4 py-2 text-[10px] font-black uppercase tracking-widest bg-[var(--kyro-surface)] shadow-sm rounded-xl border border-[var(--kyro-border)]">
100
+ <button
101
+ type="button"
102
+ className="px-4 py-2 text-[10px] font-black uppercase tracking-widest bg-[var(--kyro-surface)] shadow-sm rounded-xl border border-[var(--kyro-border)]"
103
+ >
104
104
  All Users
105
105
  </button>
106
- <button type="button" className="px-4 py-2 text-[10px] font-black uppercase tracking-widest opacity-40 hover:opacity-100 transition-all">
106
+ <button
107
+ type="button"
108
+ className="px-4 py-2 text-[10px] font-black uppercase tracking-widest opacity-40 hover:opacity-100 transition-all"
109
+ >
107
110
  Admins
108
111
  </button>
109
- <button type="button" className="px-4 py-2 text-[10px] font-black uppercase tracking-widest opacity-40 hover:opacity-100 transition-all">
112
+ <button
113
+ type="button"
114
+ className="px-4 py-2 text-[10px] font-black uppercase tracking-widest opacity-40 hover:opacity-100 transition-all"
115
+ >
110
116
  Restricted
111
117
  </button>
112
118
  </div>
@@ -177,7 +183,8 @@ export function UserManagement() {
177
183
  </div>
178
184
 
179
185
  <div className="flex items-center gap-3">
180
- <button type="button"
186
+ <button
187
+ type="button"
181
188
  onClick={() => handleToggleLock(user)}
182
189
  className={`flex-1 py-2.5 rounded-xl font-black text-[10px] uppercase tracking-widest transition-all flex items-center justify-center gap-2 ${user.locked ? "bg-green-500/10 text-green-500 hover:bg-green-500/20" : "bg-red-500/10 text-red-500 hover:bg-red-500/20"}`}
183
190
  >
@@ -188,7 +195,10 @@ export function UserManagement() {
188
195
  )}
189
196
  {user.locked ? "Unlock Account" : "Lock Account"}
190
197
  </button>
191
- <button type="button" className="p-2.5 bg-[var(--kyro-bg-secondary)] rounded-xl border border-[var(--kyro-border)] hover:bg-[var(--kyro-surface)] transition-all">
198
+ <button
199
+ type="button"
200
+ className="p-2.5 bg-[var(--kyro-bg-secondary)] rounded-xl border border-[var(--kyro-border)] hover:bg-[var(--kyro-surface)] transition-all"
201
+ >
192
202
  <MoreVertical className="w-4 h-4 opacity-40" />
193
203
  </button>
194
204
  </div>