@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.
- package/dist/index.cjs +966 -585
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +29 -9
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +649 -268
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ActionBar.tsx +254 -70
- package/src/components/Admin.tsx +10 -17
- package/src/components/ApiKeysManager.tsx +1 -0
- package/src/components/AuditLogsPage.tsx +3 -3
- package/src/components/AutoForm.tsx +51 -34
- package/src/components/DetailView.tsx +37 -13
- package/src/components/GraphQLPlayground.tsx +460 -224
- package/src/components/ListView.tsx +3 -3
- package/src/components/LoginPage.tsx +5 -30
- package/src/components/MediaGallery.tsx +122 -15
- package/src/components/RestPlayground.tsx +443 -519
- package/src/components/Sidebar.astro +6 -2
- package/src/components/UserManagement.tsx +4 -4
- package/src/components/WebhookManager.tsx +4 -4
- package/src/components/blocks/AccordionBlock.tsx +1 -1
- package/src/components/blocks/ArrayBlock.tsx +1 -1
- package/src/components/blocks/ChildBlocksTree.tsx +6 -6
- package/src/components/blocks/CodeBlock.tsx +1 -1
- package/src/components/blocks/FileBlock.tsx +1 -1
- package/src/components/blocks/HeroBlock.tsx +1 -1
- package/src/components/blocks/ListBlock.tsx +1 -1
- package/src/components/blocks/RelationshipBlock.tsx +1 -1
- package/src/components/blocks/RichTextBlock.tsx +1 -1
- package/src/components/blocks/VideoBlock.tsx +1 -1
- package/src/components/fields/BlocksField.tsx +17 -19
- package/src/components/ui/PageHeader.tsx +205 -83
- package/src/components/ui/Pagination.tsx +2 -2
- package/src/components/ui/SlidePanel.tsx +4 -4
- package/src/layouts/AdminLayout.astro +64 -4
- package/src/lib/useResourceManager.ts +1 -0
- package/src/pages/graphql-explorer.astro +7 -51
- package/src/pages/graphql.astro +7 -119
- package/src/pages/index.astro +4 -63
- package/src/pages/rest-playground.astro +3 -29
- 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 {
|
|
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
|
-
</
|
|
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
|
|
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
|
|
474
|
-
<
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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={() =>
|
|
482
|
-
className=
|
|
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
|
-
<
|
|
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
|
|
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"
|