@kyro-cms/admin 0.1.6 → 0.1.7

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 (163) hide show
  1. package/README.md +149 -51
  2. package/package.json +53 -6
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +136 -27
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +1417 -661
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +3 -3
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +199 -57
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +786 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +191 -53
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +149 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/UserManagement.tsx +204 -0
  26. package/src/components/VersionHistoryPanel.tsx +3 -3
  27. package/src/components/WebhookManager.tsx +608 -0
  28. package/src/components/blocks/AccordionBlock.tsx +97 -0
  29. package/src/components/blocks/ArrayBlock.tsx +75 -0
  30. package/src/components/blocks/BlockEditModal.MARKER +12 -0
  31. package/src/components/blocks/BlockEditModal.tsx +774 -0
  32. package/src/components/blocks/ButtonBlock.tsx +165 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +66 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +151 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +64 -0
  38. package/src/components/blocks/HeadingBlock.tsx +81 -0
  39. package/src/components/blocks/HeroBlock.tsx +157 -0
  40. package/src/components/blocks/ImageBlock.tsx +83 -0
  41. package/src/components/blocks/LinkBlock.tsx +71 -0
  42. package/src/components/blocks/ListBlock.tsx +39 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +279 -0
  45. package/src/components/blocks/VStackBlock.tsx +75 -0
  46. package/src/components/blocks/VideoBlock.tsx +45 -0
  47. package/src/components/blocks/index.ts +10 -0
  48. package/src/components/fields/BlocksField.tsx +323 -0
  49. package/src/components/fields/CheckboxField.tsx +15 -9
  50. package/src/components/fields/CodeField.tsx +234 -0
  51. package/src/components/fields/DateField.tsx +38 -11
  52. package/src/components/fields/EditorClient.tsx +271 -0
  53. package/src/components/fields/FileField.tsx +390 -0
  54. package/src/components/fields/HybridContentField.tsx +109 -0
  55. package/src/components/fields/ImageField.tsx +429 -0
  56. package/src/components/fields/JSONField.tsx +361 -0
  57. package/src/components/fields/MarkdownField.tsx +282 -0
  58. package/src/components/fields/NumberField.tsx +42 -12
  59. package/src/components/fields/PortableTextField.tsx +143 -0
  60. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  61. package/src/components/fields/RelationshipField.tsx +231 -59
  62. package/src/components/fields/SelectField.tsx +25 -15
  63. package/src/components/fields/TextField.tsx +45 -14
  64. package/src/components/fields/extensions/blockComponents.tsx +237 -0
  65. package/src/components/fields/extensions/blocksStore.ts +273 -0
  66. package/src/components/fields/index.ts +13 -0
  67. package/src/components/index.ts +1 -2
  68. package/src/components/layout/Header.tsx +2 -2
  69. package/src/components/layout/Layout.tsx +2 -2
  70. package/src/components/ui/Badge.tsx +9 -4
  71. package/src/components/ui/BlockDrawer.tsx +79 -0
  72. package/src/components/ui/Button.tsx +1 -1
  73. package/src/components/ui/CommandPalette.tsx +362 -0
  74. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  75. package/src/components/ui/Dropdown.tsx +1 -1
  76. package/src/components/ui/Modal.tsx +37 -12
  77. package/src/components/ui/PromptModal.tsx +94 -0
  78. package/src/components/ui/SlidePanel.tsx +43 -16
  79. package/src/components/ui/Toast.tsx +80 -14
  80. package/src/env.d.ts +16 -0
  81. package/src/env.ts +20 -0
  82. package/src/index.ts +0 -1
  83. package/src/layouts/AdminLayout.astro +164 -170
  84. package/src/layouts/AuthLayout.astro +23 -6
  85. package/src/lib/MediaService.ts +541 -0
  86. package/src/lib/auth/sqlite-adapter.ts +319 -0
  87. package/src/lib/config.ts +22 -6
  88. package/src/lib/dataStore.ts +132 -74
  89. package/src/lib/db/adapter.ts +54 -0
  90. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  91. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  92. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  93. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  94. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  95. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  96. package/src/lib/db/index.ts +449 -0
  97. package/src/lib/db/mongodb-adapter.ts +207 -0
  98. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  99. package/src/lib/db/schema/mysql-auth.ts +113 -0
  100. package/src/lib/db/schema/mysql-content.ts +20 -0
  101. package/src/lib/db/schema/postgres-auth.ts +116 -0
  102. package/src/lib/db/schema/postgres-content.ts +35 -0
  103. package/src/lib/db/schema/postgres-media.ts +52 -0
  104. package/src/lib/db/schema/postgres-settings.ts +11 -0
  105. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  106. package/src/lib/db/schema/sqlite-content.ts +20 -0
  107. package/src/lib/graphql/index.ts +1 -0
  108. package/src/lib/graphql/schema.ts +443 -0
  109. package/src/lib/rate-limit.ts +267 -0
  110. package/src/lib/storage.ts +374 -0
  111. package/src/lib/store.ts +85 -0
  112. package/src/middleware.ts +70 -11
  113. package/src/pages/[collection]/[id].astro +178 -122
  114. package/src/pages/[collection]/index.astro +24 -156
  115. package/src/pages/admin/api-explorer.astro +98 -0
  116. package/src/pages/admin/graphql-explorer.astro +40 -0
  117. package/src/pages/admin/graphql.astro +97 -0
  118. package/src/pages/admin/index.astro +200 -139
  119. package/src/pages/admin/keys.astro +8 -0
  120. package/src/pages/admin/rest-playground.astro +44 -0
  121. package/src/pages/admin/webhooks.astro +8 -0
  122. package/src/pages/api/[collection]/[id]/publish.ts +44 -0
  123. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  124. package/src/pages/api/[collection]/[id]/versions.ts +36 -0
  125. package/src/pages/api/[collection]/[id].ts +102 -159
  126. package/src/pages/api/[collection]/index.ts +151 -230
  127. package/src/pages/api/auth/[id].ts +48 -69
  128. package/src/pages/api/auth/audit-logs.ts +20 -43
  129. package/src/pages/api/auth/login.ts +159 -45
  130. package/src/pages/api/auth/logout.ts +42 -24
  131. package/src/pages/api/auth/refresh.ts +119 -0
  132. package/src/pages/api/auth/register.ts +110 -40
  133. package/src/pages/api/auth/users.ts +22 -97
  134. package/src/pages/api/collections.ts +59 -0
  135. package/src/pages/api/globals/[slug]/test.ts +172 -0
  136. package/src/pages/api/globals/[slug].ts +42 -0
  137. package/src/pages/api/graphql.ts +90 -0
  138. package/src/pages/api/health.ts +417 -40
  139. package/src/pages/api/keys/[id].ts +26 -0
  140. package/src/pages/api/keys/index.ts +75 -0
  141. package/src/pages/api/media/[id].ts +309 -0
  142. package/src/pages/api/media/folders.ts +609 -0
  143. package/src/pages/api/media/index.ts +146 -0
  144. package/src/pages/api/media/resize.ts +267 -0
  145. package/src/pages/api/search.ts +82 -0
  146. package/src/pages/api/slug-availability.ts +70 -0
  147. package/src/pages/api/storage-config.ts +20 -0
  148. package/src/pages/api/storage-status.ts +206 -0
  149. package/src/pages/api/upload.ts +334 -0
  150. package/src/pages/api/webhooks/index.ts +71 -0
  151. package/src/pages/audit/index.astro +2 -104
  152. package/src/pages/login.astro +11 -11
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +13 -13
  156. package/src/pages/roles/index.astro +21 -21
  157. package/src/pages/settings/[slug].astro +162 -0
  158. package/src/pages/settings/index.astro +9 -0
  159. package/src/pages/users/[id].astro +29 -21
  160. package/src/pages/users/index.astro +22 -17
  161. package/src/pages/users/new.astro +18 -17
  162. package/src/styles/main.css +553 -128
  163. package/src/components/layout/Sidebar.tsx +0 -497
@@ -0,0 +1,323 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from "react";
2
+ import { useBlocksStore, createNewBlock } from "./extensions/blocksStore";
3
+ import { BlockDrawer, DraggableBlockType } from "../ui/BlockDrawer";
4
+ import { Plus, Box } from "lucide-react";
5
+ import {
6
+ BLOCK_COMPONENTS,
7
+ getBlockComponent,
8
+ blockCategories,
9
+ blockIcons,
10
+ } from "./extensions/blockComponents";
11
+ import {
12
+ DndContext,
13
+ closestCenter,
14
+ PointerSensor,
15
+ useSensor,
16
+ useSensors,
17
+ KeyboardSensor,
18
+ useDraggable,
19
+ DragOverlay,
20
+ } from "@dnd-kit/core";
21
+ import type { DragEndEvent, DragStartEvent, Active } from "@dnd-kit/core";
22
+ import {
23
+ SortableContext,
24
+ useSortable,
25
+ verticalListSortingStrategy,
26
+ } from "@dnd-kit/sortable";
27
+ import { CSS } from "@dnd-kit/utilities";
28
+
29
+ interface BlocksFieldProps {
30
+ field: any;
31
+ value: any;
32
+ onChange?: (value: any) => void;
33
+ onBlocksChange?: () => void;
34
+ error?: string;
35
+ disabled?: boolean;
36
+ documentStatus?: "draft" | "published" | "scheduled" | "archived";
37
+ justSaved?: boolean;
38
+ }
39
+
40
+ import { GripVertical } from "lucide-react";
41
+
42
+ // Sortable block wrapper for drag-and-drop
43
+ const SortableBlockComponent = ({
44
+ block,
45
+ index,
46
+ }: {
47
+ block: any;
48
+ index: number;
49
+ }) => {
50
+ const {
51
+ attributes,
52
+ listeners,
53
+ setNodeRef,
54
+ transform,
55
+ transition,
56
+ isDragging,
57
+ } = useSortable({ id: block.id });
58
+
59
+ const style = {
60
+ transform: CSS.Transform.toString(transform),
61
+ transition,
62
+ zIndex: isDragging ? 10 : 1,
63
+ opacity: isDragging ? 0.8 : 1,
64
+ };
65
+
66
+ const Component = getBlockComponent(block.type);
67
+
68
+ return (
69
+ <div ref={setNodeRef} style={style} className="relative group pl-8">
70
+ <div
71
+ className="absolute left-0 top-1/2 -translate-y-1/2 p-1.5 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity hover:bg-[var(--kyro-surface-accent)] rounded"
72
+ {...attributes}
73
+ {...listeners}
74
+ >
75
+ <GripVertical className="w-4 h-4" />
76
+ </div>
77
+ {Component ? (
78
+ <Component block={block} index={index} />
79
+ ) : (
80
+ <div className="p-4 border border-[var(--kyro-border)] rounded-md">
81
+ Unknown block: {block.type}
82
+ </div>
83
+ )}
84
+ </div>
85
+ );
86
+ };
87
+ // Memoize per-block to minimize re-renders when unrelated blocks change
88
+ const SortableBlock = React.memo(SortableBlockComponent);
89
+
90
+ export const BlocksField: React.FC<BlocksFieldProps> = ({
91
+ field,
92
+ value,
93
+ onChange,
94
+ onBlocksChange,
95
+ error,
96
+ disabled,
97
+ documentStatus,
98
+ justSaved,
99
+ }) => {
100
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
101
+ const { blocks, setBlocks, addBlock, setOnBlocksChange } = useBlocksStore();
102
+ const [activeDrag, setActiveDrag] = useState<Active | null>(null);
103
+
104
+ // Register blocks change callback
105
+ useEffect(() => {
106
+ if (onBlocksChange) {
107
+ setOnBlocksChange(onBlocksChange);
108
+ }
109
+ return () => {
110
+ setOnBlocksChange(() => { });
111
+ };
112
+ }, [onBlocksChange, setOnBlocksChange]);
113
+
114
+ // Sync external value changes (e.g., auto-save restore) to store
115
+ useEffect(() => {
116
+ const currentIds = blocks.map((b) => b.id).join(",");
117
+ const valueIds = (value || []).map((b: any) => b.id).join(",");
118
+
119
+ // Only update if the IDs don't match (external change)
120
+ if (valueIds !== currentIds) {
121
+ if (value && Array.isArray(value) && value.length > 0) {
122
+ setBlocks(value);
123
+ } else if (!value || (Array.isArray(value) && value.length === 0)) {
124
+ setBlocks([]);
125
+ }
126
+ }
127
+ }, [value]);
128
+
129
+ // Debounced sync of store changes back to parent form to reduce re-renders
130
+ const onChangeTimer = useRef<number | null>(null);
131
+ useEffect(() => {
132
+ if (!onChange) return;
133
+ if (onChangeTimer.current) {
134
+ window.clearTimeout(onChangeTimer.current);
135
+ onChangeTimer.current = null;
136
+ }
137
+ onChangeTimer.current = window.setTimeout(() => {
138
+ onChange(blocks);
139
+ }, 250);
140
+ return () => {
141
+ if (onChangeTimer.current) {
142
+ window.clearTimeout(onChangeTimer.current);
143
+ onChangeTimer.current = null;
144
+ }
145
+ };
146
+ }, [blocks, onChange]);
147
+
148
+ // Determine left border style based on document status
149
+ const getBorderClass = () => {
150
+ if (justSaved) {
151
+ return "border-l-[3px] border-green-500";
152
+ }
153
+ if (
154
+ documentStatus === "draft" ||
155
+ documentStatus === "scheduled" ||
156
+ documentStatus === "archived"
157
+ ) {
158
+ return "border-l-[3px] border-amber-500";
159
+ }
160
+ return "";
161
+ };
162
+
163
+ const handleAddBlock = useCallback(
164
+ (blockType: string) => {
165
+ addBlock(blockType);
166
+ },
167
+ [addBlock],
168
+ );
169
+
170
+ // Set up dnd-kit sensors
171
+ const sensors = useSensors(
172
+ useSensor(PointerSensor, {
173
+ activationConstraint: {
174
+ distance: 8,
175
+ },
176
+ }),
177
+ useSensor(KeyboardSensor),
178
+ );
179
+
180
+ const handleDragStart = (event: DragStartEvent) => {
181
+ setActiveDrag(event.active);
182
+ };
183
+
184
+ const handleDragEnd = (event: DragEndEvent) => {
185
+ const { active, over } = event;
186
+ setActiveDrag(null);
187
+
188
+ if (!over) return;
189
+
190
+ // Case 1: Dragged from drawer
191
+ if (active.id.toString().startsWith("drawer-")) {
192
+ const blockType = active.id.toString().replace("drawer-", "");
193
+
194
+ // Check if dropped on a container
195
+ if (over.id.toString().startsWith("container-")) {
196
+ const containerId = over.id.toString().replace("container-", "");
197
+ const container = blocks.find((b) => b.id === containerId);
198
+ if (container) {
199
+ const { updateBlock } = useBlocksStore.getState();
200
+ const newBlock = createNewBlock(blockType);
201
+ updateBlock(containerId, {
202
+ children: [...(container.children || []), newBlock],
203
+ });
204
+ }
205
+ } else {
206
+ // Dropped on root level - add as top-level block
207
+ addBlock(blockType);
208
+ }
209
+ return;
210
+ }
211
+
212
+ // Case 2: Reordering existing blocks
213
+ if (active.id !== over.id) {
214
+ const oldIndex = blocks.findIndex((b) => b.id === active.id);
215
+ const newIndex = blocks.findIndex((b) => b.id === over.id);
216
+
217
+ if (oldIndex !== -1 && newIndex !== -1) {
218
+ const newBlocks = [...blocks];
219
+ const [movedBlock] = newBlocks.splice(oldIndex, 1);
220
+ newBlocks.splice(newIndex, 0, movedBlock);
221
+ setBlocks(newBlocks);
222
+ }
223
+ }
224
+ };
225
+
226
+ // Render active drag overlay
227
+ const activeBlock = activeDrag
228
+ ? blockCategories
229
+ .flatMap((cat) => cat.blocks)
230
+ .find((b) => `drawer-${b.type}` === activeDrag.id) ||
231
+ blocks.find((b) => b.id === activeDrag.id)
232
+ : null;
233
+
234
+ const activeBlockLabel = activeBlock
235
+ ? "label" in activeBlock
236
+ ? (activeBlock as any).label
237
+ : activeBlock.type
238
+ : "Block";
239
+
240
+ const borderClass = getBorderClass();
241
+
242
+ return (
243
+ <div className={`kyro-form-field ${borderClass}`}>
244
+ <DndContext
245
+ sensors={sensors}
246
+ collisionDetection={closestCenter}
247
+ onDragStart={handleDragStart}
248
+ onDragEnd={handleDragEnd}
249
+ >
250
+ {/* Block Builder Toolbar */}
251
+ <div className="flex items-center justify-between mb-2">
252
+ <label className="kyro-form-label">{field.label || field.name}</label>
253
+ <button
254
+ type="button"
255
+ onClick={() => setIsDrawerOpen(true)}
256
+ disabled={disabled}
257
+ className="flex items-center gap-2 px-3 py-2 text-sm text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)]/30 rounded-md transition-colors disabled:opacity-50"
258
+ >
259
+ <Plus className="w-4 h-4" />
260
+ Add Block
261
+ </button>
262
+ <BlockDrawer
263
+ open={isDrawerOpen}
264
+ onClose={() => setIsDrawerOpen(false)}
265
+ onSelect={handleAddBlock}
266
+ >
267
+ <div className="space-y-4">
268
+ {blockCategories.map((category) => (
269
+ <div key={category.title}>
270
+ <h3 className="text-xs font-semibold text-[var(--kyro-text-muted)] mb-2 uppercase tracking-wider">
271
+ {category.title}
272
+ </h3>
273
+ <div className="grid grid-cols-3 gap-2">
274
+ {category.blocks.map((block) => (
275
+ <DraggableBlockType
276
+ key={block.type}
277
+ block={block}
278
+ onSelect={handleAddBlock}
279
+ >
280
+ <div className="w-6 h-6 flex items-center justify-center rounded group-hover:bg-[var(--kyro-primary)]/10 group-hover:text-[var(--kyro-primary)] transition-all duration-300">
281
+ <span className="text-[var(--kyro-text-muted)]">
282
+ {blockIcons[
283
+ block.icon as keyof typeof blockIcons
284
+ ] || <Box className="w-4 h-4" />}
285
+ </span>
286
+ </div>
287
+ </DraggableBlockType>
288
+ ))}
289
+ </div>
290
+ </div>
291
+ ))}
292
+ </div>
293
+ </BlockDrawer>
294
+ </div>
295
+
296
+ {/* Block List with Drag-and-Drop */}
297
+ <SortableContext
298
+ items={blocks.map((b) => b.id)}
299
+ strategy={verticalListSortingStrategy}
300
+ >
301
+ <div className="space-y-4">
302
+ {blocks.map((block, index) => (
303
+ <SortableBlock key={block.id} block={block} index={index} />
304
+ ))}
305
+ {blocks.length === 0 && (
306
+ <div className="text-center py-12 text-[var(--kyro-text-muted)] border-2 border-dashed border-[var(--kyro-border)] rounded-lg">
307
+ Click the button above to add your first block
308
+ </div>
309
+ )}
310
+ </div>
311
+ </SortableContext>
312
+ <DragOverlay>
313
+ {activeDrag && activeBlock && (
314
+ <div className="bg-[var(--kyro-surface)] border border-[var(--kyro-primary)] rounded-md p-3 shadow-lg">
315
+ {(activeBlock as any).label || activeBlock.type || "Block"}
316
+ </div>
317
+ )}
318
+ </DragOverlay>
319
+ </DndContext>
320
+ {error && <p className="kyro-form-error">{error}</p>}
321
+ </div>
322
+ );
323
+ };
@@ -1,4 +1,4 @@
1
- import type { CheckboxField as CheckboxFieldType } from '@kyro-cms/core';
1
+ import type { CheckboxField as CheckboxFieldType } from "@kyro-cms/core";
2
2
 
3
3
  interface CheckboxFieldComponentProps {
4
4
  field: CheckboxFieldType;
@@ -8,7 +8,13 @@ interface CheckboxFieldComponentProps {
8
8
  disabled?: boolean;
9
9
  }
10
10
 
11
- export default function CheckboxField({ field, value = false, onChange, error, disabled }: CheckboxFieldComponentProps) {
11
+ export default function CheckboxField({
12
+ field,
13
+ value = false,
14
+ onChange,
15
+ error,
16
+ disabled,
17
+ }: CheckboxFieldComponentProps) {
12
18
  return (
13
19
  <div className="space-y-1">
14
20
  <label className="flex items-center gap-2 cursor-pointer">
@@ -17,21 +23,21 @@ export default function CheckboxField({ field, value = false, onChange, error, d
17
23
  checked={value}
18
24
  onChange={(e) => onChange?.(e.target.checked)}
19
25
  disabled={disabled || field.admin?.readOnly}
20
- className={`w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 ${
21
- disabled || field.admin?.readOnly ? 'opacity-50' : ''
26
+ className={`w-4 h-4 rounded border-[var(--kyro-border)] text-[var(--kyro-sidebar-active)] focus:ring-[var(--kyro-sidebar-active)] ${
27
+ disabled || field.admin?.readOnly ? "opacity-50" : ""
22
28
  }`}
23
29
  />
24
- <span className="text-sm font-medium text-gray-700">
30
+ <span className="text-sm font-medium text-[var(--kyro-text-primary)]">
25
31
  {field.label || field.name}
26
32
  {field.required && <span className="text-red-500 ml-1">*</span>}
27
33
  </span>
28
34
  </label>
29
35
  {field.admin?.description && !error && (
30
- <p className="text-xs text-gray-500 ml-6">{field.admin.description}</p>
31
- )}
32
- {error && (
33
- <p className="text-xs text-red-600 ml-6">{error}</p>
36
+ <p className="text-xs text-[var(--kyro-text-secondary)] ml-6">
37
+ {field.admin.description}
38
+ </p>
34
39
  )}
40
+ {error && <p className="text-xs text-red-500 ml-6">{error}</p>}
35
41
  </div>
36
42
  );
37
43
  }
@@ -0,0 +1,234 @@
1
+ import React, {
2
+ useCallback,
3
+ useMemo,
4
+ useEffect,
5
+ useState,
6
+ Suspense,
7
+ lazy,
8
+ } from "react";
9
+ import { githubLight } from "@uiw/codemirror-theme-github";
10
+ import { aura } from "@uiw/codemirror-theme-aura";
11
+ import type { CodeField as CodeFieldType } from "@kyro-cms/core";
12
+
13
+ interface CodeFieldProps {
14
+ field: CodeFieldType;
15
+ value?: string;
16
+ onChange?: (value: string) => void;
17
+ error?: string;
18
+ disabled?: boolean;
19
+ }
20
+
21
+ // Lazy-loaded CodeMirror wrapper component
22
+ const CodeMirrorEditor = lazy(() =>
23
+ import("@uiw/react-codemirror").then((mod) => ({ default: mod.default })),
24
+ );
25
+
26
+ const LANGUAGES = [
27
+ { value: "javascript", label: "JavaScript", ext: "javascript" },
28
+ { value: "typescript", label: "TypeScript", ext: "javascript" },
29
+ { value: "json", label: "JSON", ext: "json" },
30
+ { value: "html", label: "HTML", ext: "html" },
31
+ { value: "css", label: "CSS", ext: "css" },
32
+ { value: "sql", label: "SQL", ext: "sql" },
33
+ { value: "python", label: "Python", ext: "python" },
34
+ { value: "rust", label: "Rust", ext: "rust" },
35
+ { value: "java", label: "Java", ext: "java" },
36
+ { value: "cpp", label: "C++", ext: "cpp" },
37
+ { value: "php", label: "PHP", ext: "php" },
38
+ { value: "markdown", label: "Markdown", ext: "markdown" },
39
+ ];
40
+
41
+ // Language extension loader
42
+ const languageExtensions: Record<string, () => Promise<any>> = {
43
+ javascript: () =>
44
+ import("@codemirror/lang-javascript").then((m) =>
45
+ m.javascript({ jsx: true, typescript: true }),
46
+ ),
47
+ typescript: () =>
48
+ import("@codemirror/lang-javascript").then((m) =>
49
+ m.javascript({ jsx: true, typescript: true }),
50
+ ),
51
+ js: () =>
52
+ import("@codemirror/lang-javascript").then((m) =>
53
+ m.javascript({ jsx: true }),
54
+ ),
55
+ jsx: () =>
56
+ import("@codemirror/lang-javascript").then((m) =>
57
+ m.javascript({ jsx: true }),
58
+ ),
59
+ ts: () =>
60
+ import("@codemirror/lang-javascript").then((m) =>
61
+ m.javascript({ typescript: true }),
62
+ ),
63
+ json: () => import("@codemirror/lang-json").then((m) => m.json()),
64
+ css: () => import("@codemirror/lang-css").then((m) => m.css()),
65
+ sql: () => import("@codemirror/lang-sql").then((m) => m.sql()),
66
+ python: () => import("@codemirror/lang-python").then((m) => m.python()),
67
+ html: () => import("@codemirror/lang-html").then((m) => m.html()),
68
+ rust: () => import("@codemirror/lang-rust").then((m) => m.rust()),
69
+ java: () => import("@codemirror/lang-java").then((m) => m.java()),
70
+ cpp: () => import("@codemirror/lang-cpp").then((m) => m.cpp()),
71
+ c: () => import("@codemirror/lang-cpp").then((m) => m.cpp()),
72
+ php: () => import("@codemirror/lang-php").then((m) => m.php()),
73
+ markdown: () => import("@codemirror/lang-markdown").then((m) => m.markdown()),
74
+ py: () => import("@codemirror/lang-python").then((m) => m.python()),
75
+ };
76
+
77
+ export const CodeField: React.FC<CodeFieldProps> = ({
78
+ field,
79
+ value = "",
80
+ onChange,
81
+ error,
82
+ disabled,
83
+ }) => {
84
+ const [isMounted, setIsMounted] = useState(false);
85
+ const [isDark, setIsDark] = useState(false);
86
+ const [extensions, setExtensions] = useState<any[]>([]);
87
+ const [loading, setLoading] = useState(false);
88
+
89
+ const language = field.language?.toLowerCase() || "javascript";
90
+
91
+ useEffect(() => {
92
+ setIsMounted(true);
93
+ setIsDark(document.documentElement.classList.contains("dark"));
94
+
95
+ const observer = new MutationObserver(() => {
96
+ setIsDark(document.documentElement.classList.contains("dark"));
97
+ });
98
+
99
+ observer.observe(document.documentElement, {
100
+ attributes: true,
101
+ attributeFilter: ["class"],
102
+ });
103
+
104
+ return () => observer.disconnect();
105
+ }, []);
106
+
107
+ // Load language extensions dynamically
108
+ useEffect(() => {
109
+ if (!isMounted) return;
110
+
111
+ const loadExtensions = async () => {
112
+ setLoading(true);
113
+ try {
114
+ const loader =
115
+ languageExtensions[language] || languageExtensions.javascript;
116
+ const ext = await loader();
117
+ setExtensions([ext]);
118
+ } catch (err) {
119
+ console.error("Failed to load language extension:", err);
120
+ setExtensions([]);
121
+ } finally {
122
+ setLoading(false);
123
+ }
124
+ };
125
+
126
+ loadExtensions();
127
+ }, [language, isMounted]);
128
+
129
+ const handleChange = useCallback(
130
+ (val: string) => {
131
+ onChange?.(val);
132
+ },
133
+ [onChange],
134
+ );
135
+
136
+ const theme = isDark ? aura : githubLight;
137
+
138
+ if (!isMounted) {
139
+ return (
140
+ <div className="kyro-form-field">
141
+ <label className="kyro-form-label">
142
+ {field.label || field.name}
143
+ {field.required && (
144
+ <span className="kyro-form-label-required">*</span>
145
+ )}
146
+ </label>
147
+ <div className="h-[300px] bg-[var(--kyro-surface)] animate-pulse rounded-md" />
148
+ </div>
149
+ );
150
+ }
151
+
152
+ return (
153
+ <div className="kyro-form-field">
154
+ <div className="flex items-center justify-between mb-2">
155
+ <label className="kyro-form-label">
156
+ {field.label || field.name}
157
+ {field.required && (
158
+ <span className="kyro-form-label-required">*</span>
159
+ )}
160
+ </label>
161
+
162
+ {/* Language indicator */}
163
+ <div className="flex items-center gap-2">
164
+ {loading && (
165
+ <span className="text-xs text-[var(--kyro-text-muted)] animate-pulse">
166
+ Loading...
167
+ </span>
168
+ )}
169
+ <span className="text-xs text-[var(--kyro-text-muted)]">
170
+ {LANGUAGES.find((l) => l.value === language)?.label || language}
171
+ </span>
172
+ </div>
173
+ </div>
174
+
175
+ <div
176
+ className={`border border-[var(--kyro-border)] rounded-md overflow-hidden ${
177
+ disabled ? "opacity-50 cursor-not-allowed" : ""
178
+ } ${error ? "border-red-500" : ""}`}
179
+ >
180
+ <Suspense
181
+ fallback={
182
+ <div className="h-[300px] bg-[var(--kyro-surface)] animate-pulse flex items-center justify-center">
183
+ <span className="text-xs text-[var(--kyro-text-muted)]">
184
+ Loading editor...
185
+ </span>
186
+ </div>
187
+ }
188
+ >
189
+ <CodeMirrorEditor
190
+ value={value}
191
+ height="300px"
192
+ extensions={extensions}
193
+ theme={theme}
194
+ onChange={handleChange}
195
+ editable={!disabled}
196
+ basicSetup={true}
197
+ style={{
198
+ fontSize: "13px",
199
+ fontFamily:
200
+ "'Fira Code', 'JetBrains Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace",
201
+ }}
202
+ />
203
+ </Suspense>
204
+ </div>
205
+
206
+ {/* Editor hints */}
207
+ <div className="flex items-center gap-4 mt-2 text-xs text-[var(--kyro-text-muted)]">
208
+ <span>
209
+ <kbd className="px-1.5 py-0.5 bg-[var(--kyro-surface-accent)] rounded text-[10px] font-mono">
210
+ Ctrl+F
211
+ </kbd>{" "}
212
+ Search
213
+ </span>
214
+ <span>
215
+ <kbd className="px-1.5 py-0.5 bg-[var(--kyro-surface-accent)] rounded text-[10px] font-mono">
216
+ Ctrl+Space
217
+ </kbd>{" "}
218
+ Autocomplete
219
+ </span>
220
+ <span>
221
+ <kbd className="px-1.5 py-0.5 bg-[var(--kyro-surface-accent)] rounded text-[10px] font-mono">
222
+ Ctrl+Z
223
+ </kbd>{" "}
224
+ Undo
225
+ </span>
226
+ </div>
227
+
228
+ {field.admin?.description && !error && (
229
+ <p className="kyro-form-help">{field.admin.description}</p>
230
+ )}
231
+ {error && <p className="kyro-form-error">{error}</p>}
232
+ </div>
233
+ );
234
+ };
@@ -1,4 +1,5 @@
1
- import type { DateField as DateFieldType } from '@kyro-cms/core';
1
+ import { useEffect, useState } from "react";
2
+ import type { DateField as DateFieldType } from "@kyro-cms/core";
2
3
 
3
4
  interface DateFieldComponentProps {
4
5
  field: DateFieldType;
@@ -8,17 +9,37 @@ interface DateFieldComponentProps {
8
9
  disabled?: boolean;
9
10
  }
10
11
 
11
- export default function DateField({ field, value = '', onChange, error, disabled }: DateFieldComponentProps) {
12
+ export default function DateField({
13
+ field,
14
+ value = "",
15
+ onChange,
16
+ error,
17
+ disabled,
18
+ }: DateFieldComponentProps) {
19
+ const [isDark, setIsDark] = useState(false);
20
+
21
+ useEffect(() => {
22
+ setIsDark(document.documentElement.classList.contains("dark"));
23
+ const observer = new MutationObserver(() => {
24
+ setIsDark(document.documentElement.classList.contains("dark"));
25
+ });
26
+ observer.observe(document.documentElement, {
27
+ attributes: true,
28
+ attributeFilter: ["class"],
29
+ });
30
+ return () => observer.disconnect();
31
+ }, []);
32
+
12
33
  return (
13
34
  <div className="space-y-1">
14
35
  {field.label && (
15
- <label className="block text-sm font-medium text-gray-700">
36
+ <label className="block text-sm font-medium">
16
37
  {field.label}
17
38
  {field.required && <span className="text-red-500 ml-1">*</span>}
18
39
  </label>
19
40
  )}
20
41
  <input
21
- type={field.time ? 'datetime-local' : 'date'}
42
+ type={field.time ? "datetime-local" : "date"}
22
43
  value={value}
23
44
  onChange={(e) => onChange?.(e.target.value)}
24
45
  disabled={disabled || field.admin?.readOnly}
@@ -27,16 +48,22 @@ export default function DateField({ field, value = '', onChange, error, disabled
27
48
  required={field.required}
28
49
  className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
29
50
  error
30
- ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
31
- : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
32
- } ${disabled || field.admin?.readOnly ? 'bg-gray-50 text-gray-500' : 'bg-white'}`}
51
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500"
52
+ : "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
53
+ } ${
54
+ disabled || field.admin?.readOnly
55
+ ? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] opacity-50"
56
+ : isDark
57
+ ? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
58
+ : "bg-white text-gray-900"
59
+ }`}
33
60
  />
34
61
  {field.admin?.description && !error && (
35
- <p className="text-xs text-gray-500">{field.admin.description}</p>
36
- )}
37
- {error && (
38
- <p className="text-xs text-red-600">{error}</p>
62
+ <p className="text-xs text-[var(--kyro-text-secondary)]">
63
+ {field.admin.description}
64
+ </p>
39
65
  )}
66
+ {error && <p className="text-xs text-red-600">{error}</p>}
40
67
  </div>
41
68
  );
42
69
  }