@kyro-cms/admin 0.9.4 → 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 -564
- 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 -247
- 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/layouts/AdminLayout.astro +49 -4
- package/src/lib/useResourceManager.ts +1 -0
- package/src/styles/main.css +34 -19
|
@@ -27,6 +27,104 @@ interface PageHeaderProps {
|
|
|
27
27
|
children?: ReactNode;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function BackButton({ back }: { back: NonNullable<PageHeaderProps["back"]> }) {
|
|
31
|
+
if (back.href) {
|
|
32
|
+
return (
|
|
33
|
+
<a
|
|
34
|
+
href={back.href}
|
|
35
|
+
onClick={(e) => {
|
|
36
|
+
if (back.onClick) {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
back.onClick();
|
|
39
|
+
}
|
|
40
|
+
}}
|
|
41
|
+
className="p-1.5 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
42
|
+
>
|
|
43
|
+
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
44
|
+
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
45
|
+
</svg>
|
|
46
|
+
</a>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return (
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
onClick={back.onClick}
|
|
53
|
+
className="p-1.5 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
54
|
+
>
|
|
55
|
+
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
56
|
+
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
57
|
+
</svg>
|
|
58
|
+
</button>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function DesktopBreadcrumbs({ breadcrumbs }: { breadcrumbs: Breadcrumb[] }) {
|
|
63
|
+
return breadcrumbs?.map((crumb: Breadcrumb, i: number) => (
|
|
64
|
+
<React.Fragment key={i}>
|
|
65
|
+
{i > 0 && <span className="opacity-20 text-[10px]">/</span>}
|
|
66
|
+
{crumb.href || crumb.onClick ? (
|
|
67
|
+
<a
|
|
68
|
+
href={crumb.href}
|
|
69
|
+
onClick={(e) => {
|
|
70
|
+
if (crumb.onClick) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
crumb.onClick();
|
|
73
|
+
}
|
|
74
|
+
}}
|
|
75
|
+
className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] transition-all"
|
|
76
|
+
>
|
|
77
|
+
{crumb.label}
|
|
78
|
+
</a>
|
|
79
|
+
) : (
|
|
80
|
+
<span className="text-[10px] font-bold tracking-widest opacity-40">
|
|
81
|
+
{crumb.label}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
</React.Fragment>
|
|
85
|
+
));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ActionsSlot({ actions }: { actions: NonNullable<PageHeaderProps["actions"]> }) {
|
|
89
|
+
if (Array.isArray(actions)) {
|
|
90
|
+
return (
|
|
91
|
+
<div className="flex items-center gap-3">
|
|
92
|
+
{actions.map((act, i) => (
|
|
93
|
+
<button
|
|
94
|
+
key={i}
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={act.onClick}
|
|
97
|
+
className={`flex items-center gap-2 px-6 py-2.5 rounded-xl font-bold text-sm transition-all shadow-lg shadow-[var(--kyro-primary)]/10 ${
|
|
98
|
+
act.variant === "outline"
|
|
99
|
+
? "border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
100
|
+
: act.variant === "ghost"
|
|
101
|
+
? "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] shadow-none"
|
|
102
|
+
: "kyro-btn-primary hover:opacity-90"
|
|
103
|
+
} ${act.className || ""}`}
|
|
104
|
+
>
|
|
105
|
+
{act.icon && <act.icon className="w-4 h-4" />}
|
|
106
|
+
{act.label}
|
|
107
|
+
</button>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return <>{actions}</>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function SingleAction({ action }: { action: NonNullable<PageHeaderProps["action"]> }) {
|
|
116
|
+
return (
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
onClick={action.onClick}
|
|
120
|
+
className={`kyro-btn kyro-btn-primary flex items-center gap-2 px-6 py-2.5 rounded-xl font-bold text-sm hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/10 w-full lg:w-auto justify-center ${action.className || ""}`}
|
|
121
|
+
>
|
|
122
|
+
{action.icon && <action.icon className="w-4 h-4" />}
|
|
123
|
+
{action.label}
|
|
124
|
+
</button>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
30
128
|
export function PageHeader({
|
|
31
129
|
title,
|
|
32
130
|
description,
|
|
@@ -38,66 +136,75 @@ export function PageHeader({
|
|
|
38
136
|
actions,
|
|
39
137
|
children,
|
|
40
138
|
}: PageHeaderProps) {
|
|
139
|
+
const lastBreadcrumb = breadcrumbs?.[breadcrumbs.length - 1];
|
|
140
|
+
|
|
41
141
|
return (
|
|
42
|
-
<div className="
|
|
43
|
-
|
|
44
|
-
|
|
142
|
+
<div className="surface-tile px-3 md:px-6 py-3 md:pt-4 mb-4 md:mb-8">
|
|
143
|
+
{/* ─── MOBILE ─── */}
|
|
144
|
+
<div className="md:hidden space-y-2">
|
|
45
145
|
{(breadcrumbs || back) && (
|
|
46
|
-
<div className="flex items-center gap-2
|
|
47
|
-
{back &&
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}}
|
|
56
|
-
className="p-1.5 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
57
|
-
>
|
|
58
|
-
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
59
|
-
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
146
|
+
<div className="flex items-center gap-2">
|
|
147
|
+
{back && <BackButton back={back} />}
|
|
148
|
+
<details className="group [&::-webkit-details-marker]:hidden flex-1 min-w-0">
|
|
149
|
+
<summary className="flex items-center gap-2 cursor-pointer list-none">
|
|
150
|
+
<span className="flex-1 text-[10px] font-bold tracking-widest text-[var(--kyro-text-secondary)] truncate">
|
|
151
|
+
{lastBreadcrumb?.label || ""}
|
|
152
|
+
</span>
|
|
153
|
+
<svg className="w-3 h-3 text-[var(--kyro-text-secondary)] opacity-40 group-open:rotate-180 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
154
|
+
<path d="M6 9l6 6 6-6" />
|
|
60
155
|
</svg>
|
|
61
|
-
</
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
156
|
+
</summary>
|
|
157
|
+
<div className="mt-2 pt-2 border-t border-[var(--kyro-border)] space-y-2">
|
|
158
|
+
{breadcrumbs && (
|
|
159
|
+
<div className="flex items-center gap-2">
|
|
160
|
+
{breadcrumbs.map((crumb: Breadcrumb, i: number) => (
|
|
161
|
+
<React.Fragment key={i}>
|
|
162
|
+
{i > 0 && <span className="opacity-20 text-[10px]">/</span>}
|
|
163
|
+
{crumb.href || crumb.onClick ? (
|
|
164
|
+
<a
|
|
165
|
+
href={crumb.href}
|
|
166
|
+
onClick={(e) => {
|
|
167
|
+
if (crumb.onClick) { e.preventDefault(); crumb.onClick(); }
|
|
168
|
+
}}
|
|
169
|
+
className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] transition-all"
|
|
170
|
+
>
|
|
171
|
+
{crumb.label}
|
|
172
|
+
</a>
|
|
173
|
+
) : (
|
|
174
|
+
<span className="text-[10px] font-bold tracking-widest opacity-40">{crumb.label}</span>
|
|
175
|
+
)}
|
|
176
|
+
</React.Fragment>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
83
179
|
)}
|
|
84
|
-
|
|
85
|
-
|
|
180
|
+
{metadata && (
|
|
181
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
182
|
+
{metadata.map((item, i) => (
|
|
183
|
+
<React.Fragment key={i}>{item}</React.Fragment>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
{children}
|
|
188
|
+
</div>
|
|
189
|
+
</details>
|
|
86
190
|
</div>
|
|
87
191
|
)}
|
|
88
192
|
|
|
89
|
-
<div className="flex items-center gap-
|
|
90
|
-
{Icon && <Icon className="w-
|
|
193
|
+
<div className="flex items-center gap-2">
|
|
194
|
+
{Icon && <Icon className="w-5 h-5 text-[var(--kyro-primary)] shrink-0" />}
|
|
91
195
|
{title && (
|
|
92
|
-
<h1 className="text-
|
|
196
|
+
<h1 className="text-lg font-bold tracking-tighter text-[var(--kyro-text-primary)] truncate">
|
|
93
197
|
{title}
|
|
94
198
|
</h1>
|
|
95
199
|
)}
|
|
200
|
+
{metadata && !description && (
|
|
201
|
+
<span className="h-2 w-2 rounded-full bg-[var(--kyro-primary)] shrink-0" />
|
|
202
|
+
)}
|
|
96
203
|
</div>
|
|
97
204
|
|
|
98
205
|
{description && (
|
|
99
|
-
<div className="flex items-center gap-2
|
|
100
|
-
<p className="text-[var(--kyro-text-secondary)] font-medium opacity-60 line-clamp-1">
|
|
206
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
207
|
+
<p className="text-[var(--kyro-text-secondary)] font-medium opacity-60 line-clamp-1 min-w-0 text-xs">
|
|
101
208
|
{description}
|
|
102
209
|
</p>
|
|
103
210
|
{metadata && (
|
|
@@ -110,49 +217,64 @@ export function PageHeader({
|
|
|
110
217
|
))}
|
|
111
218
|
</div>
|
|
112
219
|
)}
|
|
113
|
-
{children}
|
|
114
220
|
</div>
|
|
115
221
|
)}
|
|
116
222
|
</div>
|
|
117
223
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
type="button"
|
|
126
|
-
onClick={act.onClick}
|
|
127
|
-
className={`flex items-center gap-2 px-6 py-2.5 rounded-xl font-bold text-sm transition-all shadow-lg shadow-[var(--kyro-primary)]/10 ${
|
|
128
|
-
act.variant === "outline"
|
|
129
|
-
? "border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
130
|
-
: act.variant === "ghost"
|
|
131
|
-
? "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] shadow-none"
|
|
132
|
-
: "kyro-btn-primary hover:opacity-90"
|
|
133
|
-
} ${act.className || ""}`}
|
|
134
|
-
>
|
|
135
|
-
{act.icon && <act.icon className="w-4 h-4" />}
|
|
136
|
-
{act.label}
|
|
137
|
-
</button>
|
|
138
|
-
))}
|
|
224
|
+
{/* ─── DESKTOP ─── */}
|
|
225
|
+
<div className="hidden md:flex md:flex-row md:items-center justify-between gap-6">
|
|
226
|
+
<div className="min-w-0 flex-1">
|
|
227
|
+
{(breadcrumbs || back) && (
|
|
228
|
+
<div className="flex items-center gap-2 mb-3">
|
|
229
|
+
{back && <BackButton back={back} />}
|
|
230
|
+
{breadcrumbs && <DesktopBreadcrumbs breadcrumbs={breadcrumbs} />}
|
|
139
231
|
</div>
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
<div className="flex items-center gap-3">
|
|
235
|
+
{Icon && <Icon className="w-6 h-6 text-[var(--kyro-primary)]" />}
|
|
236
|
+
{title && (
|
|
237
|
+
<h1 className="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)] truncate">
|
|
238
|
+
{title}
|
|
239
|
+
</h1>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
{(description || metadata) && (
|
|
244
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 mt-1">
|
|
245
|
+
{description && (
|
|
246
|
+
<p className="text-[var(--kyro-text-secondary)] font-medium opacity-60 line-clamp-1 min-w-0">
|
|
247
|
+
{description}
|
|
248
|
+
</p>
|
|
249
|
+
)}
|
|
250
|
+
{metadata && (
|
|
251
|
+
<div className="flex items-center gap-2">
|
|
252
|
+
{metadata.map((item: ReactNode, i: number) => (
|
|
253
|
+
<React.Fragment key={i}>
|
|
254
|
+
{i === 0 && (description || i > 0) && <span className="opacity-20 ml-1">·</span>}
|
|
255
|
+
{item}
|
|
256
|
+
</React.Fragment>
|
|
257
|
+
))}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
{children}
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div className="flex items-center gap-3 flex-wrap shrink-0">
|
|
266
|
+
{actions && <ActionsSlot actions={actions} />}
|
|
267
|
+
{action && <SingleAction action={action} />}
|
|
268
|
+
</div>
|
|
154
269
|
</div>
|
|
270
|
+
|
|
271
|
+
{/* mobile actions */}
|
|
272
|
+
{(actions || action) && (
|
|
273
|
+
<div className="md:hidden flex items-center gap-2 mt-3 pt-3 border-t border-[var(--kyro-border)]">
|
|
274
|
+
{action && <SingleAction action={action} />}
|
|
275
|
+
{actions && <ActionsSlot actions={actions} />}
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
155
278
|
</div>
|
|
156
279
|
);
|
|
157
280
|
}
|
|
158
|
-
|
|
@@ -13,7 +13,7 @@ export function Pagination({ page, totalPages, totalDocs, limit, onPageChange, o
|
|
|
13
13
|
if (totalPages <= 1) return null;
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
|
-
<div className="flex items-center justify-between px-4 py-3 border-t border-[var(--kyro-border)]">
|
|
16
|
+
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 px-4 py-3 border-t border-[var(--kyro-border)]">
|
|
17
17
|
{totalDocs !== undefined && limit ? (
|
|
18
18
|
<span className="text-xs text-[var(--kyro-text-secondary)] font-medium">
|
|
19
19
|
Showing {(page - 1) * limit + 1} to {Math.min(page * limit, totalDocs)} of {totalDocs}
|
|
@@ -21,7 +21,7 @@ export function Pagination({ page, totalPages, totalDocs, limit, onPageChange, o
|
|
|
21
21
|
) : (
|
|
22
22
|
<span />
|
|
23
23
|
)}
|
|
24
|
-
<div className="flex items-center gap-2">
|
|
24
|
+
<div className="flex items-center gap-2 flex-wrap justify-center">
|
|
25
25
|
{onLimitChange && (
|
|
26
26
|
<select
|
|
27
27
|
value={limit}
|
|
@@ -47,10 +47,10 @@ export function SlidePanel({
|
|
|
47
47
|
}, [open, onClose]);
|
|
48
48
|
|
|
49
49
|
const widthClasses = {
|
|
50
|
-
sm: "w-[320px]",
|
|
51
|
-
md: "w-[400px]",
|
|
52
|
-
lg: "w-[550px]",
|
|
53
|
-
xl: "w-[700px]",
|
|
50
|
+
sm: "w-full sm:w-[320px]",
|
|
51
|
+
md: "w-full sm:w-[400px]",
|
|
52
|
+
lg: "w-full sm:w-[550px]",
|
|
53
|
+
xl: "w-full sm:w-[700px]",
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
if (!open || !hydrated) return null;
|
|
@@ -52,7 +52,6 @@ export function Toast({ type, message, onClose }: ToastProps) {
|
|
|
52
52
|
onMouseEnter={() => setIsPaused(true)}
|
|
53
53
|
onMouseLeave={() => setIsPaused(false)}
|
|
54
54
|
>
|
|
55
|
-
<div className="kyro-toast-accent" />
|
|
56
55
|
<div className="kyro-toast-icon-container">
|
|
57
56
|
<Icon className="w-4 h-4" />
|
|
58
57
|
</div>
|
|
@@ -61,7 +60,7 @@ export function Toast({ type, message, onClose }: ToastProps) {
|
|
|
61
60
|
</div>
|
|
62
61
|
<button
|
|
63
62
|
type="button"
|
|
64
|
-
className="kyro-toast-close
|
|
63
|
+
className="kyro-toast-close"
|
|
65
64
|
onClick={onClose}
|
|
66
65
|
>
|
|
67
66
|
<X className="w-3.5 h-3.5" />
|
|
@@ -177,17 +177,62 @@ if (includeSiteName) {
|
|
|
177
177
|
})();
|
|
178
178
|
</script>
|
|
179
179
|
</head>
|
|
180
|
-
<body class="bg-[var(--kyro-bg)] antialiased text-[var(--kyro-text-primary)]">
|
|
180
|
+
<body class="bg-[var(--kyro-bg)] antialiased text-[var(--kyro-text-primary)] overflow-x-hidden overflow-y-auto">
|
|
181
181
|
<div id="kyro-user-data" data-user=""></div>
|
|
182
|
-
|
|
182
|
+
|
|
183
|
+
<!-- Mobile Sidebar Backdrop -->
|
|
184
|
+
<div id="mobile-sidebar-backdrop" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-40 hidden md:hidden transition-opacity opacity-0 duration-300"></div>
|
|
185
|
+
|
|
186
|
+
<div class="flex h-[100dvh] md:h-screen p-0 md:p-6 gap-0 md:gap-6 overflow-hidden w-full relative">
|
|
183
187
|
<Sidebar title={title} />
|
|
184
188
|
|
|
185
189
|
<!-- Main Content Column -->
|
|
186
|
-
<main class="flex-1 flex flex-col gap-6 overflow-hidden">
|
|
187
|
-
|
|
190
|
+
<main class="flex-1 flex flex-col gap-0 md:gap-6 overflow-hidden relative w-full h-full max-w-full">
|
|
191
|
+
<!-- Mobile Header -->
|
|
192
|
+
<header class="md:hidden flex items-center justify-between p-4 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)] shrink-0 z-30 shadow-sm">
|
|
193
|
+
<div class="flex items-center gap-3">
|
|
194
|
+
<button id="mobile-menu-btn" class="p-2 -ml-2 rounded-lg text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors">
|
|
195
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
|
|
196
|
+
</button>
|
|
197
|
+
<span class="font-bold text-lg">{title}</span>
|
|
198
|
+
</div>
|
|
199
|
+
</header>
|
|
200
|
+
|
|
201
|
+
<div class="flex-1 overflow-y-auto p-4 md:p-0">
|
|
202
|
+
<slot />
|
|
203
|
+
</div>
|
|
188
204
|
</main>
|
|
189
205
|
</div>
|
|
190
206
|
|
|
207
|
+
<!-- Mobile Sidebar Logic -->
|
|
208
|
+
<script is:inline>
|
|
209
|
+
const menuBtn = document.getElementById("mobile-menu-btn");
|
|
210
|
+
const closeBtn = document.getElementById("mobile-close-btn");
|
|
211
|
+
const backdrop = document.getElementById("mobile-sidebar-backdrop");
|
|
212
|
+
const sidebar = document.getElementById("kyro-sidebar");
|
|
213
|
+
|
|
214
|
+
const toggleSidebar = () => {
|
|
215
|
+
if (!sidebar) return;
|
|
216
|
+
const isOpen = sidebar.classList.contains("translate-x-0");
|
|
217
|
+
if (isOpen) {
|
|
218
|
+
sidebar.classList.remove("translate-x-0");
|
|
219
|
+
sidebar.classList.add("-translate-x-full");
|
|
220
|
+
backdrop?.classList.remove("opacity-100");
|
|
221
|
+
setTimeout(() => backdrop?.classList.add("hidden"), 300);
|
|
222
|
+
} else {
|
|
223
|
+
sidebar.classList.remove("-translate-x-full");
|
|
224
|
+
sidebar.classList.add("translate-x-0");
|
|
225
|
+
backdrop?.classList.remove("hidden");
|
|
226
|
+
// small delay to allow display:block to apply before opacity transition
|
|
227
|
+
setTimeout(() => backdrop?.classList.add("opacity-100"), 10);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
menuBtn?.addEventListener("click", toggleSidebar);
|
|
232
|
+
closeBtn?.addEventListener("click", toggleSidebar);
|
|
233
|
+
backdrop?.addEventListener("click", toggleSidebar);
|
|
234
|
+
</script>
|
|
235
|
+
|
|
191
236
|
<!-- Logout Confirmation Modal -->
|
|
192
237
|
<div
|
|
193
238
|
id="logout-modal"
|
|
@@ -50,6 +50,7 @@ export function useResourceManager<T extends { id: string }>(
|
|
|
50
50
|
await apiDelete(`${options.endpoint}/${id}`);
|
|
51
51
|
setItems((prev) => prev.filter((item) => item.id !== id));
|
|
52
52
|
options.onSuccess?.("delete", id);
|
|
53
|
+
toast.success(`${resourceName} deleted`);
|
|
53
54
|
} catch (e: unknown) {
|
|
54
55
|
const message = e instanceof Error ? e.message : `Failed to delete ${resourceName}`;
|
|
55
56
|
toast.error(message);
|
package/src/styles/main.css
CHANGED
|
@@ -1393,14 +1393,14 @@
|
|
|
1393
1393
|
color: var(--kyro-text-secondary);
|
|
1394
1394
|
}
|
|
1395
1395
|
|
|
1396
|
-
/* Toast —
|
|
1396
|
+
/* Toast — Subtle Modern */
|
|
1397
1397
|
.kyro-toasts-container {
|
|
1398
1398
|
position: fixed;
|
|
1399
1399
|
bottom: 16px;
|
|
1400
1400
|
right: 16px;
|
|
1401
1401
|
display: flex;
|
|
1402
1402
|
flex-direction: column;
|
|
1403
|
-
gap:
|
|
1403
|
+
gap: 8px;
|
|
1404
1404
|
z-index: 9999;
|
|
1405
1405
|
pointer-events: none;
|
|
1406
1406
|
}
|
|
@@ -1408,25 +1408,34 @@
|
|
|
1408
1408
|
.kyro-toast {
|
|
1409
1409
|
display: flex;
|
|
1410
1410
|
align-items: center;
|
|
1411
|
-
gap:
|
|
1411
|
+
gap: 10px;
|
|
1412
1412
|
min-width: 240px;
|
|
1413
1413
|
max-width: 340px;
|
|
1414
|
-
padding:
|
|
1415
|
-
|
|
1416
|
-
border-radius: 8px;
|
|
1414
|
+
padding: 12px 14px;
|
|
1415
|
+
border-radius: 12px;
|
|
1417
1416
|
box-shadow:
|
|
1418
|
-
0
|
|
1419
|
-
0 8px
|
|
1420
|
-
0 16px 24px -8px rgba(0, 0, 0, 0.2);
|
|
1417
|
+
0 4px 12px rgba(0, 0, 0, 0.06),
|
|
1418
|
+
0 8px 24px rgba(0, 0, 0, 0.04);
|
|
1421
1419
|
pointer-events: auto;
|
|
1422
1420
|
position: relative;
|
|
1423
|
-
|
|
1421
|
+
overflow: hidden;
|
|
1422
|
+
border: 1px solid var(--kyro-border);
|
|
1423
|
+
background: var(--kyro-surface);
|
|
1424
1424
|
}
|
|
1425
1425
|
|
|
1426
|
-
.kyro-toast
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1426
|
+
.kyro-toast::before {
|
|
1427
|
+
content: '';
|
|
1428
|
+
position: absolute;
|
|
1429
|
+
left: 0;
|
|
1430
|
+
top: 0;
|
|
1431
|
+
bottom: 0;
|
|
1432
|
+
width: 3px;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.kyro-toast-success::before { background: var(--kyro-success); }
|
|
1436
|
+
.kyro-toast-error::before { background: var(--kyro-danger); }
|
|
1437
|
+
.kyro-toast-warning::before { background: var(--kyro-warning); }
|
|
1438
|
+
.kyro-toast-info::before { background: #6366f1; }
|
|
1430
1439
|
|
|
1431
1440
|
.kyro-toast-icon-container {
|
|
1432
1441
|
flex-shrink: 0;
|
|
@@ -1435,9 +1444,13 @@
|
|
|
1435
1444
|
display: flex;
|
|
1436
1445
|
align-items: center;
|
|
1437
1446
|
justify-content: center;
|
|
1438
|
-
color: rgba(255, 255, 255, 0.85);
|
|
1439
1447
|
}
|
|
1440
1448
|
|
|
1449
|
+
.kyro-toast-success .kyro-toast-icon-container { color: var(--kyro-success); }
|
|
1450
|
+
.kyro-toast-error .kyro-toast-icon-container { color: var(--kyro-danger); }
|
|
1451
|
+
.kyro-toast-warning .kyro-toast-icon-container { color: var(--kyro-warning); }
|
|
1452
|
+
.kyro-toast-info .kyro-toast-icon-container { color: #6366f1; }
|
|
1453
|
+
|
|
1441
1454
|
.kyro-toast-content {
|
|
1442
1455
|
flex: 1;
|
|
1443
1456
|
min-width: 0;
|
|
@@ -1447,21 +1460,23 @@
|
|
|
1447
1460
|
font-size: 12px;
|
|
1448
1461
|
font-weight: 500;
|
|
1449
1462
|
letter-spacing: -0.01em;
|
|
1450
|
-
color:
|
|
1463
|
+
color: var(--kyro-text-primary);
|
|
1451
1464
|
line-height: 1.3;
|
|
1452
1465
|
}
|
|
1453
1466
|
|
|
1454
1467
|
.kyro-toast-close {
|
|
1455
1468
|
padding: 3px;
|
|
1456
1469
|
border-radius: 4px;
|
|
1457
|
-
color:
|
|
1470
|
+
color: var(--kyro-text-muted);
|
|
1458
1471
|
transition: all 0.15s ease;
|
|
1459
1472
|
flex-shrink: 0;
|
|
1473
|
+
opacity: 0.4;
|
|
1460
1474
|
}
|
|
1461
1475
|
|
|
1462
1476
|
.kyro-toast-close:hover {
|
|
1463
|
-
background:
|
|
1464
|
-
|
|
1477
|
+
background: var(--kyro-surface-accent);
|
|
1478
|
+
opacity: 1;
|
|
1479
|
+
color: var(--kyro-text-primary);
|
|
1465
1480
|
}
|
|
1466
1481
|
|
|
1467
1482
|
/* Spinner — Monochrome */
|