@actuate-media/cms-admin 0.10.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 (284) 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.js +1 -1
  34. package/dist/components/SharePreviewLinkDialog.js +1 -1
  35. package/dist/components/TipTapEditor.js +5 -5
  36. package/dist/components/VersionHistory.js +2 -2
  37. package/dist/components/ui/Badge.d.ts +33 -3
  38. package/dist/components/ui/Badge.d.ts.map +1 -1
  39. package/dist/components/ui/Badge.js +42 -8
  40. package/dist/components/ui/Badge.js.map +1 -1
  41. package/dist/components/ui/Button.d.ts +19 -8
  42. package/dist/components/ui/Button.d.ts.map +1 -1
  43. package/dist/components/ui/Button.js +35 -14
  44. package/dist/components/ui/Button.js.map +1 -1
  45. package/dist/components/ui/Card.d.ts +26 -0
  46. package/dist/components/ui/Card.d.ts.map +1 -0
  47. package/dist/components/ui/Card.js +45 -0
  48. package/dist/components/ui/Card.js.map +1 -0
  49. package/dist/components/ui/DataTable.js +1 -1
  50. package/dist/components/ui/Input.d.ts +15 -0
  51. package/dist/components/ui/Input.d.ts.map +1 -0
  52. package/dist/components/ui/Input.js +23 -0
  53. package/dist/components/ui/Input.js.map +1 -0
  54. package/dist/components/ui/SearchInput.js +1 -1
  55. package/dist/components/ui/Select.d.ts +16 -0
  56. package/dist/components/ui/Select.d.ts.map +1 -0
  57. package/dist/components/ui/Select.js +25 -0
  58. package/dist/components/ui/Select.js.map +1 -0
  59. package/dist/components/ui/Toast.js +1 -1
  60. package/dist/components/ui/index.d.ts +10 -4
  61. package/dist/components/ui/index.d.ts.map +1 -1
  62. package/dist/components/ui/index.js +5 -2
  63. package/dist/components/ui/index.js.map +1 -1
  64. package/dist/fields/BlockBuilderField.js +3 -3
  65. package/dist/fields/DateField.js +1 -1
  66. package/dist/fields/RelationshipField.js +3 -3
  67. package/dist/fields/TextField.js +1 -1
  68. package/dist/index.d.ts +2 -0
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +3 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/layout/Header.js +1 -1
  73. package/dist/layout/Layout.d.ts +14 -0
  74. package/dist/layout/Layout.d.ts.map +1 -1
  75. package/dist/layout/Layout.js +17 -11
  76. package/dist/layout/Layout.js.map +1 -1
  77. package/dist/layout/Sidebar.d.ts.map +1 -1
  78. package/dist/layout/Sidebar.js +21 -11
  79. package/dist/layout/Sidebar.js.map +1 -1
  80. package/dist/layout/primitives/AdminShell.d.ts +43 -0
  81. package/dist/layout/primitives/AdminShell.d.ts.map +1 -0
  82. package/dist/layout/primitives/AdminShell.js +51 -0
  83. package/dist/layout/primitives/AdminShell.js.map +1 -0
  84. package/dist/layout/primitives/Box.d.ts +19 -0
  85. package/dist/layout/primitives/Box.d.ts.map +1 -0
  86. package/dist/layout/primitives/Box.js +12 -0
  87. package/dist/layout/primitives/Box.js.map +1 -0
  88. package/dist/layout/primitives/Cluster.d.ts +27 -0
  89. package/dist/layout/primitives/Cluster.d.ts.map +1 -0
  90. package/dist/layout/primitives/Cluster.js +37 -0
  91. package/dist/layout/primitives/Cluster.js.map +1 -0
  92. package/dist/layout/primitives/Grid.d.ts +45 -0
  93. package/dist/layout/primitives/Grid.d.ts.map +1 -0
  94. package/dist/layout/primitives/Grid.js +59 -0
  95. package/dist/layout/primitives/Grid.js.map +1 -0
  96. package/dist/layout/primitives/PageContainer.d.ts +36 -0
  97. package/dist/layout/primitives/PageContainer.d.ts.map +1 -0
  98. package/dist/layout/primitives/PageContainer.js +41 -0
  99. package/dist/layout/primitives/PageContainer.js.map +1 -0
  100. package/dist/layout/primitives/Split.d.ts +34 -0
  101. package/dist/layout/primitives/Split.d.ts.map +1 -0
  102. package/dist/layout/primitives/Split.js +27 -0
  103. package/dist/layout/primitives/Split.js.map +1 -0
  104. package/dist/layout/primitives/Stack.d.ts +23 -0
  105. package/dist/layout/primitives/Stack.d.ts.map +1 -0
  106. package/dist/layout/primitives/Stack.js +34 -0
  107. package/dist/layout/primitives/Stack.js.map +1 -0
  108. package/dist/layout/primitives/index.d.ts +30 -0
  109. package/dist/layout/primitives/index.d.ts.map +1 -0
  110. package/dist/layout/primitives/index.js +22 -0
  111. package/dist/layout/primitives/index.js.map +1 -0
  112. package/dist/layout/primitives/tokens.d.ts +48 -0
  113. package/dist/layout/primitives/tokens.d.ts.map +1 -0
  114. package/dist/layout/primitives/tokens.js +54 -0
  115. package/dist/layout/primitives/tokens.js.map +1 -0
  116. package/dist/lib/cv.d.ts +53 -0
  117. package/dist/lib/cv.d.ts.map +1 -0
  118. package/dist/lib/cv.js +39 -0
  119. package/dist/lib/cv.js.map +1 -0
  120. package/dist/views/ApiKeys.js +7 -7
  121. package/dist/views/CollectionList.js +8 -8
  122. package/dist/views/Dashboard.d.ts.map +1 -1
  123. package/dist/views/Dashboard.js +333 -78
  124. package/dist/views/Dashboard.js.map +1 -1
  125. package/dist/views/DocumentEdit.js +3 -3
  126. package/dist/views/ForgotPassword.js +2 -2
  127. package/dist/views/FormEditor.js +5 -5
  128. package/dist/views/FormSubmissions.js +6 -6
  129. package/dist/views/Forms.js +2 -2
  130. package/dist/views/Login.d.ts +16 -1
  131. package/dist/views/Login.d.ts.map +1 -1
  132. package/dist/views/Login.js +17 -7
  133. package/dist/views/Login.js.map +1 -1
  134. package/dist/views/MediaBrowser.js +16 -16
  135. package/dist/views/PageEditor.js +2 -2
  136. package/dist/views/Pages.js +10 -10
  137. package/dist/views/PostEditor.js +2 -2
  138. package/dist/views/Posts.js +4 -4
  139. package/dist/views/Redirects.js +4 -4
  140. package/dist/views/ResetPassword.js +2 -2
  141. package/dist/views/SEO.js +6 -6
  142. package/dist/views/ScriptTagEditor.js +4 -4
  143. package/dist/views/ScriptTags.js +2 -2
  144. package/dist/views/Settings.d.ts.map +1 -1
  145. package/dist/views/Settings.js +9 -8
  146. package/dist/views/Settings.js.map +1 -1
  147. package/dist/views/SetupWizard.js +2 -2
  148. package/dist/views/Users.js +4 -4
  149. package/dist/views/page-builder/AIBlockAssist.js +1 -1
  150. package/dist/views/page-builder/AIGenerateDialog.js +10 -10
  151. package/dist/views/page-builder/BlockEditor.js +10 -10
  152. package/dist/views/page-builder/BlockPicker.js +4 -4
  153. package/dist/views/page-builder/BottomBar.js +1 -1
  154. package/dist/views/page-builder/BuilderToolbar.js +2 -2
  155. package/dist/views/page-builder/ContextPanel.js +2 -2
  156. package/dist/views/page-builder/DesignScore.js +9 -9
  157. package/dist/views/page-builder/NodeSettings.js +8 -8
  158. package/dist/views/page-builder/PageBuilder.js +3 -3
  159. package/dist/views/page-builder/PageSettings.js +1 -1
  160. package/dist/views/page-builder/PageTemplates.js +2 -2
  161. package/dist/views/page-builder/SEOPanel.js +13 -13
  162. package/dist/views/page-builder/SavedSections.js +5 -5
  163. package/dist/views/page-builder/TemplatePicker.js +2 -2
  164. package/dist/views/page-builder/block-renderers/CTAPreview.js +5 -5
  165. package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
  166. package/dist/views/page-builder/block-renderers/CodePreview.js +1 -1
  167. package/dist/views/page-builder/block-renderers/FAQPreview.js +3 -3
  168. package/dist/views/page-builder/block-renderers/FallbackPreview.js +1 -1
  169. package/dist/views/page-builder/block-renderers/FormPreview.js +3 -3
  170. package/dist/views/page-builder/block-renderers/GalleryPreview.js +5 -5
  171. package/dist/views/page-builder/block-renderers/HeroPreview.js +3 -3
  172. package/dist/views/page-builder/block-renderers/ImagePreview.js +3 -3
  173. package/dist/views/page-builder/block-renderers/TextPreview.js +3 -3
  174. package/dist/views/page-builder/block-renderers/VideoPreview.js +4 -4
  175. package/dist/views/page-builder/canvas/BlockRenderer.js +1 -1
  176. package/dist/views/page-builder/canvas/BuilderCanvas.js +3 -3
  177. package/dist/views/page-builder/canvas/ColumnRenderer.js +2 -2
  178. package/dist/views/page-builder/canvas/ContainerRenderer.js +2 -2
  179. package/dist/views/page-builder/canvas/RowRenderer.js +2 -2
  180. package/dist/views/page-builder/canvas/SectionRenderer.js +2 -2
  181. package/package.json +6 -2
  182. package/src/AdminRoot.tsx +21 -11
  183. package/src/__tests__/layout/primitives.test.ts +37 -0
  184. package/src/__tests__/lib/cv.test.ts +74 -0
  185. package/src/assets/actuate-logo.tsx +72 -0
  186. package/src/components/Breadcrumbs.tsx +6 -6
  187. package/src/components/CommandPalette.tsx +34 -34
  188. package/src/components/ContentOverviewChart.tsx +3 -3
  189. package/src/components/ErrorBoundary.tsx +3 -3
  190. package/src/components/FocalPointPicker.tsx +4 -4
  191. package/src/components/FolderTree.tsx +38 -38
  192. package/src/components/LivePreview.tsx +16 -16
  193. package/src/components/LocaleSwitcher.tsx +7 -7
  194. package/src/components/MediaPickerModal.tsx +21 -21
  195. package/src/components/PresenceIndicator.tsx +2 -2
  196. package/src/components/SEOConfigPanel.tsx +582 -0
  197. package/src/components/SEOPanel.tsx +46 -46
  198. package/src/components/SEOPerformance.tsx +21 -21
  199. package/src/components/SchedulePublishDialog.tsx +4 -4
  200. package/src/components/SharePreviewLinkDialog.tsx +1 -1
  201. package/src/components/TipTapEditor.tsx +33 -33
  202. package/src/components/VersionHistory.tsx +16 -16
  203. package/src/components/ui/Badge.tsx +66 -14
  204. package/src/components/ui/Button.tsx +70 -33
  205. package/src/components/ui/Card.tsx +101 -0
  206. package/src/components/ui/DataTable.tsx +1 -1
  207. package/src/components/ui/Input.tsx +35 -0
  208. package/src/components/ui/SearchInput.tsx +4 -4
  209. package/src/components/ui/Select.tsx +56 -0
  210. package/src/components/ui/Toast.tsx +1 -1
  211. package/src/components/ui/index.ts +18 -4
  212. package/src/fields/BlockBuilderField.tsx +3 -3
  213. package/src/fields/DateField.tsx +1 -1
  214. package/src/fields/RelationshipField.tsx +10 -10
  215. package/src/fields/TextField.tsx +1 -1
  216. package/src/index.ts +28 -0
  217. package/src/layout/Header.tsx +28 -28
  218. package/src/layout/Layout.tsx +39 -46
  219. package/src/layout/Sidebar.tsx +37 -64
  220. package/src/layout/primitives/AdminShell.tsx +118 -0
  221. package/src/layout/primitives/Box.tsx +30 -0
  222. package/src/layout/primitives/Cluster.tsx +74 -0
  223. package/src/layout/primitives/Grid.tsx +120 -0
  224. package/src/layout/primitives/PageContainer.tsx +96 -0
  225. package/src/layout/primitives/Split.tsx +73 -0
  226. package/src/layout/primitives/Stack.tsx +67 -0
  227. package/src/layout/primitives/index.ts +36 -0
  228. package/src/layout/primitives/tokens.ts +76 -0
  229. package/src/lib/cv.ts +96 -0
  230. package/src/styles/build-input.css +1 -1
  231. package/src/views/ApiKeys.tsx +57 -57
  232. package/src/views/CollectionList.tsx +30 -30
  233. package/src/views/Dashboard.tsx +737 -186
  234. package/src/views/DocumentEdit.tsx +9 -9
  235. package/src/views/ForgotPassword.tsx +18 -18
  236. package/src/views/FormEditor.tsx +75 -75
  237. package/src/views/FormSubmissions.tsx +76 -76
  238. package/src/views/Forms.tsx +27 -27
  239. package/src/views/Login.tsx +65 -25
  240. package/src/views/MediaBrowser.tsx +127 -127
  241. package/src/views/PageEditor.tsx +25 -25
  242. package/src/views/Pages.tsx +59 -59
  243. package/src/views/PostEditor.tsx +37 -37
  244. package/src/views/Posts.tsx +48 -48
  245. package/src/views/Redirects.tsx +21 -21
  246. package/src/views/ResetPassword.tsx +28 -28
  247. package/src/views/SEO.tsx +144 -144
  248. package/src/views/ScriptTagEditor.tsx +24 -24
  249. package/src/views/ScriptTags.tsx +10 -10
  250. package/src/views/Settings.tsx +88 -80
  251. package/src/views/SetupWizard.tsx +28 -28
  252. package/src/views/Users.tsx +20 -20
  253. package/src/views/page-builder/AIBlockAssist.tsx +1 -1
  254. package/src/views/page-builder/AIGenerateDialog.tsx +63 -63
  255. package/src/views/page-builder/BlockEditor.tsx +26 -26
  256. package/src/views/page-builder/BlockPicker.tsx +22 -22
  257. package/src/views/page-builder/BottomBar.tsx +8 -8
  258. package/src/views/page-builder/BuilderToolbar.tsx +17 -17
  259. package/src/views/page-builder/ContextPanel.tsx +3 -3
  260. package/src/views/page-builder/DesignScore.tsx +21 -21
  261. package/src/views/page-builder/NodeSettings.tsx +27 -27
  262. package/src/views/page-builder/PageBuilder.tsx +11 -11
  263. package/src/views/page-builder/PageSettings.tsx +4 -4
  264. package/src/views/page-builder/PageTemplates.tsx +18 -18
  265. package/src/views/page-builder/SEOPanel.tsx +53 -53
  266. package/src/views/page-builder/SavedSections.tsx +37 -37
  267. package/src/views/page-builder/TemplatePicker.tsx +17 -17
  268. package/src/views/page-builder/block-renderers/CTAPreview.tsx +13 -13
  269. package/src/views/page-builder/block-renderers/CardsPreview.tsx +5 -5
  270. package/src/views/page-builder/block-renderers/CodePreview.tsx +6 -6
  271. package/src/views/page-builder/block-renderers/FAQPreview.tsx +13 -13
  272. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +3 -3
  273. package/src/views/page-builder/block-renderers/FormPreview.tsx +20 -20
  274. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +8 -8
  275. package/src/views/page-builder/block-renderers/HeroPreview.tsx +16 -16
  276. package/src/views/page-builder/block-renderers/ImagePreview.tsx +4 -4
  277. package/src/views/page-builder/block-renderers/TextPreview.tsx +14 -14
  278. package/src/views/page-builder/block-renderers/VideoPreview.tsx +12 -12
  279. package/src/views/page-builder/canvas/BlockRenderer.tsx +4 -4
  280. package/src/views/page-builder/canvas/BuilderCanvas.tsx +6 -6
  281. package/src/views/page-builder/canvas/ColumnRenderer.tsx +3 -3
  282. package/src/views/page-builder/canvas/ContainerRenderer.tsx +2 -2
  283. package/src/views/page-builder/canvas/RowRenderer.tsx +2 -2
  284. package/src/views/page-builder/canvas/SectionRenderer.tsx +2 -2
@@ -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
+ }
@@ -0,0 +1,120 @@
1
+ import type { HTMLAttributes, ReactNode, ElementType } from 'react'
2
+ import type { SpaceToken } from './tokens.js'
3
+
4
+ /**
5
+ * Responsive column count. Pass a number for static grids, or a map to
6
+ * change column count at each breakpoint.
7
+ *
8
+ * @example
9
+ * columns={3} // 3 cols at every breakpoint
10
+ * columns={{ base: 1, md: 2, lg: 4 }} // responsive
11
+ */
12
+ export type GridResponsive =
13
+ | number
14
+ | { base?: number; sm?: number; md?: number; lg?: number; xl?: number; '2xl'?: number }
15
+
16
+ export interface GridProps extends HTMLAttributes<HTMLElement> {
17
+ columns: GridResponsive
18
+ /** Gap between cells. */
19
+ space?: SpaceToken
20
+ /** Row gap (defaults to `space`). */
21
+ rowSpace?: SpaceToken
22
+ /** Render as a different element. Defaults to `div`. */
23
+ as?: ElementType
24
+ /**
25
+ * Use auto-fit instead of explicit column counts. When set, `minItemWidth`
26
+ * controls the minimum tile width before the grid reflows. Use this for
27
+ * card grids that should adapt to container width without specific
28
+ * breakpoints.
29
+ */
30
+ autoFit?: boolean
31
+ /** Min tile width when `autoFit` is true. Defaults to `16rem`. */
32
+ minItemWidth?: string
33
+ children?: ReactNode
34
+ }
35
+
36
+ const COL_CLASS: Record<number, string> = {
37
+ 1: 'grid-cols-1',
38
+ 2: 'grid-cols-2',
39
+ 3: 'grid-cols-3',
40
+ 4: 'grid-cols-4',
41
+ 5: 'grid-cols-5',
42
+ 6: 'grid-cols-6',
43
+ 7: 'grid-cols-7',
44
+ 8: 'grid-cols-8',
45
+ 9: 'grid-cols-9',
46
+ 10: 'grid-cols-10',
47
+ 11: 'grid-cols-11',
48
+ 12: 'grid-cols-12',
49
+ }
50
+
51
+ const BP_PREFIX = ['', 'sm:', 'md:', 'lg:', 'xl:', '2xl:'] as const
52
+ type BpKey = 'base' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
53
+
54
+ function columnsToClasses(columns: GridResponsive): string {
55
+ if (typeof columns === 'number') {
56
+ return COL_CLASS[columns] ?? 'grid-cols-1'
57
+ }
58
+ const order: BpKey[] = ['base', 'sm', 'md', 'lg', 'xl', '2xl']
59
+ return order
60
+ .map((bp, i) => {
61
+ const n = columns[bp]
62
+ if (!n) return ''
63
+ const base = COL_CLASS[n] ?? 'grid-cols-1'
64
+ return BP_PREFIX[i] + base
65
+ })
66
+ .filter(Boolean)
67
+ .join(' ')
68
+ }
69
+
70
+ /**
71
+ * Responsive CSS grid primitive. Pass either an explicit `columns` count
72
+ * (per-breakpoint or static) or set `autoFit` for a `repeat(auto-fit, …)`
73
+ * tile grid. Both modes share the same `space` / `rowSpace` knobs so
74
+ * usage is consistent across views.
75
+ */
76
+ export function Grid({
77
+ columns,
78
+ space = '4',
79
+ rowSpace,
80
+ as: Component = 'div',
81
+ autoFit,
82
+ minItemWidth = '16rem',
83
+ className = '',
84
+ style,
85
+ children,
86
+ ...rest
87
+ }: GridProps) {
88
+ if (autoFit) {
89
+ return (
90
+ <Component
91
+ className={['grid', `gap-${space}`, rowSpace ? `gap-y-${rowSpace}` : '', className]
92
+ .filter(Boolean)
93
+ .join(' ')}
94
+ style={{
95
+ gridTemplateColumns: `repeat(auto-fit, minmax(${minItemWidth}, 1fr))`,
96
+ ...style,
97
+ }}
98
+ {...rest}
99
+ >
100
+ {children}
101
+ </Component>
102
+ )
103
+ }
104
+
105
+ const classes = [
106
+ 'grid',
107
+ columnsToClasses(columns),
108
+ `gap-${space}`,
109
+ rowSpace ? `gap-y-${rowSpace}` : '',
110
+ className,
111
+ ]
112
+ .filter(Boolean)
113
+ .join(' ')
114
+
115
+ return (
116
+ <Component className={classes} style={style} {...rest}>
117
+ {children}
118
+ </Component>
119
+ )
120
+ }
@@ -0,0 +1,96 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+ import { Stack } from './Stack.js'
3
+
4
+ export interface PageContainerProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
5
+ /** Page title rendered as an h1 inside the page header. */
6
+ title?: ReactNode
7
+ /** Optional sub-headline shown beneath the title. */
8
+ description?: ReactNode
9
+ /** Right-aligned action area (buttons, filters). Rendered in the header. */
10
+ actions?: ReactNode
11
+ /**
12
+ * Maximum width of the content. Defaults to `7xl` (Tailwind's max-w-7xl,
13
+ * ~1280px). Pass `'full'` for edge-to-edge content.
14
+ */
15
+ maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl' | 'full'
16
+ /** Horizontal padding token. Defaults to `6` (24px). */
17
+ paddingX?: '4' | '6' | '8'
18
+ /** Vertical padding token. Defaults to `6`. */
19
+ paddingY?: '4' | '6' | '8' | '10'
20
+ /** Vertical gap between the header and the body. Defaults to `6`. */
21
+ gap?: '4' | '6' | '8'
22
+ children?: ReactNode
23
+ }
24
+
25
+ const MAX_WIDTH_CLASS: Record<NonNullable<PageContainerProps['maxWidth']>, string> = {
26
+ sm: 'max-w-sm',
27
+ md: 'max-w-md',
28
+ lg: 'max-w-lg',
29
+ xl: 'max-w-xl',
30
+ '2xl': 'max-w-2xl',
31
+ '3xl': 'max-w-3xl',
32
+ '4xl': 'max-w-4xl',
33
+ '5xl': 'max-w-5xl',
34
+ '6xl': 'max-w-6xl',
35
+ '7xl': 'max-w-7xl',
36
+ full: 'max-w-none',
37
+ }
38
+
39
+ /**
40
+ * The single canonical wrapper for every admin page. PageContainer owns
41
+ * three responsibilities so individual views can stop redoing them:
42
+ *
43
+ * 1. Consistent max-width + horizontal padding (so dashboards, lists, and
44
+ * forms all line up visually).
45
+ * 2. A standard header slot with title + description + actions.
46
+ * 3. A standard body slot rendered as a `<Stack>` so children stack with
47
+ * consistent vertical rhythm.
48
+ *
49
+ * Use PageContainer at the top of every screen. Compose with `<Stack>` /
50
+ * `<Grid>` / `<Cluster>` inside. Never roll a per-screen wrapper.
51
+ */
52
+ export function PageContainer({
53
+ title,
54
+ description,
55
+ actions,
56
+ maxWidth = '7xl',
57
+ paddingX = '6',
58
+ paddingY = '6',
59
+ gap = '6',
60
+ className = '',
61
+ children,
62
+ ...rest
63
+ }: PageContainerProps) {
64
+ const outerClasses = [
65
+ 'mx-auto w-full',
66
+ MAX_WIDTH_CLASS[maxWidth],
67
+ `px-${paddingX}`,
68
+ `py-${paddingY}`,
69
+ className,
70
+ ]
71
+ .filter(Boolean)
72
+ .join(' ')
73
+
74
+ return (
75
+ <section className={outerClasses} {...rest}>
76
+ <Stack space={gap}>
77
+ {(title || description || actions) && (
78
+ <header className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
79
+ {(title || description) && (
80
+ <div className="min-w-0">
81
+ {title && (
82
+ <h1 className="text-foreground truncate text-2xl font-semibold tracking-tight">
83
+ {title}
84
+ </h1>
85
+ )}
86
+ {description && <p className="text-muted-foreground mt-1 text-sm">{description}</p>}
87
+ </div>
88
+ )}
89
+ {actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
90
+ </header>
91
+ )}
92
+ {children}
93
+ </Stack>
94
+ </section>
95
+ )
96
+ }