@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.
Files changed (44) hide show
  1. package/dist/index.cjs +966 -585
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +29 -9
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +3 -1
  6. package/dist/index.d.ts +3 -1
  7. package/dist/index.js +649 -268
  8. package/dist/index.js.map +1 -1
  9. package/package.json +2 -2
  10. package/src/components/ActionBar.tsx +254 -70
  11. package/src/components/Admin.tsx +10 -17
  12. package/src/components/ApiKeysManager.tsx +1 -0
  13. package/src/components/AuditLogsPage.tsx +3 -3
  14. package/src/components/AutoForm.tsx +51 -34
  15. package/src/components/DetailView.tsx +37 -13
  16. package/src/components/GraphQLPlayground.tsx +460 -224
  17. package/src/components/ListView.tsx +3 -3
  18. package/src/components/LoginPage.tsx +5 -30
  19. package/src/components/MediaGallery.tsx +122 -15
  20. package/src/components/RestPlayground.tsx +443 -519
  21. package/src/components/Sidebar.astro +6 -2
  22. package/src/components/UserManagement.tsx +4 -4
  23. package/src/components/WebhookManager.tsx +4 -4
  24. package/src/components/blocks/AccordionBlock.tsx +1 -1
  25. package/src/components/blocks/ArrayBlock.tsx +1 -1
  26. package/src/components/blocks/ChildBlocksTree.tsx +6 -6
  27. package/src/components/blocks/CodeBlock.tsx +1 -1
  28. package/src/components/blocks/FileBlock.tsx +1 -1
  29. package/src/components/blocks/HeroBlock.tsx +1 -1
  30. package/src/components/blocks/ListBlock.tsx +1 -1
  31. package/src/components/blocks/RelationshipBlock.tsx +1 -1
  32. package/src/components/blocks/RichTextBlock.tsx +1 -1
  33. package/src/components/blocks/VideoBlock.tsx +1 -1
  34. package/src/components/fields/BlocksField.tsx +17 -19
  35. package/src/components/ui/PageHeader.tsx +205 -83
  36. package/src/components/ui/Pagination.tsx +2 -2
  37. package/src/components/ui/SlidePanel.tsx +4 -4
  38. package/src/layouts/AdminLayout.astro +64 -4
  39. package/src/lib/useResourceManager.ts +1 -0
  40. package/src/pages/graphql-explorer.astro +7 -51
  41. package/src/pages/graphql.astro +7 -119
  42. package/src/pages/index.astro +4 -63
  43. package/src/pages/rest-playground.astro +3 -29
  44. package/src/styles/main.css +32 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyro-cms/admin",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
@@ -128,4 +128,4 @@
128
128
  "type": "git",
129
129
  "url": "https://github.com/danielDozie/kyro-cms"
130
130
  }
131
- }
131
+ }
@@ -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
- <div className="flex items-center justify-between py-3 px-1">
88
- <div className="flex items-center gap-4">
89
- <div className="flex items-center gap-2">
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-sm ${saveStatus === "error" ? "text-[var(--kyro-error)]" : "text-[var(--kyro-text-muted)]"}`}
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
- <div className="text-xs space-y-0.5">
103
- {updatedAt && (
104
- <div className="text-[var(--kyro-text-muted)]">
105
- Updated: {formatDate(updatedAt)}
106
- </div>
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
- {publishedAt && status === "published" && (
109
- <div className="text-[var(--kyro-primary)] font-medium">
110
- Published: {formatDate(publishedAt)}
111
- </div>
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
- <div className="flex items-center gap-2">
117
- {status === "draft" && onPublish && (
118
- <button type="button"
119
- onClick={onPublish}
120
- disabled={saveStatus === "saving"}
121
- className="kyro-btn kyro-btn-primary kyro-btn-md flex items-center gap-2"
122
- >
123
- <IconSend className="w-4 h-4" />
124
- Publish
125
- </button>
126
- )}
127
- {status === "published" && onUnpublish && (
128
- <button type="button"
129
- onClick={onUnpublish}
130
- disabled={saveStatus === "saving"}
131
- className="kyro-btn kyro-btn-secondary kyro-btn-md flex items-center gap-2"
132
- >
133
- <IconUndo className="w-4 h-4" />
134
- Unpublish
135
- </button>
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
- {/* Button Group: Save + Dropdown */}
139
- <SplitButton
140
- status={status}
141
- saveStatus={saveStatus}
142
- hasChanges={hasChanges}
143
- onPublish={onSave}
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
- View History
157
- </DropdownItem>
307
+ <IconSend className="w-4 h-4" />
308
+ Publish
309
+ </button>
158
310
  )}
159
- {onPreview && (
160
- <DropdownItem
161
- icon={<IconEye className="w-4 h-4" />}
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
- Preview
164
- </DropdownItem>
317
+ <IconUndo className="w-4 h-4" />
318
+ Unpublish
319
+ </button>
165
320
  )}
166
- {(onDuplicate || onViewHistory || onPreview) && <DropdownSeparator />}
167
- {onDelete && (
168
- <DropdownItem
169
- onClick={onDelete}
170
- danger
171
- icon={<IconTrash2 className="w-4 h-4" />}
172
- >
173
- Delete
174
- </DropdownItem>
175
- )}
176
- </SplitButton>
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
- </div>
362
+ </>
179
363
  );
180
364
  }
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useMemo } from "react";
2
2
  import { apiPost } from "../lib/api";
3
- import { useToastStore, toast, useAuthStore, type AuthUser } from "../lib/stores";
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 { Toast } from "./ui/Toast";
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 () => {
@@ -107,7 +104,13 @@ const toasts = useToastStore((state) => state.toasts);
107
104
  }
108
105
  };
109
106
  window.addEventListener("keydown", handleKeyDown);
110
- return () => window.removeEventListener("keydown", handleKeyDown);
107
+
108
+ (window as { openCommandPalette?: () => void }).openCommandPalette = () => setIsCommandPaletteOpen(true);
109
+
110
+ return () => {
111
+ window.removeEventListener("keydown", handleKeyDown);
112
+ delete (window as { openCommandPalette?: () => void }).openCommandPalette;
113
+ };
111
114
  }, []);
112
115
 
113
116
  const handleLogin = async (data: Record<string, unknown>) => {
@@ -215,17 +218,7 @@ const toasts = useToastStore((state) => state.toasts);
215
218
  </main>
216
219
  </div>
217
220
  <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>
221
+ <Toaster />
229
222
  </div>
230
223
  </ThemeProvider>
231
224
  );
@@ -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-hidden">
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
- <div className="surface-tile p-6 space-y-6">
1282
- <h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40">
1283
- Settings
1284
- </h3>
1285
- {config.fields
1286
- .filter((f: Field) => f.admin?.position === "sidebar")
1287
- .map((f: Field) => renderField(f))}
1288
- </div>
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
- if (response.ok) {
1646
- const result = await response.json();
1647
- const savedData = result.data || formData;
1648
- setFormData({ ...formData, ...savedData });
1649
- setLastSavedData({ ...formData, ...savedData });
1650
- onActionSuccess?.("Changes saved");
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()}