@kyro-cms/admin 0.9.0 → 0.9.2

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 (114) hide show
  1. package/dist/index.cjs +11715 -11292
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +67 -65
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +564 -0
  6. package/dist/index.d.ts +11 -10
  7. package/dist/index.js +11326 -10912
  8. package/dist/index.js.map +1 -1
  9. package/package.json +16 -12
  10. package/src/components/ActionBar.tsx +25 -161
  11. package/src/components/Admin.tsx +2 -4
  12. package/src/components/ApiKeysManager.tsx +5 -5
  13. package/src/components/AuditLogsPage.tsx +2 -13
  14. package/src/components/AutoForm.tsx +572 -461
  15. package/src/components/BrandingHub.tsx +7 -4
  16. package/src/components/CreateView.tsx +2 -0
  17. package/src/components/DetailView.tsx +52 -65
  18. package/src/components/DeveloperCenter.tsx +8 -6
  19. package/src/components/FieldRenderer.tsx +94 -19
  20. package/src/components/ListView.tsx +57 -216
  21. package/src/components/MediaGallery.tsx +334 -367
  22. package/src/components/PluginsManager.tsx +197 -70
  23. package/src/components/RestPlayground.tsx +59 -52
  24. package/src/components/SessionsManager.tsx +1 -1
  25. package/src/components/SettingsPage.tsx +22 -0
  26. package/src/components/Sidebar.astro +13 -41
  27. package/src/components/UserManagement.tsx +153 -15
  28. package/src/components/UserMenu.tsx +30 -4
  29. package/src/components/VersionHistoryPanel.tsx +112 -119
  30. package/src/components/WebhookManager.tsx +6 -4
  31. package/src/components/blocks/ArrayBlock.tsx +6 -23
  32. package/src/components/blocks/BlockEditModal.tsx +82 -309
  33. package/src/components/blocks/CardBlock.tsx +35 -0
  34. package/src/components/blocks/ChildBlocksTree.tsx +57 -31
  35. package/src/components/blocks/GenericBlock.tsx +44 -0
  36. package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
  37. package/src/components/blocks/HeroBlock.tsx +5 -14
  38. package/src/components/blocks/RichTextBlock.tsx +5 -5
  39. package/src/components/blocks/index.ts +5 -3
  40. package/src/components/fields/AccordionField.tsx +2 -2
  41. package/src/components/fields/ArrayField.tsx +1 -1
  42. package/src/components/fields/ArrayLayout.tsx +120 -29
  43. package/src/components/fields/BlocksField.tsx +433 -55
  44. package/src/components/fields/CardField.tsx +73 -0
  45. package/src/components/fields/CheckboxField.tsx +7 -3
  46. package/src/components/fields/DateField.tsx +4 -1
  47. package/src/components/fields/GroupLayout.tsx +2 -2
  48. package/src/components/fields/HeadingSubheadingField.tsx +43 -0
  49. package/src/components/fields/ListField.tsx +2 -2
  50. package/src/components/fields/NumberField.tsx +4 -1
  51. package/src/components/fields/RelationshipBlockField.tsx +2 -3
  52. package/src/components/fields/RelationshipField.tsx +155 -90
  53. package/src/components/fields/RichTextField.tsx +781 -0
  54. package/src/components/fields/SecretField.tsx +102 -0
  55. package/src/components/fields/SelectField.tsx +19 -6
  56. package/src/components/fields/TabsLayout.tsx +19 -9
  57. package/src/components/fields/TextField.tsx +4 -1
  58. package/src/components/fields/UploadField.tsx +122 -56
  59. package/src/components/fields/extensions/blockComponents.tsx +103 -174
  60. package/src/components/fields/extensions/blocksStore.ts +8 -1
  61. package/src/components/fields/index.ts +4 -2
  62. package/src/components/fix_imports.cjs +23 -0
  63. package/src/components/fix_imports2.cjs +19 -0
  64. package/src/components/replace_svgs.cjs +63 -0
  65. package/src/components/ui/Dropdown.tsx +7 -2
  66. package/src/components/ui/Modal.tsx +24 -27
  67. package/src/components/ui/PageHeader.tsx +5 -5
  68. package/src/components/ui/PromptModal.tsx +2 -10
  69. package/src/components/ui/SlidePanel.tsx +10 -13
  70. package/src/components/ui/SplitButton.tsx +107 -0
  71. package/src/components/ui/Toaster.tsx +0 -1
  72. package/src/components/ui/icons.tsx +110 -109
  73. package/src/components/users/UserDetail.tsx +79 -16
  74. package/src/components/users/UsersList.tsx +8 -85
  75. package/src/hooks/useAutoFormState.ts +187 -196
  76. package/src/hooks/useQueue.ts +60 -0
  77. package/src/integration.ts +148 -46
  78. package/src/kyro-cms.d.ts +7 -2
  79. package/src/layouts/AdminLayout.astro +22 -2
  80. package/src/layouts/AuthLayout.astro +67 -7
  81. package/src/lib/autoform-store.ts +90 -53
  82. package/src/lib/change-source.ts +9 -0
  83. package/src/lib/config.ts +104 -8
  84. package/src/lib/globals.ts +48 -11
  85. package/src/lib/normalize-upload-fields.ts +41 -0
  86. package/src/lib/paths.ts +2 -2
  87. package/src/lib/resolve-field-value.ts +110 -0
  88. package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
  89. package/src/lib/shim/use-sync-external-store.js +1 -0
  90. package/src/lib/stores/index.ts +1 -0
  91. package/src/lib/useResourceManager.ts +4 -4
  92. package/src/lib/vite-shim-plugin.ts +100 -0
  93. package/src/pages/[collection]/[id].astro +1 -1
  94. package/src/pages/auth/register.astro +5 -2
  95. package/src/pages/preview/[collection]/[id].astro +4 -4
  96. package/src/pages/settings/[slug].astro +2 -2
  97. package/src/styles/main.css +60 -54
  98. package/README.md +0 -46
  99. package/dist/EditorClient-Q23UXR37.cjs +0 -468
  100. package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
  101. package/dist/EditorClient-T5PASFNR.js +0 -466
  102. package/dist/EditorClient-T5PASFNR.js.map +0 -1
  103. package/dist/chunk-3BGDYKTD.cjs +0 -348
  104. package/dist/chunk-3BGDYKTD.cjs.map +0 -1
  105. package/dist/chunk-EEFXLQVT.js +0 -3
  106. package/dist/chunk-EEFXLQVT.js.map +0 -1
  107. package/src/components/blocks/ButtonBlock.tsx +0 -64
  108. package/src/components/blocks/ColumnsBlock.tsx +0 -55
  109. package/src/components/blocks/DividerBlock.tsx +0 -43
  110. package/src/components/blocks/LinkBlock.tsx +0 -65
  111. package/src/components/blocks/VStackBlock.tsx +0 -29
  112. package/src/components/fields/EditorClient.tsx +0 -535
  113. package/src/components/fields/PortableTextField.tsx +0 -155
  114. package/src/components/fields/PortableTextRenderer.tsx +0 -68
@@ -1,9 +1,7 @@
1
- import React, { useState } from "react";
1
+ import React, { useState, useEffect } from "react";
2
2
  import {
3
3
  Blocks,
4
4
  Settings,
5
- ToggleRight,
6
- ToggleLeft,
7
5
  CheckCircle2,
8
6
  Clock,
9
7
  RefreshCw,
@@ -11,6 +9,7 @@ import {
11
9
  Search,
12
10
  Plus,
13
11
  X,
12
+ AlertTriangle,
14
13
  } from "./ui/icons";
15
14
  import { Modal, ModalContent, ModalActions } from "./ui/Modal";
16
15
  import { PageHeader } from "./ui/PageHeader";
@@ -22,71 +21,118 @@ interface Plugin {
22
21
  description: string;
23
22
  version: string;
24
23
  enabled: boolean;
25
- author: string;
26
- updatedAt: string;
27
- icon: string;
28
- status: "active" | "error" | "update_available";
24
+ status: "active" | "disabled" | "error" | "update_available";
29
25
  }
30
26
 
31
- const mockPlugins: Plugin[] = [
32
- {
33
- id: "seo-optimizer",
34
- name: "SEO Optimizer Pro",
35
- description:
36
- "Advanced meta tags, sitemaps, and rich snippets for all collections.",
37
- version: "2.1.4",
38
- enabled: true,
39
- author: "Kyro Team",
40
- updatedAt: "2024-05-10T14:30:00Z",
41
- icon: "search",
42
- status: "active",
43
- },
44
- {
45
- id: "analytics-dashboard",
46
- name: "Analytics Integration",
47
- description:
48
- "Connect to Google Analytics, Plausible, or Mixpanel for traffic insights.",
49
- version: "1.0.2",
50
- enabled: true,
51
- author: "Kyro Team",
52
- updatedAt: "2024-04-20T09:15:00Z",
53
- icon: "activity",
54
- status: "update_available",
55
- },
56
- {
57
- id: "aws-s3-adapter",
58
- name: "AWS S3 Storage",
59
- description: "Cloud storage adapter for Media Library with S3 integration.",
60
- version: "3.0.0",
61
- enabled: false,
62
- author: "AWS",
63
- updatedAt: "2024-01-15T11:00:00Z",
64
- icon: "database",
65
- status: "active",
66
- },
67
- {
68
- id: "ai-writer",
69
- name: "AI Content Writer",
70
- description:
71
- "Generate blog posts and product descriptions with AI assistance.",
72
- version: "1.2.0",
73
- enabled: true,
74
- author: "Kyro Team",
75
- updatedAt: "2024-05-01T10:00:00Z",
76
- icon: "sparkles",
77
- status: "active",
78
- },
79
- ];
27
+ interface ToggleError {
28
+ error: string;
29
+ requiresAction?: boolean;
30
+ activeProvider?: string;
31
+ }
80
32
 
81
33
  export function PluginsManager() {
82
- const [plugins, setPlugins] = useState<Plugin[]>(mockPlugins);
34
+ const [plugins, setPlugins] = useState<Plugin[]>([]);
35
+ const [loading, setLoading] = useState(true);
36
+ const [toggleLoading, setToggleLoading] = useState<string | null>(null);
83
37
  const [searchQuery, setSearchQuery] = useState("");
84
38
  const [showConfigModal, setShowConfigModal] = useState<string | null>(null);
39
+ const [confirmDisable, setConfirmDisable] = useState<{
40
+ id: string;
41
+ name: string;
42
+ activeProvider: string;
43
+ } | null>(null);
44
+ const [error, setError] = useState<string | null>(null);
85
45
 
86
- const togglePlugin = (id: string) => {
87
- setPlugins((prev) =>
88
- prev.map((p) => (p.id === id ? { ...p, enabled: !p.enabled } : p)),
89
- );
46
+ const fetchPlugins = async () => {
47
+ try {
48
+ setLoading(true);
49
+ const res = await fetch("/api/plugins");
50
+ if (res.ok) {
51
+ const data = await res.json();
52
+ setPlugins(data);
53
+ }
54
+ } catch (e) {
55
+ console.error("Failed to fetch plugins:", e);
56
+ } finally {
57
+ setLoading(false);
58
+ }
59
+ };
60
+
61
+ useEffect(() => {
62
+ fetchPlugins();
63
+ }, []);
64
+
65
+ const togglePlugin = async (id: string) => {
66
+ setError(null);
67
+ setToggleLoading(id);
68
+
69
+ try {
70
+ const res = await fetch(`/api/plugins/${encodeURIComponent(id)}/toggle`, {
71
+ method: "PUT",
72
+ });
73
+
74
+ if (res.status === 409) {
75
+ const errData: ToggleError = await res.json();
76
+ const plugin = plugins.find((p) => p.id === id);
77
+ if (errData.requiresAction && plugin) {
78
+ setConfirmDisable({
79
+ id,
80
+ name: plugin.name,
81
+ activeProvider: errData.activeProvider || "unknown",
82
+ });
83
+ } else {
84
+ setError(errData.error);
85
+ }
86
+ return;
87
+ }
88
+
89
+ if (!res.ok) {
90
+ const errData = await res.json().catch(() => ({ error: "Toggle failed" }));
91
+ setError(errData.error);
92
+ return;
93
+ }
94
+
95
+ const result = await res.json();
96
+ setPlugins((prev) =>
97
+ prev.map((p) =>
98
+ p.id === id ? { ...p, enabled: result.enabled } : p,
99
+ ),
100
+ );
101
+ } catch (e: any) {
102
+ setError(e.message || "Network error");
103
+ } finally {
104
+ setToggleLoading(null);
105
+ }
106
+ };
107
+
108
+ const handleForceDisable = async () => {
109
+ if (!confirmDisable) return;
110
+ setError(null);
111
+
112
+ try {
113
+ const res = await fetch(
114
+ `/api/plugins/${encodeURIComponent(confirmDisable.id)}/toggle?force=1`,
115
+ { method: "PUT" },
116
+ );
117
+
118
+ if (res.ok) {
119
+ const result = await res.json();
120
+ setPlugins((prev) =>
121
+ prev.map((p) =>
122
+ p.id === confirmDisable.id
123
+ ? { ...p, enabled: result.enabled }
124
+ : p,
125
+ ),
126
+ );
127
+ } else {
128
+ const errData = await res.json().catch(() => ({ error: "Toggle failed" }));
129
+ setError(errData.error);
130
+ }
131
+ } catch (e: any) {
132
+ setError(e.message || "Network error");
133
+ } finally {
134
+ setConfirmDisable(null);
135
+ }
90
136
  };
91
137
 
92
138
  const filteredPlugins = plugins.filter(
@@ -100,6 +146,21 @@ export function PluginsManager() {
100
146
  (p) => p.status === "update_available",
101
147
  ).length;
102
148
 
149
+ if (loading) {
150
+ return (
151
+ <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
152
+ <PageHeader
153
+ title="Plugins"
154
+ description="Extend Kyro CMS with modular features and integrations."
155
+ icon={Blocks}
156
+ />
157
+ <div className="flex items-center justify-center p-12">
158
+ <RefreshCw className="w-6 h-6 animate-spin text-[var(--kyro-text-secondary)] opacity-40" />
159
+ </div>
160
+ </div>
161
+ );
162
+ }
163
+
103
164
  return (
104
165
  <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
105
166
  <PageHeader
@@ -115,6 +176,20 @@ export function PluginsManager() {
115
176
  ]}
116
177
  />
117
178
 
179
+ {error && (
180
+ <div className="p-4 rounded-2xl bg-red-500/10 border border-red-500/20 flex items-center gap-3">
181
+ <AlertTriangle className="w-4 h-4 text-red-500 shrink-0" />
182
+ <p className="text-xs text-red-500">{error}</p>
183
+ <button
184
+ type="button"
185
+ onClick={() => setError(null)}
186
+ className="ml-auto p-1 hover:bg-red-500/20 rounded-lg transition-colors"
187
+ >
188
+ <X className="w-3 h-3 text-red-500" />
189
+ </button>
190
+ </div>
191
+ )}
192
+
118
193
  <div className="flex flex-col gap-8 surface-tile p-8">
119
194
  {/* Stats Summary */}
120
195
  <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
@@ -155,14 +230,14 @@ export function PluginsManager() {
155
230
  <Search className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-[var(--kyro-text-secondary)] opacity-40" />
156
231
  <input
157
232
  type="text"
158
- placeholder="Search ecosystem..."
233
+ placeholder="Search plugins..."
159
234
  value={searchQuery}
160
235
  onChange={(e) => setSearchQuery(e.target.value)}
161
236
  className="w-full pl-10 pr-4 py-3 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-xl text-xs focus:outline-none focus:border-[var(--kyro-primary)]/50 transition-all"
162
237
  />
163
238
  </div>
164
239
  </div>
165
-
240
+
166
241
  <div className="flex items-center gap-2 px-1">
167
242
  <div className="w-0.5 h-3 bg-[var(--kyro-primary)] rounded-full" />
168
243
  <h2 className="text-[10px] font-bold tracking-[0.2em] opacity-40 uppercase">Installed Extensions</h2>
@@ -199,7 +274,7 @@ export function PluginsManager() {
199
274
  </div>
200
275
 
201
276
  <p className="text-xs text-[var(--kyro-text-secondary)] opacity-70 leading-relaxed min-h-[32px] line-clamp-2">
202
- {plugin.description}
277
+ {plugin.description || "No description available."}
203
278
  </p>
204
279
 
205
280
  <div className="flex items-center justify-between pt-3 border-t border-[var(--kyro-border)]/50">
@@ -222,12 +297,19 @@ export function PluginsManager() {
222
297
  <button
223
298
  type="button"
224
299
  onClick={() => togglePlugin(plugin.id)}
300
+ disabled={toggleLoading === plugin.id}
225
301
  className={`p-2 border rounded-lg transition-all shadow-sm ${plugin.enabled
226
302
  ? "bg-red-500/5 border-red-500/10 text-red-500/40 hover:text-red-500 hover:border-red-500/30"
227
303
  : "bg-green-500/5 border-green-500/10 text-green-500/40 hover:text-green-500 hover:border-green-500/30"
228
- }`}
304
+ } disabled:opacity-30 disabled:cursor-not-allowed`}
229
305
  >
230
- {plugin.enabled ? <X className="w-3.5 h-3.5" /> : <Plus className="w-3.5 h-3.5" />}
306
+ {toggleLoading === plugin.id ? (
307
+ <RefreshCw className="w-3.5 h-3.5 animate-spin" />
308
+ ) : plugin.enabled ? (
309
+ <X className="w-3.5 h-3.5" />
310
+ ) : (
311
+ <Plus className="w-3.5 h-3.5" />
312
+ )}
231
313
  </button>
232
314
  </div>
233
315
  </div>
@@ -244,11 +326,56 @@ export function PluginsManager() {
244
326
  </div>
245
327
  </div>
246
328
 
247
- {/* Modal */}
329
+ {/* Confirmation Modal for Disabling Active Storage Plugin */}
330
+ <Modal
331
+ open={!!confirmDisable}
332
+ onClose={() => setConfirmDisable(null)}
333
+ title="Disable Storage Plugin?"
334
+ size="md"
335
+ >
336
+ <ModalContent>
337
+ <div className="p-6 text-center space-y-4">
338
+ <div className="w-16 h-16 mx-auto bg-amber-500/10 rounded-2xl flex items-center justify-center border border-amber-500/20">
339
+ <AlertTriangle className="w-8 h-8 text-amber-500" />
340
+ </div>
341
+ <div>
342
+ <h4 className="text-lg font-bold mb-2">Storage Plugin In Use</h4>
343
+ <p className="text-sm text-[var(--kyro-text-secondary)] opacity-70 leading-relaxed">
344
+ "{confirmDisable?.name}" is currently the active storage provider.
345
+ Disabling it will switch storage to <strong>Local</strong>.
346
+ </p>
347
+ <p className="text-xs text-[var(--kyro-text-secondary)] opacity-50 mt-2">
348
+ Existing media URLs will remain accessible, but new uploads will
349
+ use Local storage. You can re-enable the plugin at any time.
350
+ </p>
351
+ </div>
352
+ </div>
353
+ </ModalContent>
354
+ <ModalActions>
355
+ <div className="flex gap-3 w-full">
356
+ <button
357
+ type="button"
358
+ onClick={() => setConfirmDisable(null)}
359
+ className="kyro-btn kyro-btn-primary flex-1 py-3 rounded-xl font-bold text-sm"
360
+ >
361
+ Cancel
362
+ </button>
363
+ <button
364
+ type="button"
365
+ onClick={handleForceDisable}
366
+ className="kyro-btn flex-1 py-3 rounded-xl font-bold text-sm bg-amber-500 hover:bg-amber-600 text-white transition-all"
367
+ >
368
+ Switch to Local & Disable
369
+ </button>
370
+ </div>
371
+ </ModalActions>
372
+ </Modal>
373
+
374
+ {/* Config Modal */}
248
375
  <Modal
249
376
  open={!!showConfigModal}
250
377
  onClose={() => setShowConfigModal(null)}
251
- title="Plugin Architecture"
378
+ title="Plugin Configuration"
252
379
  size="lg"
253
380
  >
254
381
  <ModalContent>
@@ -266,9 +393,9 @@ export function PluginsManager() {
266
393
  <button
267
394
  type="button"
268
395
  onClick={() => setShowConfigModal(null)}
269
- className="w-full py-3 rounded-xl font-bold text-sm bg-[var(--kyro-primary)] text-white hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/20"
396
+ className="kyro-btn kyro-btn-primary w-full py-3 rounded-xl font-bold text-sm hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/20"
270
397
  >
271
- Save Configuration
398
+ Close
272
399
  </button>
273
400
  </ModalActions>
274
401
  </Modal>
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect, useCallback, useRef } from "react";
2
- import { useUIStore } from "../lib/stores";
2
+ import { useUIStore, toast } from "../lib/stores";
3
3
  import { apiPath } from "../lib/paths";
4
+ import { Modal } from "./ui/Modal";
4
5
 
5
6
  interface EnvVariable {
6
7
  key: string;
@@ -82,7 +83,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
82
83
  const [newFolderName, setNewFolderName] = useState("");
83
84
  const [saveToFolderId, setSaveToFolderId] = useState("");
84
85
  const [saveRequestName, setSaveRequestName] = useState("");
85
- const { confirm, alert } = useUIStore();
86
+ const { confirm } = useUIStore();
86
87
 
87
88
  // Load from localStorage
88
89
  useEffect(() => {
@@ -325,9 +326,9 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
325
326
  setEnvVars((prev) => [...prev, ...data.envVars]);
326
327
  }
327
328
 
328
- alert({ title: "Import Successful", message: "Your playground data has been imported." });
329
- } catch (error) {
330
- alert({ title: "Import Failed", message: "Invalid JSON file structure." });
329
+ toast.success("Your playground data has been imported.");
330
+ } catch (err) {
331
+ toast.error("Invalid JSON file structure.");
331
332
  }
332
333
  };
333
334
  reader.readAsText(file);
@@ -731,59 +732,65 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
731
732
  </div>
732
733
 
733
734
  {/* Modals */}
734
- {showFolderModal && (
735
- <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100] p-4">
736
- <div className="surface-tile w-full max-w-md p-6 rounded-2xl shadow-2xl border border-[var(--kyro-border)]">
737
- <h2 className="text-xl font-bold mb-4">Create Folder</h2>
735
+ <Modal
736
+ open={showFolderModal}
737
+ onClose={() => setShowFolderModal(false)}
738
+ title="Create Folder"
739
+ size="md"
740
+ footer={
741
+ <>
742
+ <button type="button" onClick={() => setShowFolderModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
743
+ <button type="button" onClick={createFolder} className="kyro-btn kyro-btn-md bg-pink-500 border-pink-500 text-white hover:bg-pink-600 hover:border-pink-600">Create</button>
744
+ </>
745
+ }
746
+ >
747
+ <div className="py-2">
748
+ <input
749
+ type="text"
750
+ value={newFolderName}
751
+ onChange={(e) => setNewFolderName(e.target.value)}
752
+ placeholder="Folder name..."
753
+ className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
754
+ />
755
+ </div>
756
+ </Modal>
757
+
758
+ <Modal
759
+ open={showSaveModal}
760
+ onClose={() => setShowSaveModal(false)}
761
+ title="Save Request"
762
+ size="md"
763
+ footer={
764
+ <>
765
+ <button type="button" onClick={() => setShowSaveModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
766
+ <button type="button" onClick={saveRequest} className="kyro-btn kyro-btn-md bg-pink-500 border-pink-500 text-white hover:bg-pink-600 hover:border-pink-600 disabled:opacity-50 disabled:cursor-not-allowed" disabled={!saveRequestName || !saveToFolderId}>Save</button>
767
+ </>
768
+ }
769
+ >
770
+ <div className="space-y-4 py-2">
771
+ <div>
772
+ <label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Request Name</label>
738
773
  <input
739
774
  type="text"
740
- value={newFolderName}
741
- onChange={(e) => setNewFolderName(e.target.value)}
742
- placeholder="Folder name..."
743
- className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2 mb-6"
775
+ value={saveRequestName}
776
+ onChange={(e) => setSaveRequestName(e.target.value)}
777
+ placeholder="e.g. List Posts..."
778
+ className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
744
779
  />
745
- <div className="p-4 border-t border-[var(--kyro-border)] flex justify-end gap-2 bg-[var(--kyro-surface-accent)]">
746
- <button type="button" onClick={() => setShowFolderModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
747
- <button type="button" onClick={createFolder} className="kyro-btn kyro-btn-md kyro-btn-primary bg-pink-500 border-pink-500 text-white hover:bg-pink-600 hover:border-pink-600">Create</button>
748
- </div>
749
780
  </div>
750
- </div>
751
- )}
752
-
753
- {showSaveModal && (
754
- <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100] p-4">
755
- <div className="surface-tile w-full max-w-md p-6 rounded-2xl shadow-2xl border border-[var(--kyro-border)]">
756
- <h2 className="text-xl font-bold mb-4">Save Request</h2>
757
- <div className="space-y-4">
758
- <div>
759
- <label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Request Name</label>
760
- <input
761
- type="text"
762
- value={saveRequestName}
763
- onChange={(e) => setSaveRequestName(e.target.value)}
764
- placeholder="e.g. List Posts..."
765
- className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
766
- />
767
- </div>
768
- <div>
769
- <label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Folder</label>
770
- <select
771
- value={saveToFolderId}
772
- onChange={(e) => setSaveToFolderId(e.target.value)}
773
- className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
774
- >
775
- <option value="">Select Folder...</option>
776
- {folders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
777
- </select>
778
- </div>
779
- </div>
780
- <div className="p-4 border-t border-[var(--kyro-border)] flex justify-end gap-2 bg-[var(--kyro-surface-accent)]">
781
- <button type="button" onClick={() => setShowSaveModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
782
- <button type="button" onClick={saveRequest} className="kyro-btn kyro-btn-md kyro-btn-primary bg-pink-500 border-pink-500 text-white hover:bg-pink-600 hover:border-pink-600 disabled:opacity-50 disabled:cursor-not-allowed" disabled={!saveRequestName || !saveToFolderId}>Save</button>
783
- </div>
781
+ <div>
782
+ <label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Folder</label>
783
+ <select
784
+ value={saveToFolderId}
785
+ onChange={(e) => setSaveToFolderId(e.target.value)}
786
+ className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
787
+ >
788
+ <option value="">Select Folder...</option>
789
+ {folders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
790
+ </select>
784
791
  </div>
785
792
  </div>
786
- )}
793
+ </Modal>
787
794
  </div>
788
795
  );
789
796
  }
@@ -151,7 +151,7 @@ export function SessionsManager() {
151
151
  >
152
152
  <div className="flex items-start justify-between gap-4 relative z-10">
153
153
  <div className="flex items-center gap-3">
154
- <div className={`p-2.5 rounded-xl transition-colors shadow-sm ${s.currentSession ? "bg-[var(--kyro-primary)] text-white" : "bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] border border-[var(--kyro-border)]"}`}>
154
+ <div className={`kyro-btn-primary p-2.5 rounded-xl transition-colors shadow-sm ${s.currentSession ? "" : "bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] border border-[var(--kyro-border)]"}`}>
155
155
  {s.deviceInfo?.platform?.toLowerCase().includes("android") || s.deviceInfo?.platform?.toLowerCase().includes("ios")
156
156
  ? <Smartphone className="w-4 h-4" />
157
157
  : <Laptop className="w-4 h-4" />}
@@ -0,0 +1,22 @@
1
+ import { AutoForm } from "./AutoForm";
2
+ import { toast } from "../lib/stores";
3
+
4
+ interface SettingsPageProps {
5
+ config: any;
6
+ globalSlug?: string;
7
+ data?: Record<string, unknown>;
8
+ layout?: "split" | "single";
9
+ }
10
+
11
+ export function SettingsPage(props: SettingsPageProps) {
12
+ return (
13
+ <AutoForm
14
+ {...props}
15
+ onActionSuccess={(msg: string) => {
16
+ toast.success(msg);
17
+ setTimeout(() => window.location.reload(), 800);
18
+ }}
19
+ onActionError={(msg: string) => toast.error(msg)}
20
+ />
21
+ );
22
+ }
@@ -3,25 +3,7 @@ import "../styles/main.css";
3
3
  import { nonAuthCollections } from "../lib/config";
4
4
  import { adminPath } from "../lib/paths";
5
5
  import { getSiteSettings } from "../lib/globals";
6
- import {
7
- Home,
8
- Database,
9
- Settings,
10
- Users,
11
- Shield,
12
- FileText,
13
- Clock,
14
- Blocks,
15
- Key,
16
- Webhook,
17
- Grid,
18
- User,
19
- LogOut,
20
- Sun,
21
- Moon,
22
- Menu,
23
- Dot,
24
- } from "./ui/icons";
6
+ import * as Icons from "lucide-react";
25
7
  import { UserMenu } from "./UserMenu";
26
8
 
27
9
  interface NavItem {
@@ -49,7 +31,7 @@ const collectionItems: NavItem[] = nonAuthCollections
49
31
  .map((col) => ({
50
32
  href: `${adminPath}/${col.slug}`,
51
33
  label: col.label || col.slug,
52
- icon: "collection",
34
+ icon: col.admin?.icon || "collection",
53
35
  }));
54
36
 
55
37
  const navSections: { label: string; items: NavItem[] }[] = [
@@ -82,24 +64,13 @@ const navSections: { label: string; items: NavItem[] }[] = [
82
64
  },
83
65
  ];
84
66
 
85
- const icons: Record<string, any> = {
86
- home: Home,
87
- media: Grid,
88
- collection: Dot,
89
- menu: Menu,
90
- settings: Settings,
91
- users: Users,
92
- shield: Shield,
93
- audit: FileText,
94
- plugins: Blocks,
95
- keys: Key,
96
- webhooks: Webhook,
97
- marketplace: Grid,
98
- user: User,
99
- logout: LogOut,
100
- sessions: Clock,
101
- sun: Sun,
102
- moon: Moon,
67
+ const iconAliases: Record<string, string> = {
68
+ collection: "Dot",
69
+ media: "Grid",
70
+ home: "Home",
71
+ users: "Users",
72
+ plugins: "Blocks",
73
+ settings: "Settings",
103
74
  };
104
75
 
105
76
  function isActive(item: NavItem): boolean {
@@ -177,7 +148,8 @@ function isActive(item: NavItem): boolean {
177
148
  }`}
178
149
  >
179
150
  {(() => {
180
- const Icon = icons[item.icon] || Database;
151
+ const iconName = iconAliases[item.icon] || item.icon;
152
+ const Icon = (Icons as any)[iconName] || Icons.Dot;
181
153
  return <Icon className="w-4 h-4" strokeWidth={2.5} />;
182
154
  })()}
183
155
  <span>{item.label}</span>
@@ -245,14 +217,14 @@ function isActive(item: NavItem): boolean {
245
217
  class="p-2 rounded-lg transition-all active:scale-95"
246
218
  title="Light Mode"
247
219
  >
248
- <Sun className="w-4 h-4" strokeWidth={2.5} />
220
+ <Icons.Sun className="w-4 h-4" strokeWidth={2.5} />
249
221
  </button>
250
222
  <button
251
223
  id="theme-dark-btn"
252
224
  class="p-2 rounded-lg transition-all active:scale-95"
253
225
  title="Dark Mode"
254
226
  >
255
- <Moon className="w-4 h-4" strokeWidth={2.5} />
227
+ <Icons.Moon className="w-4 h-4" strokeWidth={2.5} />
256
228
  </button>
257
229
  </div>
258
230