@kyro-cms/admin 0.8.0 → 0.9.1

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 (100) hide show
  1. package/dist/index.cjs +11960 -11006
  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 +563 -0
  6. package/dist/index.d.ts +7 -7
  7. package/dist/index.js +12183 -11238
  8. package/dist/index.js.map +1 -1
  9. package/package.json +15 -11
  10. package/src/components/ActionBar.tsx +27 -14
  11. package/src/components/Admin.tsx +1 -1
  12. package/src/components/ApiKeysManager.tsx +5 -5
  13. package/src/components/AutoForm.tsx +585 -369
  14. package/src/components/BrandingHub.tsx +7 -4
  15. package/src/components/CreateView.tsx +2 -0
  16. package/src/components/DetailView.tsx +71 -56
  17. package/src/components/DeveloperCenter.tsx +8 -6
  18. package/src/components/FieldRenderer.tsx +94 -19
  19. package/src/components/ListView.tsx +33 -20
  20. package/src/components/MediaGallery.tsx +219 -194
  21. package/src/components/PluginsManager.tsx +197 -70
  22. package/src/components/RestPlayground.tsx +7 -7
  23. package/src/components/SessionsManager.tsx +1 -1
  24. package/src/components/SettingsPage.tsx +22 -0
  25. package/src/components/Sidebar.astro +13 -41
  26. package/src/components/UserManagement.tsx +153 -15
  27. package/src/components/UserMenu.tsx +30 -4
  28. package/src/components/VersionHistoryPanel.tsx +112 -119
  29. package/src/components/WebhookManager.tsx +6 -4
  30. package/src/components/blocks/ArrayBlock.tsx +6 -23
  31. package/src/components/blocks/BlockEditModal.tsx +82 -309
  32. package/src/components/blocks/CardBlock.tsx +35 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +57 -31
  34. package/src/components/blocks/GenericBlock.tsx +44 -0
  35. package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
  36. package/src/components/blocks/HeroBlock.tsx +5 -14
  37. package/src/components/blocks/RichTextBlock.tsx +5 -5
  38. package/src/components/blocks/index.ts +5 -3
  39. package/src/components/fields/AccordionField.tsx +2 -2
  40. package/src/components/fields/ArrayField.tsx +1 -1
  41. package/src/components/fields/ArrayLayout.tsx +120 -29
  42. package/src/components/fields/BlocksField.tsx +430 -50
  43. package/src/components/fields/CardField.tsx +73 -0
  44. package/src/components/fields/CheckboxField.tsx +7 -3
  45. package/src/components/fields/DateField.tsx +4 -1
  46. package/src/components/fields/GroupLayout.tsx +2 -2
  47. package/src/components/fields/HeadingSubheadingField.tsx +43 -0
  48. package/src/components/fields/ListField.tsx +2 -2
  49. package/src/components/fields/NumberField.tsx +4 -1
  50. package/src/components/fields/RelationshipField.tsx +153 -87
  51. package/src/components/fields/RichTextField.tsx +781 -0
  52. package/src/components/fields/SecretField.tsx +102 -0
  53. package/src/components/fields/SelectField.tsx +19 -6
  54. package/src/components/fields/TabsLayout.tsx +19 -9
  55. package/src/components/fields/TextField.tsx +4 -1
  56. package/src/components/fields/UploadField.tsx +122 -56
  57. package/src/components/fields/extensions/blockComponents.tsx +103 -174
  58. package/src/components/fields/extensions/blocksStore.ts +8 -1
  59. package/src/components/fields/index.ts +4 -2
  60. package/src/components/ui/PageHeader.tsx +5 -5
  61. package/src/components/ui/SlidePanel.tsx +8 -3
  62. package/src/components/ui/icons.tsx +109 -109
  63. package/src/components/users/UserDetail.tsx +79 -16
  64. package/src/hooks/useAutoFormState.ts +125 -62
  65. package/src/integration.ts +148 -46
  66. package/src/kyro-cms.d.ts +7 -2
  67. package/src/layouts/AuthLayout.astro +14 -2
  68. package/src/lib/autoform-store.ts +85 -52
  69. package/src/lib/change-source.ts +9 -0
  70. package/src/lib/config.ts +104 -8
  71. package/src/lib/globals.ts +44 -9
  72. package/src/lib/normalize-upload-fields.ts +41 -0
  73. package/src/lib/paths.ts +2 -2
  74. package/src/lib/resolve-field-value.ts +110 -0
  75. package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
  76. package/src/lib/shim/use-sync-external-store.js +1 -0
  77. package/src/lib/stores/index.ts +1 -0
  78. package/src/lib/useResourceManager.ts +4 -4
  79. package/src/lib/vite-shim-plugin.ts +100 -0
  80. package/src/pages/[collection]/[id].astro +1 -1
  81. package/src/pages/preview/[collection]/[id].astro +4 -4
  82. package/src/pages/settings/[slug].astro +2 -2
  83. package/src/styles/main.css +60 -54
  84. package/README.md +0 -46
  85. package/dist/EditorClient-Q23UXR37.cjs +0 -468
  86. package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
  87. package/dist/EditorClient-T5PASFNR.js +0 -466
  88. package/dist/EditorClient-T5PASFNR.js.map +0 -1
  89. package/dist/chunk-3BGDYKTD.cjs +0 -348
  90. package/dist/chunk-3BGDYKTD.cjs.map +0 -1
  91. package/dist/chunk-EEFXLQVT.js +0 -3
  92. package/dist/chunk-EEFXLQVT.js.map +0 -1
  93. package/src/components/blocks/ButtonBlock.tsx +0 -64
  94. package/src/components/blocks/ColumnsBlock.tsx +0 -55
  95. package/src/components/blocks/DividerBlock.tsx +0 -43
  96. package/src/components/blocks/LinkBlock.tsx +0 -65
  97. package/src/components/blocks/VStackBlock.tsx +0 -29
  98. package/src/components/fields/EditorClient.tsx +0 -535
  99. package/src/components/fields/PortableTextField.tsx +0 -155
  100. 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,5 +1,5 @@
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
4
 
5
5
  interface EnvVariable {
@@ -82,7 +82,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
82
82
  const [newFolderName, setNewFolderName] = useState("");
83
83
  const [saveToFolderId, setSaveToFolderId] = useState("");
84
84
  const [saveRequestName, setSaveRequestName] = useState("");
85
- const { confirm, alert } = useUIStore();
85
+ const { confirm } = useUIStore();
86
86
 
87
87
  // Load from localStorage
88
88
  useEffect(() => {
@@ -325,9 +325,9 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
325
325
  setEnvVars((prev) => [...prev, ...data.envVars]);
326
326
  }
327
327
 
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." });
328
+ toast.success("Your playground data has been imported.");
329
+ } catch (err) {
330
+ toast.error("Invalid JSON file structure.");
331
331
  }
332
332
  };
333
333
  reader.readAsText(file);
@@ -744,7 +744,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
744
744
  />
745
745
  <div className="p-4 border-t border-[var(--kyro-border)] flex justify-end gap-2 bg-[var(--kyro-surface-accent)]">
746
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>
747
+ <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>
748
748
  </div>
749
749
  </div>
750
750
  </div>
@@ -779,7 +779,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
779
779
  </div>
780
780
  <div className="p-4 border-t border-[var(--kyro-border)] flex justify-end gap-2 bg-[var(--kyro-surface-accent)]">
781
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>
782
+ <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>
783
783
  </div>
784
784
  </div>
785
785
  </div>
@@ -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