@actuate-media/cms-admin 0.10.0 → 0.12.0

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 (315) hide show
  1. package/LICENSE +21 -21
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +8 -5
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/fields/component-block-helpers.test.d.ts +7 -0
  6. package/dist/__tests__/fields/component-block-helpers.test.d.ts.map +1 -0
  7. package/dist/__tests__/fields/component-block-helpers.test.js +592 -0
  8. package/dist/__tests__/fields/component-block-helpers.test.js.map +1 -0
  9. package/dist/__tests__/layout/primitives.test.d.ts +2 -0
  10. package/dist/__tests__/layout/primitives.test.d.ts.map +1 -0
  11. package/dist/__tests__/layout/primitives.test.js +34 -0
  12. package/dist/__tests__/layout/primitives.test.js.map +1 -0
  13. package/dist/__tests__/lib/cv.test.d.ts +2 -0
  14. package/dist/__tests__/lib/cv.test.d.ts.map +1 -0
  15. package/dist/__tests__/lib/cv.test.js +66 -0
  16. package/dist/__tests__/lib/cv.test.js.map +1 -0
  17. package/dist/actuate-admin.css +1 -1
  18. package/dist/assets/actuate-logo.d.ts +36 -0
  19. package/dist/assets/actuate-logo.d.ts.map +1 -0
  20. package/dist/assets/actuate-logo.js +15 -0
  21. package/dist/assets/actuate-logo.js.map +1 -0
  22. package/dist/components/Breadcrumbs.js +2 -2
  23. package/dist/components/CommandPalette.js +10 -10
  24. package/dist/components/ContentOverviewChart.js +3 -3
  25. package/dist/components/ErrorBoundary.js +1 -1
  26. package/dist/components/FocalPointPicker.js +2 -2
  27. package/dist/components/FolderTree.js +20 -20
  28. package/dist/components/LivePreview.js +3 -3
  29. package/dist/components/LocaleSwitcher.js +1 -1
  30. package/dist/components/MediaPickerModal.js +4 -4
  31. package/dist/components/PresenceIndicator.js +1 -1
  32. package/dist/components/SEOConfigPanel.d.ts +2 -0
  33. package/dist/components/SEOConfigPanel.d.ts.map +1 -0
  34. package/dist/components/SEOConfigPanel.js +174 -0
  35. package/dist/components/SEOConfigPanel.js.map +1 -0
  36. package/dist/components/SEOPanel.js +9 -9
  37. package/dist/components/SEOPerformance.js +2 -2
  38. package/dist/components/SchedulePublishDialog.js +1 -1
  39. package/dist/components/SharePreviewLinkDialog.js +1 -1
  40. package/dist/components/TipTapEditor.js +5 -5
  41. package/dist/components/VersionHistory.js +2 -2
  42. package/dist/components/ui/Badge.d.ts +33 -3
  43. package/dist/components/ui/Badge.d.ts.map +1 -1
  44. package/dist/components/ui/Badge.js +42 -8
  45. package/dist/components/ui/Badge.js.map +1 -1
  46. package/dist/components/ui/Button.d.ts +19 -8
  47. package/dist/components/ui/Button.d.ts.map +1 -1
  48. package/dist/components/ui/Button.js +35 -14
  49. package/dist/components/ui/Button.js.map +1 -1
  50. package/dist/components/ui/Card.d.ts +26 -0
  51. package/dist/components/ui/Card.d.ts.map +1 -0
  52. package/dist/components/ui/Card.js +45 -0
  53. package/dist/components/ui/Card.js.map +1 -0
  54. package/dist/components/ui/DataTable.js +1 -1
  55. package/dist/components/ui/Input.d.ts +15 -0
  56. package/dist/components/ui/Input.d.ts.map +1 -0
  57. package/dist/components/ui/Input.js +23 -0
  58. package/dist/components/ui/Input.js.map +1 -0
  59. package/dist/components/ui/SearchInput.js +1 -1
  60. package/dist/components/ui/Select.d.ts +16 -0
  61. package/dist/components/ui/Select.d.ts.map +1 -0
  62. package/dist/components/ui/Select.js +25 -0
  63. package/dist/components/ui/Select.js.map +1 -0
  64. package/dist/components/ui/Toast.js +1 -1
  65. package/dist/components/ui/index.d.ts +10 -4
  66. package/dist/components/ui/index.d.ts.map +1 -1
  67. package/dist/components/ui/index.js +5 -2
  68. package/dist/components/ui/index.js.map +1 -1
  69. package/dist/fields/BlockBuilderField.js +3 -3
  70. package/dist/fields/ComponentBlockField.d.ts +25 -0
  71. package/dist/fields/ComponentBlockField.d.ts.map +1 -0
  72. package/dist/fields/ComponentBlockField.js +74 -0
  73. package/dist/fields/ComponentBlockField.js.map +1 -0
  74. package/dist/fields/DateField.js +1 -1
  75. package/dist/fields/FieldRenderer.d.ts +3 -0
  76. package/dist/fields/FieldRenderer.d.ts.map +1 -1
  77. package/dist/fields/FieldRenderer.js +3 -1
  78. package/dist/fields/FieldRenderer.js.map +1 -1
  79. package/dist/fields/PropInput.d.ts +14 -0
  80. package/dist/fields/PropInput.d.ts.map +1 -0
  81. package/dist/fields/PropInput.js +163 -0
  82. package/dist/fields/PropInput.js.map +1 -0
  83. package/dist/fields/RelationshipField.js +3 -3
  84. package/dist/fields/TextField.js +1 -1
  85. package/dist/fields/component-block-helpers.d.ts +96 -0
  86. package/dist/fields/component-block-helpers.d.ts.map +1 -0
  87. package/dist/fields/component-block-helpers.js +323 -0
  88. package/dist/fields/component-block-helpers.js.map +1 -0
  89. package/dist/fields/index.d.ts +4 -0
  90. package/dist/fields/index.d.ts.map +1 -1
  91. package/dist/fields/index.js +2 -0
  92. package/dist/fields/index.js.map +1 -1
  93. package/dist/index.d.ts +4 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +4 -0
  96. package/dist/index.js.map +1 -1
  97. package/dist/layout/Header.js +1 -1
  98. package/dist/layout/Layout.d.ts +14 -0
  99. package/dist/layout/Layout.d.ts.map +1 -1
  100. package/dist/layout/Layout.js +17 -11
  101. package/dist/layout/Layout.js.map +1 -1
  102. package/dist/layout/Sidebar.d.ts.map +1 -1
  103. package/dist/layout/Sidebar.js +21 -11
  104. package/dist/layout/Sidebar.js.map +1 -1
  105. package/dist/layout/primitives/AdminShell.d.ts +43 -0
  106. package/dist/layout/primitives/AdminShell.d.ts.map +1 -0
  107. package/dist/layout/primitives/AdminShell.js +51 -0
  108. package/dist/layout/primitives/AdminShell.js.map +1 -0
  109. package/dist/layout/primitives/Box.d.ts +19 -0
  110. package/dist/layout/primitives/Box.d.ts.map +1 -0
  111. package/dist/layout/primitives/Box.js +12 -0
  112. package/dist/layout/primitives/Box.js.map +1 -0
  113. package/dist/layout/primitives/Cluster.d.ts +27 -0
  114. package/dist/layout/primitives/Cluster.d.ts.map +1 -0
  115. package/dist/layout/primitives/Cluster.js +37 -0
  116. package/dist/layout/primitives/Cluster.js.map +1 -0
  117. package/dist/layout/primitives/Grid.d.ts +45 -0
  118. package/dist/layout/primitives/Grid.d.ts.map +1 -0
  119. package/dist/layout/primitives/Grid.js +59 -0
  120. package/dist/layout/primitives/Grid.js.map +1 -0
  121. package/dist/layout/primitives/PageContainer.d.ts +36 -0
  122. package/dist/layout/primitives/PageContainer.d.ts.map +1 -0
  123. package/dist/layout/primitives/PageContainer.js +41 -0
  124. package/dist/layout/primitives/PageContainer.js.map +1 -0
  125. package/dist/layout/primitives/Split.d.ts +34 -0
  126. package/dist/layout/primitives/Split.d.ts.map +1 -0
  127. package/dist/layout/primitives/Split.js +27 -0
  128. package/dist/layout/primitives/Split.js.map +1 -0
  129. package/dist/layout/primitives/Stack.d.ts +23 -0
  130. package/dist/layout/primitives/Stack.d.ts.map +1 -0
  131. package/dist/layout/primitives/Stack.js +34 -0
  132. package/dist/layout/primitives/Stack.js.map +1 -0
  133. package/dist/layout/primitives/index.d.ts +30 -0
  134. package/dist/layout/primitives/index.d.ts.map +1 -0
  135. package/dist/layout/primitives/index.js +22 -0
  136. package/dist/layout/primitives/index.js.map +1 -0
  137. package/dist/layout/primitives/tokens.d.ts +48 -0
  138. package/dist/layout/primitives/tokens.d.ts.map +1 -0
  139. package/dist/layout/primitives/tokens.js +54 -0
  140. package/dist/layout/primitives/tokens.js.map +1 -0
  141. package/dist/lib/cv.d.ts +53 -0
  142. package/dist/lib/cv.d.ts.map +1 -0
  143. package/dist/lib/cv.js +39 -0
  144. package/dist/lib/cv.js.map +1 -0
  145. package/dist/views/ApiKeys.js +7 -7
  146. package/dist/views/CollectionList.js +8 -8
  147. package/dist/views/Dashboard.d.ts.map +1 -1
  148. package/dist/views/Dashboard.js +333 -78
  149. package/dist/views/Dashboard.js.map +1 -1
  150. package/dist/views/DocumentEdit.js +3 -3
  151. package/dist/views/ForgotPassword.js +2 -2
  152. package/dist/views/FormEditor.js +5 -5
  153. package/dist/views/FormSubmissions.js +6 -6
  154. package/dist/views/Forms.js +2 -2
  155. package/dist/views/Login.d.ts +16 -1
  156. package/dist/views/Login.d.ts.map +1 -1
  157. package/dist/views/Login.js +17 -7
  158. package/dist/views/Login.js.map +1 -1
  159. package/dist/views/MediaBrowser.js +16 -16
  160. package/dist/views/PageEditor.js +2 -2
  161. package/dist/views/Pages.js +10 -10
  162. package/dist/views/PostEditor.js +2 -2
  163. package/dist/views/Posts.js +4 -4
  164. package/dist/views/Redirects.js +4 -4
  165. package/dist/views/ResetPassword.js +2 -2
  166. package/dist/views/SEO.js +6 -6
  167. package/dist/views/ScriptTagEditor.js +4 -4
  168. package/dist/views/ScriptTags.js +2 -2
  169. package/dist/views/Settings.d.ts.map +1 -1
  170. package/dist/views/Settings.js +9 -8
  171. package/dist/views/Settings.js.map +1 -1
  172. package/dist/views/SetupWizard.js +2 -2
  173. package/dist/views/Users.js +4 -4
  174. package/dist/views/page-builder/AIBlockAssist.js +1 -1
  175. package/dist/views/page-builder/AIGenerateDialog.js +10 -10
  176. package/dist/views/page-builder/BlockEditor.js +10 -10
  177. package/dist/views/page-builder/BlockPicker.js +4 -4
  178. package/dist/views/page-builder/BottomBar.js +1 -1
  179. package/dist/views/page-builder/BuilderToolbar.js +2 -2
  180. package/dist/views/page-builder/ContextPanel.js +2 -2
  181. package/dist/views/page-builder/DesignScore.js +9 -9
  182. package/dist/views/page-builder/NodeSettings.js +8 -8
  183. package/dist/views/page-builder/PageBuilder.js +3 -3
  184. package/dist/views/page-builder/PageSettings.js +1 -1
  185. package/dist/views/page-builder/PageTemplates.js +2 -2
  186. package/dist/views/page-builder/SEOPanel.js +13 -13
  187. package/dist/views/page-builder/SavedSections.js +5 -5
  188. package/dist/views/page-builder/TemplatePicker.js +2 -2
  189. package/dist/views/page-builder/block-renderers/CTAPreview.js +5 -5
  190. package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
  191. package/dist/views/page-builder/block-renderers/CodePreview.js +1 -1
  192. package/dist/views/page-builder/block-renderers/FAQPreview.js +3 -3
  193. package/dist/views/page-builder/block-renderers/FallbackPreview.js +1 -1
  194. package/dist/views/page-builder/block-renderers/FormPreview.js +3 -3
  195. package/dist/views/page-builder/block-renderers/GalleryPreview.js +5 -5
  196. package/dist/views/page-builder/block-renderers/HeroPreview.js +3 -3
  197. package/dist/views/page-builder/block-renderers/ImagePreview.js +3 -3
  198. package/dist/views/page-builder/block-renderers/TextPreview.js +3 -3
  199. package/dist/views/page-builder/block-renderers/VideoPreview.js +4 -4
  200. package/dist/views/page-builder/canvas/BlockRenderer.js +1 -1
  201. package/dist/views/page-builder/canvas/BuilderCanvas.js +3 -3
  202. package/dist/views/page-builder/canvas/ColumnRenderer.js +2 -2
  203. package/dist/views/page-builder/canvas/ContainerRenderer.js +2 -2
  204. package/dist/views/page-builder/canvas/RowRenderer.js +2 -2
  205. package/dist/views/page-builder/canvas/SectionRenderer.js +2 -2
  206. package/package.json +14 -3
  207. package/src/AdminRoot.tsx +21 -11
  208. package/src/__tests__/fields/component-block-helpers.test.ts +674 -0
  209. package/src/__tests__/layout/primitives.test.ts +37 -0
  210. package/src/__tests__/lib/cv.test.ts +74 -0
  211. package/src/assets/actuate-logo.tsx +72 -0
  212. package/src/components/Breadcrumbs.tsx +6 -6
  213. package/src/components/CommandPalette.tsx +34 -34
  214. package/src/components/ContentOverviewChart.tsx +3 -3
  215. package/src/components/ErrorBoundary.tsx +3 -3
  216. package/src/components/FocalPointPicker.tsx +4 -4
  217. package/src/components/FolderTree.tsx +38 -38
  218. package/src/components/LivePreview.tsx +16 -16
  219. package/src/components/LocaleSwitcher.tsx +7 -7
  220. package/src/components/MediaPickerModal.tsx +21 -21
  221. package/src/components/PresenceIndicator.tsx +2 -2
  222. package/src/components/SEOConfigPanel.tsx +582 -0
  223. package/src/components/SEOPanel.tsx +46 -46
  224. package/src/components/SEOPerformance.tsx +21 -21
  225. package/src/components/SchedulePublishDialog.tsx +4 -4
  226. package/src/components/SharePreviewLinkDialog.tsx +1 -1
  227. package/src/components/TipTapEditor.tsx +33 -33
  228. package/src/components/VersionHistory.tsx +16 -16
  229. package/src/components/ui/Badge.tsx +66 -14
  230. package/src/components/ui/Button.tsx +70 -33
  231. package/src/components/ui/Card.tsx +101 -0
  232. package/src/components/ui/DataTable.tsx +1 -1
  233. package/src/components/ui/Input.tsx +35 -0
  234. package/src/components/ui/SearchInput.tsx +4 -4
  235. package/src/components/ui/Select.tsx +56 -0
  236. package/src/components/ui/Toast.tsx +1 -1
  237. package/src/components/ui/index.ts +18 -4
  238. package/src/fields/BlockBuilderField.tsx +3 -3
  239. package/src/fields/ComponentBlockField.tsx +179 -0
  240. package/src/fields/DateField.tsx +1 -1
  241. package/src/fields/FieldRenderer.tsx +8 -0
  242. package/src/fields/PropInput.tsx +552 -0
  243. package/src/fields/RelationshipField.tsx +10 -10
  244. package/src/fields/TextField.tsx +1 -1
  245. package/src/fields/component-block-helpers.ts +341 -0
  246. package/src/fields/index.ts +4 -0
  247. package/src/index.ts +35 -0
  248. package/src/layout/Header.tsx +28 -28
  249. package/src/layout/Layout.tsx +39 -46
  250. package/src/layout/Sidebar.tsx +37 -64
  251. package/src/layout/primitives/AdminShell.tsx +118 -0
  252. package/src/layout/primitives/Box.tsx +30 -0
  253. package/src/layout/primitives/Cluster.tsx +74 -0
  254. package/src/layout/primitives/Grid.tsx +120 -0
  255. package/src/layout/primitives/PageContainer.tsx +96 -0
  256. package/src/layout/primitives/Split.tsx +73 -0
  257. package/src/layout/primitives/Stack.tsx +67 -0
  258. package/src/layout/primitives/index.ts +36 -0
  259. package/src/layout/primitives/tokens.ts +76 -0
  260. package/src/lib/cv.ts +96 -0
  261. package/src/styles/build-input.css +1 -1
  262. package/src/views/ApiKeys.tsx +57 -57
  263. package/src/views/CollectionList.tsx +30 -30
  264. package/src/views/Dashboard.tsx +737 -186
  265. package/src/views/DocumentEdit.tsx +9 -9
  266. package/src/views/ForgotPassword.tsx +18 -18
  267. package/src/views/FormEditor.tsx +75 -75
  268. package/src/views/FormSubmissions.tsx +76 -76
  269. package/src/views/Forms.tsx +27 -27
  270. package/src/views/Login.tsx +65 -25
  271. package/src/views/MediaBrowser.tsx +127 -127
  272. package/src/views/PageEditor.tsx +25 -25
  273. package/src/views/Pages.tsx +59 -59
  274. package/src/views/PostEditor.tsx +37 -37
  275. package/src/views/Posts.tsx +48 -48
  276. package/src/views/Redirects.tsx +21 -21
  277. package/src/views/ResetPassword.tsx +28 -28
  278. package/src/views/SEO.tsx +144 -144
  279. package/src/views/ScriptTagEditor.tsx +24 -24
  280. package/src/views/ScriptTags.tsx +10 -10
  281. package/src/views/Settings.tsx +88 -80
  282. package/src/views/SetupWizard.tsx +28 -28
  283. package/src/views/Users.tsx +20 -20
  284. package/src/views/page-builder/AIBlockAssist.tsx +1 -1
  285. package/src/views/page-builder/AIGenerateDialog.tsx +63 -63
  286. package/src/views/page-builder/BlockEditor.tsx +26 -26
  287. package/src/views/page-builder/BlockPicker.tsx +22 -22
  288. package/src/views/page-builder/BottomBar.tsx +8 -8
  289. package/src/views/page-builder/BuilderToolbar.tsx +17 -17
  290. package/src/views/page-builder/ContextPanel.tsx +3 -3
  291. package/src/views/page-builder/DesignScore.tsx +21 -21
  292. package/src/views/page-builder/NodeSettings.tsx +27 -27
  293. package/src/views/page-builder/PageBuilder.tsx +11 -11
  294. package/src/views/page-builder/PageSettings.tsx +4 -4
  295. package/src/views/page-builder/PageTemplates.tsx +18 -18
  296. package/src/views/page-builder/SEOPanel.tsx +53 -53
  297. package/src/views/page-builder/SavedSections.tsx +37 -37
  298. package/src/views/page-builder/TemplatePicker.tsx +17 -17
  299. package/src/views/page-builder/block-renderers/CTAPreview.tsx +13 -13
  300. package/src/views/page-builder/block-renderers/CardsPreview.tsx +5 -5
  301. package/src/views/page-builder/block-renderers/CodePreview.tsx +6 -6
  302. package/src/views/page-builder/block-renderers/FAQPreview.tsx +13 -13
  303. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +3 -3
  304. package/src/views/page-builder/block-renderers/FormPreview.tsx +20 -20
  305. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +8 -8
  306. package/src/views/page-builder/block-renderers/HeroPreview.tsx +16 -16
  307. package/src/views/page-builder/block-renderers/ImagePreview.tsx +4 -4
  308. package/src/views/page-builder/block-renderers/TextPreview.tsx +14 -14
  309. package/src/views/page-builder/block-renderers/VideoPreview.tsx +12 -12
  310. package/src/views/page-builder/canvas/BlockRenderer.tsx +4 -4
  311. package/src/views/page-builder/canvas/BuilderCanvas.tsx +6 -6
  312. package/src/views/page-builder/canvas/ColumnRenderer.tsx +3 -3
  313. package/src/views/page-builder/canvas/ContainerRenderer.tsx +2 -2
  314. package/src/views/page-builder/canvas/RowRenderer.tsx +2 -2
  315. package/src/views/page-builder/canvas/SectionRenderer.tsx +2 -2
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Pure helpers behind `<ComponentBlockField>`. Extracted so vitest can
3
+ * exercise them in node without spinning up jsdom — the React component
4
+ * just composes these into UI.
5
+ *
6
+ * Kept structurally typed (no peer-dep import of types here would be
7
+ * cleaner, but the `PropType` discriminated union is the contract we
8
+ * promise consumers, so we lean on the package's types).
9
+ */
10
+
11
+ import {
12
+ detectDiscriminator,
13
+ findVariant,
14
+ } from '@actuate-media/component-blocks/discriminated-union'
15
+ import type {
16
+ DiscriminatedUnion,
17
+ DiscriminatedVariant,
18
+ } from '@actuate-media/component-blocks/discriminated-union'
19
+ import type { ComponentSpec, Manifest, PropSpec, PropType } from '@actuate-media/component-blocks'
20
+
21
+ import type { ComponentBlockValue } from './ComponentBlockField.js'
22
+
23
+ /**
24
+ * Compute the list of components an editor can pick from given the
25
+ * manifest + the optional allow-list. Returns the full list when no
26
+ * filter is provided; returns `[]` when every entry is filtered out
27
+ * (the caller renders an error in that case).
28
+ */
29
+ export function getAllowedComponents(
30
+ manifest: Manifest,
31
+ allow: string[] | undefined,
32
+ ): ComponentSpec[] {
33
+ if (!allow || allow.length === 0) return manifest.components
34
+ return manifest.components.filter((c) => allow.includes(c.name))
35
+ }
36
+
37
+ /**
38
+ * JSON-encode any value so it round-trips cleanly through the
39
+ * JsonFallback textarea. Previously the helper short-circuited for
40
+ * strings (returning the raw string), which made the textarea
41
+ * stateful: typing `"hello"` parsed to the JS string `hello`,
42
+ * re-rendered as bare `hello`, then `JSON.parse('hello')` threw on
43
+ * the next edit and the field "locked" as a raw string. Always
44
+ * stringifying keeps the textarea visually consistent with the
45
+ * underlying JSON value.
46
+ *
47
+ * Falls back to `String(v)` for values that can't be JSON-encoded
48
+ * (e.g. circular refs) so the field remains editable.
49
+ */
50
+ export function safeJsonStringify(v: unknown): string {
51
+ try {
52
+ return JSON.stringify(v, null, 2)
53
+ } catch {
54
+ return String(v)
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Map an `<select>` change-event value to an enum-prop value.
60
+ *
61
+ * The placeholder `<option value="">Select…</option>` clears the field
62
+ * to `undefined`; any other value is the stringified array index into
63
+ * `options`. Extracted so we can test the clear semantics without
64
+ * spinning up jsdom: previously the inline handler used `Number("")`,
65
+ * which evaluates to `0` and silently picked the first option instead
66
+ * of clearing.
67
+ */
68
+ export function parseEnumSelection<T>(raw: string, options: readonly T[]): T | undefined {
69
+ if (raw === '') return undefined
70
+ const idx = Number(raw)
71
+ if (!Number.isInteger(idx) || idx < 0 || idx >= options.length) return undefined
72
+ return options[idx]
73
+ }
74
+
75
+ /**
76
+ * Produce a sensible empty value for a freshly-created array item or
77
+ * required prop. The admin form generator never wants `undefined` in
78
+ * an array; that breaks downstream PropInput defaults and confuses
79
+ * validators.
80
+ */
81
+ export function defaultForType(type: PropType): unknown {
82
+ switch (type.kind) {
83
+ case 'string':
84
+ return ''
85
+ case 'number':
86
+ return 0
87
+ case 'boolean':
88
+ return false
89
+ case 'enum':
90
+ return type.values[0] ?? ''
91
+ case 'literal':
92
+ return type.value
93
+ case 'array':
94
+ return []
95
+ case 'object':
96
+ return Object.fromEntries(
97
+ type.fields.filter((f) => f.required).map((f) => [f.name, defaultForType(f.type)]),
98
+ )
99
+ case 'union': {
100
+ // Discriminated unions DO have a sensible structural default:
101
+ // pick the first variant and seed its required fields (plus the
102
+ // discriminator). This makes a Hero with a `cta: Link | Modal`
103
+ // prop render an editable form on first paint instead of forcing
104
+ // the editor through the JSON textarea fallback.
105
+ const detected = detectDiscriminator(type)
106
+ if (detected) {
107
+ const variant = detected.variants[0]!
108
+ const out: Record<string, unknown> = { [detected.field]: variant.value }
109
+ for (const field of variant.remainingFields) {
110
+ if (field.required) out[field.name] = defaultForType(field.type)
111
+ }
112
+ return out
113
+ }
114
+ return null
115
+ }
116
+ default:
117
+ // `reference`, `unknown`: there's no single sensible structural
118
+ // default. We return `null` instead of `undefined` so the value
119
+ // survives a JSON round-trip — `JSON.stringify` turns
120
+ // `undefined` array items into `null` anyway, which would
121
+ // contradict the documented "never undefined in an array"
122
+ // invariant and surface as the literal string `null` in the
123
+ // JsonFallback textarea.
124
+ return null
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Seed an empty props object for a freshly-picked component using each
130
+ * prop's explicit `defaultValue` when set, else the structural default
131
+ * from {@link defaultForType} (only for required props — optional
132
+ * unset props stay absent so they render as empty inputs).
133
+ */
134
+ export function seedPropsForComponent(spec: ComponentSpec): Record<string, unknown> {
135
+ const out: Record<string, unknown> = {}
136
+ for (const prop of spec.props) {
137
+ if (prop.defaultValue !== undefined && prop.defaultValue !== null) {
138
+ out[prop.name] = prop.defaultValue
139
+ continue
140
+ }
141
+ if (prop.required) {
142
+ const seed = defaultForType(prop.type)
143
+ // For non-discriminated `union` / `reference` / `unknown` kinds
144
+ // defaultForType returns `null` (so arrays survive a JSON
145
+ // round-trip), but seeding a top-level required prop with
146
+ // `null` would immediately trip the validator's "Missing
147
+ // required prop" branch on first render — the user would see
148
+ // a red error before they touched anything. Leave those
149
+ // required props absent so the absence and the validation
150
+ // message describe the same state honestly; the user provides
151
+ // a value via the JsonFallback. Discriminated unions return
152
+ // a structural default and seed cleanly.
153
+ if (seed !== null) {
154
+ out[prop.name] = seed
155
+ }
156
+ }
157
+ }
158
+ return out
159
+ }
160
+
161
+ /**
162
+ * Compute the next value for a discriminated-union field when the
163
+ * editor switches variants. Pure helper so the React component stays
164
+ * thin and the seeding rules can be unit-tested without rendering.
165
+ *
166
+ * Seeding rules:
167
+ * 1. The discriminator field is always written to the variant's
168
+ * tag value.
169
+ * 2. Optional fields shared by both variants survive the switch
170
+ * (preserves "edited the same label" intent across, e.g.,
171
+ * Link → Modal where both carry `label`).
172
+ * 3. Required fields exclusive to the new variant are seeded by
173
+ * priority: explicit `field.defaultValue` first (matches
174
+ * `seedPropsForComponent`), else the structural default from
175
+ * `defaultForType` — but only when that default is non-null.
176
+ * A `null` from `defaultForType` (reference / unknown / non-
177
+ * discriminated union types) is treated as "no sensible default"
178
+ * and the field is left absent so the validator's
179
+ * "missing required prop" message and the data agree.
180
+ * 4. Fields exclusive to the previous variant are dropped so the
181
+ * value stays structurally valid against the chosen variant.
182
+ */
183
+ export function switchUnionVariant(
184
+ current: unknown,
185
+ union: DiscriminatedUnion,
186
+ variant: DiscriminatedVariant,
187
+ ): Record<string, unknown> {
188
+ const obj = isPlainObject(current) ? current : {}
189
+ const next: Record<string, unknown> = { [union.field]: variant.value }
190
+ const carryable = new Set(variant.remainingFields.map((f) => f.name))
191
+ for (const [k, v] of Object.entries(obj)) {
192
+ if (k === union.field) continue
193
+ if (carryable.has(k)) next[k] = v
194
+ }
195
+ for (const field of variant.remainingFields) {
196
+ if (!field.required || next[field.name] !== undefined) continue
197
+ if (field.defaultValue !== undefined && field.defaultValue !== null) {
198
+ next[field.name] = field.defaultValue
199
+ continue
200
+ }
201
+ const seed = defaultForType(field.type)
202
+ if (seed !== null) {
203
+ next[field.name] = seed
204
+ }
205
+ }
206
+ return next
207
+ }
208
+
209
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
210
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
211
+ }
212
+
213
+ /**
214
+ * Build a minimal structural validator that mirrors the rules the
215
+ * cms-core `validateComponentBlockValue` runs server-side, so the form
216
+ * can show feedback without a round-trip. Kept in sync with cms-core
217
+ * by inspection; the canonical implementation lives there.
218
+ */
219
+ export function buildClientValidator(
220
+ manifest: Manifest,
221
+ allow: string[] | undefined,
222
+ ): (value: ComponentBlockValue) => string | true {
223
+ // An empty `allow` array is treated the same way `getAllowedComponents`
224
+ // treats it: as "no filter". Without this, the picker shows every
225
+ // component while the validator rejects every selection, leading to
226
+ // a UI/validator deadlock.
227
+ const allowed = allow && allow.length > 0 ? new Set(allow) : null
228
+ return (value) => {
229
+ const spec = manifest.components.find((c) => c.name === value.component)
230
+ if (!spec) {
231
+ const known = manifest.components.map((c) => c.name).join(', ') || '<none>'
232
+ return `Unknown component '${value.component}'. Manifest knows: ${known}.`
233
+ }
234
+ if (allowed && !allowed.has(value.component)) {
235
+ return `Component '${value.component}' is not in the allow-list for this field.`
236
+ }
237
+ for (const propSpec of spec.props) {
238
+ const v = value.props[propSpec.name]
239
+ if (v === undefined || v === null) {
240
+ if (propSpec.required) {
241
+ return `Missing required prop '${propSpec.name}' for component '${value.component}'.`
242
+ }
243
+ continue
244
+ }
245
+ const err = clientShapeError(v, propSpec)
246
+ if (err) return err
247
+ }
248
+ return true
249
+ }
250
+ }
251
+
252
+ function clientShapeError(value: unknown, prop: PropSpec): string | null {
253
+ const t = prop.type
254
+ switch (t.kind) {
255
+ case 'string':
256
+ return typeof value === 'string' ? null : `Prop '${prop.name}' must be a string.`
257
+ case 'number':
258
+ return typeof value === 'number' && !Number.isNaN(value)
259
+ ? null
260
+ : `Prop '${prop.name}' must be a number.`
261
+ case 'boolean':
262
+ return typeof value === 'boolean' ? null : `Prop '${prop.name}' must be a boolean.`
263
+ case 'enum':
264
+ return t.values.includes(value as never)
265
+ ? null
266
+ : `Prop '${prop.name}' must be one of: ${t.values.join(', ')}.`
267
+ case 'array':
268
+ return Array.isArray(value) ? null : `Prop '${prop.name}' must be an array.`
269
+ case 'object': {
270
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
271
+ return `Prop '${prop.name}' must be an object.`
272
+ }
273
+ // Recurse: check that each required nested field is present and
274
+ // structurally compatible. Without this, passing `{}` for an
275
+ // object prop with required `label` and `href` children sailed
276
+ // through the client validator while the server-side
277
+ // validateComponentBlockValue would reject it on save — a
278
+ // silent gap in the "live structural validation" promise.
279
+ const obj = value as Record<string, unknown>
280
+ for (const field of t.fields) {
281
+ const child = obj[field.name]
282
+ if (child === undefined || child === null) {
283
+ if (field.required) {
284
+ return `Prop '${prop.name}.${field.name}' is required but missing.`
285
+ }
286
+ continue
287
+ }
288
+ const childError = clientShapeError(child, {
289
+ ...field,
290
+ // Build a dotted path so parsePerPropErrors can attribute
291
+ // the error to the nested input, not just the outer prop.
292
+ name: `${prop.name}.${field.name}`,
293
+ })
294
+ if (childError) return childError
295
+ }
296
+ return null
297
+ }
298
+ case 'union': {
299
+ // Discriminated unions get the same recursive treatment as plain
300
+ // objects: route the value into the matching variant's schema
301
+ // and validate against that. Non-discriminated unions remain
302
+ // unvalidated on the client (server still catches them) — the
303
+ // shape is too open to assert structurally.
304
+ const detected = detectDiscriminator(t)
305
+ if (!detected) return null
306
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
307
+ return `Prop '${prop.name}' must be an object with a '${detected.field}' discriminator.`
308
+ }
309
+ const variant = findVariant(value, detected)
310
+ if (!variant) {
311
+ const allowed = detected.variants.map((v) => JSON.stringify(v.value)).join(', ')
312
+ return `Prop '${prop.name}.${detected.field}' must be one of: ${allowed}.`
313
+ }
314
+ // Recurse into the variant's fields, scoped to the prop path.
315
+ // We treat the variant as an inline object so the existing
316
+ // object-branch logic applies verbatim.
317
+ return clientShapeError(value, {
318
+ name: prop.name,
319
+ required: prop.required,
320
+ type: variant.type,
321
+ })
322
+ }
323
+ default:
324
+ return null
325
+ }
326
+ }
327
+
328
+ /**
329
+ * The validator returns a single error string. The form wants to show
330
+ * the message under the specific input that triggered it — so we parse
331
+ * the message back into a sparse `{ propPath → message }` map. Pure
332
+ * function so it's trivial to unit-test.
333
+ */
334
+ export function parsePerPropErrors(message: string | null): Record<string, string> {
335
+ if (!message) return {}
336
+ const propMatch = /Prop '([^']+)'/.exec(message)
337
+ if (propMatch) return { [propMatch[1]!]: message }
338
+ const missing = /Missing required prop '([^']+)'/.exec(message)
339
+ if (missing) return { [missing[1]!]: message }
340
+ return {}
341
+ }
@@ -15,3 +15,7 @@ export type { BlockTypeDefinition } from './block-types.js'
15
15
  export { GroupField } from './GroupField.js'
16
16
  export { NavBuilderField } from './NavBuilderField.js'
17
17
  export { NumberField } from './NumberField.js'
18
+ export { ComponentBlockField } from './ComponentBlockField.js'
19
+ export type { ComponentBlockFieldProps, ComponentBlockValue } from './ComponentBlockField.js'
20
+ export { PropInput, defaultForType } from './PropInput.js'
21
+ export type { PropInputProps } from './PropInput.js'
package/src/index.ts CHANGED
@@ -8,6 +8,34 @@ export type { SidebarProps } from './layout/Sidebar.js'
8
8
  export { Header } from './layout/Header.js'
9
9
  export type { HeaderProps } from './layout/Header.js'
10
10
 
11
+ // Layout primitives — the sanctioned way to compose admin views.
12
+ // Always prefer these to hand-rolled flex/grid utilities.
13
+ export {
14
+ AdminShell,
15
+ PageContainer,
16
+ Stack,
17
+ Cluster,
18
+ Grid,
19
+ Split,
20
+ Box,
21
+ tokens,
22
+ } from './layout/primitives/index.js'
23
+ export type {
24
+ AdminShellProps,
25
+ PageContainerProps,
26
+ StackProps,
27
+ StackSpace,
28
+ ClusterProps,
29
+ ClusterAlign,
30
+ ClusterJustify,
31
+ GridProps,
32
+ GridResponsive,
33
+ SplitProps,
34
+ BoxProps,
35
+ SpaceToken,
36
+ RadiusToken,
37
+ } from './layout/primitives/index.js'
38
+
11
39
  export { Dashboard } from './views/Dashboard.js'
12
40
  export { Posts } from './views/Posts.js'
13
41
  export { Pages } from './views/Pages.js'
@@ -77,3 +105,10 @@ export { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts.js'
77
105
  export { cmsApi, setApiBase, ensureCsrfToken } from './lib/api.js'
78
106
  export { useApiData } from './lib/useApiData.js'
79
107
  export type { UseApiDataResult } from './lib/useApiData.js'
108
+
109
+ export { ComponentBlockField, PropInput, defaultForType } from './fields/index.js'
110
+ export type {
111
+ ComponentBlockFieldProps,
112
+ ComponentBlockValue,
113
+ PropInputProps,
114
+ } from './fields/index.js'
@@ -24,100 +24,100 @@ export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
24
24
 
25
25
  return (
26
26
  <>
27
- <header className="h-14 border-b border-border bg-background flex items-center justify-between px-4 gap-4">
27
+ <header className="border-border bg-background flex h-14 items-center justify-between gap-4 border-b px-4">
28
28
  <button
29
29
  onClick={onToggleSidebar}
30
- className="lg:hidden p-2 hover:bg-accent rounded-lg transition-colors"
30
+ className="hover:bg-accent rounded-lg p-2 transition-colors md:hidden"
31
31
  aria-label="Toggle sidebar"
32
32
  >
33
- <Menu className="w-5 h-5 text-foreground" strokeWidth={2} />
33
+ <Menu className="text-foreground h-5 w-5" strokeWidth={2} />
34
34
  </button>
35
35
 
36
- <div className="flex items-center lg:hidden">
37
- <span className="text-lg font-semibold text-foreground">Actuate</span>
36
+ <div className="flex items-center md:hidden">
37
+ <span className="text-foreground text-lg font-semibold">Actuate</span>
38
38
  </div>
39
39
 
40
40
  <div className="flex-1" />
41
41
 
42
42
  <div className="flex items-center gap-2 sm:gap-3">
43
- <div className="hidden md:block relative">
44
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
43
+ <div className="relative hidden md:block">
44
+ <Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
45
45
  <input
46
46
  type="text"
47
47
  placeholder="Search... (⌘K)"
48
48
  value={searchQuery}
49
49
  onChange={(e) => setSearchQuery(e.target.value)}
50
50
  onFocus={() => setShowCommandPalette(true)}
51
- className="w-64 pl-9 pr-3 py-1.5 text-sm border border-border rounded-lg bg-input-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
51
+ className="border-border bg-input-background text-foreground focus:ring-ring w-64 rounded-lg border py-1.5 pr-3 pl-9 text-sm focus:border-transparent focus:ring-2 focus:outline-none"
52
52
  />
53
53
  </div>
54
54
 
55
55
  <button
56
56
  onClick={() => setShowCommandPalette(true)}
57
- className="md:hidden p-2 hover:bg-accent rounded-lg transition-colors"
57
+ className="hover:bg-accent rounded-lg p-2 transition-colors md:hidden"
58
58
  aria-label="Search"
59
59
  >
60
- <Search className="w-5 h-5 text-muted-foreground" />
60
+ <Search className="text-muted-foreground h-5 w-5" />
61
61
  </button>
62
62
 
63
63
  <LocaleSwitcher />
64
64
 
65
65
  <button
66
66
  onClick={toggleTheme}
67
- className="p-2 hover:bg-accent rounded-lg transition-colors"
67
+ className="hover:bg-accent rounded-lg p-2 transition-colors"
68
68
  aria-label="Toggle theme"
69
69
  >
70
70
  {resolvedTheme === 'dark' ? (
71
- <Sun className="w-5 h-5 text-muted-foreground" />
71
+ <Sun className="text-muted-foreground h-5 w-5" />
72
72
  ) : (
73
- <Moon className="w-5 h-5 text-muted-foreground" />
73
+ <Moon className="text-muted-foreground h-5 w-5" />
74
74
  )}
75
75
  </button>
76
76
 
77
77
  <button
78
- className="p-2 hover:bg-accent rounded-lg transition-colors relative"
78
+ className="hover:bg-accent relative rounded-lg p-2 transition-colors"
79
79
  aria-label="Notifications"
80
80
  >
81
- <Bell className="w-5 h-5 text-muted-foreground" />
82
- <span className="absolute top-1.5 right-1.5 w-2 h-2 bg-destructive rounded-full" />
81
+ <Bell className="text-muted-foreground h-5 w-5" />
82
+ <span className="bg-destructive absolute top-1.5 right-1.5 h-2 w-2 rounded-full" />
83
83
  </button>
84
84
 
85
85
  <DropdownMenu.Root>
86
86
  <DropdownMenu.Trigger asChild>
87
- <button className="flex items-center gap-2 p-1.5 hover:bg-accent rounded-lg transition-colors">
88
- <div className="w-8 h-8 bg-linear-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center">
89
- <span className="text-white text-sm font-medium">
87
+ <button className="hover:bg-accent flex items-center gap-2 rounded-lg p-1.5 transition-colors">
88
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-linear-to-br from-blue-500 to-purple-600">
89
+ <span className="text-sm font-medium text-white">
90
90
  {session?.user?.name?.charAt(0)?.toUpperCase() ?? 'A'}
91
91
  </span>
92
92
  </div>
93
- <ChevronDown className="w-4 h-4 text-muted-foreground hidden sm:block" />
93
+ <ChevronDown className="text-muted-foreground hidden h-4 w-4 sm:block" />
94
94
  </button>
95
95
  </DropdownMenu.Trigger>
96
96
 
97
97
  <DropdownMenu.Portal>
98
98
  <DropdownMenu.Content
99
- className="min-w-[200px] bg-popover text-popover-foreground rounded-lg border border-border shadow-lg p-1 z-50"
99
+ className="bg-popover text-popover-foreground border-border z-50 min-w-[200px] rounded-lg border p-1 shadow-lg"
100
100
  align="end"
101
101
  sideOffset={5}
102
102
  >
103
- <div className="px-3 py-2 border-b border-border">
103
+ <div className="border-border border-b px-3 py-2">
104
104
  <p className="text-sm font-medium">{session?.user?.name ?? 'Admin User'}</p>
105
- <p className="text-xs text-muted-foreground">
105
+ <p className="text-muted-foreground text-xs">
106
106
  {session?.user?.email ?? 'admin@example.com'}
107
107
  </p>
108
108
  </div>
109
- <DropdownMenu.Item className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-accent rounded cursor-pointer outline-none">
110
- <User className="w-4 h-4" />
109
+ <DropdownMenu.Item className="hover:bg-accent flex cursor-pointer items-center gap-2 rounded px-3 py-2 text-sm outline-none">
110
+ <User className="h-4 w-4" />
111
111
  Profile
112
112
  </DropdownMenu.Item>
113
113
  <DropdownMenu.Item
114
- className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-accent rounded cursor-pointer outline-none"
114
+ className="hover:bg-accent flex cursor-pointer items-center gap-2 rounded px-3 py-2 text-sm outline-none"
115
115
  onSelect={() => onNavigate('/settings')}
116
116
  >
117
117
  Settings
118
118
  </DropdownMenu.Item>
119
- <DropdownMenu.Separator className="h-px bg-border my-1" />
120
- <DropdownMenu.Item className="flex items-center gap-2 px-3 py-2 text-sm text-destructive hover:bg-accent rounded cursor-pointer outline-none">
119
+ <DropdownMenu.Separator className="bg-border my-1 h-px" />
120
+ <DropdownMenu.Item className="text-destructive hover:bg-accent flex cursor-pointer items-center gap-2 rounded px-3 py-2 text-sm outline-none">
121
121
  Logout
122
122
  </DropdownMenu.Item>
123
123
  </DropdownMenu.Content>
@@ -4,6 +4,7 @@ import { useState, useEffect, type ReactNode } from 'react'
4
4
  import { Sidebar } from './Sidebar.js'
5
5
  import { Header } from './Header.js'
6
6
  import { Breadcrumbs } from '../components/Breadcrumbs.js'
7
+ import { AdminShell } from './primitives/AdminShell.js'
7
8
  import { Toaster } from 'sonner'
8
9
 
9
10
  export interface LayoutProps {
@@ -14,6 +15,20 @@ export interface LayoutProps {
14
15
  children: ReactNode
15
16
  }
16
17
 
18
+ /**
19
+ * Layout — thin shell that wires Sidebar/Header/Breadcrumbs into the
20
+ * `<AdminShell>` primitive.
21
+ *
22
+ * `AdminShell` (in `./primitives/AdminShell.tsx`) owns the actual layout
23
+ * algorithm (CSS Grid on desktop, fixed slide-in overlay on mobile,
24
+ * JS-driven `matchMedia` breakpoint) so that every layout decision lives
25
+ * in one place. Per-product chrome (Sidebar, Header, Breadcrumbs)
26
+ * composes on top of it.
27
+ *
28
+ * DO NOT re-implement layout logic in this file — extend `AdminShell`
29
+ * instead. The previous flex + `fixed`↔`static` toggle caused recurring
30
+ * sidebar-overlap bugs; the grid-based primitive is the contract.
31
+ */
17
32
  export function Layout({ config, session, currentPath, onNavigate, children }: LayoutProps) {
18
33
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
19
34
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
@@ -22,54 +37,32 @@ export function Layout({ config, session, currentPath, onNavigate, children }: L
22
37
  setMobileSidebarOpen(false)
23
38
  }, [currentPath])
24
39
 
25
- useEffect(() => {
26
- const handleResize = () => {
27
- if (window.innerWidth < 1024) {
28
- setMobileSidebarOpen(false)
29
- }
30
- }
31
- window.addEventListener('resize', handleResize)
32
- return () => window.removeEventListener('resize', handleResize)
33
- }, [])
34
-
35
40
  return (
36
- <div className="h-screen flex overflow-hidden bg-background text-foreground">
41
+ <>
37
42
  <Toaster position="bottom-right" />
38
-
39
- {mobileSidebarOpen && (
40
- <div
41
- className="fixed inset-0 bg-black/50 z-40 lg:hidden"
42
- onClick={() => setMobileSidebarOpen(false)}
43
- />
44
- )}
45
-
46
- <div
47
- className={`fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 lg:transform-none ${
48
- mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
49
- }`}
43
+ <AdminShell
44
+ mobileSidebarOpen={mobileSidebarOpen}
45
+ onMobileSidebarClose={() => setMobileSidebarOpen(false)}
46
+ sidebar={
47
+ <Sidebar
48
+ collapsed={sidebarCollapsed}
49
+ onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)}
50
+ currentPath={currentPath}
51
+ onNavigate={onNavigate}
52
+ config={config}
53
+ />
54
+ }
55
+ header={
56
+ <Header
57
+ onToggleSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
58
+ session={session}
59
+ onNavigate={onNavigate}
60
+ />
61
+ }
62
+ breadcrumbs={<Breadcrumbs currentPath={currentPath} onNavigate={onNavigate} />}
50
63
  >
51
- <Sidebar
52
- collapsed={sidebarCollapsed}
53
- onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)}
54
- currentPath={currentPath}
55
- onNavigate={onNavigate}
56
- config={config}
57
- />
58
- </div>
59
-
60
- <div className="flex-1 flex flex-col overflow-hidden">
61
- <Header
62
- onToggleSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
63
- session={session}
64
- onNavigate={onNavigate}
65
- />
66
-
67
- <Breadcrumbs currentPath={currentPath} onNavigate={onNavigate} />
68
-
69
- <main className="flex-1 overflow-auto bg-background">
70
- <div className="h-full">{children}</div>
71
- </main>
72
- </div>
73
- </div>
64
+ <div className="h-full">{children}</div>
65
+ </AdminShell>
66
+ </>
74
67
  )
75
68
  }