@actuate-media/cms-admin 0.9.0 → 0.11.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 (294) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +8 -5
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/layout/primitives.test.d.ts +2 -0
  5. package/dist/__tests__/layout/primitives.test.d.ts.map +1 -0
  6. package/dist/__tests__/layout/primitives.test.js +34 -0
  7. package/dist/__tests__/layout/primitives.test.js.map +1 -0
  8. package/dist/__tests__/lib/cv.test.d.ts +2 -0
  9. package/dist/__tests__/lib/cv.test.d.ts.map +1 -0
  10. package/dist/__tests__/lib/cv.test.js +66 -0
  11. package/dist/__tests__/lib/cv.test.js.map +1 -0
  12. package/dist/actuate-admin.css +1 -1
  13. package/dist/assets/actuate-logo.d.ts +36 -0
  14. package/dist/assets/actuate-logo.d.ts.map +1 -0
  15. package/dist/assets/actuate-logo.js +15 -0
  16. package/dist/assets/actuate-logo.js.map +1 -0
  17. package/dist/components/Breadcrumbs.js +2 -2
  18. package/dist/components/CommandPalette.js +10 -10
  19. package/dist/components/ContentOverviewChart.js +3 -3
  20. package/dist/components/ErrorBoundary.js +1 -1
  21. package/dist/components/FocalPointPicker.js +2 -2
  22. package/dist/components/FolderTree.js +20 -20
  23. package/dist/components/LivePreview.js +3 -3
  24. package/dist/components/LocaleSwitcher.js +1 -1
  25. package/dist/components/MediaPickerModal.js +4 -4
  26. package/dist/components/PresenceIndicator.js +1 -1
  27. package/dist/components/SEOConfigPanel.d.ts +2 -0
  28. package/dist/components/SEOConfigPanel.d.ts.map +1 -0
  29. package/dist/components/SEOConfigPanel.js +174 -0
  30. package/dist/components/SEOConfigPanel.js.map +1 -0
  31. package/dist/components/SEOPanel.js +9 -9
  32. package/dist/components/SEOPerformance.js +2 -2
  33. package/dist/components/SchedulePublishDialog.d.ts +18 -0
  34. package/dist/components/SchedulePublishDialog.d.ts.map +1 -0
  35. package/dist/components/SchedulePublishDialog.js +106 -0
  36. package/dist/components/SchedulePublishDialog.js.map +1 -0
  37. package/dist/components/SharePreviewLinkDialog.d.ts +17 -0
  38. package/dist/components/SharePreviewLinkDialog.d.ts.map +1 -0
  39. package/dist/components/SharePreviewLinkDialog.js +83 -0
  40. package/dist/components/SharePreviewLinkDialog.js.map +1 -0
  41. package/dist/components/TipTapEditor.js +5 -5
  42. package/dist/components/VersionHistory.js +2 -2
  43. package/dist/components/ui/Badge.d.ts +33 -3
  44. package/dist/components/ui/Badge.d.ts.map +1 -1
  45. package/dist/components/ui/Badge.js +42 -8
  46. package/dist/components/ui/Badge.js.map +1 -1
  47. package/dist/components/ui/Button.d.ts +19 -8
  48. package/dist/components/ui/Button.d.ts.map +1 -1
  49. package/dist/components/ui/Button.js +35 -14
  50. package/dist/components/ui/Button.js.map +1 -1
  51. package/dist/components/ui/Card.d.ts +26 -0
  52. package/dist/components/ui/Card.d.ts.map +1 -0
  53. package/dist/components/ui/Card.js +45 -0
  54. package/dist/components/ui/Card.js.map +1 -0
  55. package/dist/components/ui/DataTable.js +1 -1
  56. package/dist/components/ui/Input.d.ts +15 -0
  57. package/dist/components/ui/Input.d.ts.map +1 -0
  58. package/dist/components/ui/Input.js +23 -0
  59. package/dist/components/ui/Input.js.map +1 -0
  60. package/dist/components/ui/SearchInput.js +1 -1
  61. package/dist/components/ui/Select.d.ts +16 -0
  62. package/dist/components/ui/Select.d.ts.map +1 -0
  63. package/dist/components/ui/Select.js +25 -0
  64. package/dist/components/ui/Select.js.map +1 -0
  65. package/dist/components/ui/Toast.js +1 -1
  66. package/dist/components/ui/index.d.ts +10 -4
  67. package/dist/components/ui/index.d.ts.map +1 -1
  68. package/dist/components/ui/index.js +5 -2
  69. package/dist/components/ui/index.js.map +1 -1
  70. package/dist/fields/BlockBuilderField.js +3 -3
  71. package/dist/fields/DateField.js +1 -1
  72. package/dist/fields/RelationshipField.js +3 -3
  73. package/dist/fields/TextField.js +1 -1
  74. package/dist/index.d.ts +6 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +5 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/layout/Header.js +1 -1
  79. package/dist/layout/Layout.d.ts +14 -0
  80. package/dist/layout/Layout.d.ts.map +1 -1
  81. package/dist/layout/Layout.js +17 -11
  82. package/dist/layout/Layout.js.map +1 -1
  83. package/dist/layout/Sidebar.d.ts.map +1 -1
  84. package/dist/layout/Sidebar.js +21 -11
  85. package/dist/layout/Sidebar.js.map +1 -1
  86. package/dist/layout/primitives/AdminShell.d.ts +43 -0
  87. package/dist/layout/primitives/AdminShell.d.ts.map +1 -0
  88. package/dist/layout/primitives/AdminShell.js +51 -0
  89. package/dist/layout/primitives/AdminShell.js.map +1 -0
  90. package/dist/layout/primitives/Box.d.ts +19 -0
  91. package/dist/layout/primitives/Box.d.ts.map +1 -0
  92. package/dist/layout/primitives/Box.js +12 -0
  93. package/dist/layout/primitives/Box.js.map +1 -0
  94. package/dist/layout/primitives/Cluster.d.ts +27 -0
  95. package/dist/layout/primitives/Cluster.d.ts.map +1 -0
  96. package/dist/layout/primitives/Cluster.js +37 -0
  97. package/dist/layout/primitives/Cluster.js.map +1 -0
  98. package/dist/layout/primitives/Grid.d.ts +45 -0
  99. package/dist/layout/primitives/Grid.d.ts.map +1 -0
  100. package/dist/layout/primitives/Grid.js +59 -0
  101. package/dist/layout/primitives/Grid.js.map +1 -0
  102. package/dist/layout/primitives/PageContainer.d.ts +36 -0
  103. package/dist/layout/primitives/PageContainer.d.ts.map +1 -0
  104. package/dist/layout/primitives/PageContainer.js +41 -0
  105. package/dist/layout/primitives/PageContainer.js.map +1 -0
  106. package/dist/layout/primitives/Split.d.ts +34 -0
  107. package/dist/layout/primitives/Split.d.ts.map +1 -0
  108. package/dist/layout/primitives/Split.js +27 -0
  109. package/dist/layout/primitives/Split.js.map +1 -0
  110. package/dist/layout/primitives/Stack.d.ts +23 -0
  111. package/dist/layout/primitives/Stack.d.ts.map +1 -0
  112. package/dist/layout/primitives/Stack.js +34 -0
  113. package/dist/layout/primitives/Stack.js.map +1 -0
  114. package/dist/layout/primitives/index.d.ts +30 -0
  115. package/dist/layout/primitives/index.d.ts.map +1 -0
  116. package/dist/layout/primitives/index.js +22 -0
  117. package/dist/layout/primitives/index.js.map +1 -0
  118. package/dist/layout/primitives/tokens.d.ts +48 -0
  119. package/dist/layout/primitives/tokens.d.ts.map +1 -0
  120. package/dist/layout/primitives/tokens.js +54 -0
  121. package/dist/layout/primitives/tokens.js.map +1 -0
  122. package/dist/lib/cv.d.ts +53 -0
  123. package/dist/lib/cv.d.ts.map +1 -0
  124. package/dist/lib/cv.js +39 -0
  125. package/dist/lib/cv.js.map +1 -0
  126. package/dist/views/ApiKeys.d.ts.map +1 -1
  127. package/dist/views/ApiKeys.js +13 -11
  128. package/dist/views/ApiKeys.js.map +1 -1
  129. package/dist/views/CollectionList.js +8 -8
  130. package/dist/views/Dashboard.d.ts.map +1 -1
  131. package/dist/views/Dashboard.js +333 -78
  132. package/dist/views/Dashboard.js.map +1 -1
  133. package/dist/views/DocumentEdit.d.ts.map +1 -1
  134. package/dist/views/DocumentEdit.js +17 -5
  135. package/dist/views/DocumentEdit.js.map +1 -1
  136. package/dist/views/ForgotPassword.js +2 -2
  137. package/dist/views/FormEditor.js +5 -5
  138. package/dist/views/FormSubmissions.js +6 -6
  139. package/dist/views/Forms.js +2 -2
  140. package/dist/views/Login.d.ts +16 -1
  141. package/dist/views/Login.d.ts.map +1 -1
  142. package/dist/views/Login.js +17 -7
  143. package/dist/views/Login.js.map +1 -1
  144. package/dist/views/MediaBrowser.js +16 -16
  145. package/dist/views/PageEditor.js +2 -2
  146. package/dist/views/Pages.js +10 -10
  147. package/dist/views/PostEditor.js +2 -2
  148. package/dist/views/Posts.js +4 -4
  149. package/dist/views/Redirects.js +4 -4
  150. package/dist/views/ResetPassword.js +2 -2
  151. package/dist/views/SEO.js +6 -6
  152. package/dist/views/ScriptTagEditor.js +4 -4
  153. package/dist/views/ScriptTags.js +2 -2
  154. package/dist/views/Settings.d.ts.map +1 -1
  155. package/dist/views/Settings.js +9 -8
  156. package/dist/views/Settings.js.map +1 -1
  157. package/dist/views/SetupWizard.js +2 -2
  158. package/dist/views/Users.js +4 -4
  159. package/dist/views/page-builder/AIBlockAssist.js +1 -1
  160. package/dist/views/page-builder/AIGenerateDialog.js +10 -10
  161. package/dist/views/page-builder/BlockEditor.js +10 -10
  162. package/dist/views/page-builder/BlockPicker.js +4 -4
  163. package/dist/views/page-builder/BottomBar.js +1 -1
  164. package/dist/views/page-builder/BuilderToolbar.js +2 -2
  165. package/dist/views/page-builder/ContextPanel.js +2 -2
  166. package/dist/views/page-builder/DesignScore.js +9 -9
  167. package/dist/views/page-builder/NodeSettings.js +8 -8
  168. package/dist/views/page-builder/PageBuilder.js +3 -3
  169. package/dist/views/page-builder/PageSettings.js +1 -1
  170. package/dist/views/page-builder/PageTemplates.js +2 -2
  171. package/dist/views/page-builder/SEOPanel.js +13 -13
  172. package/dist/views/page-builder/SavedSections.js +5 -5
  173. package/dist/views/page-builder/TemplatePicker.js +2 -2
  174. package/dist/views/page-builder/block-renderers/CTAPreview.js +5 -5
  175. package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
  176. package/dist/views/page-builder/block-renderers/CodePreview.js +1 -1
  177. package/dist/views/page-builder/block-renderers/FAQPreview.js +3 -3
  178. package/dist/views/page-builder/block-renderers/FallbackPreview.js +1 -1
  179. package/dist/views/page-builder/block-renderers/FormPreview.js +3 -3
  180. package/dist/views/page-builder/block-renderers/GalleryPreview.js +5 -5
  181. package/dist/views/page-builder/block-renderers/HeroPreview.js +3 -3
  182. package/dist/views/page-builder/block-renderers/ImagePreview.js +3 -3
  183. package/dist/views/page-builder/block-renderers/TextPreview.js +3 -3
  184. package/dist/views/page-builder/block-renderers/VideoPreview.js +4 -4
  185. package/dist/views/page-builder/canvas/BlockRenderer.js +1 -1
  186. package/dist/views/page-builder/canvas/BuilderCanvas.js +3 -3
  187. package/dist/views/page-builder/canvas/ColumnRenderer.js +2 -2
  188. package/dist/views/page-builder/canvas/ContainerRenderer.js +2 -2
  189. package/dist/views/page-builder/canvas/RowRenderer.js +2 -2
  190. package/dist/views/page-builder/canvas/SectionRenderer.js +2 -2
  191. package/package.json +6 -2
  192. package/src/AdminRoot.tsx +21 -11
  193. package/src/__tests__/layout/primitives.test.ts +37 -0
  194. package/src/__tests__/lib/cv.test.ts +74 -0
  195. package/src/assets/actuate-logo.tsx +72 -0
  196. package/src/components/Breadcrumbs.tsx +6 -6
  197. package/src/components/CommandPalette.tsx +34 -34
  198. package/src/components/ContentOverviewChart.tsx +3 -3
  199. package/src/components/ErrorBoundary.tsx +3 -3
  200. package/src/components/FocalPointPicker.tsx +4 -4
  201. package/src/components/FolderTree.tsx +38 -38
  202. package/src/components/LivePreview.tsx +16 -16
  203. package/src/components/LocaleSwitcher.tsx +7 -7
  204. package/src/components/MediaPickerModal.tsx +21 -21
  205. package/src/components/PresenceIndicator.tsx +2 -2
  206. package/src/components/SEOConfigPanel.tsx +582 -0
  207. package/src/components/SEOPanel.tsx +46 -46
  208. package/src/components/SEOPerformance.tsx +21 -21
  209. package/src/components/SchedulePublishDialog.tsx +241 -0
  210. package/src/components/SharePreviewLinkDialog.tsx +227 -0
  211. package/src/components/TipTapEditor.tsx +33 -33
  212. package/src/components/VersionHistory.tsx +16 -16
  213. package/src/components/ui/Badge.tsx +66 -14
  214. package/src/components/ui/Button.tsx +70 -33
  215. package/src/components/ui/Card.tsx +101 -0
  216. package/src/components/ui/DataTable.tsx +1 -1
  217. package/src/components/ui/Input.tsx +35 -0
  218. package/src/components/ui/SearchInput.tsx +4 -4
  219. package/src/components/ui/Select.tsx +56 -0
  220. package/src/components/ui/Toast.tsx +1 -1
  221. package/src/components/ui/index.ts +18 -4
  222. package/src/fields/BlockBuilderField.tsx +3 -3
  223. package/src/fields/DateField.tsx +1 -1
  224. package/src/fields/RelationshipField.tsx +10 -10
  225. package/src/fields/TextField.tsx +1 -1
  226. package/src/index.ts +32 -0
  227. package/src/layout/Header.tsx +28 -28
  228. package/src/layout/Layout.tsx +39 -46
  229. package/src/layout/Sidebar.tsx +37 -64
  230. package/src/layout/primitives/AdminShell.tsx +118 -0
  231. package/src/layout/primitives/Box.tsx +30 -0
  232. package/src/layout/primitives/Cluster.tsx +74 -0
  233. package/src/layout/primitives/Grid.tsx +120 -0
  234. package/src/layout/primitives/PageContainer.tsx +96 -0
  235. package/src/layout/primitives/Split.tsx +73 -0
  236. package/src/layout/primitives/Stack.tsx +67 -0
  237. package/src/layout/primitives/index.ts +36 -0
  238. package/src/layout/primitives/tokens.ts +76 -0
  239. package/src/lib/cv.ts +96 -0
  240. package/src/styles/build-input.css +1 -1
  241. package/src/views/ApiKeys.tsx +57 -57
  242. package/src/views/CollectionList.tsx +30 -30
  243. package/src/views/Dashboard.tsx +737 -186
  244. package/src/views/DocumentEdit.tsx +90 -10
  245. package/src/views/ForgotPassword.tsx +18 -18
  246. package/src/views/FormEditor.tsx +75 -75
  247. package/src/views/FormSubmissions.tsx +76 -76
  248. package/src/views/Forms.tsx +27 -27
  249. package/src/views/Login.tsx +65 -25
  250. package/src/views/MediaBrowser.tsx +127 -127
  251. package/src/views/PageEditor.tsx +25 -25
  252. package/src/views/Pages.tsx +59 -59
  253. package/src/views/PostEditor.tsx +37 -37
  254. package/src/views/Posts.tsx +48 -48
  255. package/src/views/Redirects.tsx +21 -21
  256. package/src/views/ResetPassword.tsx +28 -28
  257. package/src/views/SEO.tsx +144 -144
  258. package/src/views/ScriptTagEditor.tsx +24 -24
  259. package/src/views/ScriptTags.tsx +10 -10
  260. package/src/views/Settings.tsx +88 -80
  261. package/src/views/SetupWizard.tsx +28 -28
  262. package/src/views/Users.tsx +20 -20
  263. package/src/views/page-builder/AIBlockAssist.tsx +1 -1
  264. package/src/views/page-builder/AIGenerateDialog.tsx +63 -63
  265. package/src/views/page-builder/BlockEditor.tsx +26 -26
  266. package/src/views/page-builder/BlockPicker.tsx +22 -22
  267. package/src/views/page-builder/BottomBar.tsx +8 -8
  268. package/src/views/page-builder/BuilderToolbar.tsx +17 -17
  269. package/src/views/page-builder/ContextPanel.tsx +3 -3
  270. package/src/views/page-builder/DesignScore.tsx +21 -21
  271. package/src/views/page-builder/NodeSettings.tsx +27 -27
  272. package/src/views/page-builder/PageBuilder.tsx +11 -11
  273. package/src/views/page-builder/PageSettings.tsx +4 -4
  274. package/src/views/page-builder/PageTemplates.tsx +18 -18
  275. package/src/views/page-builder/SEOPanel.tsx +53 -53
  276. package/src/views/page-builder/SavedSections.tsx +37 -37
  277. package/src/views/page-builder/TemplatePicker.tsx +17 -17
  278. package/src/views/page-builder/block-renderers/CTAPreview.tsx +13 -13
  279. package/src/views/page-builder/block-renderers/CardsPreview.tsx +5 -5
  280. package/src/views/page-builder/block-renderers/CodePreview.tsx +6 -6
  281. package/src/views/page-builder/block-renderers/FAQPreview.tsx +13 -13
  282. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +3 -3
  283. package/src/views/page-builder/block-renderers/FormPreview.tsx +20 -20
  284. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +8 -8
  285. package/src/views/page-builder/block-renderers/HeroPreview.tsx +16 -16
  286. package/src/views/page-builder/block-renderers/ImagePreview.tsx +4 -4
  287. package/src/views/page-builder/block-renderers/TextPreview.tsx +14 -14
  288. package/src/views/page-builder/block-renderers/VideoPreview.tsx +12 -12
  289. package/src/views/page-builder/canvas/BlockRenderer.tsx +4 -4
  290. package/src/views/page-builder/canvas/BuilderCanvas.tsx +6 -6
  291. package/src/views/page-builder/canvas/ColumnRenderer.tsx +3 -3
  292. package/src/views/page-builder/canvas/ContainerRenderer.tsx +2 -2
  293. package/src/views/page-builder/canvas/RowRenderer.tsx +2 -2
  294. package/src/views/page-builder/canvas/SectionRenderer.tsx +2 -2
@@ -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
  }
@@ -25,8 +25,13 @@ import {
25
25
  KeyRound,
26
26
  } from 'lucide-react'
27
27
  import type { LucideIcon } from 'lucide-react'
28
+ import { ActuateBrandLogo } from '../assets/actuate-logo.js'
28
29
 
29
- function ActuateLogo({ className }: { className?: string }) {
30
+ /**
31
+ * Compact mark used in the collapsed sidebar — just the "C" symbol from the
32
+ * full lockup, drawn as its own simple SVG so it stays crisp at 32×32.
33
+ */
34
+ function ActuateMark({ className }: { className?: string }) {
30
35
  return (
31
36
  <svg
32
37
  viewBox="0 0 40 44"
@@ -35,53 +40,21 @@ function ActuateLogo({ className }: { className?: string }) {
35
40
  className={className}
36
41
  aria-hidden="true"
37
42
  >
38
- {/* Upward arrow / chevron */}
39
- <polygon points="20,2 6,18 12,18 20,8 28,18 34,18" fill="#E8646A" />
40
- {/* Three vertical bars */}
41
- <rect x="11" y="20" width="4" height="22" rx="1" fill="#E8646A" />
42
- <rect x="18" y="20" width="4" height="22" rx="1" fill="#E8646A" />
43
- <rect x="25" y="20" width="4" height="22" rx="1" fill="#E8646A" />
43
+ <polygon points="20,2 6,18 12,18 20,8 28,18 34,18" fill="#F05E65" />
44
+ <rect x="11" y="20" width="4" height="22" rx="1" fill="#F05E65" />
45
+ <rect x="18" y="20" width="4" height="22" rx="1" fill="#F05E65" />
46
+ <rect x="25" y="20" width="4" height="22" rx="1" fill="#F05E65" />
44
47
  </svg>
45
48
  )
46
49
  }
47
50
 
51
+ /**
52
+ * Full Actuate Media lockup. Inline SVG with a transparent background so it
53
+ * sits naturally on whatever surface the sidebar uses (light or dark theme,
54
+ * custom branding background, etc.).
55
+ */
48
56
  function ActuateWordmark({ className }: { className?: string }) {
49
- return (
50
- <svg
51
- viewBox="0 0 170 44"
52
- fill="none"
53
- xmlns="http://www.w3.org/2000/svg"
54
- className={className}
55
- aria-hidden="true"
56
- >
57
- <polygon points="20,2 6,18 12,18 20,8 28,18 34,18" fill="#E8646A" />
58
- <rect x="11" y="20" width="4" height="22" rx="1" fill="#E8646A" />
59
- <rect x="18" y="20" width="4" height="22" rx="1" fill="#E8646A" />
60
- <rect x="25" y="20" width="4" height="22" rx="1" fill="#E8646A" />
61
- <text
62
- x="44"
63
- y="25"
64
- fontFamily="system-ui, sans-serif"
65
- fontSize="17"
66
- fontWeight="600"
67
- letterSpacing="1.5"
68
- fill="#E8646A"
69
- >
70
- ACTUATE
71
- </text>
72
- <text
73
- x="44"
74
- y="40"
75
- fontFamily="system-ui, sans-serif"
76
- fontSize="10"
77
- fontWeight="500"
78
- letterSpacing="4"
79
- fill="#9CA3AF"
80
- >
81
- MEDIA
82
- </text>
83
- </svg>
84
- )
57
+ return <ActuateBrandLogo className={className} />
85
58
  }
86
59
 
87
60
  const ICON_MAP: Record<string, LucideIcon> = {
@@ -104,21 +77,21 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
104
77
 
105
78
  if (collapsed) {
106
79
  if (customLogo) {
107
- return <img src={customLogo} alt={brandName ?? 'Admin'} className="w-8 h-8 object-contain" />
80
+ return <img src={customLogo} alt={brandName ?? 'Admin'} className="h-8 w-8 object-contain" />
108
81
  }
109
- return <ActuateLogo className="w-8 h-8" />
82
+ return <ActuateMark className="h-8 w-8" />
110
83
  }
111
84
 
112
85
  if (customLogo) {
113
86
  return (
114
- <div className="flex items-center gap-2.5 min-w-0">
87
+ <div className="flex min-w-0 items-center gap-2.5">
115
88
  <img
116
89
  src={customLogo}
117
90
  alt={brandName ?? 'Admin'}
118
- className="h-8 w-auto object-contain shrink-0"
91
+ className="h-8 w-auto shrink-0 object-contain"
119
92
  />
120
93
  {brandName && (
121
- <span className="text-sm font-semibold text-sidebar-foreground truncate">
94
+ <span className="text-sidebar-foreground truncate text-sm font-semibold">
122
95
  {brandName}
123
96
  </span>
124
97
  )}
@@ -128,14 +101,14 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
128
101
 
129
102
  if (brandName) {
130
103
  return (
131
- <div className="flex items-center gap-2.5 min-w-0">
132
- <ActuateLogo className="w-7 h-7 shrink-0" />
133
- <span className="text-sm font-semibold text-sidebar-foreground truncate">{brandName}</span>
104
+ <div className="flex min-w-0 items-center gap-2.5">
105
+ <ActuateMark className="h-7 w-7 shrink-0" />
106
+ <span className="text-sidebar-foreground truncate text-sm font-semibold">{brandName}</span>
134
107
  </div>
135
108
  )
136
109
  }
137
110
 
138
- return <ActuateWordmark className="h-8 w-auto" />
111
+ return <ActuateWordmark className="h-9" />
139
112
  }
140
113
 
141
114
  const defaultNavItems = [
@@ -169,12 +142,12 @@ export function Sidebar({
169
142
 
170
143
  return (
171
144
  <aside
172
- className={`h-full bg-sidebar border-r border-sidebar-border transition-all duration-200 ${
145
+ className={`bg-sidebar border-sidebar-border h-full border-r transition-all duration-200 ${
173
146
  collapsed ? 'w-20' : 'w-64'
174
147
  }`}
175
148
  >
176
149
  <div
177
- className={`flex items-center h-14 border-b border-sidebar-border px-4 ${
150
+ className={`border-sidebar-border flex h-14 items-center border-b px-4 ${
178
151
  collapsed ? 'justify-center' : 'justify-between'
179
152
  }`}
180
153
  >
@@ -182,18 +155,18 @@ export function Sidebar({
182
155
  {collapsed && <BrandLogo config={config} collapsed={true} />}
183
156
  <button
184
157
  onClick={onToggleCollapse}
185
- className="hidden lg:block p-2 hover:bg-sidebar-accent rounded-lg transition-colors shrink-0"
158
+ className="hover:bg-sidebar-accent hidden shrink-0 rounded-lg p-2 transition-colors md:block"
186
159
  aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
187
160
  >
188
161
  {collapsed ? (
189
- <ChevronRight className="w-4 h-4 text-sidebar-foreground" />
162
+ <ChevronRight className="text-sidebar-foreground h-4 w-4" />
190
163
  ) : (
191
- <ChevronLeft className="w-4 h-4 text-sidebar-foreground" />
164
+ <ChevronLeft className="text-sidebar-foreground h-4 w-4" />
192
165
  )}
193
166
  </button>
194
167
  </div>
195
168
 
196
- <nav className="p-3 space-y-1">
169
+ <nav className="space-y-1 p-3">
197
170
  {navItems.map((item, idx) => {
198
171
  const Icon = item.icon
199
172
  const isActive =
@@ -205,27 +178,27 @@ export function Sidebar({
205
178
  return (
206
179
  <div key={item.path}>
207
180
  {showGroupLabel && !collapsed && (
208
- <div className="pt-3 pb-1 px-3">
209
- <span className="text-[10px] font-semibold uppercase tracking-wider text-sidebar-foreground/50">
181
+ <div className="px-3 pt-3 pb-1">
182
+ <span className="text-sidebar-foreground/50 text-[10px] font-semibold tracking-wider uppercase">
210
183
  {item.group}
211
184
  </span>
212
185
  </div>
213
186
  )}
214
187
  {showGroupLabel && collapsed && (
215
- <div className="pt-2 pb-1 flex justify-center">
216
- <span className="w-4 border-t border-sidebar-foreground/20" />
188
+ <div className="flex justify-center pt-2 pb-1">
189
+ <span className="border-sidebar-foreground/20 w-4 border-t" />
217
190
  </div>
218
191
  )}
219
192
  <button
220
193
  onClick={() => onNavigate(item.path)}
221
- className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors w-full text-left ${
194
+ className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left transition-colors ${
222
195
  isActive
223
196
  ? 'bg-sidebar-accent text-sidebar-primary'
224
197
  : 'text-sidebar-foreground hover:bg-sidebar-accent'
225
198
  } ${collapsed ? 'justify-center' : ''}`}
226
199
  title={collapsed ? item.label : ''}
227
200
  >
228
- <Icon className="w-5 h-5 shrink-0" />
201
+ <Icon className="h-5 w-5 shrink-0" />
229
202
  {!collapsed && <span className="text-sm font-medium">{item.label}</span>}
230
203
  </button>
231
204
  </div>
@@ -0,0 +1,118 @@
1
+ import { useEffect, useState } from 'react'
2
+ import type { ReactNode } from 'react'
3
+
4
+ export interface AdminShellProps {
5
+ /** Sidebar/navigation slot. Rendered in the left column on desktop. */
6
+ sidebar: ReactNode
7
+ /** Top header slot. Rendered above the main content. */
8
+ header?: ReactNode
9
+ /** Optional breadcrumb / sub-header slot rendered below the header. */
10
+ breadcrumbs?: ReactNode
11
+ /** Main content. Scrolls independently of the sidebar. */
12
+ children: ReactNode
13
+ /**
14
+ * Whether the mobile sidebar overlay is currently open. Controlled.
15
+ * AdminShell renders the backdrop + slide transform; the consumer owns
16
+ * the open state and the hamburger button that toggles it.
17
+ */
18
+ mobileSidebarOpen?: boolean
19
+ onMobileSidebarClose?: () => void
20
+ /**
21
+ * Breakpoint above which the sidebar docks beside the content instead
22
+ * of overlaying on top of it. Mirrors Tailwind's `md` (768px) default.
23
+ */
24
+ desktopBreakpoint?: number
25
+ }
26
+
27
+ /**
28
+ * The canonical admin chrome. Owns the entire viewport, splits into
29
+ * `sidebar | (header + breadcrumbs + content)`, and handles the mobile
30
+ * overlay transition.
31
+ *
32
+ * Implementation notes:
33
+ * - Desktop layout is CSS Grid with `gridTemplateColumns: 'auto minmax(0, 1fr)'`.
34
+ * The `minmax(0, 1fr)` is load-bearing: it lets the content shrink
35
+ * below its intrinsic width when the sidebar takes its share, which
36
+ * is the one thing flex-based shells fail at.
37
+ * - Mobile layout is a single block with the sidebar absolutely
38
+ * positioned and slid in via `transform: translateX(…)`. We avoid
39
+ * the brittle `fixed` ↔ `static` toggle that caused recurring sidebar
40
+ * overlap bugs in the old layout.
41
+ * - The desktop/mobile decision is made in JS via `matchMedia` so the
42
+ * layout is independent of Tailwind's `md:` utility compilation. If
43
+ * the CSS bundle is stale or partially loaded the layout still works.
44
+ */
45
+ export function AdminShell({
46
+ sidebar,
47
+ header,
48
+ breadcrumbs,
49
+ children,
50
+ mobileSidebarOpen = false,
51
+ onMobileSidebarClose,
52
+ desktopBreakpoint = 768,
53
+ }: AdminShellProps) {
54
+ const [isDesktop, setIsDesktop] = useState(false)
55
+
56
+ useEffect(() => {
57
+ if (typeof window === 'undefined') return
58
+ const mq = window.matchMedia(`(min-width: ${desktopBreakpoint}px)`)
59
+ const handler = () => setIsDesktop(mq.matches)
60
+ handler()
61
+ mq.addEventListener('change', handler)
62
+ return () => mq.removeEventListener('change', handler)
63
+ }, [desktopBreakpoint])
64
+
65
+ return (
66
+ <div
67
+ className="bg-background text-foreground h-screen overflow-hidden"
68
+ style={
69
+ isDesktop
70
+ ? { display: 'grid', gridTemplateColumns: 'auto minmax(0, 1fr)' }
71
+ : { display: 'block', position: 'relative' }
72
+ }
73
+ >
74
+ {!isDesktop && mobileSidebarOpen && (
75
+ <div
76
+ aria-hidden
77
+ className="fixed inset-0 z-40 bg-black/30 backdrop-blur-sm"
78
+ onClick={onMobileSidebarClose}
79
+ />
80
+ )}
81
+
82
+ <div
83
+ className="z-50"
84
+ style={
85
+ isDesktop
86
+ ? {
87
+ gridColumn: '1 / 2',
88
+ height: '100vh',
89
+ overflow: 'hidden',
90
+ }
91
+ : {
92
+ position: 'fixed',
93
+ top: 0,
94
+ bottom: 0,
95
+ left: 0,
96
+ transform: mobileSidebarOpen ? 'translateX(0)' : 'translateX(-100%)',
97
+ transition: 'transform 300ms ease',
98
+ }
99
+ }
100
+ >
101
+ {sidebar}
102
+ </div>
103
+
104
+ <div
105
+ className="flex flex-col overflow-hidden"
106
+ style={
107
+ isDesktop
108
+ ? { gridColumn: '2 / 3', height: '100vh', minWidth: 0 }
109
+ : { height: '100vh', minWidth: 0 }
110
+ }
111
+ >
112
+ {header}
113
+ {breadcrumbs}
114
+ <main className="flex-1 overflow-y-auto">{children}</main>
115
+ </div>
116
+ </div>
117
+ )
118
+ }
@@ -0,0 +1,30 @@
1
+ import { forwardRef } from 'react'
2
+ import type { HTMLAttributes, ReactNode, ElementType } from 'react'
3
+
4
+ export interface BoxProps extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
5
+ /**
6
+ * The element to render. Defaults to `div`. Use semantic elements (`<section>`,
7
+ * `<article>`, etc.) for accessibility — Box is the lowest-level escape
8
+ * hatch and exists primarily so views don't reach for raw `<div>` with
9
+ * five Tailwind utilities.
10
+ */
11
+ as?: ElementType
12
+ children?: ReactNode
13
+ }
14
+
15
+ /**
16
+ * The lowest-level primitive — a forwardRef'd div (by default) that exists
17
+ * so consumers always have a stable, semantically-tagged container to
18
+ * compose around. Prefer Box over a bare `<div>` to keep the design system
19
+ * audit clean.
20
+ */
21
+ export const Box = forwardRef<HTMLElement, BoxProps>(function Box(
22
+ { as: Component = 'div', children, ...rest },
23
+ ref,
24
+ ) {
25
+ return (
26
+ <Component ref={ref} {...rest}>
27
+ {children}
28
+ </Component>
29
+ )
30
+ })
@@ -0,0 +1,74 @@
1
+ import type { HTMLAttributes, ReactNode, ElementType } from 'react'
2
+ import type { SpaceToken } from './tokens.js'
3
+
4
+ export type ClusterAlign = 'start' | 'center' | 'end' | 'baseline' | 'stretch'
5
+ export type ClusterJustify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'
6
+
7
+ export interface ClusterProps extends HTMLAttributes<HTMLElement> {
8
+ /** Gap between children. Maps to Tailwind `gap-{space}`. */
9
+ space?: SpaceToken
10
+ /** Render as a different element. Defaults to `div`. */
11
+ as?: ElementType
12
+ /** Align items on the cross axis. */
13
+ align?: ClusterAlign
14
+ /** Distribute items along the main axis. */
15
+ justify?: ClusterJustify
16
+ /** Reverse the order of children. */
17
+ reverse?: boolean
18
+ /** Disable wrapping (default: true, items wrap on overflow). */
19
+ noWrap?: boolean
20
+ children?: ReactNode
21
+ }
22
+
23
+ const ALIGN_CLASS: Record<ClusterAlign, string> = {
24
+ start: 'items-start',
25
+ center: 'items-center',
26
+ end: 'items-end',
27
+ baseline: 'items-baseline',
28
+ stretch: 'items-stretch',
29
+ }
30
+
31
+ const JUSTIFY_CLASS: Record<ClusterJustify, string> = {
32
+ start: 'justify-start',
33
+ center: 'justify-center',
34
+ end: 'justify-end',
35
+ between: 'justify-between',
36
+ around: 'justify-around',
37
+ evenly: 'justify-evenly',
38
+ }
39
+
40
+ /**
41
+ * Horizontal cluster of items with a consistent gap, wrapping by default.
42
+ * The canonical use case is action toolbars (`<Cluster justify="end">`),
43
+ * badge groups, breadcrumbs, etc. Never reach for `flex flex-wrap gap-2`
44
+ * directly — use Cluster.
45
+ */
46
+ export function Cluster({
47
+ space = '2',
48
+ as: Component = 'div',
49
+ align = 'center',
50
+ justify = 'start',
51
+ reverse,
52
+ noWrap,
53
+ className = '',
54
+ children,
55
+ ...rest
56
+ }: ClusterProps) {
57
+ const classes = [
58
+ 'flex',
59
+ reverse ? 'flex-row-reverse' : 'flex-row',
60
+ noWrap ? 'flex-nowrap' : 'flex-wrap',
61
+ `gap-${space}`,
62
+ ALIGN_CLASS[align],
63
+ JUSTIFY_CLASS[justify],
64
+ className,
65
+ ]
66
+ .filter(Boolean)
67
+ .join(' ')
68
+
69
+ return (
70
+ <Component className={classes} {...rest}>
71
+ {children}
72
+ </Component>
73
+ )
74
+ }