@kyro-cms/admin 0.1.6 → 0.1.8
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 +54 -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 +137 -28
- 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 +2155 -770
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +4 -4
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +200 -58
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +890 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +192 -54
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +206 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- package/src/components/ThemeProvider.tsx +8 -2
- 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 +65 -0
- package/src/components/blocks/ArrayBlock.tsx +84 -0
- package/src/components/blocks/BlockEditModal.tsx +363 -0
- package/src/components/blocks/ButtonBlock.tsx +64 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +114 -0
- package/src/components/blocks/ColumnsBlock.tsx +93 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +63 -0
- package/src/components/blocks/HeadingBlock.tsx +59 -0
- package/src/components/blocks/HeroBlock.tsx +99 -0
- package/src/components/blocks/ImageBlock.tsx +82 -0
- package/src/components/blocks/LinkBlock.tsx +65 -0
- package/src/components/blocks/ListBlock.tsx +60 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +72 -0
- package/src/components/blocks/RichTextBlock.tsx +66 -0
- package/src/components/blocks/VStackBlock.tsx +61 -0
- package/src/components/blocks/VideoBlock.tsx +65 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/AccordionField.tsx +213 -0
- package/src/components/fields/ArrayField.tsx +241 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/ButtonField.tsx +53 -0
- package/src/components/fields/CheckboxField.tsx +18 -8
- package/src/components/fields/ChildrenField.tsx +48 -0
- package/src/components/fields/CodeField.tsx +294 -0
- package/src/components/fields/ColumnsField.tsx +137 -0
- package/src/components/fields/DateField.tsx +24 -12
- package/src/components/fields/EditorClient.tsx +537 -0
- package/src/components/fields/HeadingField.tsx +31 -0
- package/src/components/fields/HeroField.tsx +101 -0
- package/src/components/fields/JSONField.tsx +341 -0
- package/src/components/fields/LinkField.tsx +81 -0
- package/src/components/fields/ListField.tsx +74 -0
- package/src/components/fields/MarkdownField.tsx +260 -0
- package/src/components/fields/NumberField.tsx +25 -13
- package/src/components/fields/PortableTextField.tsx +155 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipBlockField.tsx +233 -0
- package/src/components/fields/RelationshipField.tsx +278 -60
- package/src/components/fields/SelectField.tsx +28 -16
- package/src/components/fields/TextField.tsx +31 -15
- package/src/components/fields/UploadField.tsx +613 -0
- package/src/components/fields/VideoField.tsx +73 -0
- package/src/components/fields/extensions/blockComponents.tsx +247 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +24 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +3 -3
- 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 +23 -6
- package/src/lib/MediaService.ts +541 -0
- package/src/lib/api.ts +163 -0
- package/src/lib/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +23 -7
- package/src/lib/dataStore.ts +188 -73
- package/src/lib/date-utils.ts +69 -0
- 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/db/version-adapter.ts +248 -0
- package/src/lib/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/i18n.tsx +353 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/slugify.ts +15 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -0
- package/src/lib/validation.ts +250 -0
- package/src/middleware.ts +70 -11
- 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 +200 -139
- 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 +52 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +66 -0
- package/src/pages/api/[collection]/[id].ts +114 -159
- package/src/pages/api/[collection]/index.ts +150 -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 +42 -24
- 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 +11 -11
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +13 -13
- 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 +563 -128
- package/src/components/layout/Sidebar.tsx +0 -497
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Plus, ChevronDown, ChevronUp, X } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface ArrayFieldItem {
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ArrayFieldProps {
|
|
9
|
+
items?: ArrayFieldItem[];
|
|
10
|
+
labelField?: string;
|
|
11
|
+
onChange: (items: ArrayFieldItem[]) => void;
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
16
|
+
items = [],
|
|
17
|
+
labelField = "title",
|
|
18
|
+
onChange,
|
|
19
|
+
compact = false,
|
|
20
|
+
}) => {
|
|
21
|
+
const [openIndex, setOpenIndex] = React.useState<number | null>(0);
|
|
22
|
+
|
|
23
|
+
const handleItemChange = (index: number, field: string, value: string) => {
|
|
24
|
+
const newItems = [...items];
|
|
25
|
+
newItems[index] = { ...newItems[index], [field]: value };
|
|
26
|
+
onChange(newItems);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleRemove = (index: number) => {
|
|
30
|
+
const newItems = items.filter((_, i) => i !== index);
|
|
31
|
+
onChange(newItems);
|
|
32
|
+
if (openIndex === index) setOpenIndex(null);
|
|
33
|
+
else if (openIndex !== null && openIndex > index)
|
|
34
|
+
setOpenIndex(openIndex - 1);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleAdd = () => {
|
|
38
|
+
const newItem: ArrayFieldItem = {
|
|
39
|
+
[labelField]: `Item ${items.length + 1}`,
|
|
40
|
+
};
|
|
41
|
+
onChange([...items, newItem]);
|
|
42
|
+
setOpenIndex(items.length);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const inputClass = compact
|
|
46
|
+
? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
47
|
+
: "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
|
|
48
|
+
|
|
49
|
+
const itemKeys =
|
|
50
|
+
items.length > 0
|
|
51
|
+
? Object.keys(items[0]).filter((k) => k !== "id" && k !== "_key")
|
|
52
|
+
: [];
|
|
53
|
+
|
|
54
|
+
if (compact) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="space-y-1.5">
|
|
57
|
+
{items.length === 0 ? (
|
|
58
|
+
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
|
|
59
|
+
No items. Click "Add Item" to create one.
|
|
60
|
+
</div>
|
|
61
|
+
) : (
|
|
62
|
+
<div className="space-y-1">
|
|
63
|
+
{items.map((item, index) => {
|
|
64
|
+
const isOpen = openIndex === index;
|
|
65
|
+
const itemLabel =
|
|
66
|
+
item[labelField] ||
|
|
67
|
+
item.title ||
|
|
68
|
+
item.name ||
|
|
69
|
+
`Item ${index + 1}`;
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
key={index}
|
|
73
|
+
className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
|
|
74
|
+
>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
78
|
+
className="w-full flex items-center justify-between p-2.5 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
|
|
79
|
+
>
|
|
80
|
+
<span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
|
|
81
|
+
{itemLabel}
|
|
82
|
+
</span>
|
|
83
|
+
<div className="flex items-center gap-1">
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={(e) => {
|
|
87
|
+
e.stopPropagation();
|
|
88
|
+
handleRemove(index);
|
|
89
|
+
}}
|
|
90
|
+
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
|
|
91
|
+
title="Remove"
|
|
92
|
+
>
|
|
93
|
+
<X className="w-3.5 h-3.5" />
|
|
94
|
+
</button>
|
|
95
|
+
{isOpen ? (
|
|
96
|
+
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
97
|
+
) : (
|
|
98
|
+
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
</button>
|
|
102
|
+
{isOpen && (
|
|
103
|
+
<div className="p-2.5 bg-[var(--kyro-surface)] space-y-2">
|
|
104
|
+
{itemKeys.length > 0 ? (
|
|
105
|
+
itemKeys.map((key) => (
|
|
106
|
+
<input
|
|
107
|
+
key={key}
|
|
108
|
+
type="text"
|
|
109
|
+
value={item[key] || ""}
|
|
110
|
+
onChange={(e) =>
|
|
111
|
+
handleItemChange(index, key, e.target.value)
|
|
112
|
+
}
|
|
113
|
+
onClick={(e) => e.stopPropagation()}
|
|
114
|
+
className={inputClass}
|
|
115
|
+
placeholder={key}
|
|
116
|
+
/>
|
|
117
|
+
))
|
|
118
|
+
) : (
|
|
119
|
+
<input
|
|
120
|
+
type="text"
|
|
121
|
+
value={item.value || ""}
|
|
122
|
+
onChange={(e) =>
|
|
123
|
+
handleItemChange(index, "value", e.target.value)
|
|
124
|
+
}
|
|
125
|
+
onClick={(e) => e.stopPropagation()}
|
|
126
|
+
className={inputClass}
|
|
127
|
+
placeholder="Value..."
|
|
128
|
+
/>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
onClick={handleAdd}
|
|
140
|
+
className="flex items-center justify-center gap-1.5 w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-sidebar-active)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
141
|
+
>
|
|
142
|
+
<Plus className="w-3.5 h-3.5" />
|
|
143
|
+
Add Item
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className="space-y-2">
|
|
151
|
+
{items.length === 0 ? (
|
|
152
|
+
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
|
|
153
|
+
No items. Click "Add Item" to create one.
|
|
154
|
+
</div>
|
|
155
|
+
) : (
|
|
156
|
+
<div className="space-y-2">
|
|
157
|
+
{items.map((item, index) => {
|
|
158
|
+
const isOpen = openIndex === index;
|
|
159
|
+
const itemLabel =
|
|
160
|
+
item[labelField] ||
|
|
161
|
+
item.title ||
|
|
162
|
+
item.name ||
|
|
163
|
+
`Item ${index + 1}`;
|
|
164
|
+
return (
|
|
165
|
+
<div
|
|
166
|
+
key={index}
|
|
167
|
+
className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
|
|
168
|
+
>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
172
|
+
className="w-full flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
|
|
173
|
+
>
|
|
174
|
+
<span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
|
|
175
|
+
{itemLabel}
|
|
176
|
+
</span>
|
|
177
|
+
<div className="flex items-center gap-1">
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={(e) => {
|
|
181
|
+
e.stopPropagation();
|
|
182
|
+
handleRemove(index);
|
|
183
|
+
}}
|
|
184
|
+
className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
|
|
185
|
+
title="Remove"
|
|
186
|
+
>
|
|
187
|
+
<X className="w-4 h-4" />
|
|
188
|
+
</button>
|
|
189
|
+
{isOpen ? (
|
|
190
|
+
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
191
|
+
) : (
|
|
192
|
+
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
</button>
|
|
196
|
+
{isOpen && (
|
|
197
|
+
<div className="p-3 bg-[var(--kyro-surface)] space-y-2">
|
|
198
|
+
{itemKeys.length > 0 ? (
|
|
199
|
+
itemKeys.map((key) => (
|
|
200
|
+
<input
|
|
201
|
+
key={key}
|
|
202
|
+
type="text"
|
|
203
|
+
value={item[key] || ""}
|
|
204
|
+
onChange={(e) =>
|
|
205
|
+
handleItemChange(index, key, e.target.value)
|
|
206
|
+
}
|
|
207
|
+
className={inputClass}
|
|
208
|
+
placeholder={key}
|
|
209
|
+
/>
|
|
210
|
+
))
|
|
211
|
+
) : (
|
|
212
|
+
<input
|
|
213
|
+
type="text"
|
|
214
|
+
value={item.value || ""}
|
|
215
|
+
onChange={(e) =>
|
|
216
|
+
handleItemChange(index, "value", e.target.value)
|
|
217
|
+
}
|
|
218
|
+
className={inputClass}
|
|
219
|
+
placeholder="Value..."
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
})}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
onClick={handleAdd}
|
|
232
|
+
className="flex items-center justify-center gap-1.5 w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-sidebar-active)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
233
|
+
>
|
|
234
|
+
<Plus className="w-3.5 h-3.5" />
|
|
235
|
+
Add Item
|
|
236
|
+
</button>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export default ArrayField;
|
|
@@ -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-[var(--kyro-success)]";
|
|
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
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ExternalLink } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface ButtonFieldProps {
|
|
5
|
+
text?: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
onChange: (field: string, value: string) => void;
|
|
8
|
+
compact?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ButtonField: React.FC<ButtonFieldProps> = ({
|
|
12
|
+
text = "Button",
|
|
13
|
+
url = "",
|
|
14
|
+
onChange,
|
|
15
|
+
compact = false,
|
|
16
|
+
}) => {
|
|
17
|
+
const inputClass = compact
|
|
18
|
+
? "flex-1 px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
19
|
+
: "flex-1 px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex items-center gap-2">
|
|
23
|
+
<input
|
|
24
|
+
type="text"
|
|
25
|
+
value={text}
|
|
26
|
+
onChange={(e) => onChange("text", e.target.value)}
|
|
27
|
+
className={inputClass}
|
|
28
|
+
placeholder="Button text..."
|
|
29
|
+
/>
|
|
30
|
+
<span className="text-[var(--kyro-text-muted)] text-xs">→</span>
|
|
31
|
+
<input
|
|
32
|
+
type="url"
|
|
33
|
+
value={url}
|
|
34
|
+
onChange={(e) => onChange("url", e.target.value)}
|
|
35
|
+
className={`${inputClass} font-mono text-xs`}
|
|
36
|
+
placeholder="https://..."
|
|
37
|
+
/>
|
|
38
|
+
{text && url && (
|
|
39
|
+
<a
|
|
40
|
+
href={url}
|
|
41
|
+
target="_blank"
|
|
42
|
+
rel="noopener noreferrer"
|
|
43
|
+
className={`shrink-0 ${compact ? "p-1.5" : "p-2"} rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors`}
|
|
44
|
+
title={url}
|
|
45
|
+
>
|
|
46
|
+
<ExternalLink className={compact ? "w-3.5 h-3.5" : "w-4 h-4"} />
|
|
47
|
+
</a>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default ButtonField;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckboxField as CheckboxFieldType } from
|
|
1
|
+
import type { CheckboxField as CheckboxFieldType } from "@kyro-cms/core/client";
|
|
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,20 +23,24 @@ 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
|
-
{field.required &&
|
|
32
|
+
{field.required && (
|
|
33
|
+
<span className="text-[var(--kyro-error)] ml-1">*</span>
|
|
34
|
+
)}
|
|
27
35
|
</span>
|
|
28
36
|
</label>
|
|
29
37
|
{field.admin?.description && !error && (
|
|
30
|
-
<p className="text-xs text-
|
|
38
|
+
<p className="text-xs text-[var(--kyro-text-secondary)] ml-6">
|
|
39
|
+
{field.admin.description}
|
|
40
|
+
</p>
|
|
31
41
|
)}
|
|
32
42
|
{error && (
|
|
33
|
-
<p className="text-xs text-
|
|
43
|
+
<p className="text-xs text-[var(--kyro-error)] ml-6">{error}</p>
|
|
34
44
|
)}
|
|
35
45
|
</div>
|
|
36
46
|
);
|