@kyro-cms/admin 0.1.5 → 0.1.7

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 (164) hide show
  1. package/README.md +149 -51
  2. package/package.json +52 -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 +136 -27
  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 +1417 -661
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +3 -3
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +199 -57
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +786 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +191 -53
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +149 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/UserManagement.tsx +204 -0
  26. package/src/components/VersionHistoryPanel.tsx +3 -3
  27. package/src/components/WebhookManager.tsx +608 -0
  28. package/src/components/blocks/AccordionBlock.tsx +97 -0
  29. package/src/components/blocks/ArrayBlock.tsx +75 -0
  30. package/src/components/blocks/BlockEditModal.MARKER +12 -0
  31. package/src/components/blocks/BlockEditModal.tsx +774 -0
  32. package/src/components/blocks/ButtonBlock.tsx +165 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +66 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +151 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +64 -0
  38. package/src/components/blocks/HeadingBlock.tsx +81 -0
  39. package/src/components/blocks/HeroBlock.tsx +157 -0
  40. package/src/components/blocks/ImageBlock.tsx +83 -0
  41. package/src/components/blocks/LinkBlock.tsx +71 -0
  42. package/src/components/blocks/ListBlock.tsx +39 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +279 -0
  45. package/src/components/blocks/VStackBlock.tsx +75 -0
  46. package/src/components/blocks/VideoBlock.tsx +45 -0
  47. package/src/components/blocks/index.ts +10 -0
  48. package/src/components/fields/BlocksField.tsx +323 -0
  49. package/src/components/fields/CheckboxField.tsx +15 -9
  50. package/src/components/fields/CodeField.tsx +234 -0
  51. package/src/components/fields/DateField.tsx +38 -11
  52. package/src/components/fields/EditorClient.tsx +271 -0
  53. package/src/components/fields/FileField.tsx +390 -0
  54. package/src/components/fields/HybridContentField.tsx +109 -0
  55. package/src/components/fields/ImageField.tsx +429 -0
  56. package/src/components/fields/JSONField.tsx +361 -0
  57. package/src/components/fields/MarkdownField.tsx +282 -0
  58. package/src/components/fields/NumberField.tsx +42 -12
  59. package/src/components/fields/PortableTextField.tsx +143 -0
  60. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  61. package/src/components/fields/RelationshipField.tsx +231 -59
  62. package/src/components/fields/SelectField.tsx +25 -15
  63. package/src/components/fields/TextField.tsx +45 -14
  64. package/src/components/fields/extensions/blockComponents.tsx +237 -0
  65. package/src/components/fields/extensions/blocksStore.ts +273 -0
  66. package/src/components/fields/index.ts +13 -0
  67. package/src/components/index.ts +1 -2
  68. package/src/components/layout/Header.tsx +2 -2
  69. package/src/components/layout/Layout.tsx +2 -2
  70. package/src/components/ui/Badge.tsx +9 -4
  71. package/src/components/ui/BlockDrawer.tsx +79 -0
  72. package/src/components/ui/Button.tsx +1 -1
  73. package/src/components/ui/CommandPalette.tsx +362 -0
  74. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  75. package/src/components/ui/Dropdown.tsx +1 -1
  76. package/src/components/ui/Modal.tsx +37 -12
  77. package/src/components/ui/PromptModal.tsx +94 -0
  78. package/src/components/ui/SlidePanel.tsx +43 -16
  79. package/src/components/ui/Toast.tsx +80 -14
  80. package/src/env.d.ts +16 -0
  81. package/src/env.ts +20 -0
  82. package/src/index.ts +0 -1
  83. package/src/layouts/AdminLayout.astro +164 -170
  84. package/src/layouts/AuthLayout.astro +50 -0
  85. package/src/lib/MediaService.ts +541 -0
  86. package/src/lib/auth/sqlite-adapter.ts +319 -0
  87. package/src/lib/config.ts +22 -6
  88. package/src/lib/dataStore.ts +132 -74
  89. package/src/lib/db/adapter.ts +54 -0
  90. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  91. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  92. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  93. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  94. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  95. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  96. package/src/lib/db/index.ts +449 -0
  97. package/src/lib/db/mongodb-adapter.ts +207 -0
  98. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  99. package/src/lib/db/schema/mysql-auth.ts +113 -0
  100. package/src/lib/db/schema/mysql-content.ts +20 -0
  101. package/src/lib/db/schema/postgres-auth.ts +116 -0
  102. package/src/lib/db/schema/postgres-content.ts +35 -0
  103. package/src/lib/db/schema/postgres-media.ts +52 -0
  104. package/src/lib/db/schema/postgres-settings.ts +11 -0
  105. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  106. package/src/lib/db/schema/sqlite-content.ts +20 -0
  107. package/src/lib/graphql/index.ts +1 -0
  108. package/src/lib/graphql/schema.ts +443 -0
  109. package/src/lib/rate-limit.ts +267 -0
  110. package/src/lib/storage.ts +374 -0
  111. package/src/lib/store.ts +85 -0
  112. package/src/middleware.ts +116 -28
  113. package/src/pages/[collection]/[id].astro +178 -122
  114. package/src/pages/[collection]/index.astro +24 -156
  115. package/src/pages/admin/api-explorer.astro +98 -0
  116. package/src/pages/admin/graphql-explorer.astro +40 -0
  117. package/src/pages/admin/graphql.astro +97 -0
  118. package/src/pages/admin/index.astro +286 -0
  119. package/src/pages/admin/keys.astro +8 -0
  120. package/src/pages/admin/rest-playground.astro +44 -0
  121. package/src/pages/admin/webhooks.astro +8 -0
  122. package/src/pages/api/[collection]/[id]/publish.ts +44 -0
  123. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  124. package/src/pages/api/[collection]/[id]/versions.ts +36 -0
  125. package/src/pages/api/[collection]/[id].ts +102 -159
  126. package/src/pages/api/[collection]/index.ts +151 -230
  127. package/src/pages/api/auth/[id].ts +48 -69
  128. package/src/pages/api/auth/audit-logs.ts +20 -43
  129. package/src/pages/api/auth/login.ts +159 -45
  130. package/src/pages/api/auth/logout.ts +50 -20
  131. package/src/pages/api/auth/refresh.ts +119 -0
  132. package/src/pages/api/auth/register.ts +110 -40
  133. package/src/pages/api/auth/users.ts +22 -97
  134. package/src/pages/api/collections.ts +59 -0
  135. package/src/pages/api/globals/[slug]/test.ts +172 -0
  136. package/src/pages/api/globals/[slug].ts +42 -0
  137. package/src/pages/api/graphql.ts +90 -0
  138. package/src/pages/api/health.ts +417 -40
  139. package/src/pages/api/keys/[id].ts +26 -0
  140. package/src/pages/api/keys/index.ts +75 -0
  141. package/src/pages/api/media/[id].ts +309 -0
  142. package/src/pages/api/media/folders.ts +609 -0
  143. package/src/pages/api/media/index.ts +146 -0
  144. package/src/pages/api/media/resize.ts +267 -0
  145. package/src/pages/api/search.ts +82 -0
  146. package/src/pages/api/slug-availability.ts +70 -0
  147. package/src/pages/api/storage-config.ts +20 -0
  148. package/src/pages/api/storage-status.ts +206 -0
  149. package/src/pages/api/upload.ts +334 -0
  150. package/src/pages/api/webhooks/index.ts +71 -0
  151. package/src/pages/audit/index.astro +2 -104
  152. package/src/pages/login.astro +82 -0
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +102 -0
  156. package/src/pages/roles/index.astro +21 -21
  157. package/src/pages/settings/[slug].astro +162 -0
  158. package/src/pages/settings/index.astro +9 -0
  159. package/src/pages/users/[id].astro +29 -21
  160. package/src/pages/users/index.astro +22 -17
  161. package/src/pages/users/new.astro +18 -17
  162. package/src/styles/main.css +553 -128
  163. package/src/components/layout/Sidebar.tsx +0 -497
  164. package/src/pages/index.astro +0 -225
@@ -1,4 +1,4 @@
1
- import type { SelectField as SelectFieldType } from '@kyro-cms/core';
1
+ import type { SelectField as SelectFieldType } from "@kyro-cms/core";
2
2
 
3
3
  interface SelectFieldComponentProps {
4
4
  field: SelectFieldType;
@@ -8,20 +8,32 @@ interface SelectFieldComponentProps {
8
8
  disabled?: boolean;
9
9
  }
10
10
 
11
- export default function SelectField({ field, value, onChange, error, disabled }: SelectFieldComponentProps) {
11
+ export default function SelectField({
12
+ field,
13
+ value,
14
+ onChange,
15
+ error,
16
+ disabled,
17
+ }: SelectFieldComponentProps) {
12
18
  return (
13
19
  <div className="space-y-1">
14
20
  {field.label && (
15
- <label className="block text-sm font-medium text-gray-700">
21
+ <label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
16
22
  {field.label}
17
23
  {field.required && <span className="text-red-500 ml-1">*</span>}
18
24
  </label>
19
25
  )}
20
26
  <select
21
- value={field.hasMany ? (Array.isArray(value) ? value.join(',') : '') : (value || '')}
27
+ value={
28
+ field.hasMany
29
+ ? Array.isArray(value)
30
+ ? value.join(",")
31
+ : ""
32
+ : value || ""
33
+ }
22
34
  onChange={(e) => {
23
35
  if (field.hasMany) {
24
- const selected = e.target.value ? e.target.value.split(',') : [];
36
+ const selected = e.target.value ? e.target.value.split(",") : [];
25
37
  onChange?.(selected);
26
38
  } else {
27
39
  onChange?.(e.target.value);
@@ -32,13 +44,11 @@ export default function SelectField({ field, value, onChange, error, disabled }:
32
44
  required={field.required}
33
45
  className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
34
46
  error
35
- ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
36
- : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
37
- } ${disabled || field.admin?.readOnly ? 'bg-gray-50 text-gray-500' : 'bg-white'}`}
47
+ ? "border-red-500 focus:border-red-500 focus:ring-red-500"
48
+ : "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
49
+ } ${disabled || field.admin?.readOnly ? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-muted)]" : "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"}`}
38
50
  >
39
- {!field.required && (
40
- <option value="">Select...</option>
41
- )}
51
+ {!field.required && <option value="">Select...</option>}
42
52
  {field.options.map((option) => (
43
53
  <option key={option.value} value={option.value}>
44
54
  {option.label}
@@ -46,11 +56,11 @@ export default function SelectField({ field, value, onChange, error, disabled }:
46
56
  ))}
47
57
  </select>
48
58
  {field.admin?.description && !error && (
49
- <p className="text-xs text-gray-500">{field.admin.description}</p>
50
- )}
51
- {error && (
52
- <p className="text-xs text-red-600">{error}</p>
59
+ <p className="text-xs text-[var(--kyro-text-secondary)]">
60
+ {field.admin.description}
61
+ </p>
53
62
  )}
63
+ {error && <p className="text-xs text-red-500">{error}</p>}
54
64
  </div>
55
65
  );
56
66
  }
@@ -1,4 +1,5 @@
1
- import type { TextField as TextFieldType } from '@kyro-cms/core';
1
+ import { useEffect, useState } from "react";
2
+ import type { TextField as TextFieldType } from "@kyro-cms/core";
2
3
 
3
4
  interface TextFieldComponentProps {
4
5
  field: TextFieldType;
@@ -8,16 +9,40 @@ interface TextFieldComponentProps {
8
9
  disabled?: boolean;
9
10
  }
10
11
 
11
- export default function TextField({ field, value = '', onChange, error, disabled }: TextFieldComponentProps) {
12
- const inputType = field.variant === 'email' ? 'email'
13
- : field.variant === 'password' ? 'password'
14
- : field.variant === 'url' ? 'url'
15
- : 'text';
12
+ export default function TextField({
13
+ field,
14
+ value = "",
15
+ onChange,
16
+ error,
17
+ disabled,
18
+ }: TextFieldComponentProps) {
19
+ const [isDark, setIsDark] = useState(false);
20
+
21
+ useEffect(() => {
22
+ setIsDark(document.documentElement.classList.contains("dark"));
23
+ const observer = new MutationObserver(() => {
24
+ setIsDark(document.documentElement.classList.contains("dark"));
25
+ });
26
+ observer.observe(document.documentElement, {
27
+ attributes: true,
28
+ attributeFilter: ["class"],
29
+ });
30
+ return () => observer.disconnect();
31
+ }, []);
32
+
33
+ const inputType =
34
+ field.variant === "email"
35
+ ? "email"
36
+ : field.variant === "password"
37
+ ? "password"
38
+ : field.variant === "url"
39
+ ? "url"
40
+ : "text";
16
41
 
17
42
  return (
18
43
  <div className="space-y-1">
19
44
  {field.label && (
20
- <label className="block text-sm font-medium text-gray-700">
45
+ <label className="block text-sm font-medium">
21
46
  {field.label}
22
47
  {field.required && <span className="text-red-500 ml-1">*</span>}
23
48
  </label>
@@ -34,16 +59,22 @@ export default function TextField({ field, value = '', onChange, error, disabled
34
59
  required={field.required}
35
60
  className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
36
61
  error
37
- ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
38
- : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
39
- } ${disabled || field.admin?.readOnly ? 'bg-gray-50 text-gray-500' : 'bg-white'}`}
62
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500"
63
+ : "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
64
+ } ${
65
+ disabled || field.admin?.readOnly
66
+ ? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] opacity-50"
67
+ : isDark
68
+ ? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
69
+ : "bg-white text-gray-900"
70
+ }`}
40
71
  />
41
72
  {field.admin?.description && !error && (
42
- <p className="text-xs text-gray-500">{field.admin.description}</p>
43
- )}
44
- {error && (
45
- <p className="text-xs text-red-600">{error}</p>
73
+ <p className="text-xs text-[var(--kyro-text-secondary)]">
74
+ {field.admin.description}
75
+ </p>
46
76
  )}
77
+ {error && <p className="text-xs text-red-600">{error}</p>}
47
78
  </div>
48
79
  );
49
80
  }
@@ -0,0 +1,237 @@
1
+ import React from "react";
2
+ import { ColumnsBlock } from "../../blocks/ColumnsBlock";
3
+ import { HeadingBlock } from "../../blocks/HeadingBlock";
4
+ import { ParagraphBlock } from "../../blocks/ParagraphBlock";
5
+ import { DividerBlock } from "../../blocks/DividerBlock";
6
+ import { ImageBlock } from "../../blocks/ImageBlock";
7
+ import { VideoBlock } from "../../blocks/VideoBlock";
8
+ import { ListBlock } from "../../blocks/ListBlock";
9
+ import { CodeBlock } from "../../blocks/CodeBlock";
10
+ import { LinkBlock } from "../../blocks/LinkBlock";
11
+ import { FileBlock } from "../../blocks/FileBlock";
12
+ import { VStackBlock } from "../../blocks/VStackBlock";
13
+ import { ButtonBlock } from "../../blocks/ButtonBlock";
14
+ import { AccordionBlock } from "../../blocks/AccordionBlock";
15
+
16
+ import { HeroBlock } from "../../blocks/HeroBlock";
17
+ import { ArrayBlock } from "../../blocks/ArrayBlock";
18
+ import { RelationshipBlock } from "../../blocks/RelationshipBlock";
19
+
20
+ import {
21
+ Columns3,
22
+ Heading1,
23
+ AlignLeft,
24
+ Minus,
25
+ Image,
26
+ Video,
27
+ List,
28
+ Code,
29
+ Link,
30
+ File,
31
+ ArrowDown,
32
+ MousePointerClick,
33
+ ChevronDown,
34
+ Star,
35
+ ListOrdered,
36
+ Link2,
37
+ } from "lucide-react";
38
+
39
+ // Block component registry
40
+ export const BLOCK_COMPONENTS: Record<string, React.ComponentType<any>> = {
41
+ columns: ColumnsBlock,
42
+ heading: HeadingBlock,
43
+ paragraph: ParagraphBlock,
44
+ divider: DividerBlock,
45
+ image: ImageBlock,
46
+ video: VideoBlock,
47
+ list: ListBlock,
48
+ code: CodeBlock,
49
+ link: LinkBlock,
50
+ file: FileBlock,
51
+ vstack: VStackBlock,
52
+ button: ButtonBlock,
53
+ accordion: AccordionBlock,
54
+
55
+ hero: HeroBlock,
56
+ array: ArrayBlock,
57
+ relationship: RelationshipBlock,
58
+ };
59
+
60
+ // Icon mapping for drawer (actual Lucide components)
61
+ export const blockIcons: Record<string, React.ReactNode> = {
62
+ columns: <Columns3 className="w-4 h-4" />,
63
+ heading: <Heading1 className="w-4 h-4" />,
64
+ paragraph: <AlignLeft className="w-4 h-4" />,
65
+ divider: <Minus className="w-4 h-4" />,
66
+ image: <Image className="w-4 h-4" />,
67
+ video: <Video className="w-4 h-4" />,
68
+ list: <List className="w-4 h-4" />,
69
+ code: <Code className="w-4 h-4" />,
70
+ link: <Link className="w-4 h-4" />,
71
+ file: <File className="w-4 h-4" />,
72
+ vstack: <ArrowDown className="w-4 h-4" />,
73
+ button: <MousePointerClick className="w-4 h-4" />,
74
+ accordion: <ChevronDown className="w-4 h-4" />,
75
+
76
+ hero: <Star className="w-4 h-4" />,
77
+ array: <ListOrdered className="w-4 h-4" />,
78
+ relationship: <Link2 className="w-4 h-4" />,
79
+ };
80
+
81
+ // Get block component by type
82
+ export function getBlockComponent(type: string) {
83
+ return BLOCK_COMPONENTS[type];
84
+ }
85
+
86
+ // Check if block type is supported
87
+ export function isSupportedBlockType(type: string): boolean {
88
+ return type in BLOCK_COMPONENTS;
89
+ }
90
+
91
+ // Get human-readable label for block type
92
+ export function getBlockLabel(type: string): string {
93
+ const labelMap: Record<string, string> = {
94
+ paragraph: "Paragraph",
95
+ heading: "Heading",
96
+ image: "Image",
97
+ video: "Video",
98
+ link: "Link",
99
+ button: "Button",
100
+ list: "List",
101
+ code: "Code",
102
+ file: "File",
103
+ divider: "Divider",
104
+ accordion: "Accordion",
105
+ array: "Repeater",
106
+ hero: "Hero",
107
+ vstack: "VStack",
108
+ columns: "Columns",
109
+ relationship: "Relationship",
110
+ };
111
+ return labelMap[type] || type;
112
+ }
113
+
114
+ // Block categories for the drawer
115
+ export const blockCategories = [
116
+ {
117
+ title: "Layout",
118
+ blocks: [
119
+ {
120
+ type: "columns",
121
+ label: "Columns",
122
+ icon: "columns",
123
+ description: "1-6 columns side-by-side",
124
+ },
125
+ {
126
+ type: "vstack",
127
+ label: "VStack",
128
+ icon: "vstack",
129
+ description: "Stack blocks vertically",
130
+ },
131
+ {
132
+ type: "hero",
133
+ label: "Hero",
134
+ icon: "hero",
135
+ description: "Hero with content + video",
136
+ },
137
+ ],
138
+ },
139
+ {
140
+ title: "Text",
141
+ blocks: [
142
+ {
143
+ type: "heading",
144
+ label: "Heading",
145
+ icon: "heading",
146
+ description: "H1-H3 heading",
147
+ },
148
+ {
149
+ type: "paragraph",
150
+ label: "Paragraph",
151
+ icon: "paragraph",
152
+ description: "Plain text content",
153
+ },
154
+ {
155
+ type: "list",
156
+ label: "List",
157
+ icon: "list",
158
+ description: "Ordered/unordered list",
159
+ },
160
+ { type: "link", label: "Link", icon: "link", description: "Hyperlink" },
161
+ ],
162
+ },
163
+ {
164
+ title: "Media",
165
+ blocks: [
166
+ {
167
+ type: "image",
168
+ label: "Image",
169
+ icon: "image",
170
+ description: "Single image",
171
+ },
172
+ {
173
+ type: "video",
174
+ label: "Video",
175
+ icon: "video",
176
+ description: "Embed video",
177
+ },
178
+ {
179
+ type: "file",
180
+ label: "File",
181
+ icon: "file",
182
+ description: "File download link",
183
+ },
184
+ ],
185
+ },
186
+ {
187
+ title: "Interactive",
188
+ blocks: [
189
+ {
190
+ type: "button",
191
+ label: "Button",
192
+ icon: "button",
193
+ description: "CTA button",
194
+ },
195
+ {
196
+ type: "accordion",
197
+ label: "Accordion",
198
+ icon: "accordion",
199
+ description: "Collapsible sections",
200
+ },
201
+ ],
202
+ },
203
+ {
204
+ title: "Data",
205
+ blocks: [
206
+ {
207
+ type: "array",
208
+ label: "Repeater",
209
+ icon: "array",
210
+ description: "Add multiple child blocks",
211
+ },
212
+ {
213
+ type: "code",
214
+ label: "Code",
215
+ icon: "code",
216
+ description: "Code snippet",
217
+ },
218
+ {
219
+ type: "relationship",
220
+ label: "Relationship",
221
+ icon: "relationship",
222
+ description: "Link to other collection",
223
+ },
224
+ ],
225
+ },
226
+ {
227
+ title: "Basic",
228
+ blocks: [
229
+ {
230
+ type: "divider",
231
+ label: "Divider",
232
+ icon: "divider",
233
+ description: "Horizontal separator",
234
+ },
235
+ ],
236
+ },
237
+ ];
@@ -0,0 +1,273 @@
1
+ import { create } from "zustand";
2
+ import type { BlockData } from "@kyro-cms/core";
3
+
4
+ export interface BlocksStore {
5
+ blocks: BlockData[];
6
+ setBlocks: (blocks: BlockData[]) => void;
7
+ addBlock: (type: string, index?: number) => void;
8
+ updateBlock: (id: string, data: Partial<BlockData>) => void;
9
+ removeBlock: (id: string) => void;
10
+ moveBlock: (id: string, direction: "up" | "down") => void;
11
+ onBlocksChange: (() => void) | null;
12
+ setOnBlocksChange: (cb: () => void) => void;
13
+ }
14
+
15
+ // Helper to create new block (since we can't import createBlock from core in zustand)
16
+ export function createNewBlock(type: string): BlockData {
17
+ const defaultData = getDefaultData(type);
18
+ // Extract options and children from defaultData if present
19
+ const { options, children, ...data } = defaultData;
20
+ return {
21
+ id: Math.random().toString(36).substr(2, 9),
22
+ type,
23
+ data,
24
+ options,
25
+ children,
26
+ order: Date.now(),
27
+ };
28
+ }
29
+
30
+ function getDefaultData(type: string): Record<string, any> {
31
+ const defaults: Record<string, any> = {
32
+ heading: { level: 1, text: "" },
33
+ paragraph: { text: "" },
34
+ divider: {},
35
+ callout: { text: "", variant: "info" },
36
+ image: { src: "", alt: "", caption: "" },
37
+ video: { src: "", title: "" },
38
+ list: { type: "unordered", items: "" },
39
+ code: { language: "plaintext", code: "" },
40
+ link: { url: "", text: "" },
41
+ table: { rows: 3, columns: 3, content: "" },
42
+ quote: { text: "", author: "" },
43
+ file: { filename: "", url: "" },
44
+ kyroColumns: { columns: 2, direction: "horizontal" },
45
+ vstack: { direction: "vertical", gap: "md" },
46
+ container: {
47
+ options: {
48
+ backgroundColor: "transparent",
49
+ padding: "md",
50
+ width: "full",
51
+ margin: "none",
52
+ minHeight: "none",
53
+ borderRadius: "none",
54
+ },
55
+ children: [],
56
+ },
57
+ button: { text: "Button", url: "", variant: "primary", size: "md" },
58
+ accordion: { title: "Accordion Item", content: "" },
59
+ gallery: { images: [] },
60
+ tabs: { tabs: [{ label: "Tab 1", content: "" }] },
61
+ };
62
+ return defaults[type] || {};
63
+ }
64
+
65
+ export const useBlocksStore = create<BlocksStore>((set, get) => ({
66
+ blocks: [],
67
+ setBlocks: (blocks) => set({ blocks }),
68
+ onBlocksChange: null,
69
+ setOnBlocksChange: (cb) => set({ onBlocksChange: cb }),
70
+ addBlock: (type, index) => {
71
+ const newBlock = createNewBlock(type);
72
+ const { blocks } = get();
73
+ const newBlocks = [...blocks];
74
+ if (index !== undefined) {
75
+ newBlocks.splice(index, 0, newBlock);
76
+ } else {
77
+ newBlocks.push(newBlock);
78
+ }
79
+ set({ blocks: newBlocks });
80
+ const { onBlocksChange } = get();
81
+ if (onBlocksChange) onBlocksChange();
82
+ },
83
+ updateBlock: (id, data) => {
84
+ const { blocks } = get();
85
+
86
+ const updateRecursive = (blocksList: BlockData[]): BlockData[] => {
87
+ let changed = false;
88
+ const newList = blocksList.map(b => {
89
+ if (b.id === id) {
90
+ changed = true;
91
+ return { ...b, ...data };
92
+ }
93
+
94
+ let newB = b;
95
+ if (b.children && b.children.length > 0) {
96
+ const newChildren = updateRecursive(b.children);
97
+ if (newChildren !== b.children) {
98
+ newB = { ...newB, children: newChildren };
99
+ changed = true;
100
+ }
101
+ }
102
+
103
+ if (b.data?.columnData) {
104
+ const newColumnData = b.data.columnData.map((col: any) => {
105
+ if (col.children && col.children.length > 0) {
106
+ const newChildren = updateRecursive(col.children);
107
+ if (newChildren !== col.children) {
108
+ return { ...col, children: newChildren };
109
+ }
110
+ }
111
+ return col;
112
+ });
113
+
114
+ if (newColumnData.some((col: any, i: number) => col !== b.data.columnData[i])) {
115
+ newB = { ...newB, data: { ...newB.data, columnData: newColumnData } };
116
+ changed = true;
117
+ }
118
+ }
119
+
120
+ return newB;
121
+ });
122
+ return changed ? newList : blocksList;
123
+ };
124
+
125
+ const newBlocks = updateRecursive(blocks);
126
+ if (newBlocks !== blocks) {
127
+ set({ blocks: newBlocks });
128
+ const { onBlocksChange } = get();
129
+ if (onBlocksChange) onBlocksChange();
130
+ }
131
+ },
132
+ removeBlock: (id) => {
133
+ const { blocks } = get();
134
+
135
+ const removeRecursive = (blocksList: BlockData[]): BlockData[] => {
136
+ const filtered = blocksList.filter(b => b.id !== id);
137
+ if (filtered.length !== blocksList.length) {
138
+ return filtered; // found and removed at this level
139
+ }
140
+
141
+ let changed = false;
142
+ const newList = blocksList.map(b => {
143
+ let newB = b;
144
+ if (b.children && b.children.length > 0) {
145
+ const newChildren = removeRecursive(b.children);
146
+ if (newChildren !== b.children) {
147
+ newB = { ...newB, children: newChildren };
148
+ changed = true;
149
+ }
150
+ }
151
+
152
+ if (b.data?.columnData) {
153
+ const newColumnData = b.data.columnData.map((col: any) => {
154
+ if (col.children && col.children.length > 0) {
155
+ const newChildren = removeRecursive(col.children);
156
+ if (newChildren !== col.children) {
157
+ return { ...col, children: newChildren };
158
+ }
159
+ }
160
+ return col;
161
+ });
162
+
163
+ if (newColumnData.some((col: any, i: number) => col !== b.data.columnData[i])) {
164
+ newB = { ...newB, data: { ...newB.data, columnData: newColumnData } };
165
+ changed = true;
166
+ }
167
+ }
168
+
169
+ return newB;
170
+ });
171
+ return changed ? newList : blocksList;
172
+ };
173
+
174
+ const newBlocks = removeRecursive(blocks);
175
+ if (newBlocks !== blocks) {
176
+ set({ blocks: newBlocks });
177
+ const { onBlocksChange } = get();
178
+ if (onBlocksChange) onBlocksChange();
179
+ }
180
+ },
181
+ moveBlock: (id, direction) => {
182
+ const { blocks } = get();
183
+
184
+ const moveRecursive = (blocksList: BlockData[]): BlockData[] => {
185
+ const index = blocksList.findIndex(b => b.id === id);
186
+ if (index !== -1) {
187
+ const targetIndex = direction === "up" ? index - 1 : index + 1;
188
+ if (targetIndex >= 0 && targetIndex < blocksList.length) {
189
+ const newBlocksList = [...blocksList];
190
+ [newBlocksList[index], newBlocksList[targetIndex]] = [
191
+ newBlocksList[targetIndex],
192
+ newBlocksList[index],
193
+ ];
194
+ return newBlocksList;
195
+ }
196
+ return blocksList;
197
+ }
198
+
199
+ let changed = false;
200
+ const newList = blocksList.map(b => {
201
+ let newB = b;
202
+ if (b.children && b.children.length > 0) {
203
+ const newChildren = moveRecursive(b.children);
204
+ if (newChildren !== b.children) {
205
+ newB = { ...newB, children: newChildren };
206
+ changed = true;
207
+ }
208
+ }
209
+
210
+ if (b.data?.columnData) {
211
+ const newColumnData = b.data.columnData.map((col: any) => {
212
+ if (col.children && col.children.length > 0) {
213
+ const newChildren = moveRecursive(col.children);
214
+ if (newChildren !== col.children) {
215
+ return { ...col, children: newChildren };
216
+ }
217
+ }
218
+ return col;
219
+ });
220
+
221
+ if (newColumnData.some((col: any, i: number) => col !== b.data.columnData[i])) {
222
+ newB = { ...newB, data: { ...newB.data, columnData: newColumnData } };
223
+ changed = true;
224
+ }
225
+ }
226
+
227
+ return newB;
228
+ });
229
+ return changed ? newList : blocksList;
230
+ };
231
+
232
+ const newBlocks = moveRecursive(blocks);
233
+ if (newBlocks !== blocks) {
234
+ set({ blocks: newBlocks });
235
+ const { onBlocksChange } = get();
236
+ if (onBlocksChange) onBlocksChange();
237
+ }
238
+ },
239
+ }));
240
+
241
+ // Selector for individual block - only re-renders when that specific block changes
242
+ export const useBlockById = (id: string) =>
243
+ useBlocksStore((state) => {
244
+ const findRecursive = (blocksList: BlockData[]): BlockData | undefined => {
245
+ for (const b of blocksList) {
246
+ if (b.id === id) return b;
247
+ if (b.children && b.children.length > 0) {
248
+ const found = findRecursive(b.children);
249
+ if (found) return found;
250
+ }
251
+ if (b.data?.columnData) {
252
+ for (const col of b.data.columnData) {
253
+ if (col.children && col.children.length > 0) {
254
+ const found = findRecursive(col.children);
255
+ if (found) return found;
256
+ }
257
+ }
258
+ }
259
+ }
260
+ return undefined;
261
+ };
262
+ return findRecursive(state.blocks);
263
+ });
264
+
265
+ // Selector for block count - for quick checks
266
+ export const useBlockCount = () =>
267
+ useBlocksStore((state) => state.blocks.length);
268
+
269
+ // Get action functions without subscription - uses getState() for direct access
270
+ // This prevents re-renders when other blocks change
271
+ export const useBlockActions = () => {
272
+ return useBlocksStore.getState();
273
+ };
@@ -0,0 +1,13 @@
1
+ export { default as PortableTextField } from "./PortableTextField";
2
+ export { PortableTextRenderer } from "./PortableTextRenderer";
3
+ export { HybridContentField } from "./HybridContentField";
4
+ export { CodeField } from "./CodeField";
5
+ export { JSONField } from "./JSONField";
6
+ export { MarkdownField } from "./MarkdownField";
7
+ export { default as TextField } from "./TextField";
8
+ export { default as NumberField } from "./NumberField";
9
+ export { default as CheckboxField } from "./CheckboxField";
10
+ export { default as DateField } from "./DateField";
11
+ export { default as SelectField } from "./SelectField";
12
+ export { default as RelationshipField } from "./RelationshipField";
13
+ export { BlocksField } from "./BlocksField";