@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.
Files changed (179) hide show
  1. package/README.md +149 -51
  2. package/package.json +54 -5
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +137 -28
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +2155 -770
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +4 -4
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +200 -58
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +890 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +192 -54
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +206 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/ThemeProvider.tsx +8 -2
  26. package/src/components/UserManagement.tsx +204 -0
  27. package/src/components/VersionHistoryPanel.tsx +3 -3
  28. package/src/components/WebhookManager.tsx +608 -0
  29. package/src/components/blocks/AccordionBlock.tsx +65 -0
  30. package/src/components/blocks/ArrayBlock.tsx +84 -0
  31. package/src/components/blocks/BlockEditModal.tsx +363 -0
  32. package/src/components/blocks/ButtonBlock.tsx +64 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +114 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +93 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +63 -0
  38. package/src/components/blocks/HeadingBlock.tsx +59 -0
  39. package/src/components/blocks/HeroBlock.tsx +99 -0
  40. package/src/components/blocks/ImageBlock.tsx +82 -0
  41. package/src/components/blocks/LinkBlock.tsx +65 -0
  42. package/src/components/blocks/ListBlock.tsx +60 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +72 -0
  45. package/src/components/blocks/RichTextBlock.tsx +66 -0
  46. package/src/components/blocks/VStackBlock.tsx +61 -0
  47. package/src/components/blocks/VideoBlock.tsx +65 -0
  48. package/src/components/blocks/index.ts +10 -0
  49. package/src/components/fields/AccordionField.tsx +213 -0
  50. package/src/components/fields/ArrayField.tsx +241 -0
  51. package/src/components/fields/BlocksField.tsx +323 -0
  52. package/src/components/fields/ButtonField.tsx +53 -0
  53. package/src/components/fields/CheckboxField.tsx +18 -8
  54. package/src/components/fields/ChildrenField.tsx +48 -0
  55. package/src/components/fields/CodeField.tsx +294 -0
  56. package/src/components/fields/ColumnsField.tsx +137 -0
  57. package/src/components/fields/DateField.tsx +24 -12
  58. package/src/components/fields/EditorClient.tsx +537 -0
  59. package/src/components/fields/HeadingField.tsx +31 -0
  60. package/src/components/fields/HeroField.tsx +101 -0
  61. package/src/components/fields/JSONField.tsx +341 -0
  62. package/src/components/fields/LinkField.tsx +81 -0
  63. package/src/components/fields/ListField.tsx +74 -0
  64. package/src/components/fields/MarkdownField.tsx +260 -0
  65. package/src/components/fields/NumberField.tsx +25 -13
  66. package/src/components/fields/PortableTextField.tsx +155 -0
  67. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  68. package/src/components/fields/RelationshipBlockField.tsx +233 -0
  69. package/src/components/fields/RelationshipField.tsx +278 -60
  70. package/src/components/fields/SelectField.tsx +28 -16
  71. package/src/components/fields/TextField.tsx +31 -15
  72. package/src/components/fields/UploadField.tsx +613 -0
  73. package/src/components/fields/VideoField.tsx +73 -0
  74. package/src/components/fields/extensions/blockComponents.tsx +247 -0
  75. package/src/components/fields/extensions/blocksStore.ts +273 -0
  76. package/src/components/fields/index.ts +24 -0
  77. package/src/components/index.ts +1 -2
  78. package/src/components/layout/Header.tsx +2 -2
  79. package/src/components/layout/Layout.tsx +3 -3
  80. package/src/components/ui/Badge.tsx +9 -4
  81. package/src/components/ui/BlockDrawer.tsx +79 -0
  82. package/src/components/ui/Button.tsx +1 -1
  83. package/src/components/ui/CommandPalette.tsx +362 -0
  84. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  85. package/src/components/ui/Dropdown.tsx +1 -1
  86. package/src/components/ui/Modal.tsx +37 -12
  87. package/src/components/ui/PromptModal.tsx +94 -0
  88. package/src/components/ui/SlidePanel.tsx +43 -16
  89. package/src/components/ui/Toast.tsx +80 -14
  90. package/src/env.d.ts +16 -0
  91. package/src/env.ts +20 -0
  92. package/src/index.ts +0 -1
  93. package/src/layouts/AdminLayout.astro +164 -170
  94. package/src/layouts/AuthLayout.astro +23 -6
  95. package/src/lib/MediaService.ts +541 -0
  96. package/src/lib/api.ts +163 -0
  97. package/src/lib/auth/sqlite-adapter.ts +319 -0
  98. package/src/lib/config.ts +23 -7
  99. package/src/lib/dataStore.ts +188 -73
  100. package/src/lib/date-utils.ts +69 -0
  101. package/src/lib/db/adapter.ts +54 -0
  102. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  103. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  104. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  105. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  106. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  107. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  108. package/src/lib/db/index.ts +449 -0
  109. package/src/lib/db/mongodb-adapter.ts +207 -0
  110. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  111. package/src/lib/db/schema/mysql-auth.ts +113 -0
  112. package/src/lib/db/schema/mysql-content.ts +20 -0
  113. package/src/lib/db/schema/postgres-auth.ts +116 -0
  114. package/src/lib/db/schema/postgres-content.ts +35 -0
  115. package/src/lib/db/schema/postgres-media.ts +52 -0
  116. package/src/lib/db/schema/postgres-settings.ts +11 -0
  117. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  118. package/src/lib/db/schema/sqlite-content.ts +20 -0
  119. package/src/lib/db/version-adapter.ts +248 -0
  120. package/src/lib/graphql/index.ts +1 -0
  121. package/src/lib/graphql/schema.ts +443 -0
  122. package/src/lib/i18n.tsx +353 -0
  123. package/src/lib/rate-limit.ts +267 -0
  124. package/src/lib/slugify.ts +15 -0
  125. package/src/lib/storage.ts +374 -0
  126. package/src/lib/store.ts +85 -0
  127. package/src/lib/validation.ts +250 -0
  128. package/src/middleware.ts +70 -11
  129. package/src/pages/[collection]/[id].astro +178 -122
  130. package/src/pages/[collection]/index.astro +24 -156
  131. package/src/pages/admin/api-explorer.astro +98 -0
  132. package/src/pages/admin/graphql-explorer.astro +40 -0
  133. package/src/pages/admin/graphql.astro +97 -0
  134. package/src/pages/admin/index.astro +200 -139
  135. package/src/pages/admin/keys.astro +8 -0
  136. package/src/pages/admin/rest-playground.astro +44 -0
  137. package/src/pages/admin/webhooks.astro +8 -0
  138. package/src/pages/api/[collection]/[id]/publish.ts +52 -0
  139. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  140. package/src/pages/api/[collection]/[id]/versions.ts +66 -0
  141. package/src/pages/api/[collection]/[id].ts +114 -159
  142. package/src/pages/api/[collection]/index.ts +150 -230
  143. package/src/pages/api/auth/[id].ts +48 -69
  144. package/src/pages/api/auth/audit-logs.ts +20 -43
  145. package/src/pages/api/auth/login.ts +159 -45
  146. package/src/pages/api/auth/logout.ts +42 -24
  147. package/src/pages/api/auth/refresh.ts +119 -0
  148. package/src/pages/api/auth/register.ts +110 -40
  149. package/src/pages/api/auth/users.ts +22 -97
  150. package/src/pages/api/collections.ts +59 -0
  151. package/src/pages/api/globals/[slug]/test.ts +172 -0
  152. package/src/pages/api/globals/[slug].ts +42 -0
  153. package/src/pages/api/graphql.ts +90 -0
  154. package/src/pages/api/health.ts +417 -40
  155. package/src/pages/api/keys/[id].ts +26 -0
  156. package/src/pages/api/keys/index.ts +75 -0
  157. package/src/pages/api/media/[id].ts +309 -0
  158. package/src/pages/api/media/folders.ts +609 -0
  159. package/src/pages/api/media/index.ts +146 -0
  160. package/src/pages/api/media/resize.ts +267 -0
  161. package/src/pages/api/search.ts +82 -0
  162. package/src/pages/api/slug-availability.ts +70 -0
  163. package/src/pages/api/storage-config.ts +20 -0
  164. package/src/pages/api/storage-status.ts +206 -0
  165. package/src/pages/api/upload.ts +334 -0
  166. package/src/pages/api/webhooks/index.ts +71 -0
  167. package/src/pages/audit/index.astro +2 -104
  168. package/src/pages/login.astro +11 -11
  169. package/src/pages/media.astro +10 -0
  170. package/src/pages/preview/[collection]/[id].astro +178 -0
  171. package/src/pages/register.astro +13 -13
  172. package/src/pages/roles/index.astro +21 -21
  173. package/src/pages/settings/[slug].astro +162 -0
  174. package/src/pages/settings/index.astro +9 -0
  175. package/src/pages/users/[id].astro +29 -21
  176. package/src/pages/users/index.astro +22 -17
  177. package/src/pages/users/new.astro +18 -17
  178. package/src/styles/main.css +563 -128
  179. package/src/components/layout/Sidebar.tsx +0 -497
@@ -0,0 +1,84 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { ChevronRight, X } from "lucide-react";
7
+ import { ArrayField } from "../fields/ArrayField";
8
+ import { ChildBlocksTree } from "./ChildBlocksTree";
9
+
10
+ export const ArrayBlock: React.FC<{ block: any; index: number }> = ({
11
+ block,
12
+ index,
13
+ }) => {
14
+ const blockData = useBlockById(block.id);
15
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
16
+
17
+ const data = blockData?.data ?? block.data ?? {};
18
+ const children = blockData?.children ?? block.children ?? [];
19
+ const items = Array.isArray(data.items) ? data.items : [];
20
+
21
+ return (
22
+ <div className="block-array border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
23
+ <div className="flex items-center justify-between mb-2">
24
+ <div className="flex items-center gap-2">
25
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
26
+ Repeater
27
+ </span>
28
+ <span className="text-[10px] text-[var(--kyro-text-muted)]">
29
+ ({items.length + children.length} items)
30
+ </span>
31
+ </div>
32
+ <div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
33
+ <button
34
+ type="button"
35
+ onClick={() => moveBlock(block.id, "up")}
36
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
37
+ title="Move up"
38
+ >
39
+ <ChevronRight className="w-3 h-3 rotate-90" />
40
+ </button>
41
+ <button
42
+ type="button"
43
+ onClick={() => moveBlock(block.id, "down")}
44
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
45
+ title="Move down"
46
+ >
47
+ <ChevronRight className="w-3 h-3" />
48
+ </button>
49
+ <button
50
+ type="button"
51
+ onClick={() => removeBlock(block.id)}
52
+ className="p-1 hover:bg-red-50 rounded text-red-500"
53
+ title="Remove"
54
+ >
55
+ <X className="w-3 h-3" />
56
+ </button>
57
+ </div>
58
+ </div>
59
+
60
+ <div className="space-y-3">
61
+ {items.length > 0 && (
62
+ <ArrayField
63
+ items={items}
64
+ onChange={(newItems) =>
65
+ updateBlock(block.id, { data: { ...data, items: newItems } })
66
+ }
67
+ compact
68
+ />
69
+ )}
70
+
71
+ <div className="pt-2 border-t border-[var(--kyro-border)]">
72
+ <label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1.5 block">
73
+ Children ({children.length})
74
+ </label>
75
+ <ChildBlocksTree
76
+ blockId={block.id}
77
+ children={children}
78
+ onUpdateChildren={(c) => updateBlock(block.id, { children: c })}
79
+ />
80
+ </div>
81
+ </div>
82
+ </div>
83
+ );
84
+ };
@@ -0,0 +1,363 @@
1
+ import React from "react";
2
+ import { ChevronRight } from "lucide-react";
3
+ import {
4
+ useBlockById,
5
+ useBlockActions,
6
+ } from "../fields/extensions/blocksStore";
7
+ import { SlidePanel } from "../ui/SlidePanel";
8
+ import { ChildBlocksTree } from "./ChildBlocksTree";
9
+ import { UploadField } from "../fields/UploadField";
10
+ import PortableTextField from "../fields/PortableTextField";
11
+ import {
12
+ CodeField,
13
+ LinkField,
14
+ AccordionField,
15
+ ButtonField,
16
+ HeadingField,
17
+ VideoField,
18
+ ListField,
19
+ HeroField,
20
+ ArrayField,
21
+ ChildrenField,
22
+ ColumnsField,
23
+ RelationshipBlockField,
24
+ } from "../fields";
25
+
26
+ interface BlockEditModalProps {
27
+ block: any;
28
+ onClose: () => void;
29
+ }
30
+
31
+ // @MARKER: BlockEditModal with children - 2026-04-29
32
+ // If you want to revert, check git history for previous version
33
+
34
+ export const BlockEditModal: React.FC<BlockEditModalProps> = ({
35
+ block,
36
+ onClose,
37
+ }) => {
38
+ const blockData = useBlockById(block.id);
39
+ const { updateBlock } = useBlockActions();
40
+ const data = blockData?.data || block.data || {};
41
+ const children = blockData?.children || block.children || [];
42
+
43
+ const handleChange = (field: string, value: any) => {
44
+ updateBlock(block.id, { data: { ...data, [field]: value } });
45
+ };
46
+
47
+ const handleUpdateChildren = (newChildren: any[]) => {
48
+ updateBlock(block.id, { children: newChildren });
49
+ };
50
+
51
+ const handleUpdateColumnChildren = (
52
+ columnIndex: number,
53
+ newChildren: any[],
54
+ ) => {
55
+ const columnData = data.columnData || [];
56
+ const newColumnData = [...columnData];
57
+ newColumnData[columnIndex] = {
58
+ ...newColumnData[columnIndex],
59
+ children: newChildren,
60
+ };
61
+ updateBlock(block.id, { data: { ...data, columnData: newColumnData } });
62
+ };
63
+
64
+ const renderFields = () => {
65
+ switch (block.type) {
66
+ case "heading":
67
+ return (
68
+ <HeadingField
69
+ text={data.text || ""}
70
+ onChange={handleChange}
71
+ compact
72
+ />
73
+ );
74
+
75
+ case "paragraph":
76
+ return (
77
+ <div className="space-y-3">
78
+ <textarea
79
+ value={data.text || ""}
80
+ onChange={(e) => handleChange("text", e.target.value)}
81
+ className="w-full px-3 py-3 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-primary)] text-sm min-h-[150px] resize-y focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
82
+ placeholder="Enter paragraph text..."
83
+ />
84
+ </div>
85
+ );
86
+
87
+ case "richtext":
88
+ return (
89
+ <div className="space-y-3">
90
+ <PortableTextField
91
+ field={{ name: "richtext", label: "Content" }}
92
+ value={data.content}
93
+ onChange={(value: any) => handleChange("content", value)}
94
+ />
95
+ </div>
96
+ );
97
+
98
+ case "image":
99
+ return (
100
+ <div className="space-y-4">
101
+ <UploadField
102
+ field={{ label: "Image", name: "image", maxCount: 1 }}
103
+ value={data.src}
104
+ onChange={(value) => handleChange("src", value)}
105
+ />
106
+ <div>
107
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
108
+ Alt Text
109
+ </label>
110
+ <input
111
+ type="text"
112
+ value={data.alt || ""}
113
+ onChange={(e) => handleChange("alt", e.target.value)}
114
+ 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"
115
+ placeholder="Description for accessibility..."
116
+ />
117
+ </div>
118
+ </div>
119
+ );
120
+
121
+ case "video":
122
+ return (
123
+ <VideoField
124
+ src={data.src || ""}
125
+ title={data.title || ""}
126
+ onChange={handleChange}
127
+ compact
128
+ />
129
+ );
130
+
131
+ case "link":
132
+ return (
133
+ <div className="space-y-3">
134
+ <LinkField
135
+ text={data.text || ""}
136
+ url={data.url || ""}
137
+ onChange={handleChange}
138
+ compact
139
+ />
140
+ </div>
141
+ );
142
+
143
+ case "button":
144
+ return (
145
+ <ButtonField
146
+ text={data.text || "Button"}
147
+ url={data.url || ""}
148
+ onChange={handleChange}
149
+ compact
150
+ />
151
+ );
152
+
153
+ case "list":
154
+ return (
155
+ <ListField
156
+ items={Array.isArray(data.items) ? data.items : []}
157
+ onChange={(items) => handleChange("items", items)}
158
+ compact
159
+ />
160
+ );
161
+
162
+ case "code":
163
+ const languages = [
164
+ { value: "plaintext", label: "Text", icon: "📄" },
165
+ { value: "javascript", label: "JS", icon: "🟨" },
166
+ { value: "typescript", label: "TS", icon: "🔷" },
167
+ { value: "python", label: "PY", icon: "🐍" },
168
+ { value: "html", label: "HTML", icon: "🌐" },
169
+ { value: "css", label: "CSS", icon: "🎨" },
170
+ { value: "json", label: "JSON", icon: "📋" },
171
+ ];
172
+ return (
173
+ <div className="space-y-4">
174
+ <CodeField
175
+ field={{
176
+ type: "code",
177
+ name: "code",
178
+ label: "Snippet",
179
+ language: data.language || "javascript",
180
+ }}
181
+ value={data.code || ""}
182
+ onChange={(val) => handleChange("code", val)}
183
+ />
184
+
185
+ <div className="grid grid-cols-2 gap-3">
186
+ <div>
187
+ <label className="text-[10px] font-bold uppercase tracking-widest text-[var(--kyro-text-muted)] mb-1.5 block">
188
+ Language
189
+ </label>
190
+ <div className="relative">
191
+ <select
192
+ value={data.language || "javascript"}
193
+ onChange={(e) => handleChange("language", e.target.value)}
194
+ className="w-full pl-3 pr-10 py-2.5 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-xl text-xs font-bold text-[var(--kyro-text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--kyro-primary)]/20 transition-all appearance-none cursor-pointer"
195
+ >
196
+ <option value="plaintext">Plain Text</option>
197
+ <option value="javascript">JS</option>
198
+ <option value="typescript">TS</option>
199
+ <option value="python">PY</option>
200
+ <option value="html">HTML</option>
201
+ <option value="css">CSS</option>
202
+ <option value="json">JSON</option>
203
+ <option value="rust">Rust</option>
204
+ </select>
205
+ <div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-[var(--kyro-text-muted)]">
206
+ <ChevronRight className="w-4 h-4 rotate-90" />
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ );
213
+
214
+ case "file":
215
+ return (
216
+ <UploadField
217
+ field={{ label: "File", name: "file", maxCount: 1 }}
218
+ value={data.file}
219
+ onChange={(value) => handleChange("file", value)}
220
+ />
221
+ );
222
+
223
+ case "relationship":
224
+ return (
225
+ <RelationshipBlockField
226
+ relationTo={data.relationTo || "pages"}
227
+ hasMany={data.hasMany || false}
228
+ selectedIds={
229
+ Array.isArray(data.selectedIds) ? data.selectedIds : []
230
+ }
231
+ selectedId={data.selectedId}
232
+ labelField={data.labelField || "title"}
233
+ onChange={handleChange}
234
+ />
235
+ );
236
+
237
+ case "hero":
238
+ return (
239
+ <div className="space-y-3">
240
+ <HeroField
241
+ heading={data.heading || ""}
242
+ subheading={data.subheading || ""}
243
+ ctaText={data.ctaText || ""}
244
+ ctaUrl={data.ctaUrl || ""}
245
+ onChange={handleChange}
246
+ compact
247
+ />
248
+
249
+ <div className="grid grid-cols-2 gap-2">
250
+ <UploadField
251
+ field={{ label: "Background", name: "bgImage", maxCount: 1 }}
252
+ value={data.bgImage}
253
+ onChange={(v) => handleChange("bgImage", v)}
254
+ />
255
+ <input
256
+ type="url"
257
+ value={data.videoUrl || ""}
258
+ onChange={(e) => handleChange("videoUrl", e.target.value)}
259
+ className="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 font-mono text-xs"
260
+ placeholder="Video URL..."
261
+ />
262
+ </div>
263
+
264
+ <div className="pt-2 border-t border-[var(--kyro-border)]">
265
+ <label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1.5 block">
266
+ Children ({children.length})
267
+ </label>
268
+ <ChildBlocksTree
269
+ blockId={block.id}
270
+ children={children}
271
+ onUpdateChildren={handleUpdateChildren}
272
+ />
273
+ </div>
274
+ </div>
275
+ );
276
+
277
+ case "array":
278
+ return (
279
+ <div className="space-y-3">
280
+ <ArrayField
281
+ items={Array.isArray(data.items) ? data.items : []}
282
+ onChange={(items) => handleChange("items", items)}
283
+ compact
284
+ />
285
+ <div className="pt-2 border-t border-[var(--kyro-border)]">
286
+ <label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1.5 block">
287
+ Children ({children.length})
288
+ </label>
289
+ <ChildBlocksTree
290
+ blockId={block.id}
291
+ children={children}
292
+ onUpdateChildren={handleUpdateChildren}
293
+ />
294
+ </div>
295
+ </div>
296
+ );
297
+
298
+ case "accordion":
299
+ return (
300
+ <AccordionField
301
+ items={Array.isArray(data.items) ? data.items : []}
302
+ onChange={(items) => handleChange("items", items)}
303
+ compact
304
+ />
305
+ );
306
+
307
+ case "vstack":
308
+ return (
309
+ <ChildrenField
310
+ blockId={block.id}
311
+ children={children}
312
+ onUpdateChildren={handleUpdateChildren}
313
+ />
314
+ );
315
+
316
+ case "columns":
317
+ return (
318
+ <ColumnsField
319
+ columns={data.columns || 2}
320
+ columnData={data.columnData || []}
321
+ onColumnsChange={(c) => {
322
+ const columnData = data.columnData || [];
323
+ const newColumnData = Array.from({ length: c }, (_, i) => ({
324
+ id: i,
325
+ children: columnData[i]?.children || [],
326
+ }));
327
+ updateBlock(block.id, {
328
+ data: { ...data, columns: c, columnData: newColumnData },
329
+ });
330
+ }}
331
+ onUpdateColumnChildren={handleUpdateColumnChildren}
332
+ />
333
+ );
334
+
335
+ default:
336
+ return (
337
+ <div className="text-center py-8 text-[var(--kyro-text-muted)]">
338
+ No editor for "{block.type}"
339
+ </div>
340
+ );
341
+ }
342
+ };
343
+
344
+ return (
345
+ <SlidePanel
346
+ open={true}
347
+ onClose={onClose}
348
+ title={`Edit ${block.type}`}
349
+ width="xl"
350
+ >
351
+ <div className="space-y-4">{renderFields()}</div>
352
+ <div className="mt-6 pt-4 border-t border-[var(--kyro-border)]">
353
+ <button
354
+ type="button"
355
+ onClick={onClose}
356
+ className="w-full py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-lg font-medium hover:opacity-90 transition-opacity"
357
+ >
358
+ Done
359
+ </button>
360
+ </div>
361
+ </SlidePanel>
362
+ );
363
+ };
@@ -0,0 +1,64 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { ChevronRight, X } from "lucide-react";
7
+ import { ButtonField } from "../fields/ButtonField";
8
+
9
+ export const ButtonBlock: 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 = (field: string, value: any) => {
19
+ updateBlock(block.id, { data: { ...data, [field]: value } });
20
+ };
21
+
22
+ return (
23
+ <div className="block-button border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
24
+ <div className="flex items-center justify-between mb-2">
25
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
26
+ Button
27
+ </span>
28
+ <div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
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>
53
+ </div>
54
+ </div>
55
+
56
+ <ButtonField
57
+ text={data.text || "Button"}
58
+ url={data.url || ""}
59
+ onChange={handleChange}
60
+ compact
61
+ />
62
+ </div>
63
+ );
64
+ };