@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,73 @@
1
+ import React from "react";
2
+ import { UploadField } from "./UploadField";
3
+
4
+ interface VideoFieldProps {
5
+ src?: string;
6
+ title?: string;
7
+ onChange: (field: string, value: string) => void;
8
+ onUploadChange?: (value: any) => void;
9
+ compact?: boolean;
10
+ }
11
+
12
+ export const VideoField: React.FC<VideoFieldProps> = ({
13
+ src = "",
14
+ title = "",
15
+ onChange,
16
+ onUploadChange,
17
+ compact = false,
18
+ }) => {
19
+ const isExternalUrl =
20
+ src.includes("youtube.com") ||
21
+ src.includes("vimeo.com") ||
22
+ src.includes("youtu.be");
23
+
24
+ if (compact) {
25
+ return (
26
+ <div className="space-y-2">
27
+ <input
28
+ type="url"
29
+ value={src}
30
+ onChange={(e) => onChange("src", e.target.value)}
31
+ 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"
32
+ placeholder="MP4 URL, YouTube, or Vimeo link..."
33
+ />
34
+ <input
35
+ type="text"
36
+ value={title}
37
+ onChange={(e) => onChange("title", e.target.value)}
38
+ 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"
39
+ placeholder="Video title (optional)..."
40
+ />
41
+ </div>
42
+ );
43
+ }
44
+
45
+ return (
46
+ <div className="space-y-3">
47
+ <UploadField
48
+ field={{ label: "Video Asset", name: "src", maxCount: 1 }}
49
+ value={src}
50
+ onChange={onUploadChange || ((v) => onChange("src", v))}
51
+ />
52
+ <span className="text-xs text-[var(--kyro-text-muted)]">
53
+ or paste a URL
54
+ </span>
55
+ <input
56
+ type="url"
57
+ value={src}
58
+ onChange={(e) => onChange("src", e.target.value)}
59
+ className="w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent font-mono text-xs"
60
+ placeholder="MP4 URL, YouTube, or Vimeo link..."
61
+ />
62
+ <input
63
+ type="text"
64
+ value={title}
65
+ onChange={(e) => onChange("title", e.target.value)}
66
+ className="w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
67
+ placeholder="Video title (optional)..."
68
+ />
69
+ </div>
70
+ );
71
+ };
72
+
73
+ export default VideoField;
@@ -0,0 +1,247 @@
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
+ import { RichTextBlock } from "../../blocks/RichTextBlock";
16
+
17
+ import { HeroBlock } from "../../blocks/HeroBlock";
18
+ import { ArrayBlock } from "../../blocks/ArrayBlock";
19
+ import { RelationshipBlock } from "../../blocks/RelationshipBlock";
20
+
21
+ import {
22
+ Columns3,
23
+ Heading1,
24
+ AlignLeft,
25
+ Minus,
26
+ Image,
27
+ Video,
28
+ List,
29
+ Code,
30
+ Link,
31
+ File,
32
+ ArrowDown,
33
+ MousePointerClick,
34
+ ChevronDown,
35
+ Star,
36
+ ListOrdered,
37
+ Link2,
38
+ } from "lucide-react";
39
+
40
+ // Block component registry
41
+ export const BLOCK_COMPONENTS: Record<string, React.ComponentType<any>> = {
42
+ columns: ColumnsBlock,
43
+ heading: HeadingBlock,
44
+ paragraph: ParagraphBlock,
45
+ divider: DividerBlock,
46
+ image: ImageBlock,
47
+ video: VideoBlock,
48
+ list: ListBlock,
49
+ code: CodeBlock,
50
+ link: LinkBlock,
51
+ file: FileBlock,
52
+ vstack: VStackBlock,
53
+ button: ButtonBlock,
54
+ accordion: AccordionBlock,
55
+ richtext: RichTextBlock,
56
+
57
+ hero: HeroBlock,
58
+ array: ArrayBlock,
59
+ relationship: RelationshipBlock,
60
+ };
61
+
62
+ // Icon mapping for drawer (actual Lucide components)
63
+ export const blockIcons: Record<string, React.ReactNode> = {
64
+ columns: <Columns3 className="w-4 h-4" />,
65
+ heading: <Heading1 className="w-4 h-4" />,
66
+ paragraph: <AlignLeft className="w-4 h-4" />,
67
+ divider: <Minus className="w-4 h-4" />,
68
+ image: <Image className="w-4 h-4" />,
69
+ video: <Video className="w-4 h-4" />,
70
+ list: <List className="w-4 h-4" />,
71
+ code: <Code className="w-4 h-4" />,
72
+ link: <Link className="w-4 h-4" />,
73
+ file: <File className="w-4 h-4" />,
74
+ vstack: <ArrowDown className="w-4 h-4" />,
75
+ button: <MousePointerClick className="w-4 h-4" />,
76
+ accordion: <ChevronDown className="w-4 h-4" />,
77
+ richtext: <AlignLeft className="w-4 h-4" />,
78
+
79
+ hero: <Star className="w-4 h-4" />,
80
+ array: <ListOrdered className="w-4 h-4" />,
81
+ relationship: <Link2 className="w-4 h-4" />,
82
+ };
83
+
84
+ // Get block component by type
85
+ export function getBlockComponent(type: string) {
86
+ return BLOCK_COMPONENTS[type];
87
+ }
88
+
89
+ // Check if block type is supported
90
+ export function isSupportedBlockType(type: string): boolean {
91
+ return type in BLOCK_COMPONENTS;
92
+ }
93
+
94
+ // Get human-readable label for block type
95
+ export function getBlockLabel(type: string): string {
96
+ const labelMap: Record<string, string> = {
97
+ paragraph: "Paragraph",
98
+ heading: "Heading",
99
+ image: "Image",
100
+ video: "Video",
101
+ link: "Link",
102
+ button: "Button",
103
+ list: "List",
104
+ code: "Code",
105
+ file: "File",
106
+ divider: "Divider",
107
+ accordion: "Accordion",
108
+ array: "Repeater",
109
+ hero: "Hero",
110
+ vstack: "VStack",
111
+ columns: "Columns",
112
+ relationship: "Relationship",
113
+ richtext: "Rich Text",
114
+ };
115
+ return labelMap[type] || type;
116
+ }
117
+
118
+ // Block categories for the drawer
119
+ export const blockCategories = [
120
+ {
121
+ title: "Layout",
122
+ blocks: [
123
+ {
124
+ type: "columns",
125
+ label: "Columns",
126
+ icon: "columns",
127
+ description: "1-6 columns side-by-side",
128
+ },
129
+ {
130
+ type: "vstack",
131
+ label: "VStack",
132
+ icon: "vstack",
133
+ description: "Stack blocks vertically",
134
+ },
135
+ {
136
+ type: "hero",
137
+ label: "Hero",
138
+ icon: "hero",
139
+ description: "Hero with content + video",
140
+ },
141
+ ],
142
+ },
143
+ {
144
+ title: "Text",
145
+ blocks: [
146
+ {
147
+ type: "heading",
148
+ label: "Heading",
149
+ icon: "heading",
150
+ description: "Heading text",
151
+ },
152
+ {
153
+ type: "paragraph",
154
+ label: "Paragraph",
155
+ icon: "paragraph",
156
+ description: "Plain text content",
157
+ },
158
+ {
159
+ type: "richtext",
160
+ label: "Rich Text",
161
+ icon: "richtext",
162
+ description: "Formatted text with links & styles",
163
+ },
164
+ {
165
+ type: "list",
166
+ label: "List",
167
+ icon: "list",
168
+ description: "Ordered/unordered list",
169
+ },
170
+ { type: "link", label: "Link", icon: "link", description: "Hyperlink" },
171
+ ],
172
+ },
173
+ {
174
+ title: "Media",
175
+ blocks: [
176
+ {
177
+ type: "image",
178
+ label: "Image",
179
+ icon: "image",
180
+ description: "Single image",
181
+ },
182
+ {
183
+ type: "video",
184
+ label: "Video",
185
+ icon: "video",
186
+ description: "Embed video",
187
+ },
188
+ {
189
+ type: "file",
190
+ label: "File",
191
+ icon: "file",
192
+ description: "File download link",
193
+ },
194
+ ],
195
+ },
196
+ {
197
+ title: "Interactive",
198
+ blocks: [
199
+ {
200
+ type: "button",
201
+ label: "Button",
202
+ icon: "button",
203
+ description: "CTA button",
204
+ },
205
+ {
206
+ type: "accordion",
207
+ label: "Accordion",
208
+ icon: "accordion",
209
+ description: "Collapsible sections",
210
+ },
211
+ ],
212
+ },
213
+ {
214
+ title: "Data",
215
+ blocks: [
216
+ {
217
+ type: "array",
218
+ label: "Repeater",
219
+ icon: "array",
220
+ description: "Add multiple child blocks",
221
+ },
222
+ {
223
+ type: "code",
224
+ label: "Code",
225
+ icon: "code",
226
+ description: "Code snippet",
227
+ },
228
+ {
229
+ type: "relationship",
230
+ label: "Relationship",
231
+ icon: "relationship",
232
+ description: "Link to other collection",
233
+ },
234
+ ],
235
+ },
236
+ {
237
+ title: "Basic",
238
+ blocks: [
239
+ {
240
+ type: "divider",
241
+ label: "Divider",
242
+ icon: "divider",
243
+ description: "Horizontal separator",
244
+ },
245
+ ],
246
+ },
247
+ ];
@@ -0,0 +1,273 @@
1
+ import { create } from "zustand";
2
+ import type { BlockData } from "@kyro-cms/core/client";
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,24 @@
1
+ export { default as PortableTextField } from "./PortableTextField";
2
+ export { PortableTextRenderer } from "./PortableTextRenderer";
3
+ export { CodeField } from "./CodeField";
4
+ export { JSONField } from "./JSONField";
5
+ export { MarkdownField } from "./MarkdownField";
6
+ export { default as TextField } from "./TextField";
7
+ export { default as NumberField } from "./NumberField";
8
+ export { default as CheckboxField } from "./CheckboxField";
9
+ export { default as DateField } from "./DateField";
10
+ export { default as SelectField } from "./SelectField";
11
+ export { default as RelationshipField } from "./RelationshipField";
12
+ export { BlocksField } from "./BlocksField";
13
+ export { AccordionField } from "./AccordionField";
14
+ export { ButtonField } from "./ButtonField";
15
+ export { UploadField } from "./UploadField";
16
+ export { LinkField } from "./LinkField";
17
+ export { HeadingField } from "./HeadingField";
18
+ export { VideoField } from "./VideoField";
19
+ export { ListField } from "./ListField";
20
+ export { HeroField } from "./HeroField";
21
+ export { ArrayField } from "./ArrayField";
22
+ export { ChildrenField } from "./ChildrenField";
23
+ export { ColumnsField } from "./ColumnsField";
24
+ export { RelationshipBlockField } from "./RelationshipBlockField";
@@ -2,6 +2,7 @@ export { Admin } from "./Admin";
2
2
  export { ListView } from "./ListView";
3
3
  export { DetailView } from "./DetailView";
4
4
  export { CreateView } from "./CreateView";
5
+ export { Dashboard } from "./Dashboard";
5
6
  export { AutoForm } from "./AutoForm";
6
7
  export {
7
8
  ActionBar,
@@ -19,8 +20,6 @@ export {
19
20
  useTheme,
20
21
  type ThemeMode,
21
22
  } from "./ThemeProvider";
22
- export * from "./layout/Header";
23
- export * from "./layout/Sidebar";
24
23
  export * from "./ui/Button";
25
24
  export * from "./ui/Badge";
26
25
  export * from "./ui/Spinner";
@@ -10,7 +10,7 @@ export function Header({ title, onMenuClick, actions }: HeaderProps) {
10
10
  return (
11
11
  <header className="kyro-header">
12
12
  <div className="kyro-header-left">
13
- <button
13
+ <button type="button"
14
14
  className="kyro-header-menu"
15
15
  onClick={onMenuClick}
16
16
  aria-label="Toggle menu"
@@ -24,7 +24,7 @@ export function Header({ title, onMenuClick, actions }: HeaderProps) {
24
24
  <div className="kyro-header-right">
25
25
  {actions}
26
26
  <div className="kyro-header-user">
27
- <button className="kyro-header-user-btn">
27
+ <button type="button" className="kyro-header-user-btn">
28
28
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
29
29
  <circle cx="12" cy="8" r="4" />
30
30
  <path d="M4 20c0-4 4-6 8-6s8 2 8 6" />
@@ -1,4 +1,4 @@
1
- import { type CollectionConfig } from '@kyro-cms/core';
1
+ import { type CollectionConfig } from '@kyro-cms/core/client';
2
2
 
3
3
  interface LayoutProps {
4
4
  children: any;
@@ -14,10 +14,10 @@ export default function Layout({ children, collections = [], currentSlug }: Layo
14
14
  <div id="sidebar-root" className="hidden lg:block">
15
15
  {/* Sidebar rendered here */}
16
16
  </div>
17
-
17
+
18
18
  {/* Main content */}
19
19
  <main className="flex-1 min-w-0">
20
- {children}
20
+ {/* {children} */}
21
21
  </main>
22
22
  </div>
23
23
  </div>
@@ -1,13 +1,18 @@
1
- import type { ReactNode } from 'react';
1
+ import type { ReactNode } from "react";
2
2
 
3
3
  interface BadgeProps {
4
- variant?: 'default' | 'success' | 'warning' | 'danger' | 'info';
4
+ variant?: "default" | "success" | "warning" | "danger" | "info";
5
+ className?: string;
5
6
  children: ReactNode;
6
7
  }
7
8
 
8
- export function Badge({ variant = 'default', children }: BadgeProps) {
9
+ export function Badge({
10
+ variant = "default",
11
+ className = "",
12
+ children,
13
+ }: BadgeProps) {
9
14
  return (
10
- <span className={`kyro-badge kyro-badge-${variant}`}>
15
+ <span className={`kyro-badge kyro-badge-${variant} ${className}`}>
11
16
  {children}
12
17
  </span>
13
18
  );