@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
@@ -1,7 +1,6 @@
1
1
  import React, { useState } from "react";
2
2
  import { Plus, X, ChevronRight, ChevronDown } from "../ui/icons";
3
3
  import {
4
- blockCategories,
5
4
  blockIcons,
6
5
  getBlockComponent,
7
6
  getBlockLabel,
@@ -9,6 +8,9 @@ import {
9
8
  import { createNewBlock } from "../fields/extensions/blocksStore";
10
9
  import { BlockDrawer } from "../ui/BlockDrawer";
11
10
  import { BlockEditModal } from "./BlockEditModal";
11
+ import { useStore } from "zustand";
12
+ import { BlocksContext } from "../fields/extensions/blocksStore";
13
+ import { useContext } from "react";
12
14
 
13
15
  interface ChildBlocksTreeProps {
14
16
  blockId: string;
@@ -32,7 +34,11 @@ export const ChildBlocksTree: React.FC<ChildBlocksTreeProps> = ({
32
34
  const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
33
35
  const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
34
36
 
35
- const availableBlocks = blockCategories.flatMap((cat) => cat.blocks);
37
+ const store = useContext(BlocksContext);
38
+ if (!store) throw new Error("ChildBlocksTree must be used within a BlocksContext");
39
+ const dynamicCategories = useStore(store, (s) => s.dynamicCategories);
40
+ const allowedBlocks = useStore(store, (s) => s.allowedBlocks);
41
+
36
42
  const canAddChildren = depth < maxDepth;
37
43
  const indentWidth = 16;
38
44
 
@@ -88,6 +94,7 @@ export const ChildBlocksTree: React.FC<ChildBlocksTreeProps> = ({
88
94
  const BlockComponent = getBlockComponent(child.type);
89
95
  const childHasOwnChildren = hasChildren;
90
96
  const isEditing = editingBlockId === child.id;
97
+ const blockSchema = allowedBlocks.find((b: any) => b.slug === child.type);
91
98
 
92
99
  return (
93
100
  <div key={child.id} className="relative group">
@@ -123,20 +130,27 @@ export const ChildBlocksTree: React.FC<ChildBlocksTreeProps> = ({
123
130
  )}
124
131
 
125
132
  {blockIcons[child.type] && (
126
- <span className="text-[var(--kyro-text-secondary)]">
133
+ <div className="w-8 h-8 rounded bg-[var(--kyro-surface-accent)] flex items-center justify-center text-[var(--kyro-text-secondary)]">
127
134
  {blockIcons[child.type]}
128
- </span>
135
+ </div>
129
136
  )}
130
137
 
131
- <span className="text-xs font-medium text-[var(--kyro-text-secondary)] flex-1 truncate">
132
- {getBlockLabel(child.type)}
133
- {child.data?.text ? ` - ${child.data.text.slice(0, 30)}` : ""}
134
- {child.data?.heading ? ` - ${child.data.heading.slice(0, 30)}` : ""}
135
- </span>
138
+ <div className="flex-1 min-w-0">
139
+ <div className="text-xs font-medium text-[var(--kyro-text-secondary)] truncate">
140
+ {getBlockLabel(child.type)}
141
+ {child.data?.text ? ` - ${child.data.text.slice(0, 30)}` : ""}
142
+ {child.data?.heading ? ` - ${child.data.heading.slice(0, 30)}` : ""}
143
+ </div>
144
+ {blockSchema?.admin?.description && (
145
+ <div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5 truncate opacity-80">
146
+ {blockSchema.admin.description}
147
+ </div>
148
+ )}
149
+ </div>
136
150
 
137
151
  {hasChildren && (
138
- <span className="text-[10px] text-[var(--kyro-text-muted)]">
139
- ({child.children.length})
152
+ <span className="text-[10px] bg-[var(--kyro-surface-accent)] px-2 py-0.5 rounded text-[var(--kyro-text-muted)] font-medium">
153
+ {child.children.length} nested
140
154
  </span>
141
155
  )}
142
156
 
@@ -223,7 +237,7 @@ export const ChildBlocksTree: React.FC<ChildBlocksTreeProps> = ({
223
237
  onClose={() => setShowAddModal(false)}
224
238
  onSelect={handleAddChild}
225
239
  >
226
- {blockCategories.map((category) => (
240
+ {dynamicCategories.map((category) => (
227
241
  <div key={category.title} className="mb-4">
228
242
  <h3 className="text-xs font-semibold text-[var(--kyro-text-muted)] tracking-wide mb-2">
229
243
  {category.title}
@@ -231,23 +245,23 @@ export const ChildBlocksTree: React.FC<ChildBlocksTreeProps> = ({
231
245
  <div className="grid grid-cols-3 gap-2">
232
246
  {category.blocks.map((block) => (
233
247
  <button
234
- key={block.type}
248
+ key={block.slug}
235
249
  type="button"
236
250
  onClick={() => {
237
- handleAddChild(block.type);
251
+ handleAddChild(block.slug);
238
252
  setShowAddModal(false);
239
253
  }}
240
254
  className="flex flex-col items-center text-center gap-1 p-2 rounded-md border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/60 hover:bg-[var(--kyro-surface-accent)]/30 transition-all cursor-pointer group"
241
255
  >
242
256
  <div className="w-6 h-6 flex items-center justify-center rounded group-hover:bg-[var(--kyro-primary)]/10 group-hover:text-[var(--kyro-primary)] transition-all">
243
- {blockIcons[block.icon]}
257
+ {blockIcons[block.slug as keyof typeof blockIcons]}
244
258
  </div>
245
259
  <div className="flex-1 min-w-0">
246
260
  <div className="text-xs font-medium tracking-tight text-[var(--kyro-text-primary)]">
247
261
  {block.label}
248
262
  </div>
249
263
  <div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5">
250
- {block.description}
264
+ {block.admin?.description || ""}
251
265
  </div>
252
266
  </div>
253
267
  </button>
@@ -300,7 +314,11 @@ const NestedChildBlocks: React.FC<NestedChildBlocksProps> = ({
300
314
  const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
301
315
  const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
302
316
 
303
- const availableBlocks = blockCategories.flatMap((cat) => cat.blocks);
317
+ const store = useContext(BlocksContext);
318
+ if (!store) throw new Error("NestedChildBlocks must be used within a BlocksContext");
319
+ const dynamicCategories = useStore(store, (s) => s.dynamicCategories);
320
+ const allowedBlocks = useStore(store, (s) => s.allowedBlocks);
321
+
304
322
  const canAddChildren = depth < maxDepth;
305
323
  const indentWidth = 16;
306
324
 
@@ -356,6 +374,7 @@ const NestedChildBlocks: React.FC<NestedChildBlocksProps> = ({
356
374
  const BlockComponent = getBlockComponent(child.type);
357
375
  const childHasOwnChildren = hasChildren;
358
376
  const isEditing = editingBlockId === child.id;
377
+ const blockSchema = allowedBlocks.find((b: any) => b.slug === child.type);
359
378
 
360
379
  return (
361
380
  <div key={child.id} className="relative group">
@@ -391,20 +410,27 @@ const NestedChildBlocks: React.FC<NestedChildBlocksProps> = ({
391
410
  )}
392
411
 
393
412
  {blockIcons[child.type] && (
394
- <span className="text-[var(--kyro-text-secondary)]">
413
+ <div className="w-8 h-8 rounded bg-[var(--kyro-surface-accent)] flex items-center justify-center text-[var(--kyro-text-secondary)]">
395
414
  {blockIcons[child.type]}
396
- </span>
415
+ </div>
397
416
  )}
398
417
 
399
- <span className="text-xs font-medium text-[var(--kyro-text-secondary)] flex-1 truncate">
400
- {getBlockLabel(child.type)}
401
- {child.data?.text ? ` - ${child.data.text.slice(0, 30)}` : ""}
402
- {child.data?.heading ? ` - ${child.data.heading.slice(0, 30)}` : ""}
403
- </span>
418
+ <div className="flex-1 min-w-0">
419
+ <div className="text-xs font-medium text-[var(--kyro-text-secondary)] truncate">
420
+ {getBlockLabel(child.type)}
421
+ {child.data?.text ? ` - ${child.data.text.slice(0, 30)}` : ""}
422
+ {child.data?.heading ? ` - ${child.data.heading.slice(0, 30)}` : ""}
423
+ </div>
424
+ {blockSchema?.admin?.description && (
425
+ <div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5 truncate opacity-80">
426
+ {blockSchema.admin.description}
427
+ </div>
428
+ )}
429
+ </div>
404
430
 
405
431
  {hasChildren && (
406
- <span className="text-[10px] text-[var(--kyro-text-muted)]">
407
- ({child.children.length})
432
+ <span className="text-[10px] bg-[var(--kyro-surface-accent)] px-2 py-0.5 rounded text-[var(--kyro-text-muted)] font-medium">
433
+ {child.children.length} nested
408
434
  </span>
409
435
  )}
410
436
 
@@ -491,7 +517,7 @@ const NestedChildBlocks: React.FC<NestedChildBlocksProps> = ({
491
517
  onClose={() => setShowAddModal(false)}
492
518
  onSelect={handleAddChild}
493
519
  >
494
- {blockCategories.map((category) => (
520
+ {dynamicCategories.map((category) => (
495
521
  <div key={category.title} className="mb-4">
496
522
  <h3 className="text-xs font-semibold text-[var(--kyro-text-muted)] tracking-wide mb-2">
497
523
  {category.title}
@@ -499,23 +525,23 @@ const NestedChildBlocks: React.FC<NestedChildBlocksProps> = ({
499
525
  <div className="grid grid-cols-3 gap-2">
500
526
  {category.blocks.map((block) => (
501
527
  <button
502
- key={block.type}
528
+ key={block.slug}
503
529
  type="button"
504
530
  onClick={() => {
505
- handleAddChild(block.type);
531
+ handleAddChild(block.slug);
506
532
  setShowAddModal(false);
507
533
  }}
508
534
  className="flex flex-col items-center text-center gap-1 p-2 rounded-md border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/60 hover:bg-[var(--kyro-surface-accent)]/30 transition-all cursor-pointer group"
509
535
  >
510
536
  <div className="w-6 h-6 flex items-center justify-center rounded group-hover:bg-[var(--kyro-primary)]/10 group-hover:text-[var(--kyro-primary)] transition-all">
511
- {blockIcons[block.icon]}
537
+ {blockIcons[block.slug as keyof typeof blockIcons]}
512
538
  </div>
513
539
  <div className="flex-1 min-w-0">
514
540
  <div className="text-xs font-medium tracking-tight text-[var(--kyro-text-primary)]">
515
541
  {block.label}
516
542
  </div>
517
543
  <div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5">
518
- {block.description}
544
+ {block.admin?.description || ""}
519
545
  </div>
520
546
  </div>
521
547
  </button>
@@ -0,0 +1,44 @@
1
+ import React from "react";
2
+ import { BlockWrapper } from "./BlockWrapper";
3
+ import { FieldRenderer } from "../FieldRenderer";
4
+ import { useBlockById, useBlockActions } from "../fields/extensions/blocksStore";
5
+
6
+ interface GenericBlockProps {
7
+ block: Record<string, unknown>;
8
+ index: number;
9
+ blockSchema: Record<string, any>;
10
+ }
11
+
12
+ export const GenericBlock: React.FC<GenericBlockProps> = ({
13
+ block,
14
+ index,
15
+ blockSchema,
16
+ }) => {
17
+ const blockData = useBlockById(block.id);
18
+ const { updateBlock } = useBlockActions();
19
+
20
+ const data = (blockData?.data || block.data || {}) as Record<string, unknown>;
21
+
22
+ const handleChange = (fieldName: string, value: unknown) => {
23
+ updateBlock(block.id, { data: { ...data, [fieldName]: value } });
24
+ };
25
+
26
+ return (
27
+ <BlockWrapper id={block.id} type={block.type as string} label={blockSchema.label as string}>
28
+ <div className="space-y-4 pt-2">
29
+ {blockSchema.fields?.map((field: any) => {
30
+ const value = data[field.name];
31
+ return (
32
+ <div key={field.name} className="kyro-block-field-row border-b border-[var(--kyro-border)]/30 pb-3 last:border-b-0 last:pb-0">
33
+ <FieldRenderer
34
+ field={field}
35
+ value={value}
36
+ onChange={(val) => handleChange(field.name, val)}
37
+ />
38
+ </div>
39
+ );
40
+ })}
41
+ </div>
42
+ </BlockWrapper>
43
+ );
44
+ };
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { HeadingSubheadingField } from "../fields/HeadingSubheadingField";
7
+ import { BlockWrapper } from "./BlockWrapper";
8
+
9
+ export const HeadingSubheadingBlock: 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="heading-subheading" label="Heading + Subheading">
24
+ <HeadingSubheadingField
25
+ heading={data.heading || ""}
26
+ subheading={data.subheading || ""}
27
+ onChange={handleChange}
28
+ compact
29
+ />
30
+ </BlockWrapper>
31
+ );
32
+ };
@@ -68,20 +68,11 @@ export const HeroBlock: React.FC<{ block: Record<string, unknown>; index: number
68
68
  compact
69
69
  />
70
70
 
71
- <div className="grid grid-cols-2 gap-2">
72
- <UploadField
73
- field={{ label: "Background", name: "bgImage", maxCount: 1 }}
74
- value={data.bgImage}
75
- onChange={(v) => handleChange("bgImage", v)}
76
- />
77
- <input
78
- type="url"
79
- value={data.videoUrl || ""}
80
- onChange={(e) => handleChange("videoUrl", e.target.value)}
81
- 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"
82
- placeholder="Video URL..."
83
- />
84
- </div>
71
+ <UploadField
72
+ field={{ label: "Background", name: "bgImage", maxCount: 1 }}
73
+ value={data.bgImage}
74
+ onChange={(v) => handleChange("bgImage", v)}
75
+ />
85
76
 
86
77
  <div className="pt-3 border-t border-[var(--kyro-border)]">
87
78
  <label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1.5 block">
@@ -4,16 +4,16 @@ import {
4
4
  useBlockActions,
5
5
  } from "../fields/extensions/blocksStore";
6
6
  import { ChevronRight, X, AlignLeft } from "../ui/icons";
7
- import PortableTextField from "../fields/PortableTextField";
7
+ import { RichTextField } from "../fields";
8
8
 
9
- export const RichTextBlock: React.FC<{ block: Record<string, unknown>; index: number }> = ({
9
+ export const RichTextBlock: React.FC<{ block: any; index: number }> = ({
10
10
  block,
11
11
  index,
12
12
  }) => {
13
13
  const blockData = useBlockById(block.id);
14
14
  const { updateBlock, removeBlock, moveBlock } = useBlockActions();
15
15
 
16
- const data = blockData?.data || block.data || {};
16
+ const data = (blockData?.data || block.data || {}) as Record<string, any>;
17
17
 
18
18
  const handleChange = (newValue: unknown) => {
19
19
  updateBlock(block.id, { data: { ...data, content: newValue } });
@@ -56,8 +56,8 @@ export const RichTextBlock: React.FC<{ block: Record<string, unknown>; index: nu
56
56
  </div>
57
57
  </div>
58
58
 
59
- <PortableTextField
60
- field={{ name: "content", label: "Content" }}
59
+ <RichTextField
60
+ field={{ name: "content", label: "Content", type: "richtext" } as any}
61
61
  value={data.content}
62
62
  onChange={handleChange}
63
63
  />
@@ -1,10 +1,12 @@
1
- export { ColumnsBlock } from './ColumnsBlock';
2
1
  export { HeadingBlock } from './HeadingBlock';
2
+ export { HeadingSubheadingBlock } from './HeadingSubheadingBlock';
3
3
  export { ParagraphBlock } from './ParagraphBlock';
4
- export { DividerBlock } from './DividerBlock';
5
4
  export { ImageBlock } from './ImageBlock';
6
5
  export { VideoBlock } from './VideoBlock';
7
6
  export { ListBlock } from './ListBlock';
8
7
  export { CodeBlock } from './CodeBlock';
9
- export { LinkBlock } from './LinkBlock';
10
8
  export { FileBlock } from './FileBlock';
9
+ export { CardBlock } from './CardBlock';
10
+ export { GenericBlock } from './GenericBlock';
11
+
12
+
@@ -53,7 +53,7 @@ export const AccordionField: React.FC<AccordionFieldProps> = ({
53
53
  return (
54
54
  <div className="space-y-2">
55
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">
56
+ <div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-md">
57
57
  No items. Click "Add Item" to create one.
58
58
  </div>
59
59
  ) : (
@@ -135,7 +135,7 @@ export const AccordionField: React.FC<AccordionFieldProps> = ({
135
135
  return (
136
136
  <div className="space-y-2">
137
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">
138
+ <div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-md">
139
139
  No items. Click "Add Item" to create one.
140
140
  </div>
141
141
  ) : (
@@ -55,7 +55,7 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
55
55
  return (
56
56
  <div className="space-y-1.5">
57
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">
58
+ <div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-md">
59
59
  No items. Click "Add Item" to create one.
60
60
  </div>
61
61
  ) : (
@@ -1,6 +1,9 @@
1
1
  import React from "react";
2
2
  import type { Field } from "@kyro-cms/core/client";
3
3
  import RelationshipField from "./RelationshipField";
4
+ import { ChevronDown, ChevronUp } from "../ui/icons";
5
+
6
+ const SIMPLE_TYPES = new Set(["text", "textarea", "number", "checkbox", "select", "radio", "color", "email", "password", "code", "markdown", "upload"]);
4
7
 
5
8
  interface ArrayLayoutProps {
6
9
  field: Field;
@@ -14,6 +17,12 @@ interface ArrayLayoutProps {
14
17
  disabled?: boolean;
15
18
  }
16
19
 
20
+ function isCompactArray(field: Field): boolean {
21
+ const subFields = (field as Field & { fields?: Field[] }).fields || [];
22
+ if (subFields.length === 0 || subFields.length > 4) return false;
23
+ return subFields.every((f: Field) => SIMPLE_TYPES.has(f.type));
24
+ }
25
+
17
26
  export function ArrayLayout({
18
27
  field,
19
28
  value,
@@ -22,8 +31,26 @@ export function ArrayLayout({
22
31
  disabled,
23
32
  }: ArrayLayoutProps) {
24
33
  const items = Array.isArray(value) ? value : [];
25
- const labelField = (field as Field & { fields?: { name?: string; type?: string; relationTo?: string; label?: string }[] }).fields?.[0]?.name || "user";
26
- const isRelationship = (field as Field & { fields?: { type?: string }[] }).fields?.[0]?.type === "relationship";
34
+ const fields = (field as Field & { fields?: { name?: string; type?: string; relationTo?: string; label?: string }[] }).fields || [];
35
+ const firstField = fields[0];
36
+ const labelField = firstField?.name || "user";
37
+ const isRelationship = firstField?.type === "relationship";
38
+ const [openIndex, setOpenIndex] = React.useState<number | null>(0);
39
+
40
+ function getItemLabel(item: Record<string, unknown>): string {
41
+ for (const key of ["label", "title", "name"]) {
42
+ const val = item[key];
43
+ if (val && typeof val === "string") return val;
44
+ }
45
+ for (const f of fields) {
46
+ if (f.type === "text" || f.type === "textarea") {
47
+ const val = item[f.name!];
48
+ if (val && typeof val === "string") return val;
49
+ }
50
+ }
51
+ return "";
52
+ }
53
+ const compact = isCompactArray(field);
27
54
 
28
55
  return (
29
56
  <div className="kyro-form-field">
@@ -45,42 +72,106 @@ export function ArrayLayout({
45
72
  }}
46
73
  disabled={disabled}
47
74
  />
48
- ) : (
49
- <div className="kyro-form-array border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30 rounded-[var(--kyro-radius-md)] p-4 space-y-4">
75
+ ) : compact ? (
76
+ <div className="kyro-form-array kyro-form-array--compact border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30 rounded-md overflow-hidden">
50
77
  {(items as Record<string, unknown>[]).map((item, index) => (
51
78
  <div
52
79
  key={index}
53
- className="kyro-form-array-item bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-[var(--kyro-radius-md)] p-4 relative group"
80
+ className="flex items-start gap-2 px-3 py-1.5 border-b border-[var(--kyro-border)] last:border-b-0 hover:bg-[var(--kyro-sidebar-active)]/5 transition-colors"
54
81
  >
55
- <div className="flex justify-between items-center mb-4 pb-2 border-b border-[var(--kyro-border)]">
56
- <span className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)]">
57
- Item {index + 1}
58
- </span>
59
- <button
60
- type="button"
61
- disabled={disabled}
62
- className="text-[11px] font-bold text-[var(--kyro-error)] opacity-50 hover:opacity-100 transition-opacity disabled:opacity-30"
63
- onClick={() =>
64
- onChange(items.filter((_: unknown, i: number) => i !== index))
65
- }
66
- >
67
- Remove
68
- </button>
69
- </div>
70
- <div className="space-y-4">
71
- {(field as Field & { fields?: Field[] }).fields.map((f: Field) =>
72
- renderField(f, item, (newItem) => {
73
- const newItems = [...items];
74
- newItems[index] = newItem;
75
- onChange(newItems);
76
- }),
77
- )}
82
+ <span className="text-[10px] font-bold text-[var(--kyro-text-muted)] pt-2 min-w-[18px] text-center">
83
+ {index + 1}
84
+ </span>
85
+ <div className="flex-1 flex items-start gap-1.5 min-w-0">
86
+ {(field as Field & { fields?: Field[] }).fields.map((f: Field) => {
87
+ return (
88
+ <div key={f.name} className="flex-1 min-w-0">
89
+ {renderField(f, item, (newItem) => {
90
+ const newItems = [...items];
91
+ newItems[index] = newItem;
92
+ onChange(newItems);
93
+ })}
94
+ </div>
95
+ );
96
+ })}
78
97
  </div>
98
+ <button
99
+ type="button"
100
+ disabled={disabled}
101
+ onClick={() => onChange(items.filter((_: unknown, i: number) => i !== index))}
102
+ className="text-[var(--kyro-text-muted)] hover:text-[var(--kyro-error)] transition-colors disabled:opacity-30 p-0.5 mt-1.5 flex-shrink-0"
103
+ title="Remove"
104
+ >
105
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
106
+ <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
107
+ </svg>
108
+ </button>
79
109
  </div>
80
110
  ))}
81
111
  <button
82
112
  type="button"
83
- className="w-full py-3 border-2 border-dashed border-[var(--kyro-border)] rounded-[var(--kyro-radius-md)] text-xs font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:border-[var(--kyro-primary)] transition-all disabled:opacity-50"
113
+ className="w-full py-2 border-2 border-dashed border-[var(--kyro-border)] rounded-none text-xs font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:border-[var(--kyro-primary)] transition-all disabled:opacity-50"
114
+ disabled={disabled}
115
+ onClick={() => onChange([...items, {}])}
116
+ >
117
+ + Add Item
118
+ </button>
119
+ </div>
120
+ ) : (
121
+ <div className="kyro-form-array border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30 rounded-md p-3 space-y-4">
122
+ {(items as Record<string, unknown>[]).map((item, index) => {
123
+ const isOpen = openIndex === index;
124
+ return (
125
+ <div
126
+ key={index}
127
+ className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
128
+ >
129
+ <div
130
+ role="button"
131
+ tabIndex={0}
132
+ onClick={() => setOpenIndex(isOpen ? null : index)}
133
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpenIndex(isOpen ? null : index); } }}
134
+ className="w-full flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors cursor-pointer"
135
+ >
136
+ <span className="text-xs font-bold tracking-widest text-[var(--kyro-text-muted)] truncate">
137
+ {getItemLabel(item) || `Item ${index + 1}`}
138
+ </span>
139
+ <div className="flex items-center gap-1">
140
+ <button
141
+ type="button"
142
+ disabled={disabled}
143
+ onClick={(e) => {
144
+ e.stopPropagation();
145
+ onChange(items.filter((_: unknown, i: number) => i !== index));
146
+ }}
147
+ className="text-[11px] font-bold text-[var(--kyro-error)] opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-30 hover:bg-[var(--kyro-danger-bg)] rounded px-1.5 py-0.5"
148
+ >
149
+ Remove
150
+ </button>
151
+ {isOpen ? (
152
+ <ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
153
+ ) : (
154
+ <ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
155
+ )}
156
+ </div>
157
+ </div>
158
+ {isOpen && (
159
+ <div className="p-4 bg-[var(--kyro-surface)] space-y-4">
160
+ {(field as Field & { fields?: Field[] }).fields.map((f: Field) =>
161
+ renderField(f, item, (newItem) => {
162
+ const newItems = [...items];
163
+ newItems[index] = newItem;
164
+ onChange(newItems);
165
+ }),
166
+ )}
167
+ </div>
168
+ )}
169
+ </div>
170
+ );
171
+ })}
172
+ <button
173
+ type="button"
174
+ className="w-full py-3 border-2 border-dashed border-[var(--kyro-border)] rounded-lg text-xs font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:border-[var(--kyro-primary)] transition-all disabled:opacity-50"
84
175
  disabled={disabled}
85
176
  onClick={() => onChange([...items, {}])}
86
177
  >