@kyro-cms/admin 0.3.2 → 0.3.5

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 (242) hide show
  1. package/dist/EditorClient-XEUOVAAC.js +466 -0
  2. package/dist/EditorClient-XEUOVAAC.js.map +1 -0
  3. package/dist/EditorClient-YLCGVDXY.cjs +468 -0
  4. package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
  5. package/dist/chunk-7KPIUCGT.js +384 -0
  6. package/dist/chunk-7KPIUCGT.js.map +1 -0
  7. package/dist/chunk-GOACG6R7.cjs +473 -0
  8. package/dist/chunk-GOACG6R7.cjs.map +1 -0
  9. package/dist/index.cjs +14861 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.css +1661 -0
  12. package/dist/index.css.map +1 -0
  13. package/dist/index.d.ts +563 -0
  14. package/dist/index.js +14784 -0
  15. package/dist/index.js.map +1 -0
  16. package/package.json +19 -19
  17. package/src/components/ActionBar.tsx +7 -43
  18. package/src/components/Admin.tsx +138 -277
  19. package/src/components/ApiKeysManager.tsx +428 -419
  20. package/src/components/AuditLogsPage.tsx +35 -39
  21. package/src/components/AuthBridge.tsx +51 -0
  22. package/src/components/AutoForm.tsx +495 -1230
  23. package/src/components/BrandingHub.tsx +18 -19
  24. package/src/components/BulkActionsBar.tsx +1 -1
  25. package/src/components/CreateView.tsx +22 -36
  26. package/src/components/Dashboard.tsx +60 -84
  27. package/src/components/DetailView.tsx +113 -91
  28. package/src/components/DeveloperCenter.tsx +200 -198
  29. package/src/components/FieldRenderer.tsx +206 -0
  30. package/src/components/GraphQLPlayground.tsx +340 -480
  31. package/src/components/ListView.tsx +828 -254
  32. package/src/components/LoginPage.tsx +3 -4
  33. package/src/components/MarketplaceManager.tsx +254 -0
  34. package/src/components/MediaGallery.tsx +856 -1192
  35. package/src/components/PluginsManager.tsx +277 -0
  36. package/src/components/RestPlayground.tsx +398 -560
  37. package/src/components/SessionsManager.tsx +211 -0
  38. package/src/components/Sidebar.astro +179 -151
  39. package/src/components/ThemeProvider.tsx +7 -161
  40. package/src/components/UserManagement.tsx +162 -146
  41. package/src/components/UserMenu.tsx +110 -0
  42. package/src/components/WebhookManager.tsx +305 -367
  43. package/src/components/blocks/AccordionBlock.tsx +4 -4
  44. package/src/components/blocks/ArrayBlock.tsx +3 -3
  45. package/src/components/blocks/BlockEditModal.tsx +8 -8
  46. package/src/components/blocks/BlockWrapper.tsx +61 -0
  47. package/src/components/blocks/ButtonBlock.tsx +4 -4
  48. package/src/components/blocks/ChildBlocksTree.tsx +23 -25
  49. package/src/components/blocks/CodeBlock.tsx +15 -15
  50. package/src/components/blocks/ColumnsBlock.tsx +6 -44
  51. package/src/components/blocks/DividerBlock.tsx +3 -3
  52. package/src/components/blocks/FileBlock.tsx +4 -4
  53. package/src/components/blocks/HeadingBlock.tsx +6 -38
  54. package/src/components/blocks/HeroBlock.tsx +4 -4
  55. package/src/components/blocks/ImageBlock.tsx +4 -4
  56. package/src/components/blocks/LinkBlock.tsx +4 -4
  57. package/src/components/blocks/ListBlock.tsx +3 -3
  58. package/src/components/blocks/ParagraphBlock.tsx +12 -42
  59. package/src/components/blocks/RelationshipBlock.tsx +4 -4
  60. package/src/components/blocks/RichTextBlock.tsx +4 -4
  61. package/src/components/blocks/VStackBlock.tsx +5 -37
  62. package/src/components/blocks/VideoBlock.tsx +4 -4
  63. package/src/components/blocks/types.ts +11 -0
  64. package/src/components/fields/AccordionField.tsx +1 -1
  65. package/src/components/fields/ArrayField.tsx +2 -2
  66. package/src/components/fields/ArrayLayout.tsx +93 -0
  67. package/src/components/fields/BlocksField.tsx +122 -111
  68. package/src/components/fields/ButtonField.tsx +1 -1
  69. package/src/components/fields/CheckboxField.tsx +14 -15
  70. package/src/components/fields/ChildrenField.tsx +2 -2
  71. package/src/components/fields/CodeField.tsx +3 -3
  72. package/src/components/fields/ColumnsField.tsx +2 -2
  73. package/src/components/fields/DateField.tsx +13 -26
  74. package/src/components/fields/EditorClient.tsx +26 -28
  75. package/src/components/fields/FieldLayout.tsx +52 -0
  76. package/src/components/fields/GroupLayout.tsx +35 -0
  77. package/src/components/fields/JSONField.tsx +7 -7
  78. package/src/components/fields/LinkField.tsx +1 -1
  79. package/src/components/fields/MarkdownField.tsx +1 -1
  80. package/src/components/fields/NumberField.tsx +13 -26
  81. package/src/components/fields/PortableTextField.tsx +4 -4
  82. package/src/components/fields/PortableTextRenderer.tsx +1 -1
  83. package/src/components/fields/RelationshipBlockField.tsx +31 -23
  84. package/src/components/fields/RelationshipField.tsx +14 -14
  85. package/src/components/fields/SelectField.tsx +17 -26
  86. package/src/components/fields/TabsLayout.tsx +69 -0
  87. package/src/components/fields/TextField.tsx +85 -38
  88. package/src/components/fields/UploadField.tsx +71 -41
  89. package/src/components/fields/VideoField.tsx +1 -1
  90. package/src/components/fields/extensions/blockComponents.tsx +2 -2
  91. package/src/components/fields/extensions/blocksStore.ts +207 -193
  92. package/src/components/fields/types.ts +22 -0
  93. package/src/components/layout/Layout.tsx +1 -1
  94. package/src/components/ui/ActionMenu.tsx +63 -0
  95. package/src/components/ui/Badge.tsx +59 -5
  96. package/src/components/ui/BlockDrawer.tsx +4 -5
  97. package/src/components/ui/CommandPalette.tsx +58 -36
  98. package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
  99. package/src/components/ui/Dropdown.tsx +18 -16
  100. package/src/components/ui/EmptyState.tsx +25 -0
  101. package/src/components/ui/GlobalModal.tsx +49 -0
  102. package/src/components/ui/IconButton.tsx +44 -0
  103. package/src/components/ui/Modal.tsx +19 -20
  104. package/src/components/ui/PageHeader.tsx +158 -0
  105. package/src/components/ui/Pagination.tsx +61 -0
  106. package/src/components/ui/PromptModal.tsx +1 -1
  107. package/src/components/ui/SearchInput.tsx +57 -0
  108. package/src/components/ui/SeoPreview.tsx +31 -0
  109. package/src/components/ui/SessionModal.tsx +0 -0
  110. package/src/components/ui/SlidePanel.tsx +2 -0
  111. package/src/components/ui/Toast.tsx +65 -122
  112. package/src/components/ui/Toaster.tsx +18 -0
  113. package/src/components/ui/icons.tsx +112 -0
  114. package/src/components/users/UserDetail.tsx +290 -0
  115. package/src/components/users/UserForm.tsx +242 -0
  116. package/src/components/users/UsersList.tsx +338 -0
  117. package/src/env.d.ts +13 -13
  118. package/src/fields/index.ts +2 -1
  119. package/src/global.d.ts +7 -0
  120. package/src/hooks/data.ts +2 -9
  121. package/src/hooks/useAsyncData.ts +36 -0
  122. package/src/hooks/useAutoFormState.ts +527 -0
  123. package/src/hooks/useSelection.ts +49 -0
  124. package/src/hooks/useSession.ts +0 -0
  125. package/src/index.ts +11 -1
  126. package/src/integration.ts +86 -11
  127. package/src/kyro-cms.d.ts +209 -0
  128. package/src/layouts/AdminLayout.astro +128 -11
  129. package/src/layouts/AuthLayout.astro +21 -5
  130. package/src/lib/api.ts +175 -55
  131. package/src/lib/autoform-store.ts +435 -0
  132. package/src/lib/config.ts +82 -34
  133. package/src/lib/createRegistry.ts +29 -0
  134. package/src/lib/default-kyro-config.ts +4 -0
  135. package/src/lib/globals.ts +50 -0
  136. package/src/lib/media-utils.ts +18 -0
  137. package/src/lib/object-utils.ts +77 -0
  138. package/src/lib/paths.ts +61 -0
  139. package/src/lib/stores/index.ts +370 -0
  140. package/src/lib/types.ts +43 -0
  141. package/src/lib/useResourceManager.ts +105 -0
  142. package/src/pages/403.astro +67 -0
  143. package/src/pages/[collection]/[id].astro +14 -180
  144. package/src/pages/[collection]/index.astro +11 -6
  145. package/src/pages/api-explorer.astro +173 -0
  146. package/src/pages/audit/index.astro +2 -0
  147. package/src/pages/auth/login.astro +122 -0
  148. package/src/pages/auth/register.astro +167 -0
  149. package/src/pages/graphql-explorer.astro +59 -0
  150. package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
  151. package/src/pages/index.astro +577 -0
  152. package/src/pages/index_ALT.astro +3 -0
  153. package/src/pages/keys.astro +11 -0
  154. package/src/pages/marketplace.astro +11 -0
  155. package/src/pages/media.astro +3 -0
  156. package/src/pages/plugins.astro +8 -0
  157. package/src/pages/preview/[collection]/[id].astro +188 -123
  158. package/src/pages/rest-playground.astro +62 -0
  159. package/src/pages/roles/index.astro +183 -76
  160. package/src/pages/sessions.astro +8 -0
  161. package/src/pages/settings/[slug].astro +92 -114
  162. package/src/pages/settings/index.astro +5 -3
  163. package/src/pages/users/[id].astro +25 -154
  164. package/src/pages/users/index.astro +19 -130
  165. package/src/pages/users/new.astro +9 -86
  166. package/src/pages/webhooks.astro +11 -0
  167. package/src/routes.ts +80 -0
  168. package/src/styles/main.css +119 -79
  169. package/src/theme/tokens.ts +1 -0
  170. package/src/vite-env.d.ts +14 -0
  171. package/src/collections/auth/index.ts +0 -155
  172. package/src/collections/portfolio/index.ts +0 -343
  173. package/src/components/ApiExplorer.tsx +0 -325
  174. package/src/components/EnhancedListView.tsx +0 -889
  175. package/src/components/GraphQLExplorer.tsx +0 -675
  176. package/src/components/Icons.tsx +0 -23
  177. package/src/components/StatusBadge.tsx +0 -76
  178. package/src/lib/MediaService.ts +0 -541
  179. package/src/lib/auth/sqlite-adapter.ts +0 -319
  180. package/src/lib/dataStore.ts +0 -226
  181. package/src/lib/db/adapter.ts +0 -54
  182. package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
  183. package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
  184. package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
  185. package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
  186. package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
  187. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
  188. package/src/lib/db/index.ts +0 -449
  189. package/src/lib/db/mongodb-adapter.ts +0 -207
  190. package/src/lib/db/mongodb-auth-adapter.ts +0 -305
  191. package/src/lib/db/schema/mysql-auth.ts +0 -113
  192. package/src/lib/db/schema/mysql-content.ts +0 -20
  193. package/src/lib/db/schema/postgres-auth.ts +0 -116
  194. package/src/lib/db/schema/postgres-content.ts +0 -35
  195. package/src/lib/db/schema/postgres-media.ts +0 -52
  196. package/src/lib/db/schema/postgres-settings.ts +0 -11
  197. package/src/lib/db/schema/sqlite-auth.ts +0 -112
  198. package/src/lib/db/schema/sqlite-content.ts +0 -20
  199. package/src/lib/db/version-adapter.ts +0 -248
  200. package/src/lib/graphql/index.ts +0 -1
  201. package/src/lib/graphql/schema.ts +0 -443
  202. package/src/lib/rate-limit.ts +0 -267
  203. package/src/lib/storage.ts +0 -374
  204. package/src/lib/store.ts +0 -85
  205. package/src/middleware.ts +0 -177
  206. package/src/pages/admin/api-explorer.astro +0 -98
  207. package/src/pages/admin/graphql-explorer.astro +0 -40
  208. package/src/pages/admin/index.astro +0 -286
  209. package/src/pages/admin/keys.astro +0 -8
  210. package/src/pages/admin/rest-playground.astro +0 -44
  211. package/src/pages/admin/webhooks.astro +0 -8
  212. package/src/pages/api/[collection]/[id]/publish.ts +0 -52
  213. package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
  214. package/src/pages/api/[collection]/[id]/versions.ts +0 -66
  215. package/src/pages/api/[collection]/[id].ts +0 -213
  216. package/src/pages/api/[collection]/index.ts +0 -209
  217. package/src/pages/api/auth/[id].ts +0 -121
  218. package/src/pages/api/auth/audit-logs.ts +0 -57
  219. package/src/pages/api/auth/login.ts +0 -211
  220. package/src/pages/api/auth/logout.ts +0 -66
  221. package/src/pages/api/auth/me.ts +0 -36
  222. package/src/pages/api/auth/refresh.ts +0 -119
  223. package/src/pages/api/auth/register.ts +0 -188
  224. package/src/pages/api/auth/users.ts +0 -97
  225. package/src/pages/api/collections.ts +0 -59
  226. package/src/pages/api/globals/[slug].ts +0 -42
  227. package/src/pages/api/graphql.ts +0 -90
  228. package/src/pages/api/health.ts +0 -426
  229. package/src/pages/api/keys/[id].ts +0 -26
  230. package/src/pages/api/keys/index.ts +0 -75
  231. package/src/pages/api/media/[id].ts +0 -309
  232. package/src/pages/api/media/folders.ts +0 -609
  233. package/src/pages/api/media/index.ts +0 -146
  234. package/src/pages/api/media/resize.ts +0 -267
  235. package/src/pages/api/search.ts +0 -82
  236. package/src/pages/api/slug-availability.ts +0 -70
  237. package/src/pages/api/storage-config.ts +0 -20
  238. package/src/pages/api/storage-status.ts +0 -206
  239. package/src/pages/api/upload.ts +0 -334
  240. package/src/pages/api/webhooks/index.ts +0 -71
  241. package/src/pages/login.astro +0 -82
  242. package/src/pages/register.astro +0 -102
@@ -1,4 +1,6 @@
1
- import { create } from "zustand";
1
+ import { createStore, type StoreApi } from "zustand/vanilla";
2
+ import { useStore } from "zustand";
3
+ import { createContext, useContext } from "react";
2
4
  import type { BlockData } from "@kyro-cms/core/client";
3
5
 
4
6
  export interface BlocksStore {
@@ -12,10 +14,130 @@ export interface BlocksStore {
12
14
  setOnBlocksChange: (cb: () => void) => void;
13
15
  }
14
16
 
15
- // Helper to create new block (since we can't import createBlock from core in zustand)
17
+ export type BlocksStoreApi = StoreApi<BlocksStore>;
18
+
19
+ export const BlocksContext = createContext<BlocksStoreApi | null>(null);
20
+
21
+ export function createBlocksStore(): BlocksStoreApi {
22
+ return createStore<BlocksStore>((set, get) => ({
23
+ blocks: [],
24
+ setBlocks: (blocks) => {
25
+ const ensuredBlocks = ensureIds(blocks || []);
26
+ set({ blocks: ensuredBlocks });
27
+ },
28
+ onBlocksChange: null,
29
+ setOnBlocksChange: (cb) => set({ onBlocksChange: cb }),
30
+ addBlock: (type, index) => {
31
+ const newBlock = createNewBlock(type);
32
+ const { blocks } = get();
33
+ const newBlocks = [...blocks];
34
+ if (index !== undefined) {
35
+ newBlocks.splice(index, 0, newBlock);
36
+ } else {
37
+ newBlocks.push(newBlock);
38
+ }
39
+ set({ blocks: newBlocks });
40
+ const { onBlocksChange } = get();
41
+ if (onBlocksChange) onBlocksChange();
42
+ },
43
+ updateBlock: (id, data) => {
44
+ const { blocks } = get();
45
+
46
+ const newBlocks = traverseBlocks(blocks, (blocksList) => {
47
+ const index = blocksList.findIndex(b => b.id === id);
48
+ if (index !== -1) {
49
+ const newBlocksList = [...blocksList];
50
+ newBlocksList[index] = { ...newBlocksList[index], ...data };
51
+ return { newList: newBlocksList, found: true };
52
+ }
53
+ return { newList: blocksList, found: false };
54
+ });
55
+
56
+ if (newBlocks !== blocks) {
57
+ set({ blocks: newBlocks });
58
+ const { onBlocksChange } = get();
59
+ if (onBlocksChange) onBlocksChange();
60
+ }
61
+ },
62
+ removeBlock: (id) => {
63
+ if (!id) return;
64
+ const { blocks } = get();
65
+
66
+ const newBlocks = traverseBlocks(blocks, (blocksList) => {
67
+ const filtered = blocksList.filter(b => b.id !== id);
68
+ if (filtered.length !== blocksList.length) {
69
+ return { newList: filtered, found: true };
70
+ }
71
+ return { newList: blocksList, found: false };
72
+ });
73
+
74
+ if (newBlocks !== blocks) {
75
+ set({ blocks: newBlocks });
76
+ const { onBlocksChange } = get();
77
+ if (onBlocksChange) onBlocksChange();
78
+ }
79
+ },
80
+ moveBlock: (id, direction) => {
81
+ const { blocks } = get();
82
+
83
+ const newBlocks = traverseBlocks(blocks, (blocksList) => {
84
+ const index = blocksList.findIndex(b => b.id === id);
85
+ if (index !== -1) {
86
+ const targetIndex = direction === "up" ? index - 1 : index + 1;
87
+ if (targetIndex >= 0 && targetIndex < blocksList.length) {
88
+ const newBlocksList = [...blocksList];
89
+ [newBlocksList[index], newBlocksList[targetIndex]] = [
90
+ newBlocksList[targetIndex],
91
+ newBlocksList[index],
92
+ ];
93
+ return { newList: newBlocksList, found: true };
94
+ }
95
+ }
96
+ return { newList: blocksList, found: false };
97
+ });
98
+
99
+ if (newBlocks !== blocks) {
100
+ set({ blocks: newBlocks });
101
+ const { onBlocksChange } = get();
102
+ if (onBlocksChange) onBlocksChange();
103
+ }
104
+ },
105
+ }));
106
+ }
107
+
108
+ /**
109
+ * Recursively ensures all blocks and nested children have unique IDs
110
+ */
111
+ export function ensureIds(blocks: BlockData[]): BlockData[] {
112
+ if (!Array.isArray(blocks)) return [];
113
+
114
+ return blocks.map((block) => {
115
+ const updatedBlock = {
116
+ ...block,
117
+ id: block.id || Math.random().toString(36).substr(2, 9),
118
+ };
119
+
120
+ if (updatedBlock.children && Array.isArray(updatedBlock.children)) {
121
+ updatedBlock.children = ensureIds(updatedBlock.children);
122
+ }
123
+
124
+ if (updatedBlock.data?.columnData && Array.isArray(updatedBlock.data.columnData)) {
125
+ updatedBlock.data = {
126
+ ...updatedBlock.data,
127
+ columnData: updatedBlock.data.columnData.map((col: Record<string, unknown>) => ({
128
+ ...col,
129
+ children: col.children ? ensureIds(col.children) : col.children,
130
+ })),
131
+ };
132
+ }
133
+
134
+ return updatedBlock;
135
+ });
136
+ }
137
+
138
+ // Create new block helper (pure function, no store needed)
16
139
  export function createNewBlock(type: string): BlockData {
17
140
  const defaultData = getDefaultData(type);
18
- // Extract options and children from defaultData if present
19
141
  const { options, children, ...data } = defaultData;
20
142
  return {
21
143
  id: Math.random().toString(36).substr(2, 9),
@@ -27,8 +149,8 @@ export function createNewBlock(type: string): BlockData {
27
149
  };
28
150
  }
29
151
 
30
- function getDefaultData(type: string): Record<string, any> {
31
- const defaults: Record<string, any> = {
152
+ function getDefaultData(type: string): Record<string, unknown> {
153
+ const defaults: Record<string, unknown> = {
32
154
  heading: { level: 1, text: "" },
33
155
  paragraph: { text: "" },
34
156
  divider: {},
@@ -41,7 +163,7 @@ function getDefaultData(type: string): Record<string, any> {
41
163
  table: { rows: 3, columns: 3, content: "" },
42
164
  quote: { text: "", author: "" },
43
165
  file: { filename: "", url: "" },
44
- kyroColumns: { columns: 2, direction: "horizontal" },
166
+ columns: { columns: 2, direction: "horizontal" },
45
167
  vstack: { direction: "vertical", gap: "md" },
46
168
  container: {
47
169
  options: {
@@ -62,185 +184,19 @@ function getDefaultData(type: string): Record<string, any> {
62
184
  return defaults[type] || {};
63
185
  }
64
186
 
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
- }));
187
+ // React hooks that read from context
188
+ export function useBlocksStore(): BlocksStore {
189
+ const store = useContext(BlocksContext);
190
+ if (!store) {
191
+ throw new Error("useBlocksStore must be used within a BlocksContext.Provider");
192
+ }
193
+ return useStore(store);
194
+ }
240
195
 
241
- // Selector for individual block - only re-renders when that specific block changes
242
- export const useBlockById = (id: string) =>
243
- useBlocksStore((state) => {
196
+ export function useBlockById(id: string): BlockData | undefined {
197
+ const store = useContext(BlocksContext);
198
+ if (!store) return undefined;
199
+ return useStore(store, (state) => {
244
200
  const findRecursive = (blocksList: BlockData[]): BlockData | undefined => {
245
201
  for (const b of blocksList) {
246
202
  if (b.id === id) return b;
@@ -250,7 +206,7 @@ export const useBlockById = (id: string) =>
250
206
  }
251
207
  if (b.data?.columnData) {
252
208
  for (const col of b.data.columnData) {
253
- if (col.children && col.children.length > 0) {
209
+ if (col && col.children && col.children.length > 0) {
254
210
  const found = findRecursive(col.children);
255
211
  if (found) return found;
256
212
  }
@@ -261,13 +217,71 @@ export const useBlockById = (id: string) =>
261
217
  };
262
218
  return findRecursive(state.blocks);
263
219
  });
220
+ }
221
+
222
+ export function useBlockCount(): number {
223
+ const store = useContext(BlocksContext);
224
+ if (!store) return 0;
225
+ return useStore(store, (state) => state.blocks.length);
226
+ }
227
+
228
+ export function useBlockActions() {
229
+ const store = useContext(BlocksContext);
230
+ if (!store) {
231
+ throw new Error("useBlockActions must be used within a BlocksContext.Provider");
232
+ }
233
+ return {
234
+ updateBlock: (id: string, data: Partial<BlockData>) => store.getState().updateBlock(id, data),
235
+ removeBlock: (id: string) => store.getState().removeBlock(id),
236
+ moveBlock: (id: string, direction: "up" | "down") => store.getState().moveBlock(id, direction),
237
+ };
238
+ }
264
239
 
265
- // Selector for block count - for quick checks
266
- export const useBlockCount = () =>
267
- useBlocksStore((state) => state.blocks.length);
240
+ /**
241
+ * Generic tree traversal helper for blocks
242
+ */
243
+ export function traverseBlocks(
244
+ blocks: BlockData[],
245
+ action: (blocksList: BlockData[]) => { newList: BlockData[]; found: boolean }
246
+ ): BlockData[] {
247
+ const { newList, found } = action(blocks);
248
+ if (found) return newList;
268
249
 
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
- };
250
+ let overallChanged = false;
251
+ const deepUpdatedList = blocks.map((block) => {
252
+ let updatedBlock = { ...block };
253
+ let blockChanged = false;
254
+
255
+ // Handle children
256
+ if (block.children && block.children.length > 0) {
257
+ const updatedChildren = traverseBlocks(block.children, action);
258
+ if (updatedChildren !== block.children) {
259
+ updatedBlock.children = updatedChildren;
260
+ blockChanged = true;
261
+ }
262
+ }
263
+
264
+ // Handle columnData
265
+ if (block.data?.columnData && Array.isArray(block.data.columnData)) {
266
+ const updatedColumnData = block.data.columnData.map((col: Record<string, unknown>) => {
267
+ if (col.children && col.children.length > 0) {
268
+ const updatedColChildren = traverseBlocks(col.children, action);
269
+ if (updatedColChildren !== col.children) {
270
+ return { ...col, children: updatedColChildren };
271
+ }
272
+ }
273
+ return col;
274
+ });
275
+
276
+ if (updatedColumnData.some((col: Record<string, unknown>, i: number) => col !== block.data.columnData[i])) {
277
+ updatedBlock.data = { ...updatedBlock.data, columnData: updatedColumnData };
278
+ blockChanged = true;
279
+ }
280
+ }
281
+
282
+ if (blockChanged) overallChanged = true;
283
+ return blockChanged ? updatedBlock : block;
284
+ });
285
+
286
+ return overallChanged ? deepUpdatedList : blocks;
287
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from "react";
2
+ import type { Field } from "@kyro-cms/core/client";
3
+
4
+ export interface FieldComponentProps<TField extends Field = Field> {
5
+ field: TField;
6
+ value?: unknown;
7
+ onChange?: (value: unknown) => void;
8
+ error?: string;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ export interface Compactable {
13
+ compact?: boolean;
14
+ }
15
+
16
+ export type FieldChangeHandler = (field: string, value: unknown) => void;
17
+
18
+ export type RenderFieldFn = (
19
+ field: Field,
20
+ parentData: Record<string, unknown>,
21
+ onChange: (value: unknown) => void,
22
+ ) => ReactNode;
@@ -1,7 +1,7 @@
1
1
  import { type CollectionConfig } from '@kyro-cms/core/client';
2
2
 
3
3
  interface LayoutProps {
4
- children: any;
4
+ children: React.ReactNode;
5
5
  collections?: CollectionConfig[];
6
6
  currentSlug?: string;
7
7
  }
@@ -0,0 +1,63 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import type { ReactNode } from "react";
3
+ import { IconButton } from "./IconButton";
4
+ import { IconMoreVertical } from "./icons";
5
+
6
+ export interface ActionMenuItem {
7
+ label: string;
8
+ icon?: ReactNode;
9
+ onClick: () => void;
10
+ danger?: boolean;
11
+ divider?: boolean;
12
+ }
13
+
14
+ interface ActionMenuProps {
15
+ items: ActionMenuItem[];
16
+ label?: string;
17
+ }
18
+
19
+ export function ActionMenu({ items, label = "Actions" }: ActionMenuProps) {
20
+ const [open, setOpen] = useState(false);
21
+ const ref = useRef<HTMLDivElement>(null);
22
+
23
+ useEffect(() => {
24
+ const handleClickOutside = (e: MouseEvent) => {
25
+ if (ref.current && !ref.current.contains(e.target as Node)) {
26
+ setOpen(false);
27
+ }
28
+ };
29
+ document.addEventListener("mousedown", handleClickOutside);
30
+ return () => document.removeEventListener("mousedown", handleClickOutside);
31
+ }, []);
32
+
33
+ return (
34
+ <div ref={ref} className="relative">
35
+ <IconButton
36
+ icon={<IconMoreVertical className="w-4 h-4" />}
37
+ label={label}
38
+ variant="ghost"
39
+ size="sm"
40
+ onClick={() => setOpen(!open)}
41
+ />
42
+ {open && (
43
+ <div className="absolute right-0 z-50 min-w-[160px] bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-lg shadow-lg py-1">
44
+ {items.map((item, i) => (
45
+ <div key={i}>
46
+ {item.divider && <div className="border-t border-[var(--kyro-border)] my-1" />}
47
+ <button
48
+ className={`w-full flex items-center gap-2 px-3 py-2 text-xs font-bold transition-colors ${item.danger
49
+ ? "text-red-500 hover:bg-red-50"
50
+ : "text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"
51
+ }`}
52
+ onClick={() => { item.onClick(); setOpen(false); }}
53
+ >
54
+ {item.icon && <span className="w-4 h-4">{item.icon}</span>}
55
+ {item.label}
56
+ </button>
57
+ </div>
58
+ ))}
59
+ </div>
60
+ )}
61
+ </div>
62
+ );
63
+ }
@@ -1,19 +1,73 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
+ export type BadgeVariant =
4
+ | "default"
5
+ | "success"
6
+ | "warning"
7
+ | "danger"
8
+ | "info"
9
+ | "outline"
10
+ | "draft"
11
+ | "published"
12
+ | "scheduled"
13
+ | "archived"
14
+ | "active"
15
+ | "inactive"
16
+ | "pending"
17
+ | "completed"
18
+ | "cancelled";
19
+
3
20
  interface BadgeProps {
4
- variant?: "default" | "success" | "warning" | "danger" | "info";
21
+ variant?: BadgeVariant;
22
+ status?: BadgeVariant; // Alias for variant when used for statuses
5
23
  className?: string;
6
- children: ReactNode;
24
+ children?: ReactNode;
25
+ dot?: boolean;
7
26
  }
8
27
 
28
+ const statusConfig: Record<string, { class: string; label?: string }> = {
29
+ draft: { class: "bg-gray-100 text-gray-600", label: "Draft" },
30
+ published: { class: "bg-green-100 text-green-700", label: "Published" },
31
+ scheduled: { class: "bg-blue-100 text-blue-700", label: "Scheduled" },
32
+ archived: { class: "bg-yellow-100 text-yellow-700", label: "Archived" },
33
+ active: { class: "bg-green-100 text-green-700", label: "Active" },
34
+ inactive: { class: "bg-gray-100 text-gray-600", label: "Inactive" },
35
+ pending: { class: "bg-yellow-100 text-yellow-700", label: "Pending" },
36
+ completed: { class: "bg-green-100 text-green-700", label: "Completed" },
37
+ cancelled: { class: "bg-red-100 text-red-700", label: "Cancelled" },
38
+ };
39
+
9
40
  export function Badge({
10
- variant = "default",
41
+ variant,
42
+ status,
11
43
  className = "",
12
44
  children,
45
+ dot = false,
13
46
  }: BadgeProps) {
47
+ const activeVariant = variant || status || "default";
48
+ const config = statusConfig[activeVariant];
49
+
50
+ const variantClass = config
51
+ ? config.class
52
+ : `kyro-badge-${activeVariant}`;
53
+
14
54
  return (
15
- <span className={`kyro-badge kyro-badge-${variant} ${className}`}>
16
- {children}
55
+ <span className={`kyro-badge ${variantClass} ${className}`}>
56
+ {dot && (
57
+ <span className="w-1.5 h-1.5 rounded-full bg-current mr-1.5 opacity-60" />
58
+ )}
59
+ {children || config?.label || activeVariant}
17
60
  </span>
18
61
  );
19
62
  }
63
+
64
+ export function CountBadge({ count, max = 99 }: { count: number; max?: number }) {
65
+ if (count === 0) return null;
66
+
67
+ return (
68
+ <span className="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-medium bg-gray-200 text-gray-700 rounded-full">
69
+ {count > max ? `${max}+` : count}
70
+ </span>
71
+ );
72
+ }
73
+
@@ -33,7 +33,7 @@ export function DraggableBlockType({
33
33
  onSelect,
34
34
  children,
35
35
  }: {
36
- block: { type: string; label: string; icon: any; description: string };
36
+ block: { type: string; label: string; icon: React.ReactNode; description: string };
37
37
  onSelect: (type: string) => void;
38
38
  children?: ReactNode;
39
39
  }) {
@@ -48,9 +48,8 @@ export function DraggableBlockType({
48
48
  {...listeners}
49
49
  {...attributes}
50
50
  onClick={() => onSelect(block.type)}
51
- 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 ${
52
- isDragging ? "opacity-50 border-[var(--kyro-primary)]" : ""
53
- }`}
51
+ 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 ${isDragging ? "opacity-50 border-[var(--kyro-primary)]" : ""
52
+ }`}
54
53
  style={{ opacity: isDragging ? 0.5 : 1 }}
55
54
  >
56
55
  <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 duration-300">
@@ -61,7 +60,7 @@ export function DraggableBlockType({
61
60
  )}
62
61
  </div>
63
62
  <div className="flex-1 min-w-0">
64
- <div className="text-xs font-medium uppercase tracking-tight text-[var(--kyro-text-primary)] leading-tight">
63
+ <div className="text-xs font-medium tracking-tight text-[var(--kyro-text-primary)] leading-tight">
65
64
  {block.label}
66
65
  </div>
67
66
  <div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5 leading-tight">