@kyro-cms/admin 0.1.7 → 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.
Files changed (71) hide show
  1. package/package.json +5 -3
  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
@@ -0,0 +1,213 @@
1
+ import React from "react";
2
+ import { ChevronDown, ChevronUp, Plus, X } from "lucide-react";
3
+
4
+ interface AccordionItem {
5
+ title: string;
6
+ content: string;
7
+ }
8
+
9
+ interface AccordionFieldProps {
10
+ items?: AccordionItem[];
11
+ onChange: (items: AccordionItem[]) => void;
12
+ compact?: boolean;
13
+ }
14
+
15
+ export const AccordionField: React.FC<AccordionFieldProps> = ({
16
+ items = [],
17
+ onChange,
18
+ compact = false,
19
+ }) => {
20
+ const [openIndex, setOpenIndex] = React.useState<number | null>(0);
21
+
22
+ const handleTitleChange = (index: number, value: string) => {
23
+ const newItems = [...items];
24
+ newItems[index] = { ...newItems[index], title: value };
25
+ onChange(newItems);
26
+ };
27
+
28
+ const handleContentChange = (index: number, value: string) => {
29
+ const newItems = [...items];
30
+ newItems[index] = { ...newItems[index], content: value };
31
+ onChange(newItems);
32
+ };
33
+
34
+ const handleRemove = (index: number) => {
35
+ const newItems = items.filter((_, i) => i !== index);
36
+ onChange(newItems);
37
+ if (openIndex === index) setOpenIndex(null);
38
+ else if (openIndex !== null && openIndex > index)
39
+ setOpenIndex(openIndex - 1);
40
+ };
41
+
42
+ const handleAdd = () => {
43
+ onChange([...items, { title: `Item ${items.length + 1}`, content: "" }]);
44
+ setOpenIndex(items.length);
45
+ };
46
+
47
+ const baseInputClass =
48
+ "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";
49
+ const smallInputClass =
50
+ "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";
51
+
52
+ if (compact) {
53
+ return (
54
+ <div className="space-y-2">
55
+ {items.length === 0 ? (
56
+ <div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
57
+ No items. Click "Add Item" to create one.
58
+ </div>
59
+ ) : (
60
+ <div className="space-y-1.5">
61
+ {items.map((item: AccordionItem, index: number) => {
62
+ const isOpen = openIndex === index;
63
+ return (
64
+ <div
65
+ key={index}
66
+ className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
67
+ >
68
+ <button
69
+ type="button"
70
+ onClick={() => setOpenIndex(isOpen ? null : index)}
71
+ 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"
72
+ >
73
+ <span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
74
+ {item.title || `Item ${index + 1}`}
75
+ </span>
76
+ <div className="flex items-center gap-1">
77
+ <button
78
+ type="button"
79
+ onClick={(e) => {
80
+ e.stopPropagation();
81
+ handleRemove(index);
82
+ }}
83
+ className="opacity-0 group-hover:opacity-100 p-1 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
84
+ title="Remove"
85
+ >
86
+ <X className="w-3.5 h-3.5" />
87
+ </button>
88
+ {isOpen ? (
89
+ <ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
90
+ ) : (
91
+ <ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
92
+ )}
93
+ </div>
94
+ </button>
95
+ {isOpen && (
96
+ <div className="p-2.5 bg-[var(--kyro-surface)] space-y-2">
97
+ <input
98
+ type="text"
99
+ value={item.title || ""}
100
+ onChange={(e) =>
101
+ handleTitleChange(index, e.target.value)
102
+ }
103
+ onClick={(e) => e.stopPropagation()}
104
+ className={smallInputClass}
105
+ placeholder="Item title..."
106
+ />
107
+ <textarea
108
+ value={item.content || ""}
109
+ onChange={(e) =>
110
+ handleContentChange(index, e.target.value)
111
+ }
112
+ onClick={(e) => e.stopPropagation()}
113
+ className={`${smallInputClass} min-h-[60px] resize-none`}
114
+ placeholder="Item content..."
115
+ />
116
+ </div>
117
+ )}
118
+ </div>
119
+ );
120
+ })}
121
+ </div>
122
+ )}
123
+ <button
124
+ type="button"
125
+ onClick={handleAdd}
126
+ 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"
127
+ >
128
+ <Plus className="w-3.5 h-3.5" />
129
+ Add Item
130
+ </button>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ return (
136
+ <div className="space-y-2">
137
+ {items.length === 0 ? (
138
+ <div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
139
+ No items. Click "Add Item" to create one.
140
+ </div>
141
+ ) : (
142
+ <div className="space-y-2">
143
+ {items.map((item: AccordionItem, index: number) => {
144
+ const isOpen = openIndex === index;
145
+ return (
146
+ <div
147
+ key={index}
148
+ className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
149
+ >
150
+ <button
151
+ type="button"
152
+ onClick={() => setOpenIndex(isOpen ? null : index)}
153
+ className="w-full flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
154
+ >
155
+ <span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
156
+ {item.title || `Item ${index + 1}`}
157
+ </span>
158
+ <div className="flex items-center gap-1">
159
+ <button
160
+ type="button"
161
+ onClick={(e) => {
162
+ e.stopPropagation();
163
+ handleRemove(index);
164
+ }}
165
+ className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
166
+ title="Remove"
167
+ >
168
+ <X className="w-4 h-4" />
169
+ </button>
170
+ {isOpen ? (
171
+ <ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
172
+ ) : (
173
+ <ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
174
+ )}
175
+ </div>
176
+ </button>
177
+ {isOpen && (
178
+ <div className="p-3 bg-[var(--kyro-surface)] space-y-2">
179
+ <input
180
+ type="text"
181
+ value={item.title || ""}
182
+ onChange={(e) => handleTitleChange(index, e.target.value)}
183
+ className={baseInputClass}
184
+ placeholder="Item title..."
185
+ />
186
+ <textarea
187
+ value={item.content || ""}
188
+ onChange={(e) =>
189
+ handleContentChange(index, e.target.value)
190
+ }
191
+ className={`${baseInputClass} min-h-[60px] resize-none`}
192
+ placeholder="Item content..."
193
+ />
194
+ </div>
195
+ )}
196
+ </div>
197
+ );
198
+ })}
199
+ </div>
200
+ )}
201
+ <button
202
+ type="button"
203
+ onClick={handleAdd}
204
+ 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"
205
+ >
206
+ <Plus className="w-3.5 h-3.5" />
207
+ Add Item
208
+ </button>
209
+ </div>
210
+ );
211
+ };
212
+
213
+ export default AccordionField;
@@ -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;
@@ -107,7 +107,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
107
107
  setOnBlocksChange(onBlocksChange);
108
108
  }
109
109
  return () => {
110
- setOnBlocksChange(() => { });
110
+ setOnBlocksChange(() => {});
111
111
  };
112
112
  }, [onBlocksChange, setOnBlocksChange]);
113
113
 
@@ -148,7 +148,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
148
148
  // Determine left border style based on document status
149
149
  const getBorderClass = () => {
150
150
  if (justSaved) {
151
- return "border-l-[3px] border-green-500";
151
+ return "border-l-[3px] border-[var(--kyro-success)]";
152
152
  }
153
153
  if (
154
154
  documentStatus === "draft" ||
@@ -226,9 +226,9 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
226
226
  // Render active drag overlay
227
227
  const activeBlock = activeDrag
228
228
  ? blockCategories
229
- .flatMap((cat) => cat.blocks)
230
- .find((b) => `drawer-${b.type}` === activeDrag.id) ||
231
- blocks.find((b) => b.id === activeDrag.id)
229
+ .flatMap((cat) => cat.blocks)
230
+ .find((b) => `drawer-${b.type}` === activeDrag.id) ||
231
+ blocks.find((b) => b.id === activeDrag.id)
232
232
  : null;
233
233
 
234
234
  const activeBlockLabel = activeBlock
@@ -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 "@kyro-cms/core";
1
+ import type { CheckboxField as CheckboxFieldType } from "@kyro-cms/core/client";
2
2
 
3
3
  interface CheckboxFieldComponentProps {
4
4
  field: CheckboxFieldType;
@@ -29,7 +29,9 @@ export default function CheckboxField({
29
29
  />
30
30
  <span className="text-sm font-medium text-[var(--kyro-text-primary)]">
31
31
  {field.label || field.name}
32
- {field.required && <span className="text-red-500 ml-1">*</span>}
32
+ {field.required && (
33
+ <span className="text-[var(--kyro-error)] ml-1">*</span>
34
+ )}
33
35
  </span>
34
36
  </label>
35
37
  {field.admin?.description && !error && (
@@ -37,7 +39,9 @@ export default function CheckboxField({
37
39
  {field.admin.description}
38
40
  </p>
39
41
  )}
40
- {error && <p className="text-xs text-red-500 ml-6">{error}</p>}
42
+ {error && (
43
+ <p className="text-xs text-[var(--kyro-error)] ml-6">{error}</p>
44
+ )}
41
45
  </div>
42
46
  );
43
47
  }
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+ import { ChildBlocksTree } from "../blocks/ChildBlocksTree";
3
+
4
+ interface ChildrenFieldProps {
5
+ blockId: string;
6
+ children: any[];
7
+ onUpdateChildren: (newChildren: any[]) => void;
8
+ label?: string;
9
+ compact?: boolean;
10
+ }
11
+
12
+ export const ChildrenField: React.FC<ChildrenFieldProps> = ({
13
+ blockId,
14
+ children,
15
+ onUpdateChildren,
16
+ label,
17
+ compact = false,
18
+ }) => {
19
+ if (compact) {
20
+ return (
21
+ <div className="pt-2 border-t border-[var(--kyro-border)]">
22
+ <label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1.5 block">
23
+ {label || `Children (${children.length})`}
24
+ </label>
25
+ <ChildBlocksTree
26
+ blockId={blockId}
27
+ children={children}
28
+ onUpdateChildren={onUpdateChildren}
29
+ />
30
+ </div>
31
+ );
32
+ }
33
+
34
+ return (
35
+ <div className="pt-4 border-t border-[var(--kyro-border)]">
36
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-2 block">
37
+ {label || `Children (${children.length})`}
38
+ </label>
39
+ <ChildBlocksTree
40
+ blockId={blockId}
41
+ children={children}
42
+ onUpdateChildren={onUpdateChildren}
43
+ />
44
+ </div>
45
+ );
46
+ };
47
+
48
+ export default ChildrenField;