@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
|
@@ -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;
|
|
@@ -177,17 +177,64 @@ 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
|
+
const fab = document.getElementById("kyro-fab");
|
|
218
|
+
if (isOpen) {
|
|
219
|
+
sidebar.classList.remove("translate-x-0");
|
|
220
|
+
sidebar.classList.add("-translate-x-full");
|
|
221
|
+
backdrop?.classList.remove("opacity-100");
|
|
222
|
+
setTimeout(() => backdrop?.classList.add("hidden"), 300);
|
|
223
|
+
fab?.classList.remove("fab-hidden");
|
|
224
|
+
} else {
|
|
225
|
+
sidebar.classList.remove("-translate-x-full");
|
|
226
|
+
sidebar.classList.add("translate-x-0");
|
|
227
|
+
backdrop?.classList.remove("hidden");
|
|
228
|
+
setTimeout(() => backdrop?.classList.add("opacity-100"), 10);
|
|
229
|
+
fab?.classList.add("fab-hidden");
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
menuBtn?.addEventListener("click", toggleSidebar);
|
|
234
|
+
closeBtn?.addEventListener("click", toggleSidebar);
|
|
235
|
+
backdrop?.addEventListener("click", toggleSidebar);
|
|
236
|
+
</script>
|
|
237
|
+
|
|
191
238
|
<!-- Logout Confirmation Modal -->
|
|
192
239
|
<div
|
|
193
240
|
id="logout-modal"
|
|
@@ -246,6 +293,19 @@ if (includeSiteName) {
|
|
|
246
293
|
<!-- Toast Notifications (React) -->
|
|
247
294
|
<Toaster client:only="react" />
|
|
248
295
|
|
|
296
|
+
<!-- Floating Command Palette Trigger -->
|
|
297
|
+
<button
|
|
298
|
+
id="kyro-fab"
|
|
299
|
+
type="button"
|
|
300
|
+
onclick="window.openCommandPalette?.()"
|
|
301
|
+
class="fixed bottom-6 right-6 z-50 w-12 h-12 flex md:hidden items-center justify-center rounded-2xl bg-[var(--kyro-primary)] text-white shadow-lg shadow-[var(--kyro-primary)]/30 hover:shadow-xl hover:shadow-[var(--kyro-primary)]/40 hover:scale-110 active:scale-95 transition-all duration-200"
|
|
302
|
+
title="Open command palette (Cmd+K)"
|
|
303
|
+
>
|
|
304
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
305
|
+
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
|
306
|
+
</svg>
|
|
307
|
+
</button>
|
|
308
|
+
|
|
249
309
|
<!-- Theme UI Logic -->
|
|
250
310
|
<script is:inline define:vars={{ adminPath, apiPath }}>
|
|
251
311
|
const lightBtn = document.getElementById("theme-light-btn");
|
|
@@ -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);
|
|
@@ -1,59 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
import AdminLayout from "../layouts/AdminLayout.astro";
|
|
3
3
|
import { GraphQLPlayground } from "../components/GraphQLPlayground";
|
|
4
|
-
|
|
5
|
-
import { adminPath, apiPath } from "../lib/paths";
|
|
4
|
+
import { apiPath } from "../lib/paths";
|
|
6
5
|
---
|
|
7
6
|
|
|
8
7
|
<AdminLayout title="GraphQL Explorer">
|
|
9
|
-
<div class="flex-1 overflow-hidden">
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]"
|
|
16
|
-
>
|
|
17
|
-
GraphQL Explorer
|
|
18
|
-
</h1>
|
|
19
|
-
<p
|
|
20
|
-
class="text-[var(--kyro-text-secondary)] font-bold mt-2 text-sm tracking-wider"
|
|
21
|
-
>
|
|
22
|
-
Schema documentation and type explorer
|
|
23
|
-
</p>
|
|
24
|
-
</div>
|
|
25
|
-
<div class="flex items-center gap-4">
|
|
26
|
-
<a
|
|
27
|
-
href={`${adminPath}/graphql`}
|
|
28
|
-
class="flex items-center gap-2 px-4 py-2 bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)] rounded-lg font-bold text-sm hover:bg-[var(--kyro-surface)] transition-all border border-[var(--kyro-border)]"
|
|
29
|
-
>
|
|
30
|
-
<svg
|
|
31
|
-
class="w-4 h-4"
|
|
32
|
-
fill="none"
|
|
33
|
-
stroke="currentColor"
|
|
34
|
-
viewBox="0 0 24 24"
|
|
35
|
-
>
|
|
36
|
-
<path
|
|
37
|
-
stroke-linecap="round"
|
|
38
|
-
stroke-linejoin="round"
|
|
39
|
-
stroke-width="2"
|
|
40
|
-
d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
|
41
|
-
</svg>
|
|
42
|
-
Playground
|
|
43
|
-
</a>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<!-- Explorer Container -->
|
|
49
|
-
<div class="h-[calc(100vh-200px)] surface-tile overflow-hidden">
|
|
50
|
-
<div class="surface-tile h-full overflow-hidden">
|
|
51
|
-
<GraphQLPlayground
|
|
52
|
-
client:load
|
|
53
|
-
endpoint={`${apiPath}/graphql`}
|
|
54
|
-
initialShowDocs={true}
|
|
55
|
-
/>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
8
|
+
<div class="flex-1 overflow-hidden h-[calc(100vh-64px)] md:h-[calc(100vh-80px)]">
|
|
9
|
+
<GraphQLPlayground
|
|
10
|
+
client:load
|
|
11
|
+
endpoint={`${apiPath}/graphql`}
|
|
12
|
+
initialShowDocs={true}
|
|
13
|
+
/>
|
|
58
14
|
</div>
|
|
59
15
|
</AdminLayout>
|