@kyro-cms/admin 0.1.5 → 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.
- package/README.md +149 -51
- package/package.json +52 -5
- package/src/collections/auth/index.ts +2 -2
- package/src/collections/portfolio/index.ts +343 -0
- package/src/components/ActionBar.tsx +153 -16
- package/src/components/Admin.tsx +136 -27
- package/src/components/ApiExplorer.tsx +325 -0
- package/src/components/ApiKeysManager.tsx +563 -0
- package/src/components/AuditLogsPage.tsx +664 -0
- package/src/components/AutoForm.tsx +1417 -661
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +3 -3
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +199 -57
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +786 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +191 -53
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +149 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- package/src/components/UserManagement.tsx +204 -0
- package/src/components/VersionHistoryPanel.tsx +3 -3
- package/src/components/WebhookManager.tsx +608 -0
- package/src/components/blocks/AccordionBlock.tsx +97 -0
- package/src/components/blocks/ArrayBlock.tsx +75 -0
- package/src/components/blocks/BlockEditModal.MARKER +12 -0
- package/src/components/blocks/BlockEditModal.tsx +774 -0
- package/src/components/blocks/ButtonBlock.tsx +165 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +66 -0
- package/src/components/blocks/ColumnsBlock.tsx +151 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +64 -0
- package/src/components/blocks/HeadingBlock.tsx +81 -0
- package/src/components/blocks/HeroBlock.tsx +157 -0
- package/src/components/blocks/ImageBlock.tsx +83 -0
- package/src/components/blocks/LinkBlock.tsx +71 -0
- package/src/components/blocks/ListBlock.tsx +39 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +279 -0
- package/src/components/blocks/VStackBlock.tsx +75 -0
- package/src/components/blocks/VideoBlock.tsx +45 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/CheckboxField.tsx +15 -9
- package/src/components/fields/CodeField.tsx +234 -0
- package/src/components/fields/DateField.tsx +38 -11
- package/src/components/fields/EditorClient.tsx +271 -0
- package/src/components/fields/FileField.tsx +390 -0
- package/src/components/fields/HybridContentField.tsx +109 -0
- package/src/components/fields/ImageField.tsx +429 -0
- package/src/components/fields/JSONField.tsx +361 -0
- package/src/components/fields/MarkdownField.tsx +282 -0
- package/src/components/fields/NumberField.tsx +42 -12
- package/src/components/fields/PortableTextField.tsx +143 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipField.tsx +231 -59
- package/src/components/fields/SelectField.tsx +25 -15
- package/src/components/fields/TextField.tsx +45 -14
- package/src/components/fields/extensions/blockComponents.tsx +237 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +13 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +2 -2
- package/src/components/ui/Badge.tsx +9 -4
- package/src/components/ui/BlockDrawer.tsx +79 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +362 -0
- package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
- package/src/components/ui/Dropdown.tsx +1 -1
- package/src/components/ui/Modal.tsx +37 -12
- package/src/components/ui/PromptModal.tsx +94 -0
- package/src/components/ui/SlidePanel.tsx +43 -16
- package/src/components/ui/Toast.tsx +80 -14
- package/src/env.d.ts +16 -0
- package/src/env.ts +20 -0
- package/src/index.ts +0 -1
- package/src/layouts/AdminLayout.astro +164 -170
- package/src/layouts/AuthLayout.astro +50 -0
- package/src/lib/MediaService.ts +541 -0
- package/src/lib/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +22 -6
- package/src/lib/dataStore.ts +132 -74
- package/src/lib/db/adapter.ts +54 -0
- package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
- package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
- package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
- package/src/lib/db/index.ts +449 -0
- package/src/lib/db/mongodb-adapter.ts +207 -0
- package/src/lib/db/mongodb-auth-adapter.ts +305 -0
- package/src/lib/db/schema/mysql-auth.ts +113 -0
- package/src/lib/db/schema/mysql-content.ts +20 -0
- package/src/lib/db/schema/postgres-auth.ts +116 -0
- package/src/lib/db/schema/postgres-content.ts +35 -0
- package/src/lib/db/schema/postgres-media.ts +52 -0
- package/src/lib/db/schema/postgres-settings.ts +11 -0
- package/src/lib/db/schema/sqlite-auth.ts +112 -0
- package/src/lib/db/schema/sqlite-content.ts +20 -0
- package/src/lib/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -0
- package/src/middleware.ts +116 -28
- package/src/pages/[collection]/[id].astro +178 -122
- package/src/pages/[collection]/index.astro +24 -156
- package/src/pages/admin/api-explorer.astro +98 -0
- package/src/pages/admin/graphql-explorer.astro +40 -0
- package/src/pages/admin/graphql.astro +97 -0
- package/src/pages/admin/index.astro +286 -0
- package/src/pages/admin/keys.astro +8 -0
- package/src/pages/admin/rest-playground.astro +44 -0
- package/src/pages/admin/webhooks.astro +8 -0
- package/src/pages/api/[collection]/[id]/publish.ts +44 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +36 -0
- package/src/pages/api/[collection]/[id].ts +102 -159
- package/src/pages/api/[collection]/index.ts +151 -230
- package/src/pages/api/auth/[id].ts +48 -69
- package/src/pages/api/auth/audit-logs.ts +20 -43
- package/src/pages/api/auth/login.ts +159 -45
- package/src/pages/api/auth/logout.ts +50 -20
- package/src/pages/api/auth/refresh.ts +119 -0
- package/src/pages/api/auth/register.ts +110 -40
- package/src/pages/api/auth/users.ts +22 -97
- package/src/pages/api/collections.ts +59 -0
- package/src/pages/api/globals/[slug]/test.ts +172 -0
- package/src/pages/api/globals/[slug].ts +42 -0
- package/src/pages/api/graphql.ts +90 -0
- package/src/pages/api/health.ts +417 -40
- package/src/pages/api/keys/[id].ts +26 -0
- package/src/pages/api/keys/index.ts +75 -0
- package/src/pages/api/media/[id].ts +309 -0
- package/src/pages/api/media/folders.ts +609 -0
- package/src/pages/api/media/index.ts +146 -0
- package/src/pages/api/media/resize.ts +267 -0
- package/src/pages/api/search.ts +82 -0
- package/src/pages/api/slug-availability.ts +70 -0
- package/src/pages/api/storage-config.ts +20 -0
- package/src/pages/api/storage-status.ts +206 -0
- package/src/pages/api/upload.ts +334 -0
- package/src/pages/api/webhooks/index.ts +71 -0
- package/src/pages/audit/index.astro +2 -104
- package/src/pages/login.astro +82 -0
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +102 -0
- package/src/pages/roles/index.astro +21 -21
- package/src/pages/settings/[slug].astro +162 -0
- package/src/pages/settings/index.astro +9 -0
- package/src/pages/users/[id].astro +29 -21
- package/src/pages/users/index.astro +22 -17
- package/src/pages/users/new.astro +18 -17
- package/src/styles/main.css +553 -128
- package/src/components/layout/Sidebar.tsx +0 -497
- package/src/pages/index.astro +0 -225
|
@@ -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
|
|
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({
|
|
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-
|
|
21
|
-
disabled || field.admin?.readOnly ?
|
|
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-
|
|
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-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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({
|
|
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
|
|
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 ?
|
|
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
|
-
?
|
|
31
|
-
:
|
|
32
|
-
} ${
|
|
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-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
}
|