@kyro-cms/admin 0.1.7 → 0.1.9

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 (71) hide show
  1. package/package.json +7 -2
  2. package/src/components/Admin.tsx +1 -1
  3. package/src/components/AutoForm.tsx +966 -337
  4. package/src/components/CreateView.tsx +1 -1
  5. package/src/components/DetailView.tsx +1 -1
  6. package/src/components/EnhancedListView.tsx +156 -52
  7. package/src/components/ListView.tsx +1 -1
  8. package/src/components/Modal.tsx +65 -8
  9. package/src/components/Sidebar.astro +2 -2
  10. package/src/components/ThemeProvider.tsx +8 -2
  11. package/src/components/blocks/AccordionBlock.tsx +20 -52
  12. package/src/components/blocks/ArrayBlock.tsx +40 -31
  13. package/src/components/blocks/BlockEditModal.tsx +170 -581
  14. package/src/components/blocks/ButtonBlock.tsx +27 -128
  15. package/src/components/blocks/CodeBlock.tsx +88 -40
  16. package/src/components/blocks/ColumnsBlock.tsx +27 -85
  17. package/src/components/blocks/FileBlock.tsx +38 -39
  18. package/src/components/blocks/HeadingBlock.tsx +9 -31
  19. package/src/components/blocks/HeroBlock.tsx +42 -100
  20. package/src/components/blocks/ImageBlock.tsx +6 -7
  21. package/src/components/blocks/LinkBlock.tsx +27 -33
  22. package/src/components/blocks/ListBlock.tsx +47 -26
  23. package/src/components/blocks/RelationshipBlock.tsx +26 -233
  24. package/src/components/blocks/RichTextBlock.tsx +66 -0
  25. package/src/components/blocks/VStackBlock.tsx +23 -37
  26. package/src/components/blocks/VideoBlock.tsx +52 -32
  27. package/src/components/fields/AccordionField.tsx +213 -0
  28. package/src/components/fields/ArrayField.tsx +241 -0
  29. package/src/components/fields/BlocksField.tsx +5 -5
  30. package/src/components/fields/ButtonField.tsx +53 -0
  31. package/src/components/fields/CheckboxField.tsx +7 -3
  32. package/src/components/fields/ChildrenField.tsx +48 -0
  33. package/src/components/fields/CodeField.tsx +154 -94
  34. package/src/components/fields/ColumnsField.tsx +137 -0
  35. package/src/components/fields/DateField.tsx +9 -24
  36. package/src/components/fields/EditorClient.tsx +426 -160
  37. package/src/components/fields/HeadingField.tsx +31 -0
  38. package/src/components/fields/HeroField.tsx +101 -0
  39. package/src/components/fields/JSONField.tsx +7 -27
  40. package/src/components/fields/LinkField.tsx +81 -0
  41. package/src/components/fields/ListField.tsx +74 -0
  42. package/src/components/fields/MarkdownField.tsx +4 -26
  43. package/src/components/fields/NumberField.tsx +9 -27
  44. package/src/components/fields/PortableTextField.tsx +61 -49
  45. package/src/components/fields/RelationshipBlockField.tsx +233 -0
  46. package/src/components/fields/RelationshipField.tsx +59 -13
  47. package/src/components/fields/SelectField.tsx +6 -4
  48. package/src/components/fields/TextField.tsx +9 -24
  49. package/src/components/fields/UploadField.tsx +613 -0
  50. package/src/components/fields/VideoField.tsx +73 -0
  51. package/src/components/fields/extensions/blockComponents.tsx +11 -1
  52. package/src/components/fields/extensions/blocksStore.ts +1 -1
  53. package/src/components/fields/index.ts +12 -1
  54. package/src/components/layout/Layout.tsx +1 -1
  55. package/src/lib/api.ts +163 -0
  56. package/src/lib/config.ts +1 -1
  57. package/src/lib/dataStore.ts +87 -30
  58. package/src/lib/date-utils.ts +69 -0
  59. package/src/lib/db/version-adapter.ts +248 -0
  60. package/src/lib/i18n.tsx +353 -0
  61. package/src/lib/slugify.ts +15 -0
  62. package/src/lib/validation.ts +250 -0
  63. package/src/pages/api/[collection]/[id]/publish.ts +12 -4
  64. package/src/pages/api/[collection]/[id]/versions.ts +39 -9
  65. package/src/pages/api/[collection]/[id].ts +13 -1
  66. package/src/pages/api/[collection]/index.ts +5 -6
  67. package/src/styles/main.css +12 -2
  68. package/src/components/blocks/BlockEditModal.MARKER +0 -12
  69. package/src/components/fields/FileField.tsx +0 -390
  70. package/src/components/fields/HybridContentField.tsx +0 -109
  71. package/src/components/fields/ImageField.tsx +0 -429
@@ -1,279 +1,72 @@
1
- import React, { useState, useEffect } from "react";
1
+ import React from "react";
2
2
  import {
3
3
  useBlockById,
4
4
  useBlockActions,
5
5
  } from "../fields/extensions/blocksStore";
6
- import { ChevronRight, X, Search, Loader2, ExternalLink } from "lucide-react";
6
+ import { ChevronRight, X } from "lucide-react";
7
+ import { RelationshipBlockField } from "../fields/RelationshipBlockField";
7
8
 
8
- interface RelationshipBlockProps {
9
- block: any;
10
- index: number;
11
- }
12
-
13
- export const RelationshipBlock: React.FC<RelationshipBlockProps> = ({
9
+ export const RelationshipBlock: React.FC<{ block: any; index: number }> = ({
14
10
  block,
15
11
  index,
16
12
  }) => {
17
13
  const blockData = useBlockById(block.id);
18
14
  const { updateBlock, removeBlock, moveBlock } = useBlockActions();
19
- const data = blockData?.data ?? block.data ?? {};
20
-
21
- const [isOpen, setIsOpen] = useState(false);
22
- const [search, setSearch] = useState("");
23
- const [options, setOptions] = useState<any[]>([]);
24
- const [loading, setLoading] = useState(false);
25
- const [collections, setCollections] = useState<string[]>([]);
26
- const [loadingCollections, setLoadingCollections] = useState(true);
27
-
28
- useEffect(() => {
29
- fetch("/api/collections", { credentials: "include" })
30
- .then((res) => res.json())
31
- .then((data) => {
32
- setCollections(
33
- (data.collections || []).map((c: any) => c.slug || c.name || c),
34
- );
35
- setLoadingCollections(false);
36
- })
37
- .catch(() => setLoadingCollections(false));
38
- }, []);
39
-
40
- const relationTo = data.relationTo || "pages";
41
- const isMultiple = data.hasMany;
42
-
43
- const fetchOptions = (query: string = "") => {
44
- setLoading(true);
45
- const url = query
46
- ? `/api/${relationTo}?where[title][contains]=${encodeURIComponent(query)}&limit=20`
47
- : `/api/${relationTo}?limit=20`;
48
-
49
- fetch(url, { credentials: "include" })
50
- .then((res) => res.json())
51
- .then((data) => {
52
- setOptions(data.docs || []);
53
- setLoading(false);
54
- })
55
- .catch(() => setLoading(false));
56
- };
57
-
58
- useEffect(() => {
59
- if (isOpen) fetchOptions(search);
60
- }, [isOpen, search]);
61
-
62
- const getLabel = (opt: any) => {
63
- return (
64
- opt?.title ||
65
- opt?.name ||
66
- opt?.label ||
67
- opt?.filename ||
68
- opt?.slug ||
69
- opt?.id ||
70
- "Untitled"
71
- );
72
- };
73
15
 
74
- const selectedIds = Array.isArray(data.selectedIds)
75
- ? data.selectedIds
76
- : data.selectedId
77
- ? [data.selectedId]
78
- : [];
79
-
80
- const handleSelect = (opt: any) => {
81
- if (isMultiple) {
82
- const current = selectedIds;
83
- if (current.includes(opt.id)) {
84
- updateBlock(block.id, {
85
- data: {
86
- ...data,
87
- selectedIds: current.filter((id: string) => id !== opt.id),
88
- },
89
- });
90
- } else {
91
- updateBlock(block.id, {
92
- data: { ...data, selectedIds: [...current, opt.id] },
93
- });
94
- }
95
- } else {
96
- updateBlock(block.id, {
97
- data: { ...data, selectedId: opt.id, selectedIds: [opt.id] },
98
- });
99
- setIsOpen(false);
100
- }
101
- };
102
-
103
- const isSelected = (optId: string) => selectedIds.includes(optId);
16
+ const data = blockData?.data ?? block.data ?? {};
104
17
 
105
18
  const handleChange = (field: string, value: any) => {
106
19
  updateBlock(block.id, { data: { ...data, [field]: value } });
107
20
  };
108
21
 
109
- const selectedOptions = options.filter((o) => selectedIds.includes(o.id));
110
-
111
22
  return (
112
- <div className="block-relationship border border-[var(--kyro-border)] rounded-lg p-4 mb-4 relative group bg-[var(--kyro-surface)]">
113
- <div className="flex items-center justify-between mb-4">
23
+ <div className="block-relationship border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
24
+ <div className="flex items-center justify-between mb-1">
114
25
  <div className="flex items-center gap-2">
115
- <span className="text-sm font-medium text-[var(--kyro-text-primary)]">
26
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
116
27
  Relationship
117
28
  </span>
118
- <span className="text-xs text-[var(--kyro-text-muted)]">
119
- → {relationTo}
29
+ <span className="text-[10px] text-[var(--kyro-text-muted)]">
30
+ → {data.relationTo || "pages"}
120
31
  </span>
121
32
  </div>
122
- <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
33
+ <div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
123
34
  <button
124
35
  type="button"
125
36
  onClick={() => moveBlock(block.id, "up")}
126
- className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded"
37
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
127
38
  title="Move up"
128
39
  >
129
- <ChevronRight className="w-3.5 h-3.5 rotate-[-90deg] text-[var(--kyro-text-muted)]" />
40
+ <ChevronRight className="w-3 h-3 rotate-90" />
130
41
  </button>
131
42
  <button
132
43
  type="button"
133
44
  onClick={() => moveBlock(block.id, "down")}
134
- className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded"
45
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
135
46
  title="Move down"
136
47
  >
137
- <ChevronRight className="w-3.5 h-3.5 rotate-90 text-[var(--kyro-text-muted)]" />
48
+ <ChevronRight className="w-3 h-3" />
138
49
  </button>
139
50
  <button
140
51
  type="button"
141
52
  onClick={() => removeBlock(block.id)}
142
- className="p-1.5 hover:bg-red-50 rounded"
53
+ className="p-1 hover:bg-red-50 rounded text-red-500"
143
54
  title="Remove"
144
55
  >
145
- <X className="w-3.5 h-3.5 text-red-500" />
56
+ <X className="w-3 h-3" />
146
57
  </button>
147
58
  </div>
148
59
  </div>
149
60
 
150
- <div className="space-y-3">
151
- <div className="grid grid-cols-2 gap-3">
152
- <div>
153
- <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
154
- Target Collection
155
- </label>
156
- {loadingCollections ? (
157
- <div className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-muted)]">
158
- Loading...
159
- </div>
160
- ) : (
161
- <select
162
- value={relationTo}
163
- onChange={(e) => handleChange("relationTo", e.target.value)}
164
- className="w-full px-3 py-2 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"
165
- >
166
- <option value="">Select collection...</option>
167
- {collections.map((col) => (
168
- <option key={col} value={col}>
169
- {col}
170
- </option>
171
- ))}
172
- </select>
173
- )}
174
- </div>
175
- <div>
176
- <label className="flex items-center gap-2 cursor-pointer mt-5">
177
- <input
178
- type="checkbox"
179
- checked={isMultiple || false}
180
- onChange={(e) => handleChange("hasMany", e.target.checked)}
181
- className="w-4 h-4 rounded border-[var(--kyro-border)] focus:ring-[var(--kyro-sidebar-active)] focus:ring-offset-0"
182
- />
183
- <span className="text-sm text-[var(--kyro-text-primary)]">
184
- Allow multiple
185
- </span>
186
- </label>
187
- </div>
188
- </div>
189
-
190
- <div className="relative">
191
- <div className="relative">
192
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
193
- <input
194
- type="text"
195
- value={search}
196
- onChange={(e) => {
197
- setSearch(e.target.value);
198
- setIsOpen(true);
199
- }}
200
- onFocus={() => setIsOpen(true)}
201
- placeholder={`Search ${relationTo}...`}
202
- className="w-full pl-9 pr-10 py-2 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"
203
- />
204
- <div className="absolute right-3 top-1/2 -translate-y-1/2">
205
- {loading ? (
206
- <Loader2 className="w-4 h-4 text-[var(--kyro-text-muted)] animate-spin" />
207
- ) : null}
208
- </div>
209
- </div>
210
-
211
- {isOpen && (
212
- <div className="absolute z-20 w-full mt-1 border border-[var(--kyro-border)] rounded-lg shadow-lg bg-[var(--kyro-surface)] max-h-48 overflow-auto">
213
- {loading ? (
214
- <div className="p-3 text-center text-sm text-[var(--kyro-text-muted)]">
215
- Loading...
216
- </div>
217
- ) : options.length === 0 ? (
218
- <div className="p-3 text-center text-sm text-[var(--kyro-text-muted)]">
219
- No results found
220
- </div>
221
- ) : (
222
- <div className="py-1">
223
- {options.map((opt) => (
224
- <button
225
- key={opt.id}
226
- type="button"
227
- onClick={() => handleSelect(opt)}
228
- className={`w-full px-3 py-2 text-left text-sm hover:bg-[var(--kyro-surface-accent)] transition-colors flex items-center justify-between ${
229
- isSelected(opt.id)
230
- ? "bg-[var(--kyro-sidebar-active)]/10 text-[var(--kyro-sidebar-active)]"
231
- : "text-[var(--kyro-text-primary)]"
232
- }`}
233
- >
234
- <span>{getLabel(opt)}</span>
235
- {isSelected(opt) && <span>✓</span>}
236
- </button>
237
- ))}
238
- </div>
239
- )}
240
- </div>
241
- )}
242
- </div>
243
-
244
- {selectedIds.length > 0 && (
245
- <div className="flex flex-wrap gap-2">
246
- {selectedIds.map((id: string) => {
247
- const opt = options.find((o) => o.id === id) || { id, title: id };
248
- return (
249
- <span
250
- key={id}
251
- className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-md bg-[var(--kyro-sidebar-active)]/10 text-[var(--kyro-sidebar-active)]"
252
- >
253
- {getLabel(opt)}
254
- <button
255
- type="button"
256
- onClick={() => {
257
- if (isMultiple) {
258
- handleChange(
259
- "selectedIds",
260
- selectedIds.filter((sid: string) => sid !== id),
261
- );
262
- } else {
263
- handleChange("selectedId", null);
264
- handleChange("selectedIds", []);
265
- }
266
- }}
267
- className="hover:opacity-70"
268
- >
269
- <X className="w-3 h-3" />
270
- </button>
271
- </span>
272
- );
273
- })}
274
- </div>
275
- )}
276
- </div>
61
+ <RelationshipBlockField
62
+ relationTo={data.relationTo || "pages"}
63
+ hasMany={data.hasMany || false}
64
+ selectedIds={Array.isArray(data.selectedIds) ? data.selectedIds : []}
65
+ selectedId={data.selectedId}
66
+ labelField={data.labelField || "title"}
67
+ onChange={handleChange}
68
+ compact
69
+ />
277
70
  </div>
278
71
  );
279
72
  };
@@ -0,0 +1,66 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { ChevronRight, X, AlignLeft } from "lucide-react";
7
+ import PortableTextField from "../fields/PortableTextField";
8
+
9
+ export const RichTextBlock: React.FC<{ block: any; index: number }> = ({
10
+ block,
11
+ index,
12
+ }) => {
13
+ const blockData = useBlockById(block.id);
14
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
15
+
16
+ const data = blockData?.data || block.data || {};
17
+
18
+ const handleChange = (newValue: any) => {
19
+ updateBlock(block.id, { data: { ...data, content: newValue } });
20
+ };
21
+
22
+ return (
23
+ <div className="block-richtext border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
24
+ <div className="flex items-center justify-between mb-2">
25
+ <div className="flex items-center gap-2">
26
+ <AlignLeft className="w-3.5 h-3.5 text-[var(--kyro-primary)]" />
27
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
28
+ Rich Text
29
+ </span>
30
+ </div>
31
+ <div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
32
+ <button
33
+ type="button"
34
+ onClick={() => moveBlock(block.id, "up")}
35
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
36
+ title="Move up"
37
+ >
38
+ <ChevronRight className="w-3 h-3 rotate-90" />
39
+ </button>
40
+ <button
41
+ type="button"
42
+ onClick={() => moveBlock(block.id, "down")}
43
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
44
+ title="Move down"
45
+ >
46
+ <ChevronRight className="w-3 h-3" />
47
+ </button>
48
+ <button
49
+ type="button"
50
+ onClick={() => removeBlock(block.id)}
51
+ className="p-1 hover:bg-red-50 rounded text-red-500"
52
+ title="Remove"
53
+ >
54
+ <X className="w-3 h-3" />
55
+ </button>
56
+ </div>
57
+ </div>
58
+
59
+ <PortableTextField
60
+ field={{ name: "content", label: "Content" }}
61
+ value={data.content}
62
+ onChange={handleChange}
63
+ />
64
+ </div>
65
+ );
66
+ };
@@ -4,72 +4,58 @@ import {
4
4
  useBlockActions,
5
5
  } from "../fields/extensions/blocksStore";
6
6
  import { ChevronRight, X } from "lucide-react";
7
- import { ChildBlocksTree } from "./ChildBlocksTree";
7
+ import { ChildrenField } from "../fields/ChildrenField";
8
8
 
9
- interface VStackBlockProps {
10
- block: any;
11
- index: number;
12
- }
13
-
14
- export const VStackBlock: React.FC<VStackBlockProps> = ({ block, index }) => {
9
+ export const VStackBlock: React.FC<{ block: any; index: number }> = ({
10
+ block,
11
+ index,
12
+ }) => {
15
13
  const blockData = useBlockById(block.id);
16
14
  const { updateBlock, removeBlock, moveBlock } = useBlockActions();
15
+
17
16
  const data = blockData?.data ?? block.data ?? {};
18
17
  const children = blockData?.children ?? block.children ?? [];
19
18
 
20
- const handleUpdateChildren = (newChildren: any[]) => {
21
- updateBlock(block.id, { children: newChildren });
22
- };
23
-
24
19
  return (
25
- <div className="block-vstack border border-[var(--kyro-border)] rounded-lg p-4 mb-4 relative group bg-[var(--kyro-surface)]">
26
- <div className="flex items-center justify-between mb-4">
27
- <div className="flex items-center gap-2">
28
- <span className="text-sm font-medium text-[var(--kyro-text-primary)]">
29
- Vertical Stack
30
- </span>
31
- <span className="text-xs text-[var(--kyro-text-muted)]">
32
- ({children.length} children)
33
- </span>
34
- </div>
20
+ <div className="block-vstack border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
21
+ <div className="flex items-center justify-between mb-1">
22
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
23
+ Stack
24
+ </span>
35
25
  <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
36
26
  <button
37
27
  type="button"
38
28
  onClick={() => moveBlock(block.id, "up")}
39
- className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded"
29
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
40
30
  title="Move up"
41
31
  >
42
- <ChevronRight className="w-3.5 h-3.5 rotate-[-90deg] text-[var(--kyro-text-muted)]" />
32
+ <ChevronRight className="w-3 h-3 rotate-90" />
43
33
  </button>
44
34
  <button
45
35
  type="button"
46
36
  onClick={() => moveBlock(block.id, "down")}
47
- className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded"
37
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
48
38
  title="Move down"
49
39
  >
50
- <ChevronRight className="w-3.5 h-3.5 rotate-90 text-[var(--kyro-text-muted)]" />
40
+ <ChevronRight className="w-3 h-3" />
51
41
  </button>
52
42
  <button
53
43
  type="button"
54
44
  onClick={() => removeBlock(block.id)}
55
- className="p-1.5 hover:bg-red-50 rounded"
45
+ className="p-1 hover:bg-red-50 rounded text-red-500"
56
46
  title="Remove"
57
47
  >
58
- <X className="w-3.5 h-3.5 text-red-500" />
48
+ <X className="w-3 h-3" />
59
49
  </button>
60
50
  </div>
61
51
  </div>
62
52
 
63
- <div className="pt-4 border-t border-[var(--kyro-border)]">
64
- <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-2 block">
65
- Children
66
- </label>
67
- <ChildBlocksTree
68
- blockId={block.id}
69
- children={children}
70
- onUpdateChildren={handleUpdateChildren}
71
- />
72
- </div>
53
+ <ChildrenField
54
+ blockId={block.id}
55
+ children={children}
56
+ onUpdateChildren={(c) => updateBlock(block.id, { children: c })}
57
+ compact
58
+ />
73
59
  </div>
74
60
  );
75
61
  };
@@ -1,45 +1,65 @@
1
1
  import React from "react";
2
- import { useBlocksStore } from "../fields/extensions/blocksStore";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
3
6
  import { ChevronRight, X } from "lucide-react";
7
+ import { VideoField } from "../fields/VideoField";
8
+
9
+ export const VideoBlock: React.FC<{ block: any; index: number }> = ({
10
+ block,
11
+ index,
12
+ }) => {
13
+ const blockData = useBlockById(block.id);
14
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
15
+
16
+ const data = blockData?.data ?? block.data ?? {};
4
17
 
5
- export const VideoBlock: React.FC<{ block: any; index: number }> = ({ block, index }) => {
6
- const { updateBlock, removeBlock, moveBlock } = useBlocksStore();
7
-
8
18
  const handleChange = (field: string, value: any) => {
9
- updateBlock(block.id, { data: { ...block.data, [field]: value } });
19
+ updateBlock(block.id, { data: { ...data, [field]: value } });
10
20
  };
11
21
 
12
22
  return (
13
- <div className="block-video border border-[var(--kyro-border)] rounded-md p-4 mb-4 relative group">
14
- <div className="flex items-center justify-between mb-2">
15
- <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">Video</span>
23
+ <div className="block-video border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
24
+ <div className="flex items-center justify-between mb-1">
25
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
26
+ Video
27
+ </span>
16
28
  <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
17
- <button type="button" onClick={() => moveBlock(block.id, "up")} className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded" title="Move up"><ChevronRight className="w-3 h-3 rotate-90" /></button>
18
- <button type="button" onClick={() => removeBlock(block.id)} className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded" title="Remove"><X className="w-3 h-3" /></button>
19
- </div>
20
- </div>
21
- <div className="space-y-3">
22
- <div>
23
- <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">Video URL</label>
24
- <input
25
- type="url"
26
- value={block.data.src || ""}
27
- onChange={(e) => handleChange("src", e.target.value)}
28
- className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
29
- placeholder="https://..."
30
- />
31
- </div>
32
- <div>
33
- <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">Title</label>
34
- <input
35
- type="text"
36
- value={block.data.title || ""}
37
- onChange={(e) => handleChange("title", e.target.value)}
38
- className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
39
- placeholder="Video title..."
40
- />
29
+ <button
30
+ type="button"
31
+ onClick={() => moveBlock(block.id, "up")}
32
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
33
+ title="Move up"
34
+ >
35
+ <ChevronRight className="w-3 h-3 rotate-90" />
36
+ </button>
37
+ <button
38
+ type="button"
39
+ onClick={() => moveBlock(block.id, "down")}
40
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
41
+ title="Move down"
42
+ >
43
+ <ChevronRight className="w-3 h-3" />
44
+ </button>
45
+ <button
46
+ type="button"
47
+ onClick={() => removeBlock(block.id)}
48
+ className="p-1 hover:bg-red-50 rounded text-red-500"
49
+ title="Remove"
50
+ >
51
+ <X className="w-3 h-3" />
52
+ </button>
41
53
  </div>
42
54
  </div>
55
+
56
+ <VideoField
57
+ src={data.src || ""}
58
+ title={data.title || ""}
59
+ onChange={handleChange}
60
+ onUploadChange={(v) => handleChange("src", v)}
61
+ compact
62
+ />
43
63
  </div>
44
64
  );
45
65
  };