@kyro-cms/admin 0.9.0 → 0.9.2

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 (114) hide show
  1. package/dist/index.cjs +11715 -11292
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +67 -65
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +564 -0
  6. package/dist/index.d.ts +11 -10
  7. package/dist/index.js +11326 -10912
  8. package/dist/index.js.map +1 -1
  9. package/package.json +16 -12
  10. package/src/components/ActionBar.tsx +25 -161
  11. package/src/components/Admin.tsx +2 -4
  12. package/src/components/ApiKeysManager.tsx +5 -5
  13. package/src/components/AuditLogsPage.tsx +2 -13
  14. package/src/components/AutoForm.tsx +572 -461
  15. package/src/components/BrandingHub.tsx +7 -4
  16. package/src/components/CreateView.tsx +2 -0
  17. package/src/components/DetailView.tsx +52 -65
  18. package/src/components/DeveloperCenter.tsx +8 -6
  19. package/src/components/FieldRenderer.tsx +94 -19
  20. package/src/components/ListView.tsx +57 -216
  21. package/src/components/MediaGallery.tsx +334 -367
  22. package/src/components/PluginsManager.tsx +197 -70
  23. package/src/components/RestPlayground.tsx +59 -52
  24. package/src/components/SessionsManager.tsx +1 -1
  25. package/src/components/SettingsPage.tsx +22 -0
  26. package/src/components/Sidebar.astro +13 -41
  27. package/src/components/UserManagement.tsx +153 -15
  28. package/src/components/UserMenu.tsx +30 -4
  29. package/src/components/VersionHistoryPanel.tsx +112 -119
  30. package/src/components/WebhookManager.tsx +6 -4
  31. package/src/components/blocks/ArrayBlock.tsx +6 -23
  32. package/src/components/blocks/BlockEditModal.tsx +82 -309
  33. package/src/components/blocks/CardBlock.tsx +35 -0
  34. package/src/components/blocks/ChildBlocksTree.tsx +57 -31
  35. package/src/components/blocks/GenericBlock.tsx +44 -0
  36. package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
  37. package/src/components/blocks/HeroBlock.tsx +5 -14
  38. package/src/components/blocks/RichTextBlock.tsx +5 -5
  39. package/src/components/blocks/index.ts +5 -3
  40. package/src/components/fields/AccordionField.tsx +2 -2
  41. package/src/components/fields/ArrayField.tsx +1 -1
  42. package/src/components/fields/ArrayLayout.tsx +120 -29
  43. package/src/components/fields/BlocksField.tsx +433 -55
  44. package/src/components/fields/CardField.tsx +73 -0
  45. package/src/components/fields/CheckboxField.tsx +7 -3
  46. package/src/components/fields/DateField.tsx +4 -1
  47. package/src/components/fields/GroupLayout.tsx +2 -2
  48. package/src/components/fields/HeadingSubheadingField.tsx +43 -0
  49. package/src/components/fields/ListField.tsx +2 -2
  50. package/src/components/fields/NumberField.tsx +4 -1
  51. package/src/components/fields/RelationshipBlockField.tsx +2 -3
  52. package/src/components/fields/RelationshipField.tsx +155 -90
  53. package/src/components/fields/RichTextField.tsx +781 -0
  54. package/src/components/fields/SecretField.tsx +102 -0
  55. package/src/components/fields/SelectField.tsx +19 -6
  56. package/src/components/fields/TabsLayout.tsx +19 -9
  57. package/src/components/fields/TextField.tsx +4 -1
  58. package/src/components/fields/UploadField.tsx +122 -56
  59. package/src/components/fields/extensions/blockComponents.tsx +103 -174
  60. package/src/components/fields/extensions/blocksStore.ts +8 -1
  61. package/src/components/fields/index.ts +4 -2
  62. package/src/components/fix_imports.cjs +23 -0
  63. package/src/components/fix_imports2.cjs +19 -0
  64. package/src/components/replace_svgs.cjs +63 -0
  65. package/src/components/ui/Dropdown.tsx +7 -2
  66. package/src/components/ui/Modal.tsx +24 -27
  67. package/src/components/ui/PageHeader.tsx +5 -5
  68. package/src/components/ui/PromptModal.tsx +2 -10
  69. package/src/components/ui/SlidePanel.tsx +10 -13
  70. package/src/components/ui/SplitButton.tsx +107 -0
  71. package/src/components/ui/Toaster.tsx +0 -1
  72. package/src/components/ui/icons.tsx +110 -109
  73. package/src/components/users/UserDetail.tsx +79 -16
  74. package/src/components/users/UsersList.tsx +8 -85
  75. package/src/hooks/useAutoFormState.ts +187 -196
  76. package/src/hooks/useQueue.ts +60 -0
  77. package/src/integration.ts +148 -46
  78. package/src/kyro-cms.d.ts +7 -2
  79. package/src/layouts/AdminLayout.astro +22 -2
  80. package/src/layouts/AuthLayout.astro +67 -7
  81. package/src/lib/autoform-store.ts +90 -53
  82. package/src/lib/change-source.ts +9 -0
  83. package/src/lib/config.ts +104 -8
  84. package/src/lib/globals.ts +48 -11
  85. package/src/lib/normalize-upload-fields.ts +41 -0
  86. package/src/lib/paths.ts +2 -2
  87. package/src/lib/resolve-field-value.ts +110 -0
  88. package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
  89. package/src/lib/shim/use-sync-external-store.js +1 -0
  90. package/src/lib/stores/index.ts +1 -0
  91. package/src/lib/useResourceManager.ts +4 -4
  92. package/src/lib/vite-shim-plugin.ts +100 -0
  93. package/src/pages/[collection]/[id].astro +1 -1
  94. package/src/pages/auth/register.astro +5 -2
  95. package/src/pages/preview/[collection]/[id].astro +4 -4
  96. package/src/pages/settings/[slug].astro +2 -2
  97. package/src/styles/main.css +60 -54
  98. package/README.md +0 -46
  99. package/dist/EditorClient-Q23UXR37.cjs +0 -468
  100. package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
  101. package/dist/EditorClient-T5PASFNR.js +0 -466
  102. package/dist/EditorClient-T5PASFNR.js.map +0 -1
  103. package/dist/chunk-3BGDYKTD.cjs +0 -348
  104. package/dist/chunk-3BGDYKTD.cjs.map +0 -1
  105. package/dist/chunk-EEFXLQVT.js +0 -3
  106. package/dist/chunk-EEFXLQVT.js.map +0 -1
  107. package/src/components/blocks/ButtonBlock.tsx +0 -64
  108. package/src/components/blocks/ColumnsBlock.tsx +0 -55
  109. package/src/components/blocks/DividerBlock.tsx +0 -43
  110. package/src/components/blocks/LinkBlock.tsx +0 -65
  111. package/src/components/blocks/VStackBlock.tsx +0 -29
  112. package/src/components/fields/EditorClient.tsx +0 -535
  113. package/src/components/fields/PortableTextField.tsx +0 -155
  114. package/src/components/fields/PortableTextRenderer.tsx +0 -68
@@ -4,7 +4,6 @@ import {
4
4
  useBlockActions,
5
5
  } from "../fields/extensions/blocksStore";
6
6
  import { ChevronRight, X } from "../ui/icons";
7
- import { ArrayField } from "../fields/ArrayField";
8
7
  import { ChildBlocksTree } from "./ChildBlocksTree";
9
8
 
10
9
  export const ArrayBlock: React.FC<{ block: Record<string, unknown>; index: number }> = ({
@@ -16,7 +15,6 @@ export const ArrayBlock: React.FC<{ block: Record<string, unknown>; index: numbe
16
15
 
17
16
  const data = blockData?.data ?? block.data ?? {};
18
17
  const children = blockData?.children ?? block.children ?? [];
19
- const items = Array.isArray(data.items) ? data.items : [];
20
18
 
21
19
  return (
22
20
  <div className="block-array border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
@@ -26,7 +24,7 @@ export const ArrayBlock: React.FC<{ block: Record<string, unknown>; index: numbe
26
24
  Repeater
27
25
  </span>
28
26
  <span className="text-[10px] text-[var(--kyro-text-muted)]">
29
- ({items.length + children.length} items)
27
+ ({children.length} items)
30
28
  </span>
31
29
  </div>
32
30
  <div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
@@ -58,26 +56,11 @@ export const ArrayBlock: React.FC<{ block: Record<string, unknown>; index: numbe
58
56
  </div>
59
57
 
60
58
  <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>
59
+ <ChildBlocksTree
60
+ blockId={block.id}
61
+ children={children}
62
+ onUpdateChildren={(c) => updateBlock(block.id, { children: c })}
63
+ />
81
64
  </div>
82
65
  </div>
83
66
  );
@@ -1,38 +1,22 @@
1
1
  import React from "react";
2
- import { ChevronRight } from "../ui/icons";
3
2
  import {
4
3
  useBlockById,
5
4
  useBlockActions,
6
5
  } from "../fields/extensions/blocksStore";
6
+ import { blockTheme } from "../fields/extensions/blockComponents";
7
7
  import { SlidePanel } from "../ui/SlidePanel";
8
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";
9
+ import { FieldRenderer } from "../FieldRenderer";
25
10
 
26
11
  interface BlockEditModalProps {
27
12
  block: Record<string, unknown>;
13
+ blockSchema?: Record<string, any>;
28
14
  onClose: () => void;
29
15
  }
30
16
 
31
- // @MARKER: BlockEditModal with children - 2026-04-29
32
- // If you want to revert, check git history for previous version
33
-
34
17
  export const BlockEditModal: React.FC<BlockEditModalProps> = ({
35
18
  block,
19
+ blockSchema,
36
20
  onClose,
37
21
  }) => {
38
22
  const blockData = useBlockById(block.id);
@@ -48,307 +32,96 @@ export const BlockEditModal: React.FC<BlockEditModalProps> = ({
48
32
  updateBlock(block.id, { children: newChildren });
49
33
  };
50
34
 
51
- const handleUpdateColumnChildren = (
52
- columnIndex: number,
53
- newChildren: Record<string, unknown>[],
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
35
  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: unknown) => 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 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-medium 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 : []
36
+ // If a schema is present, render all fields dynamically using FieldRenderer
37
+ if (blockSchema && Array.isArray(blockSchema.fields)) {
38
+ return (
39
+ <div className="space-y-4 pt-2">
40
+ {blockSchema.fields.map((field: any) => {
41
+ // Evaluate condition if present
42
+ if (field.admin?.condition) {
43
+ if (typeof field.admin.condition === "function") {
44
+ try {
45
+ // Compatibility wrapper: pass { values: data, ...data } to support both old and new signatures
46
+ const evalData = { values: data, ...data };
47
+ const shouldShow = field.admin.condition(evalData, evalData);
48
+ if (!shouldShow) return null;
49
+ } catch (e) {
50
+ console.warn(`Condition error for field ${field.name}:`, e);
51
+ }
52
+ } else if (typeof field.admin.condition === "object") {
53
+ try {
54
+ const cond = field.admin.condition as any;
55
+ const targetField = cond.field;
56
+ const val = data[targetField];
57
+ let shouldShow = true;
58
+ if ("equals" in cond) {
59
+ shouldShow = val === cond.equals;
60
+ } else if ("notEquals" in cond) {
61
+ shouldShow = val !== cond.notEquals;
62
+ } else if ("in" in cond && Array.isArray(cond.in)) {
63
+ shouldShow = cond.in.includes(val);
64
+ }
65
+ if (!shouldShow) return null;
66
+ } catch (e) {
67
+ console.warn(`Declarative condition error for field ${field.name}:`, e);
68
+ }
69
+ }
230
70
  }
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
71
 
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
- );
72
+ const value = data[field.name];
73
+ return (
74
+ <div
75
+ key={field.name}
76
+ className="kyro-block-field-row border-b border-[var(--kyro-border)]/30 pb-3 last:border-b-0 last:pb-0"
77
+ >
78
+ <FieldRenderer
79
+ field={field}
80
+ value={value}
81
+ onChange={(val) => handleChange(field.name, val)}
82
+ />
83
+ </div>
84
+ );
85
+ })}
86
+ </div>
87
+ );
341
88
  }
89
+
90
+ // Fallback if no schema is provided (for safety with legacy records)
91
+ return (
92
+ <div className="text-center py-8 text-[var(--kyro-text-muted)] text-sm italic">
93
+ No schema defined for block type "{block.type}"
94
+ </div>
95
+ );
342
96
  };
343
97
 
98
+ const theme = blockTheme[block.type as string] || blockTheme.default;
99
+
344
100
  return (
345
101
  <SlidePanel
346
102
  open={true}
347
103
  onClose={onClose}
348
- title={`Edit ${block.type}`}
104
+ title={`Edit ${blockSchema?.label || block.type}`}
349
105
  width="xl"
106
+ showOverlay={false}
107
+ accentClass={theme.border}
350
108
  >
351
- <div className="space-y-4">{renderFields()}</div>
109
+ <div className="space-y-4">
110
+ {renderFields()}
111
+
112
+ {children.length > 0 && (
113
+ <div className="pt-4 border-t border-[var(--kyro-border)]">
114
+ <label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1.5 block">
115
+ Children ({children.length})
116
+ </label>
117
+ <ChildBlocksTree
118
+ blockId={block.id}
119
+ children={children}
120
+ onUpdateChildren={handleUpdateChildren}
121
+ />
122
+ </div>
123
+ )}
124
+ </div>
352
125
  <div className="mt-6 pt-4 border-t border-[var(--kyro-border)]">
353
126
  <button
354
127
  type="button"
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { CardField } from "../fields/CardField";
7
+ import { BlockWrapper } from "./BlockWrapper";
8
+
9
+ export const CardBlock: React.FC<{ block: Record<string, unknown>; index: number }> = ({
10
+ block,
11
+ index,
12
+ }) => {
13
+ const blockData = useBlockById(block.id);
14
+ const { updateBlock } = useBlockActions();
15
+
16
+ const data = blockData?.data || block.data || {};
17
+
18
+ const handleChange = (field: string, value: unknown) => {
19
+ updateBlock(block.id, { data: { ...data, [field]: value } });
20
+ };
21
+
22
+ return (
23
+ <BlockWrapper id={block.id} type="card" label="Card">
24
+ <CardField
25
+ title={data.title || ""}
26
+ description={data.description || ""}
27
+ icon={data.icon || ""}
28
+ link={data.link || ""}
29
+ linkText={data.linkText || ""}
30
+ onChange={handleChange}
31
+ compact
32
+ />
33
+ </BlockWrapper>
34
+ );
35
+ };