@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
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import { IconPlus, IconSend, IconClock, IconArchive, IconUndo, IconCopy, IconEye
|
|
|
3
3
|
import { DropdownItem, DropdownSeparator } from "./ui/Dropdown";
|
|
4
4
|
import { SplitButton } from "./ui/SplitButton";
|
|
5
5
|
import { Spinner } from "./ui/Spinner";
|
|
6
|
+
import { useAutoFormStore } from "../lib/autoform-store";
|
|
6
7
|
|
|
7
8
|
export type DocumentStatus = "draft" | "published" | "scheduled" | "archived";
|
|
8
9
|
export type SaveStatus = "idle" | "saving" | "saved" | "error";
|
|
@@ -18,6 +19,8 @@ export interface ActionBarProps {
|
|
|
18
19
|
onViewHistory?: () => void;
|
|
19
20
|
onPreview?: () => void;
|
|
20
21
|
onDelete?: () => void;
|
|
22
|
+
onBack?: () => void;
|
|
23
|
+
onToggleSidebar?: () => void;
|
|
21
24
|
publishedAt?: string | null;
|
|
22
25
|
updatedAt?: string | null;
|
|
23
26
|
}
|
|
@@ -33,9 +36,14 @@ export function ActionBar({
|
|
|
33
36
|
onViewHistory,
|
|
34
37
|
onPreview,
|
|
35
38
|
onDelete,
|
|
39
|
+
onBack,
|
|
40
|
+
onToggleSidebar,
|
|
36
41
|
publishedAt,
|
|
37
42
|
updatedAt,
|
|
38
43
|
}: ActionBarProps) {
|
|
44
|
+
const view = useAutoFormStore((s) => s.view) || "edit";
|
|
45
|
+
const setView = useAutoFormStore((s) => s.setView);
|
|
46
|
+
|
|
39
47
|
const getSaveStatusText = () => {
|
|
40
48
|
if (saveStatus === "saving") return "Saving...";
|
|
41
49
|
if (saveStatus === "saved") return "Saved";
|
|
@@ -84,13 +92,103 @@ export function ActionBar({
|
|
|
84
92
|
};
|
|
85
93
|
|
|
86
94
|
return (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
<>
|
|
96
|
+
{/* ─── MOBILE LAYOUT ─── */}
|
|
97
|
+
<div className="md:hidden flex flex-col gap-3 py-3 px-4 bg-[var(--kyro-bg)] border-t border-[var(--kyro-border)]">
|
|
98
|
+
{/* Row 1: Back + icon buttons */}
|
|
99
|
+
<div className="flex items-center gap-1">
|
|
100
|
+
{onBack && (
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
onClick={onBack}
|
|
104
|
+
className="p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
105
|
+
>
|
|
106
|
+
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
107
|
+
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
108
|
+
</svg>
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
<div className="flex items-center gap-1 ml-auto">
|
|
112
|
+
{onPreview && (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={onPreview}
|
|
116
|
+
className="p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
117
|
+
title="Preview"
|
|
118
|
+
>
|
|
119
|
+
<IconEye className="w-4 h-4" />
|
|
120
|
+
</button>
|
|
121
|
+
)}
|
|
122
|
+
{onViewHistory && (
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={onViewHistory}
|
|
126
|
+
className="p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
127
|
+
title="View History"
|
|
128
|
+
>
|
|
129
|
+
<IconClock className="w-4 h-4" />
|
|
130
|
+
</button>
|
|
131
|
+
)}
|
|
132
|
+
{onDuplicate && (
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
onClick={onDuplicate}
|
|
136
|
+
className="p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
137
|
+
title="Duplicate"
|
|
138
|
+
>
|
|
139
|
+
<IconCopy className="w-4 h-4" />
|
|
140
|
+
</button>
|
|
141
|
+
)}
|
|
142
|
+
{onDelete && (
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
onClick={onDelete}
|
|
146
|
+
className="p-2 rounded-lg hover:bg-[var(--kyro-danger-bg)] text-[var(--kyro-error)] transition-all"
|
|
147
|
+
title="Delete"
|
|
148
|
+
>
|
|
149
|
+
<IconTrash2 className="w-4 h-4" />
|
|
150
|
+
</button>
|
|
151
|
+
)}
|
|
152
|
+
{onToggleSidebar && (
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
onClick={onToggleSidebar}
|
|
156
|
+
className="p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
157
|
+
title="Toggle Sidebar"
|
|
158
|
+
>
|
|
159
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
160
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
161
|
+
<line x1="9" y1="3" x2="9" y2="21" />
|
|
162
|
+
</svg>
|
|
163
|
+
</button>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{/* Row 2: Edit / Version / API tabs */}
|
|
169
|
+
<div className="flex items-center gap-1 bg-[var(--kyro-bg-secondary)] p-0.5 rounded-lg border border-[var(--kyro-border)] self-start">
|
|
170
|
+
{(["edit", "version", "api"] as const).map((v) => (
|
|
171
|
+
<button
|
|
172
|
+
key={v}
|
|
173
|
+
type="button"
|
|
174
|
+
onClick={() => setView(v)}
|
|
175
|
+
className={`px-4 py-1.5 text-xs font-bold rounded-md transition-all ${
|
|
176
|
+
view === v
|
|
177
|
+
? "bg-[var(--kyro-surface)] shadow-sm border border-[var(--kyro-border)] text-[var(--kyro-text-primary)]"
|
|
178
|
+
: "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"
|
|
179
|
+
}`}
|
|
180
|
+
>
|
|
181
|
+
{v === "edit" ? "Edit" : v === "version" ? "Version" : "API"}
|
|
182
|
+
</button>
|
|
183
|
+
))}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{/* Row 3: Status + timestamps */}
|
|
187
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
90
188
|
{getStatusBadge()}
|
|
91
189
|
{getSaveStatusText() && (
|
|
92
190
|
<span
|
|
93
|
-
className={`text-
|
|
191
|
+
className={`text-xs ${saveStatus === "error" ? "text-[var(--kyro-error)]" : "text-[var(--kyro-text-muted)]"}`}
|
|
94
192
|
>
|
|
95
193
|
{saveStatus === "saving" ? (
|
|
96
194
|
<Spinner size="sm" className="inline mr-1" />
|
|
@@ -98,83 +196,169 @@ export function ActionBar({
|
|
|
98
196
|
{getSaveStatusText()}
|
|
99
197
|
</span>
|
|
100
198
|
)}
|
|
199
|
+
<span className="text-[10px] text-[var(--kyro-text-muted)] ml-auto">
|
|
200
|
+
{updatedAt && `Updated ${formatDate(updatedAt)}`}
|
|
201
|
+
{publishedAt && status === "published" && ` · Published ${formatDate(publishedAt)}`}
|
|
202
|
+
</span>
|
|
101
203
|
</div>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
204
|
+
|
|
205
|
+
{/* Row 4: Publish + Save */}
|
|
206
|
+
<div className="flex items-center gap-2">
|
|
207
|
+
{status === "draft" && onPublish && (
|
|
208
|
+
<button type="button"
|
|
209
|
+
onClick={onPublish}
|
|
210
|
+
disabled={saveStatus === "saving"}
|
|
211
|
+
className="kyro-btn kyro-btn-primary kyro-btn-md flex items-center gap-2 flex-1 justify-center"
|
|
212
|
+
>
|
|
213
|
+
<IconSend className="w-4 h-4" />
|
|
214
|
+
Publish
|
|
215
|
+
</button>
|
|
107
216
|
)}
|
|
108
|
-
{
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
217
|
+
{status === "published" && onUnpublish && (
|
|
218
|
+
<button type="button"
|
|
219
|
+
onClick={onUnpublish}
|
|
220
|
+
disabled={saveStatus === "saving"}
|
|
221
|
+
className="kyro-btn kyro-btn-secondary kyro-btn-md flex items-center gap-2 flex-1 justify-center"
|
|
222
|
+
>
|
|
223
|
+
<IconUndo className="w-4 h-4" />
|
|
224
|
+
Unpublish
|
|
225
|
+
</button>
|
|
112
226
|
)}
|
|
227
|
+
<div className="flex-1">
|
|
228
|
+
<SplitButton
|
|
229
|
+
status={status}
|
|
230
|
+
saveStatus={saveStatus}
|
|
231
|
+
hasChanges={hasChanges}
|
|
232
|
+
onPublish={onSave}
|
|
233
|
+
>
|
|
234
|
+
{onDuplicate && (
|
|
235
|
+
<DropdownItem
|
|
236
|
+
icon={<IconCopy className="w-4 h-4" />}
|
|
237
|
+
>
|
|
238
|
+
Duplicate
|
|
239
|
+
</DropdownItem>
|
|
240
|
+
)}
|
|
241
|
+
{onViewHistory && (
|
|
242
|
+
<DropdownItem
|
|
243
|
+
icon={<IconClock className="w-4 h-4" />}
|
|
244
|
+
>
|
|
245
|
+
View History
|
|
246
|
+
</DropdownItem>
|
|
247
|
+
)}
|
|
248
|
+
{onPreview && (
|
|
249
|
+
<DropdownItem
|
|
250
|
+
icon={<IconEye className="w-4 h-4" />}
|
|
251
|
+
>
|
|
252
|
+
Preview
|
|
253
|
+
</DropdownItem>
|
|
254
|
+
)}
|
|
255
|
+
{(onDuplicate || onViewHistory || onPreview) && <DropdownSeparator />}
|
|
256
|
+
{onDelete && (
|
|
257
|
+
<DropdownItem
|
|
258
|
+
onClick={onDelete}
|
|
259
|
+
danger
|
|
260
|
+
icon={<IconTrash2 className="w-4 h-4" />}
|
|
261
|
+
>
|
|
262
|
+
Delete
|
|
263
|
+
</DropdownItem>
|
|
264
|
+
)}
|
|
265
|
+
</SplitButton>
|
|
266
|
+
</div>
|
|
113
267
|
</div>
|
|
114
268
|
</div>
|
|
115
269
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
270
|
+
{/* ─── DESKTOP LAYOUT ─── */}
|
|
271
|
+
<div className="hidden md:flex flex-row items-center justify-between gap-4 py-3 px-6 bg-transparent border-0 static z-40">
|
|
272
|
+
<div className="flex flex-row items-center gap-4">
|
|
273
|
+
<div className="flex items-center gap-2">
|
|
274
|
+
{getStatusBadge()}
|
|
275
|
+
{getSaveStatusText() && (
|
|
276
|
+
<span
|
|
277
|
+
className={`text-sm ${saveStatus === "error" ? "text-[var(--kyro-error)]" : "text-[var(--kyro-text-muted)]"}`}
|
|
278
|
+
>
|
|
279
|
+
{saveStatus === "saving" ? (
|
|
280
|
+
<Spinner size="sm" className="inline mr-1" />
|
|
281
|
+
) : null}
|
|
282
|
+
{getSaveStatusText()}
|
|
283
|
+
</span>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
<div className="text-xs space-y-0.5">
|
|
287
|
+
{updatedAt && (
|
|
288
|
+
<div className="text-[var(--kyro-text-muted)]">
|
|
289
|
+
Updated: {formatDate(updatedAt)}
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
{publishedAt && status === "published" && (
|
|
293
|
+
<div className="text-[var(--kyro-primary)] font-medium">
|
|
294
|
+
Published: {formatDate(publishedAt)}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
137
299
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
>
|
|
145
|
-
{onDuplicate && (
|
|
146
|
-
<DropdownItem
|
|
147
|
-
icon={<IconCopy className="w-4 h-4" />}
|
|
148
|
-
>
|
|
149
|
-
Duplicate
|
|
150
|
-
</DropdownItem>
|
|
151
|
-
)}
|
|
152
|
-
{onViewHistory && (
|
|
153
|
-
<DropdownItem
|
|
154
|
-
icon={<IconClock className="w-4 h-4" />}
|
|
300
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
301
|
+
{status === "draft" && onPublish && (
|
|
302
|
+
<button type="button"
|
|
303
|
+
onClick={onPublish}
|
|
304
|
+
disabled={saveStatus === "saving"}
|
|
305
|
+
className="kyro-btn kyro-btn-primary kyro-btn-md flex items-center gap-2"
|
|
155
306
|
>
|
|
156
|
-
|
|
157
|
-
|
|
307
|
+
<IconSend className="w-4 h-4" />
|
|
308
|
+
Publish
|
|
309
|
+
</button>
|
|
158
310
|
)}
|
|
159
|
-
{
|
|
160
|
-
<
|
|
161
|
-
|
|
311
|
+
{status === "published" && onUnpublish && (
|
|
312
|
+
<button type="button"
|
|
313
|
+
onClick={onUnpublish}
|
|
314
|
+
disabled={saveStatus === "saving"}
|
|
315
|
+
className="kyro-btn kyro-btn-secondary kyro-btn-md flex items-center gap-2"
|
|
162
316
|
>
|
|
163
|
-
|
|
164
|
-
|
|
317
|
+
<IconUndo className="w-4 h-4" />
|
|
318
|
+
Unpublish
|
|
319
|
+
</button>
|
|
165
320
|
)}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
321
|
+
|
|
322
|
+
<SplitButton
|
|
323
|
+
status={status}
|
|
324
|
+
saveStatus={saveStatus}
|
|
325
|
+
hasChanges={hasChanges}
|
|
326
|
+
onPublish={onSave}
|
|
327
|
+
>
|
|
328
|
+
{onDuplicate && (
|
|
329
|
+
<DropdownItem
|
|
330
|
+
icon={<IconCopy className="w-4 h-4" />}
|
|
331
|
+
>
|
|
332
|
+
Duplicate
|
|
333
|
+
</DropdownItem>
|
|
334
|
+
)}
|
|
335
|
+
{onViewHistory && (
|
|
336
|
+
<DropdownItem
|
|
337
|
+
icon={<IconClock className="w-4 h-4" />}
|
|
338
|
+
>
|
|
339
|
+
View History
|
|
340
|
+
</DropdownItem>
|
|
341
|
+
)}
|
|
342
|
+
{onPreview && (
|
|
343
|
+
<DropdownItem
|
|
344
|
+
icon={<IconEye className="w-4 h-4" />}
|
|
345
|
+
>
|
|
346
|
+
Preview
|
|
347
|
+
</DropdownItem>
|
|
348
|
+
)}
|
|
349
|
+
{(onDuplicate || onViewHistory || onPreview) && <DropdownSeparator />}
|
|
350
|
+
{onDelete && (
|
|
351
|
+
<DropdownItem
|
|
352
|
+
onClick={onDelete}
|
|
353
|
+
danger
|
|
354
|
+
icon={<IconTrash2 className="w-4 h-4" />}
|
|
355
|
+
>
|
|
356
|
+
Delete
|
|
357
|
+
</DropdownItem>
|
|
358
|
+
)}
|
|
359
|
+
</SplitButton>
|
|
360
|
+
</div>
|
|
177
361
|
</div>
|
|
178
|
-
|
|
362
|
+
</>
|
|
179
363
|
);
|
|
180
364
|
}
|
package/src/components/Admin.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo } from "react";
|
|
2
2
|
import { apiPost } from "../lib/api";
|
|
3
|
-
import {
|
|
3
|
+
import { toast, useAuthStore, type AuthUser } from "../lib/stores";
|
|
4
4
|
import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core/client";
|
|
5
5
|
import { ListView } from "./ListView";
|
|
6
6
|
import { DetailView } from "./DetailView";
|
|
@@ -14,7 +14,7 @@ import { WebhookManager } from "./WebhookManager";
|
|
|
14
14
|
import { MediaGallery } from "./MediaGallery";
|
|
15
15
|
import { CommandPalette } from "./ui/CommandPalette";
|
|
16
16
|
import { GlobalModal } from "./ui/GlobalModal";
|
|
17
|
-
import {
|
|
17
|
+
import { Toaster } from "./ui/Toaster";
|
|
18
18
|
import { ThemeProvider, type ThemeMode } from "./ThemeProvider";
|
|
19
19
|
import { toArray, toCollectionMap, toGlobalMap } from "../lib/config";
|
|
20
20
|
import "../styles/main.css";
|
|
@@ -65,9 +65,6 @@ export function Admin({ config, theme = "light", onThemeChange }: AdminProps) {
|
|
|
65
65
|
const [activeDocumentId, setActiveDocumentId] = useState<string | null>(null);
|
|
66
66
|
const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
|
|
67
67
|
|
|
68
|
-
const toasts = useToastStore((state) => state.toasts);
|
|
69
|
-
const removeToast = useToastStore((state) => state.removeToast);
|
|
70
|
-
|
|
71
68
|
useEffect(() => {
|
|
72
69
|
// Basic session check
|
|
73
70
|
const checkAuth = async () => {
|
|
@@ -215,17 +212,7 @@ const toasts = useToastStore((state) => state.toasts);
|
|
|
215
212
|
</main>
|
|
216
213
|
</div>
|
|
217
214
|
<GlobalModal />
|
|
218
|
-
|
|
219
|
-
<div className="kyro-toasts-container">
|
|
220
|
-
{toasts.map((t) => (
|
|
221
|
-
<Toast
|
|
222
|
-
key={t.id}
|
|
223
|
-
type={t.type}
|
|
224
|
-
message={t.message}
|
|
225
|
-
onClose={() => removeToast(t.id)}
|
|
226
|
-
/>
|
|
227
|
-
))}
|
|
228
|
-
</div>
|
|
215
|
+
<Toaster />
|
|
229
216
|
</div>
|
|
230
217
|
</ThemeProvider>
|
|
231
218
|
);
|
|
@@ -91,6 +91,7 @@ export function ApiKeysManager() {
|
|
|
91
91
|
try {
|
|
92
92
|
const rotated = await apiPost<ApiKeyItem>(`/api/keys/${key.id}/rotate`);
|
|
93
93
|
setNewKey(rotated);
|
|
94
|
+
toast.success("API key rotated");
|
|
94
95
|
} catch {
|
|
95
96
|
toast.error("Failed to rotate key. Please try again.");
|
|
96
97
|
} finally {
|
|
@@ -229,7 +229,7 @@ export function AuditLogsPage() {
|
|
|
229
229
|
</div>
|
|
230
230
|
|
|
231
231
|
{/* Filters */}
|
|
232
|
-
<div className="surface-tile p-4 flex flex-wrap items-center gap-3">
|
|
232
|
+
<div className="surface-tile p-4 flex flex-col md:flex-row flex-wrap items-stretch md:items-center gap-3">
|
|
233
233
|
<div className="relative flex-1 min-w-48">
|
|
234
234
|
<Search className="w-4 h-4" />
|
|
235
235
|
<input
|
|
@@ -320,7 +320,7 @@ export function AuditLogsPage() {
|
|
|
320
320
|
)}
|
|
321
321
|
|
|
322
322
|
{/* Table */}
|
|
323
|
-
<div className="surface-tile overflow-
|
|
323
|
+
<div className="surface-tile overflow-x-auto">
|
|
324
324
|
{loading ? (
|
|
325
325
|
<div className="space-y-2 p-4">
|
|
326
326
|
<Shimmer variant="table-row" count={5} />
|
|
@@ -356,7 +356,7 @@ export function AuditLogsPage() {
|
|
|
356
356
|
) : (
|
|
357
357
|
<table className="w-full text-left">
|
|
358
358
|
<thead>
|
|
359
|
-
<tr className="text-[var(--kyro-text-secondary)] font-bold text-[10px] tracking-[0.2em] border-b border-[var(--kyro-border)]">
|
|
359
|
+
<tr className="text-[var(--kyro-text-secondary)] font-bold text-[10px] tracking-[0.2em] border-b border-[var(--kyro-border)] whitespace-nowrap">
|
|
360
360
|
<th className="px-6 py-5 w-8"></th>
|
|
361
361
|
<th className="px-6 py-5">Action</th>
|
|
362
362
|
<th className="px-6 py-5">User</th>
|
|
@@ -582,12 +582,12 @@ export function AutoForm({
|
|
|
582
582
|
try {
|
|
583
583
|
const cond = field.admin.condition as any;
|
|
584
584
|
const targetField = cond.field;
|
|
585
|
-
|
|
585
|
+
|
|
586
586
|
// Get target field value, prioritizing sibling context (currentData) then root context (formData)
|
|
587
587
|
const val = (currentData && currentData[targetField] !== undefined)
|
|
588
588
|
? currentData[targetField]
|
|
589
589
|
: (formData && formData[targetField] !== undefined ? formData[targetField] : undefined);
|
|
590
|
-
|
|
590
|
+
|
|
591
591
|
let shouldShow = true;
|
|
592
592
|
if ("equals" in cond) {
|
|
593
593
|
shouldShow = val === cond.equals;
|
|
@@ -596,7 +596,7 @@ export function AutoForm({
|
|
|
596
596
|
} else if ("in" in cond && Array.isArray(cond.in)) {
|
|
597
597
|
shouldShow = cond.in.includes(val);
|
|
598
598
|
}
|
|
599
|
-
|
|
599
|
+
|
|
600
600
|
if (!shouldShow) {
|
|
601
601
|
return null;
|
|
602
602
|
}
|
|
@@ -622,7 +622,7 @@ export function AutoForm({
|
|
|
622
622
|
return (
|
|
623
623
|
<div
|
|
624
624
|
key={field.name || `row-${Math.random()}`}
|
|
625
|
-
className="kyro-form-row flex gap-6 items-end"
|
|
625
|
+
className="kyro-form-row flex flex-col md:flex-row gap-4 md:gap-6 items-start md:items-end w-full"
|
|
626
626
|
>
|
|
627
627
|
{rowFields?.map((f: Field) => {
|
|
628
628
|
const fAdmin = f.admin || {};
|
|
@@ -893,22 +893,22 @@ export function AutoForm({
|
|
|
893
893
|
: 'bg-[var(--kyro-text-muted)]/10 text-[var(--kyro-text-muted)] border-[var(--kyro-text-muted)]/20';
|
|
894
894
|
|
|
895
895
|
return (
|
|
896
|
-
<header className="surface-tile px-8 py-6 flex items-center justify-between sticky top-0 z-50 border-b border-[var(--kyro-border)] mb-8 bg-[var(--kyro-surface)] backdrop-blur-md">
|
|
897
|
-
<div className="flex flex-col gap-2">
|
|
898
|
-
<div className="flex items-center gap-3">
|
|
896
|
+
<header className="surface-tile px-3 md:px-8 py-2 md:py-6 flex items-center justify-between max-md:static sticky top-0 z-50 border-b border-[var(--kyro-border)] mb-0 md:mb-8 bg-[var(--kyro-surface)] backdrop-blur-md">
|
|
897
|
+
<div className="flex flex-col gap-1 md:gap-2 min-w-0">
|
|
898
|
+
<div className="flex items-center gap-2 md:gap-3 flex-wrap min-w-0">
|
|
899
899
|
<a
|
|
900
900
|
href={`/${collectionSlug}`}
|
|
901
|
-
className="p-2 border border-[var(--kyro-border)] rounded-xl hover:bg-[var(--kyro-bg-secondary)] transition-colors"
|
|
901
|
+
className="p-1.5 md:p-2 border-0 md:border border-[var(--kyro-border)] rounded-xl hover:bg-[var(--kyro-bg-secondary)] transition-colors shrink-0"
|
|
902
902
|
>
|
|
903
903
|
<ChevronRight className="w-4 h-4" />
|
|
904
904
|
</a>
|
|
905
|
-
<h1 className="text-xl font-bold tracking-tighter">{docTitle}</h1>
|
|
906
|
-
<span className={`inline-flex items-center gap-1.5 px-2 rounded-full text-[10px] font-regular border ${statusBadgeBg}`}>
|
|
905
|
+
<h1 className="text-lg md:text-xl font-bold tracking-tighter truncate min-w-0">{docTitle}</h1>
|
|
906
|
+
<span className={`shrink-0 inline-flex items-center gap-1.5 px-2 rounded-full text-[10px] font-regular border ${statusBadgeBg}`}>
|
|
907
907
|
<span className={`h-1.5 w-1.5 rounded-full ${statusColor}`} />
|
|
908
908
|
{statusLabel}
|
|
909
909
|
</span>
|
|
910
910
|
</div>
|
|
911
|
-
<div className="flex items-center gap-4 text-[11px] font-medium tracking-wide opacity-60 ml-12">
|
|
911
|
+
<div className="flex items-center gap-4 text-[11px] font-medium tracking-wide opacity-60 md:ml-12">
|
|
912
912
|
{autoSaveStatus === "saving" && (
|
|
913
913
|
<span className="flex items-center gap-1.5 text-[var(--kyro-text-muted)]">
|
|
914
914
|
<svg
|
|
@@ -1028,7 +1028,7 @@ export function AutoForm({
|
|
|
1028
1028
|
</div>
|
|
1029
1029
|
</div>
|
|
1030
1030
|
|
|
1031
|
-
<div className="flex items-center gap-6">
|
|
1031
|
+
<div className="max-md:hidden flex items-center gap-6">
|
|
1032
1032
|
<div className="flex items-center gap-1 bg-[var(--kyro-bg-secondary)] p-1 rounded-xl border border-[var(--kyro-border)]">
|
|
1033
1033
|
{["edit", "version", "api"].map((v) => (
|
|
1034
1034
|
<button
|
|
@@ -1221,8 +1221,8 @@ export function AutoForm({
|
|
|
1221
1221
|
// Single layout: no split grid, no sidebar column — just a clean field list
|
|
1222
1222
|
if (layout === "single") {
|
|
1223
1223
|
return (
|
|
1224
|
-
<div className="w-full space-y-8">
|
|
1225
|
-
<div className="surface-tile p-8 space-y-8">
|
|
1224
|
+
<div className="w-full space-y-6 md:space-y-8">
|
|
1225
|
+
<div className="surface-tile p-4 md:p-8 space-y-6 md:space-y-8">
|
|
1226
1226
|
{config.fields.map((f: Field) => renderField(f))}
|
|
1227
1227
|
</div>
|
|
1228
1228
|
</div>
|
|
@@ -1237,18 +1237,18 @@ export function AutoForm({
|
|
|
1237
1237
|
|
|
1238
1238
|
return (
|
|
1239
1239
|
<div
|
|
1240
|
-
className={`w-full mx-auto grid gap-8 pb-32 transition-all duration-700 ${showPreview
|
|
1240
|
+
className={`w-full mx-auto grid gap-4 md:gap-8 pb-32 transition-all duration-700 ${showPreview
|
|
1241
1241
|
? "grid-cols-1 lg:grid-cols-2"
|
|
1242
1242
|
: sidebarCollapsed || !hasSidebarFields
|
|
1243
1243
|
? "grid-cols-1"
|
|
1244
1244
|
: "grid-cols-1 lg:grid-cols-[1fr_380px]"
|
|
1245
1245
|
}`}
|
|
1246
1246
|
>
|
|
1247
|
-
<div className="space-y-8 animate-in fade-in slide-in-from-left-4 duration-500">
|
|
1247
|
+
<div className="space-y-6 md:space-y-8 animate-in fade-in slide-in-from-left-4 duration-500">
|
|
1248
1248
|
{config.tabs ? (
|
|
1249
1249
|
renderField({ type: "tabs", tabs: config.tabs } as Field)
|
|
1250
1250
|
) : (
|
|
1251
|
-
<div className="surface-tile p-8 space-y-8">
|
|
1251
|
+
<div className="surface-tile p-4 md:p-8 space-y-6 md:space-y-8">
|
|
1252
1252
|
{config.fields
|
|
1253
1253
|
.filter(
|
|
1254
1254
|
(f: Field) => !f.admin?.position || f.admin.position === "main",
|
|
@@ -1276,16 +1276,33 @@ export function AutoForm({
|
|
|
1276
1276
|
</div>
|
|
1277
1277
|
</div>
|
|
1278
1278
|
) : sidebarCollapsed ? null : (
|
|
1279
|
-
<div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
|
|
1279
|
+
<div className="space-y-4 md:space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
|
|
1280
1280
|
{config.fields.some((f: Field) => f.admin?.position === "sidebar") && (
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
.
|
|
1288
|
-
|
|
1281
|
+
<>
|
|
1282
|
+
{/* Desktop: always visible */}
|
|
1283
|
+
<div className="hidden lg:block surface-tile p-6 space-y-6">
|
|
1284
|
+
<h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40">
|
|
1285
|
+
Settings
|
|
1286
|
+
</h3>
|
|
1287
|
+
{config.fields
|
|
1288
|
+
.filter((f: Field) => f.admin?.position === "sidebar")
|
|
1289
|
+
.map((f: Field) => renderField(f))}
|
|
1290
|
+
</div>
|
|
1291
|
+
{/* Mobile: collapsible accordion */}
|
|
1292
|
+
<details className="lg:hidden surface-tile p-4 space-y-4 group">
|
|
1293
|
+
<summary className="cursor-pointer font-semibold text-xs tracking-widest opacity-40 text-[var(--kyro-text-secondary)] select-none flex items-center gap-2">
|
|
1294
|
+
<svg className="w-3 h-3 transition-transform group-open:rotate-90" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1295
|
+
<path d="M9 18l6-6-6-6" />
|
|
1296
|
+
</svg>
|
|
1297
|
+
Settings
|
|
1298
|
+
</summary>
|
|
1299
|
+
<div className="space-y-4 pt-4 border-t border-[var(--kyro-border)]">
|
|
1300
|
+
{config.fields
|
|
1301
|
+
.filter((f: Field) => f.admin?.position === "sidebar")
|
|
1302
|
+
.map((f: Field) => renderField(f))}
|
|
1303
|
+
</div>
|
|
1304
|
+
</details>
|
|
1305
|
+
</>
|
|
1289
1306
|
)}
|
|
1290
1307
|
</div>
|
|
1291
1308
|
)}
|
|
@@ -1642,13 +1659,13 @@ export function AutoForm({
|
|
|
1642
1659
|
onClick={async () => {
|
|
1643
1660
|
try {
|
|
1644
1661
|
const response = await saveDocument();
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1662
|
+
if (response.ok) {
|
|
1663
|
+
const result = await response.json();
|
|
1664
|
+
const savedData = result.data || formData;
|
|
1665
|
+
setFormData({ ...formData, ...savedData });
|
|
1666
|
+
setLastSavedData({ ...formData, ...savedData });
|
|
1667
|
+
onActionSuccess?.("Changes saved");
|
|
1668
|
+
}
|
|
1652
1669
|
} catch (e) {
|
|
1653
1670
|
console.error("Save error exception:", e);
|
|
1654
1671
|
onActionError?.("Save failed: " + (e as Error).message);
|
|
@@ -1657,7 +1674,7 @@ export function AutoForm({
|
|
|
1657
1674
|
/>
|
|
1658
1675
|
</>
|
|
1659
1676
|
)}
|
|
1660
|
-
<main className="w-full">
|
|
1677
|
+
<main className="w-full pt-6 md:pt-0">
|
|
1661
1678
|
{view === "edit" && renderEditView()}
|
|
1662
1679
|
{view === "version" && renderVersionView()}
|
|
1663
1680
|
{view === "api" && renderApiView()}
|