@kyro-cms/admin 0.9.3 → 0.9.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.
- package/dist/index.cjs +940 -568
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +42 -23
- 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 +623 -251
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ActionBar.tsx +254 -70
- package/src/components/Admin.tsx +3 -16
- 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/ListView.tsx +2 -2
- package/src/components/LoginPage.tsx +5 -30
- package/src/components/MediaGallery.tsx +120 -13
- package/src/components/Sidebar.astro +6 -2
- package/src/components/UserManagement.tsx +4 -4
- package/src/components/WebhookManager.tsx +4 -4
- package/src/components/fields/BlocksField.tsx +12 -14
- 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/components/ui/Toast.tsx +1 -2
- package/src/integration.ts +0 -4
- package/src/layouts/AdminLayout.astro +49 -4
- package/src/lib/useResourceManager.ts +1 -0
- package/src/pages/rest-playground.astro +0 -19
- package/src/styles/main.css +34 -19
- package/src/pages/api-explorer.astro +0 -173
|
@@ -292,8 +292,30 @@ export function DetailView({
|
|
|
292
292
|
]}
|
|
293
293
|
/>
|
|
294
294
|
|
|
295
|
-
{
|
|
296
|
-
|
|
295
|
+
{isSingleLayout ? (
|
|
296
|
+
<div className="md:hidden">
|
|
297
|
+
<ActionBar
|
|
298
|
+
status={status}
|
|
299
|
+
saveStatus={saveStatus}
|
|
300
|
+
hasChanges={hasChanges}
|
|
301
|
+
onSave={() => handleSave(false)}
|
|
302
|
+
onPublish={handlePublish}
|
|
303
|
+
onUnpublish={status === "published" ? handleUnpublish : undefined}
|
|
304
|
+
onDuplicate={handleDuplicate}
|
|
305
|
+
onViewHistory={() => {
|
|
306
|
+
window.dispatchEvent(new CustomEvent('kyro:show-version-history'));
|
|
307
|
+
}}
|
|
308
|
+
onPreview={() =>
|
|
309
|
+
window.open(`/preview/${slug}/${documentId}`, "_blank")
|
|
310
|
+
}
|
|
311
|
+
onDelete={handleDeleteTrigger}
|
|
312
|
+
onBack={onBack}
|
|
313
|
+
onToggleSidebar={() => window.dispatchEvent(new CustomEvent("toggle-sidebar"))}
|
|
314
|
+
publishedAt={publishedAt}
|
|
315
|
+
updatedAt={updatedAt}
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
) : (
|
|
297
319
|
<ActionBar
|
|
298
320
|
status={status}
|
|
299
321
|
saveStatus={saveStatus}
|
|
@@ -309,6 +331,8 @@ export function DetailView({
|
|
|
309
331
|
window.open(`/preview/${slug}/${documentId}`, "_blank")
|
|
310
332
|
}
|
|
311
333
|
onDelete={handleDeleteTrigger}
|
|
334
|
+
onBack={onBack}
|
|
335
|
+
onToggleSidebar={() => window.dispatchEvent(new CustomEvent("toggle-sidebar"))}
|
|
312
336
|
publishedAt={publishedAt}
|
|
313
337
|
updatedAt={updatedAt}
|
|
314
338
|
/>
|
|
@@ -317,12 +341,12 @@ export function DetailView({
|
|
|
317
341
|
<div
|
|
318
342
|
className={
|
|
319
343
|
isSingleLayout
|
|
320
|
-
? "w-full pb-32 pt-8"
|
|
321
|
-
: "w-full mx-auto grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-8 pb-32"
|
|
344
|
+
? "w-full pb-32 pt-4 md:pt-8"
|
|
345
|
+
: "w-full mx-auto grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-4 md:gap-8 pt-4 md:pt-0 pb-32"
|
|
322
346
|
}
|
|
323
347
|
>
|
|
324
|
-
<div className="space-y-8 min-w-0">
|
|
325
|
-
<div className="surface-tile p-8">
|
|
348
|
+
<div className="space-y-4 md:space-y-8 min-w-0">
|
|
349
|
+
<div className="surface-tile p-4 md:p-8">
|
|
326
350
|
<div className="flex items-center justify-between mb-8 px-1">
|
|
327
351
|
<h2 className="text-[10px] font-bold tracking-[0.2em] opacity-40">
|
|
328
352
|
Core Configuration
|
|
@@ -369,12 +393,12 @@ export function DetailView({
|
|
|
369
393
|
</div>
|
|
370
394
|
|
|
371
395
|
{!isSingleLayout && (
|
|
372
|
-
<div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
|
|
373
|
-
<div className="surface-tile p-8">
|
|
374
|
-
<h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-6">
|
|
396
|
+
<div className="space-y-4 md:space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
|
|
397
|
+
<div className="surface-tile p-4 md:p-8">
|
|
398
|
+
<h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-4 md:mb-6">
|
|
375
399
|
Metadata
|
|
376
400
|
</h3>
|
|
377
|
-
<div className="space-y-6">
|
|
401
|
+
<div className="space-y-4 md:space-y-6">
|
|
378
402
|
<div className="flex flex-col gap-2">
|
|
379
403
|
<span className="text-[10px] font-bold tracking-widest opacity-40">
|
|
380
404
|
Dynamic Status
|
|
@@ -429,11 +453,11 @@ export function DetailView({
|
|
|
429
453
|
</div>
|
|
430
454
|
</div>
|
|
431
455
|
|
|
432
|
-
<div className="surface-tile p-8 bg-[var(--kyro-bg-secondary)]">
|
|
433
|
-
<h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-4">
|
|
456
|
+
<div className="surface-tile p-4 md:p-8 bg-[var(--kyro-bg-secondary)]">
|
|
457
|
+
<h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-3 md:mb-4">
|
|
434
458
|
Quick Links
|
|
435
459
|
</h3>
|
|
436
|
-
<div className="space-y-3">
|
|
460
|
+
<div className="space-y-2 md:space-y-3">
|
|
437
461
|
<button
|
|
438
462
|
type="button"
|
|
439
463
|
onClick={handleDuplicate}
|
|
@@ -359,7 +359,7 @@ 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">
|
|
362
|
+
<div className="relative flex-1 w-full lg:max-w-md">
|
|
363
363
|
<Search className="w-4 h-4" />
|
|
364
364
|
<input
|
|
365
365
|
type="text"
|
|
@@ -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
|
|
|
@@ -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"
|
|
@@ -92,9 +92,10 @@ function isActive(item: NavItem): boolean {
|
|
|
92
92
|
---
|
|
93
93
|
|
|
94
94
|
<aside
|
|
95
|
-
|
|
95
|
+
id="kyro-sidebar"
|
|
96
|
+
class="surface-tile w-[280px] md:w-[320px] flex flex-col flex-shrink-0 overflow-hidden fixed md:static inset-y-0 left-0 z-50 md:z-auto transition-transform duration-300 -translate-x-full md:translate-x-0 h-[100dvh] md:h-auto border-r md:border-r-0 border-[var(--kyro-border)] rounded-r-2xl md:rounded-3xl rounded-l-none md:rounded-l-3xl shadow-2xl md:shadow-none"
|
|
96
97
|
>
|
|
97
|
-
<div class="px-8 py-8 flex items-center gap-3">
|
|
98
|
+
<div class="px-6 md:px-8 py-6 md:py-8 flex items-center justify-between gap-3">
|
|
98
99
|
{
|
|
99
100
|
siteLogo ? (
|
|
100
101
|
<img
|
|
@@ -113,6 +114,9 @@ function isActive(item: NavItem): boolean {
|
|
|
113
114
|
</span>
|
|
114
115
|
)
|
|
115
116
|
}
|
|
117
|
+
<button id="mobile-close-btn" class="md:hidden p-2 -mr-2 rounded-lg text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors">
|
|
118
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
119
|
+
</button>
|
|
116
120
|
</div>
|
|
117
121
|
|
|
118
122
|
<nav class="flex-1 px-4 overflow-y-auto" id="sidebar-nav">
|
|
@@ -132,7 +132,7 @@ export function UserManagement() {
|
|
|
132
132
|
);
|
|
133
133
|
|
|
134
134
|
return (
|
|
135
|
-
<div className="w-full space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-700 px-8 pb-12">
|
|
135
|
+
<div className="w-full space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-700 px-4 md:px-8 pb-12">
|
|
136
136
|
{/* Header */}
|
|
137
137
|
<PageHeader
|
|
138
138
|
title="Identity & Access"
|
|
@@ -169,10 +169,10 @@ export function UserManagement() {
|
|
|
169
169
|
</div>
|
|
170
170
|
|
|
171
171
|
{/* User Table */}
|
|
172
|
-
<div className="surface-tile overflow-
|
|
173
|
-
<table className="w-full text-left
|
|
172
|
+
<div className="surface-tile overflow-x-auto">
|
|
173
|
+
<table className="w-full text-left">
|
|
174
174
|
<thead>
|
|
175
|
-
<tr className="text-[var(--kyro-text-secondary)] font-bold text-[9px] tracking-[0.2em] uppercase border-b border-[var(--kyro-border)]">
|
|
175
|
+
<tr className="text-[var(--kyro-text-secondary)] font-bold text-[9px] tracking-[0.2em] uppercase border-b border-[var(--kyro-border)] whitespace-nowrap">
|
|
176
176
|
<th className="px-6 py-4 w-64">Member Identity</th>
|
|
177
177
|
<th className="px-6 py-4">Administrative Role</th>
|
|
178
178
|
<th className="px-6 py-4">Security Status</th>
|
|
@@ -246,14 +246,14 @@ export function WebhookManager() {
|
|
|
246
246
|
</div>
|
|
247
247
|
</div>
|
|
248
248
|
|
|
249
|
-
<div className="grid sm:grid-cols-3 gap-6 pt-2">
|
|
249
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 pt-2">
|
|
250
250
|
<div className="space-y-1">
|
|
251
251
|
<span className="text-[9px] font-bold uppercase tracking-widest opacity-30">Destination</span>
|
|
252
252
|
<div className="font-mono text-xs opacity-60 truncate max-w-[200px]" title={webhook.url}>
|
|
253
253
|
{webhook.url}
|
|
254
254
|
</div>
|
|
255
255
|
</div>
|
|
256
|
-
<div className="space-y-1 border-l border-[var(--kyro-border)] pl-6">
|
|
256
|
+
<div className="space-y-1 sm:border-l border-t sm:border-t-0 border-[var(--kyro-border)] pt-4 sm:pt-0 sm:pl-6 mt-4 sm:mt-0">
|
|
257
257
|
<span className="text-[9px] font-bold uppercase tracking-widest opacity-30">Events</span>
|
|
258
258
|
<div className="flex flex-wrap gap-1">
|
|
259
259
|
{webhook.events.slice(0, 2).map((event) => (
|
|
@@ -266,7 +266,7 @@ export function WebhookManager() {
|
|
|
266
266
|
)}
|
|
267
267
|
</div>
|
|
268
268
|
</div>
|
|
269
|
-
<div className="space-y-1 border-l border-[var(--kyro-border)] pl-6">
|
|
269
|
+
<div className="space-y-1 sm:border-l border-t sm:border-t-0 border-[var(--kyro-border)] pt-4 sm:pt-0 sm:pl-6 mt-4 sm:mt-0">
|
|
270
270
|
<span className="text-[9px] font-bold uppercase tracking-widest opacity-30">Activity</span>
|
|
271
271
|
<div className="text-[10px] font-bold opacity-60 flex items-center gap-1.5">
|
|
272
272
|
<Clock className="w-3 h-3" />
|
|
@@ -367,7 +367,7 @@ export function WebhookManager() {
|
|
|
367
367
|
|
|
368
368
|
<div className="space-y-4">
|
|
369
369
|
<label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Subscribed Events</label>
|
|
370
|
-
<div className="grid grid-cols-2 gap-4">
|
|
370
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
371
371
|
{eventOptions.map((opt) => (
|
|
372
372
|
<button
|
|
373
373
|
type="button"
|
|
@@ -106,11 +106,10 @@ const SortableBlockComponent = ({
|
|
|
106
106
|
<div ref={setNodeRef} style={style} className="relative group">
|
|
107
107
|
<div
|
|
108
108
|
onClick={() => setEditingBlockId(block.id as string)}
|
|
109
|
-
className={`flex items-center gap-1 pl-5 pr-1.5 py-1 bg-[var(--kyro-bg-secondary)] rounded-md border transition-colors cursor-pointer text-xs whitespace-nowrap ${
|
|
110
|
-
isEditing
|
|
109
|
+
className={`flex items-center gap-1 pl-5 pr-1.5 py-1 bg-[var(--kyro-bg-secondary)] rounded-md border transition-colors cursor-pointer text-xs whitespace-nowrap ${isEditing
|
|
111
110
|
? `${(blockTheme[block.type as string] || blockTheme.default).border} bg-[var(--kyro-primary)]/5`
|
|
112
111
|
: "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
|
|
113
|
-
|
|
112
|
+
}`}
|
|
114
113
|
>
|
|
115
114
|
<div
|
|
116
115
|
className="absolute left-0.5 top-1/2 -translate-y-1/2 p-0.5 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
@@ -191,9 +190,9 @@ const SortableBlockComponent = ({
|
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
return (
|
|
194
|
-
<div ref={setNodeRef} style={style} className="relative group pl-8 mb-2">
|
|
193
|
+
<div ref={setNodeRef} style={style} className="relative group md:pl-8 mb-2">
|
|
195
194
|
<div
|
|
196
|
-
className="absolute left-0 top-1/2 -translate-y-1/2 p-1.5 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
195
|
+
className="hidden md:absolute left-0 top-1/2 -translate-y-1/2 p-1.5 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
197
196
|
{...attributes}
|
|
198
197
|
{...listeners}
|
|
199
198
|
>
|
|
@@ -202,11 +201,10 @@ const SortableBlockComponent = ({
|
|
|
202
201
|
|
|
203
202
|
<div
|
|
204
203
|
onClick={() => setEditingBlockId(block.id as string)}
|
|
205
|
-
className={`flex items-center gap-3 p-3 bg-[var(--kyro-bg-secondary)] rounded-lg border transition-colors cursor-pointer ${
|
|
206
|
-
isEditing
|
|
204
|
+
className={`flex items-center gap-3 p-3 bg-[var(--kyro-bg-secondary)] rounded-lg border transition-colors cursor-pointer ${isEditing
|
|
207
205
|
? `${(blockTheme[block.type as string] || blockTheme.default).border} bg-[var(--kyro-primary)]/5`
|
|
208
206
|
: "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
|
|
209
|
-
|
|
207
|
+
}`}
|
|
210
208
|
>
|
|
211
209
|
{blockIcons[block.type as string] && (
|
|
212
210
|
<span className="text-[var(--kyro-text-secondary)]">
|
|
@@ -394,7 +392,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
394
392
|
useEffect(() => {
|
|
395
393
|
const valueArray = Array.isArray(value) ? value : [];
|
|
396
394
|
const lastValueArray = lastValueRef.current || [];
|
|
397
|
-
|
|
395
|
+
|
|
398
396
|
// Deep compare to catch external data changes (e.g. discard draft / auto-save restore)
|
|
399
397
|
if (JSON.stringify(valueArray) !== JSON.stringify(lastValueArray)) {
|
|
400
398
|
const valueArrayCopy = [...valueArray];
|
|
@@ -534,11 +532,11 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
534
532
|
blocks.find((b) => b.id === activeDrag.id)
|
|
535
533
|
: null;
|
|
536
534
|
|
|
537
|
-
const activeBlockLabel = activeBlock
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
535
|
+
const activeBlockLabel = activeBlock
|
|
536
|
+
? "label" in activeBlock
|
|
537
|
+
? (activeBlock as Record<string, unknown>).label
|
|
538
|
+
: activeBlock.type
|
|
539
|
+
: "Block";
|
|
542
540
|
|
|
543
541
|
const borderClass = getBorderClass();
|
|
544
542
|
|