@kyro-cms/admin 0.9.4 → 0.9.6

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 (44) hide show
  1. package/dist/index.cjs +966 -585
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +29 -9
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +3 -1
  6. package/dist/index.d.ts +3 -1
  7. package/dist/index.js +649 -268
  8. package/dist/index.js.map +1 -1
  9. package/package.json +2 -2
  10. package/src/components/ActionBar.tsx +254 -70
  11. package/src/components/Admin.tsx +10 -17
  12. package/src/components/ApiKeysManager.tsx +1 -0
  13. package/src/components/AuditLogsPage.tsx +3 -3
  14. package/src/components/AutoForm.tsx +51 -34
  15. package/src/components/DetailView.tsx +37 -13
  16. package/src/components/GraphQLPlayground.tsx +460 -224
  17. package/src/components/ListView.tsx +3 -3
  18. package/src/components/LoginPage.tsx +5 -30
  19. package/src/components/MediaGallery.tsx +122 -15
  20. package/src/components/RestPlayground.tsx +443 -519
  21. package/src/components/Sidebar.astro +6 -2
  22. package/src/components/UserManagement.tsx +4 -4
  23. package/src/components/WebhookManager.tsx +4 -4
  24. package/src/components/blocks/AccordionBlock.tsx +1 -1
  25. package/src/components/blocks/ArrayBlock.tsx +1 -1
  26. package/src/components/blocks/ChildBlocksTree.tsx +6 -6
  27. package/src/components/blocks/CodeBlock.tsx +1 -1
  28. package/src/components/blocks/FileBlock.tsx +1 -1
  29. package/src/components/blocks/HeroBlock.tsx +1 -1
  30. package/src/components/blocks/ListBlock.tsx +1 -1
  31. package/src/components/blocks/RelationshipBlock.tsx +1 -1
  32. package/src/components/blocks/RichTextBlock.tsx +1 -1
  33. package/src/components/blocks/VideoBlock.tsx +1 -1
  34. package/src/components/fields/BlocksField.tsx +17 -19
  35. package/src/components/ui/PageHeader.tsx +205 -83
  36. package/src/components/ui/Pagination.tsx +2 -2
  37. package/src/components/ui/SlidePanel.tsx +4 -4
  38. package/src/layouts/AdminLayout.astro +64 -4
  39. package/src/lib/useResourceManager.ts +1 -0
  40. package/src/pages/graphql-explorer.astro +7 -51
  41. package/src/pages/graphql.astro +7 -119
  42. package/src/pages/index.astro +4 -63
  43. package/src/pages/rest-playground.astro +3 -29
  44. package/src/styles/main.css +32 -9
@@ -359,8 +359,8 @@ export function ListView({
359
359
  {/* Toolbar */}
360
360
  <div className="surface-tile p-4 flex flex-col lg:flex-row gap-4 items-start lg:items-center">
361
361
  {/* Search */}
362
- <div className="relative flex-1 max-w-md">
363
- <Search className="w-4 h-4" />
362
+ <div className="relative flex-1 w-full lg:max-w-md">
363
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
364
364
  <input
365
365
  type="text"
366
366
  placeholder="Search..."
@@ -582,7 +582,7 @@ export function ListView({
582
582
  <div className="overflow-x-auto">
583
583
  <table className="w-full text-left">
584
584
  <thead>
585
- <tr className="text-[var(--kyro-text-secondary)] font-bold text-[10px] tracking-[0.3em] border-b border-[var(--kyro-border)]">
585
+ <tr className="text-[var(--kyro-text-secondary)] font-bold text-[10px] tracking-[0.3em] border-b border-[var(--kyro-border)] whitespace-nowrap">
586
586
  <th className="px-4 py-4 w-10">
587
587
  <input
588
588
  type="checkbox"
@@ -1,13 +1,8 @@
1
1
  import { useState, useEffect } from "react";
2
2
  import { apiGet, apiPost } from "../lib/api";
3
3
  import { ThemeProvider, type ThemeMode } from "./ThemeProvider";
4
- import { Toast, ToastProvider } from "./ui/Toast";
5
-
6
- interface LocalToast {
7
- id: string;
8
- type: "success" | "error" | "info" | "warning";
9
- message: string;
10
- }
4
+ import { Toaster } from "./ui/Toaster";
5
+ import { useToastStore } from "../lib/stores";
11
6
 
12
7
  interface LoginPageProps {
13
8
  onAuth: (token: string, user: Record<string, unknown>) => void;
@@ -22,8 +17,8 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
22
17
  const [password, setPassword] = useState("");
23
18
  const [confirmPassword, setConfirmPassword] = useState("");
24
19
  const [loading, setLoading] = useState(false);
25
- const [toasts, setToasts] = useState<LocalToast[]>([]);
26
20
  const [isFirstUser, setIsFirstUser] = useState(false);
21
+ const addToast = useToastStore((state) => state.addToast);
27
22
 
28
23
  useEffect(() => {
29
24
  checkIfFirstUser();
@@ -38,14 +33,6 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
38
33
  }
39
34
  };
40
35
 
41
- const addToast = (type: LocalToast["type"], message: string) => {
42
- const id = Math.random().toString(36).substring(7);
43
- setToasts((prev) => [...prev, { id, type, message }]);
44
- setTimeout(() => {
45
- setToasts((prev) => prev.filter((t) => t.id !== id));
46
- }, 5000);
47
- };
48
-
49
36
  const handleSubmit = async (e: React.FormEvent) => {
50
37
  e.preventDefault();
51
38
  setLoading(true);
@@ -79,7 +66,6 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
79
66
 
80
67
  return (
81
68
  <ThemeProvider defaultMode={theme}>
82
- <ToastProvider>
83
69
  <div className="kyro-login-page">
84
70
  <div className="kyro-login-container">
85
71
  <div className="kyro-login-header">
@@ -190,19 +176,8 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
190
176
  </div>
191
177
  )}
192
178
  </div>
193
-
194
- {toasts.map((toast) => (
195
- <Toast
196
- key={toast.id}
197
- type={toast.type}
198
- message={toast.message}
199
- onClose={() =>
200
- setToasts((prev) => prev.filter((t) => t.id !== toast.id))
201
- }
202
- />
203
- ))}
179
+ <Toaster />
204
180
  </div>
205
- </ToastProvider>
206
- </ThemeProvider>
181
+ </ThemeProvider>
207
182
  );
208
183
  }
@@ -1,4 +1,4 @@
1
- import { Search, Check, Server } from "./ui/icons";
1
+ import { Search, Check, Server, Filter } from "./ui/icons";
2
2
  import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { Spinner } from "./ui/Spinner";
@@ -146,6 +146,7 @@ export function MediaGallery({
146
146
  {},
147
147
  );
148
148
  const [showNewFolderModal, setShowNewFolderModal] = useState(false);
149
+ const [showMobileFilters, setShowMobileFilters] = useState(false);
149
150
  const [storageConfigured, setStorageConfigured] = useState<boolean | null>(null);
150
151
  const [storageChecked, setStorageChecked] = useState(false);
151
152
  const [showStorageConfigModal, setShowStorageConfigModal] = useState(false);
@@ -318,6 +319,7 @@ export function MediaGallery({
318
319
  await apiPost("/api/media/folders", { name });
319
320
  loadFolders();
320
321
  setShowNewFolderModal(false);
322
+ toast.success(`Folder "${name}" created`);
321
323
  } catch (error) {
322
324
  console.error("Failed to create folder:", error);
323
325
  toast.error("Failed to create folder");
@@ -336,6 +338,7 @@ export function MediaGallery({
336
338
  if (currentFolder === folder) setCurrentFolder("");
337
339
  loadFolders();
338
340
  loadMedia();
341
+ toast.success(`Folder "${folder}" deleted`);
339
342
  } catch (error) {
340
343
  console.error("Failed to delete folder:", error);
341
344
  toast.error("Failed to delete folder");
@@ -351,8 +354,10 @@ export function MediaGallery({
351
354
  if (panelItem?.id === id) {
352
355
  setPanelItem(result.doc);
353
356
  }
357
+ toast.success("Metadata updated");
354
358
  } catch (error) {
355
359
  console.error("Failed to update metadata:", error);
360
+ toast.error("Failed to update metadata");
356
361
  }
357
362
  };
358
363
 
@@ -410,6 +415,7 @@ export function MediaGallery({
410
415
  await apiUpload("/api/media", formData);
411
416
  loadMedia();
412
417
  setShowCrop(false);
418
+ toast.success("Cropped image saved");
413
419
  }
414
420
  }
415
421
  } catch (err) {
@@ -440,7 +446,7 @@ export function MediaGallery({
440
446
  })}
441
447
  >
442
448
  {/* Top Bar */}
443
- <div className={`flex flex-col lg:flex-row lg:items-center justify-between gap-6 border-b border-[var(--kyro-border)] backdrop-blur-md sticky top-0 z-40 ${pickerMode ? "p-2" : "p-6 rounded-xl surface-tile"}`}>
449
+ <div className={`flex flex-col lg:flex-row lg:items-center justify-between gap-6 border-b border-[var(--kyro-border)] backdrop-blur-md sticky top-0 ${pickerMode ? "p-2" : "p-6 rounded-xl surface-tile"}`}>
444
450
  {!pickerMode && (
445
451
  <div className="flex items-center gap-4">
446
452
  <div>
@@ -458,7 +464,7 @@ export function MediaGallery({
458
464
 
459
465
  <div className={`flex items-center gap-3 flex-wrap lg:flex-nowrap ${pickerMode ? "w-full" : ""}`}>
460
466
  <div className="relative group flex-1 min-w-[200px]">
461
- <Search className="w-4 h-4" />
467
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
462
468
  <input
463
469
  type="text"
464
470
  placeholder="Search assets..."
@@ -470,18 +476,27 @@ export function MediaGallery({
470
476
 
471
477
  {!pickerMode && (
472
478
  <>
473
- <div className="flex bg-[var(--kyro-surface-accent)] p-1 rounded-xl border border-[var(--kyro-border)]">
474
- <button
475
- onClick={() => setView("grid")}
476
- className={`p-2 rounded-lg transition-all ${view === "grid" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
477
- >
478
- <Grid className="w-4 h-4" />
479
- </button>
479
+ <div className="flex items-center gap-2">
480
+ <div className="flex bg-[var(--kyro-surface-accent)] p-1 rounded-xl border border-[var(--kyro-border)]">
481
+ <button
482
+ onClick={() => setView("grid")}
483
+ className={`p-2 rounded-lg transition-all ${view === "grid" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
484
+ >
485
+ <Grid className="w-4 h-4" />
486
+ </button>
487
+ <button
488
+ onClick={() => setView("list")}
489
+ className={`p-2 rounded-lg transition-all ${view === "list" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
490
+ >
491
+ <FileIcon className="w-4 h-4" />
492
+ </button>
493
+ </div>
494
+
480
495
  <button
481
- onClick={() => setView("list")}
482
- className={`p-2 rounded-lg transition-all ${view === "list" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
496
+ onClick={() => setShowMobileFilters(true)}
497
+ className="md:hidden p-2 rounded-xl bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-colors"
483
498
  >
484
- <FileIcon className="w-4 h-4" />
499
+ <Filter className="w-4 h-4" />
485
500
  </button>
486
501
  </div>
487
502
 
@@ -584,9 +599,9 @@ export function MediaGallery({
584
599
 
585
600
  {/* Main Content Area */}
586
601
  <div className="flex-1 flex flex-col min-h-0 bg-[var(--kyro-bg)]">
587
- <div className={`flex-1 overflow-y-auto custom-scrollbar ${pickerMode ? "px-2 py-4" : "py-8 px-4"}`}>
602
+ <div className={`flex-1 overflow-y-auto custom-scrollbar ${pickerMode ? "px-2 py-4" : "py-4 px-2 md:py-8 md:px-4"}`}>
588
603
  {loading ? (
589
- <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
604
+ <div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
590
605
  <Shimmer variant="media-card" count={12} />
591
606
  </div>
592
607
  ) : items.length === 0 ? (
@@ -861,6 +876,96 @@ export function MediaGallery({
861
876
  </div>
862
877
  )}
863
878
 
879
+ {/* Mobile Filters Bottom Sheet */}
880
+ {showMobileFilters && !pickerMode && (
881
+ <div className="fixed inset-0 z-[70] md:hidden">
882
+ <div
883
+ className="fixed inset-0 bg-black/50 backdrop-blur-sm"
884
+ onClick={() => setShowMobileFilters(false)}
885
+ />
886
+ <div className="fixed bottom-0 left-0 right-0 bg-[var(--kyro-surface)] rounded-t-3xl shadow-2xl max-h-[70vh] overflow-y-auto animate-in slide-in-from-bottom-12 duration-300">
887
+ <div className="sticky top-0 bg-[var(--kyro-surface)] z-10 flex items-center justify-between p-6 pb-4 border-b border-[var(--kyro-border)]">
888
+ <h3 className="text-sm font-bold tracking-tight text-[var(--kyro-text-primary)]">
889
+ Filters
890
+ </h3>
891
+ <button
892
+ onClick={() => setShowMobileFilters(false)}
893
+ className="p-2 rounded-xl hover:bg-[var(--kyro-surface-accent)] transition-colors text-[var(--kyro-text-muted)]"
894
+ >
895
+ <X className="w-4 h-4" />
896
+ </button>
897
+ </div>
898
+
899
+ <div className="p-6 space-y-8">
900
+ {/* Quick Filters */}
901
+ <div>
902
+ <span className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40 block mb-4">
903
+ Quick Filters
904
+ </span>
905
+ <div className="flex flex-wrap gap-2">
906
+ {(["all", "image", "video", "audio", "document", "archive"] as const).map((t) => (
907
+ <button
908
+ key={t}
909
+ onClick={() => { setFilter(t); setShowMobileFilters(false); }}
910
+ className={`px-4 py-2 rounded-xl text-[11px] font-bold capitalize transition-all border ${filter === t ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] border-transparent" : "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] border-[var(--kyro-border)] hover:border-[var(--kyro-text-muted)]"}`}
911
+ >
912
+ {t}
913
+ </button>
914
+ ))}
915
+ </div>
916
+ </div>
917
+
918
+ {/* Folders */}
919
+ <div>
920
+ <div className="flex items-center justify-between mb-4">
921
+ <span className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
922
+ Folders
923
+ </span>
924
+ <button
925
+ onClick={() => { setShowNewFolderModal(true); setShowMobileFilters(false); }}
926
+ className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-colors text-[var(--kyro-text-primary)]"
927
+ >
928
+ <FolderPlus className="w-4 h-4" />
929
+ </button>
930
+ </div>
931
+ <nav className="space-y-1">
932
+ <button
933
+ onClick={() => { setCurrentFolder(""); setShowMobileFilters(false); }}
934
+ className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-xs font-bold transition-all ${currentFolder === "" ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-md" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"}`}
935
+ >
936
+ <FolderInput className="w-4 h-4 opacity-70" />
937
+ All Assets
938
+ </button>
939
+ {folders.map((folder) => (
940
+ <div key={folder} className="group relative">
941
+ <button
942
+ onClick={() => { setCurrentFolder(folder); setShowMobileFilters(false); }}
943
+ className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-xs font-bold transition-all ${currentFolder === folder ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-md" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"}`}
944
+ >
945
+ <div className="w-4 h-4 flex items-center justify-center opacity-70">
946
+ <Folder fill={currentFolder === folder ? "currentColor" : "none"} />
947
+ </div>
948
+ {folder}
949
+ </button>
950
+ <button
951
+ onClick={(e) => {
952
+ e.stopPropagation();
953
+ handleDeleteFolder(folder);
954
+ setShowMobileFilters(false);
955
+ }}
956
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-red-500 opacity-0 group-hover:opacity-100 transition-all hover:bg-red-50 rounded-lg"
957
+ >
958
+ <Trash2 className="w-3.5 h-3.5" />
959
+ </button>
960
+ </div>
961
+ ))}
962
+ </nav>
963
+ </div>
964
+ </div>
965
+ </div>
966
+ </div>
967
+ )}
968
+
864
969
  {/* Asset Panel */}
865
970
  <SlidePanel
866
971
  open={!!panelItem}
@@ -1178,6 +1283,8 @@ export function MediaGallery({
1178
1283
  setShowStorageConfigModal(false);
1179
1284
  setStorageConfigured(true);
1180
1285
  window.location.reload();
1286
+ }).catch(() => {
1287
+ toast.error("Failed to configure storage");
1181
1288
  });
1182
1289
  }}
1183
1290
  className="flex-1 px-4 py-3 border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] rounded-xl font-bold hover:bg-[var(--kyro-surface-accent)] transition-colors"