@prenta/admin 2.6.0 → 2.7.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 (801) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/design-system/tokens.css +179 -85
  3. package/design-system/tokens.json +276 -85
  4. package/design-system/tokens.scss +148 -44
  5. package/design-system/tokens.tailwind.cjs +50 -1
  6. package/dist/AdminRoot.d.ts.map +1 -1
  7. package/dist/AdminRoot.js +42 -20
  8. package/dist/AdminRoot.js.map +1 -1
  9. package/dist/LoginRoot.d.ts.map +1 -1
  10. package/dist/LoginRoot.js +2 -1
  11. package/dist/LoginRoot.js.map +1 -1
  12. package/dist/__tests__/components/author-avatar.test.js +3 -17
  13. package/dist/__tests__/components/author-avatar.test.js.map +1 -1
  14. package/dist/__tests__/components/notification-bell.test.js +86 -15
  15. package/dist/__tests__/components/notification-bell.test.js.map +1 -1
  16. package/dist/__tests__/components/responsive-tiers.render.test.d.ts +2 -0
  17. package/dist/__tests__/components/responsive-tiers.render.test.d.ts.map +1 -0
  18. package/dist/__tests__/components/responsive-tiers.render.test.js +313 -0
  19. package/dist/__tests__/components/responsive-tiers.render.test.js.map +1 -0
  20. package/dist/__tests__/components/serp-preview.render.test.js +6 -3
  21. package/dist/__tests__/components/serp-preview.render.test.js.map +1 -1
  22. package/dist/__tests__/components/shell-primitives.render.test.d.ts +2 -0
  23. package/dist/__tests__/components/shell-primitives.render.test.d.ts.map +1 -0
  24. package/dist/__tests__/components/shell-primitives.render.test.js +716 -0
  25. package/dist/__tests__/components/shell-primitives.render.test.js.map +1 -0
  26. package/dist/__tests__/components/theme-provider-accent.test.js +49 -7
  27. package/dist/__tests__/components/theme-provider-accent.test.js.map +1 -1
  28. package/dist/__tests__/components/toggle.test.js +10 -3
  29. package/dist/__tests__/components/toggle.test.js.map +1 -1
  30. package/dist/__tests__/components/ui-primitives.test.js +3 -7
  31. package/dist/__tests__/components/ui-primitives.test.js.map +1 -1
  32. package/dist/__tests__/layout/build-nav-items.test.js +1 -1
  33. package/dist/__tests__/layout/build-nav-items.test.js.map +1 -1
  34. package/dist/__tests__/layout/shell-nav.render.test.d.ts +2 -0
  35. package/dist/__tests__/layout/shell-nav.render.test.d.ts.map +1 -0
  36. package/dist/__tests__/layout/shell-nav.render.test.js +736 -0
  37. package/dist/__tests__/layout/shell-nav.render.test.js.map +1 -0
  38. package/dist/__tests__/layout/sidebar-forms-submenu.render.test.js +8 -4
  39. package/dist/__tests__/layout/sidebar-forms-submenu.render.test.js.map +1 -1
  40. package/dist/__tests__/layout/sidebar-groups.render.test.d.ts +2 -0
  41. package/dist/__tests__/layout/sidebar-groups.render.test.d.ts.map +1 -0
  42. package/dist/__tests__/layout/sidebar-groups.render.test.js +56 -0
  43. package/dist/__tests__/layout/sidebar-groups.render.test.js.map +1 -0
  44. package/dist/__tests__/layout/sidebar-menus.render.test.d.ts +2 -0
  45. package/dist/__tests__/layout/sidebar-menus.render.test.d.ts.map +1 -0
  46. package/dist/__tests__/layout/sidebar-menus.render.test.js +82 -0
  47. package/dist/__tests__/layout/sidebar-menus.render.test.js.map +1 -0
  48. package/dist/__tests__/layout/sidebar-plugin-nav.test.js +1 -1
  49. package/dist/__tests__/layout/sidebar-plugin-nav.test.js.map +1 -1
  50. package/dist/__tests__/layout/sidebar-posts.test.js +48 -38
  51. package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -1
  52. package/dist/__tests__/layout/sidebar-submenu.render.test.js +26 -10
  53. package/dist/__tests__/layout/sidebar-submenu.render.test.js.map +1 -1
  54. package/dist/__tests__/layout/topbar.render.test.d.ts +2 -0
  55. package/dist/__tests__/layout/topbar.render.test.d.ts.map +1 -0
  56. package/dist/__tests__/layout/topbar.render.test.js +406 -0
  57. package/dist/__tests__/layout/topbar.render.test.js.map +1 -0
  58. package/dist/__tests__/lib/api.test.js +2 -0
  59. package/dist/__tests__/lib/api.test.js.map +1 -1
  60. package/dist/__tests__/lib/job-drawer.test.d.ts +2 -0
  61. package/dist/__tests__/lib/job-drawer.test.d.ts.map +1 -0
  62. package/dist/__tests__/lib/job-drawer.test.js +393 -0
  63. package/dist/__tests__/lib/job-drawer.test.js.map +1 -0
  64. package/dist/__tests__/lib/job-view.test.d.ts +2 -0
  65. package/dist/__tests__/lib/job-view.test.d.ts.map +1 -0
  66. package/dist/__tests__/lib/job-view.test.js +387 -0
  67. package/dist/__tests__/lib/job-view.test.js.map +1 -0
  68. package/dist/__tests__/lib/jobs-service.test.d.ts +2 -0
  69. package/dist/__tests__/lib/jobs-service.test.d.ts.map +1 -0
  70. package/dist/__tests__/lib/jobs-service.test.js +88 -0
  71. package/dist/__tests__/lib/jobs-service.test.js.map +1 -0
  72. package/dist/__tests__/router/admin-route-order.test.d.ts +2 -0
  73. package/dist/__tests__/router/admin-route-order.test.d.ts.map +1 -0
  74. package/dist/__tests__/router/admin-route-order.test.js +36 -0
  75. package/dist/__tests__/router/admin-route-order.test.js.map +1 -0
  76. package/dist/__tests__/styles/ai-gradient-contrast.test.d.ts +2 -0
  77. package/dist/__tests__/styles/ai-gradient-contrast.test.d.ts.map +1 -0
  78. package/dist/__tests__/styles/ai-gradient-contrast.test.js +110 -0
  79. package/dist/__tests__/styles/ai-gradient-contrast.test.js.map +1 -0
  80. package/dist/__tests__/styles/breakpoint-order.test.d.ts +2 -0
  81. package/dist/__tests__/styles/breakpoint-order.test.d.ts.map +1 -0
  82. package/dist/__tests__/styles/breakpoint-order.test.js +52 -0
  83. package/dist/__tests__/styles/breakpoint-order.test.js.map +1 -0
  84. package/dist/__tests__/styles/responsive-layer.test.js +303 -12
  85. package/dist/__tests__/styles/responsive-layer.test.js.map +1 -1
  86. package/dist/__tests__/styles/tailwind-preset.test.d.ts +2 -0
  87. package/dist/__tests__/styles/tailwind-preset.test.d.ts.map +1 -0
  88. package/dist/__tests__/styles/tailwind-preset.test.js +147 -0
  89. package/dist/__tests__/styles/tailwind-preset.test.js.map +1 -0
  90. package/dist/__tests__/styles/theme-token-scope.test.js +9 -3
  91. package/dist/__tests__/styles/theme-token-scope.test.js.map +1 -1
  92. package/dist/__tests__/styles/tiers.test.d.ts +2 -0
  93. package/dist/__tests__/styles/tiers.test.d.ts.map +1 -0
  94. package/dist/__tests__/styles/tiers.test.js +121 -0
  95. package/dist/__tests__/styles/tiers.test.js.map +1 -0
  96. package/dist/__tests__/styles/tokens-drift.test.js +57 -6
  97. package/dist/__tests__/styles/tokens-drift.test.js.map +1 -1
  98. package/dist/__tests__/views/admin-lazy-route.render.test.js +27 -1
  99. package/dist/__tests__/views/admin-lazy-route.render.test.js.map +1 -1
  100. package/dist/__tests__/views/ai-settings.render.test.js +4 -4
  101. package/dist/__tests__/views/appearance-menus-route.render.test.d.ts +2 -0
  102. package/dist/__tests__/views/appearance-menus-route.render.test.d.ts.map +1 -0
  103. package/dist/__tests__/views/appearance-menus-route.render.test.js +50 -0
  104. package/dist/__tests__/views/appearance-menus-route.render.test.js.map +1 -0
  105. package/dist/__tests__/views/appearance-settings.render.test.js +8 -6
  106. package/dist/__tests__/views/appearance-settings.render.test.js.map +1 -1
  107. package/dist/__tests__/views/appearance-theme-provider.render.test.d.ts +2 -0
  108. package/dist/__tests__/views/appearance-theme-provider.render.test.d.ts.map +1 -0
  109. package/dist/__tests__/views/appearance-theme-provider.render.test.js +286 -0
  110. package/dist/__tests__/views/appearance-theme-provider.render.test.js.map +1 -0
  111. package/dist/__tests__/views/dashboard.test.js +37 -0
  112. package/dist/__tests__/views/dashboard.test.js.map +1 -1
  113. package/dist/__tests__/views/document-edit-navigation.render.test.d.ts +2 -0
  114. package/dist/__tests__/views/document-edit-navigation.render.test.d.ts.map +1 -0
  115. package/dist/__tests__/views/document-edit-navigation.render.test.js +101 -0
  116. package/dist/__tests__/views/document-edit-navigation.render.test.js.map +1 -0
  117. package/dist/__tests__/views/document-edit-routes.render.test.d.ts +2 -0
  118. package/dist/__tests__/views/document-edit-routes.render.test.d.ts.map +1 -0
  119. package/dist/__tests__/views/document-edit-routes.render.test.js +113 -0
  120. package/dist/__tests__/views/document-edit-routes.render.test.js.map +1 -0
  121. package/dist/__tests__/views/email-settings.render.test.js +2 -2
  122. package/dist/__tests__/views/form-entries.render.test.js +15 -0
  123. package/dist/__tests__/views/form-entries.render.test.js.map +1 -1
  124. package/dist/__tests__/views/form-submissions.render.test.js +18 -0
  125. package/dist/__tests__/views/form-submissions.render.test.js.map +1 -1
  126. package/dist/__tests__/views/forms-list.render.test.js +12 -0
  127. package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
  128. package/dist/__tests__/views/general-settings.render.test.js +26 -10
  129. package/dist/__tests__/views/general-settings.render.test.js.map +1 -1
  130. package/dist/__tests__/views/jobs-drawer.render.test.d.ts +2 -0
  131. package/dist/__tests__/views/jobs-drawer.render.test.d.ts.map +1 -0
  132. package/dist/__tests__/views/jobs-drawer.render.test.js +908 -0
  133. package/dist/__tests__/views/jobs-drawer.render.test.js.map +1 -0
  134. package/dist/__tests__/views/jobs-list.render.test.d.ts +2 -0
  135. package/dist/__tests__/views/jobs-list.render.test.d.ts.map +1 -0
  136. package/dist/__tests__/views/jobs-list.render.test.js +695 -0
  137. package/dist/__tests__/views/jobs-list.render.test.js.map +1 -0
  138. package/dist/__tests__/views/kpi-grid.test.d.ts +2 -0
  139. package/dist/__tests__/views/kpi-grid.test.d.ts.map +1 -0
  140. package/dist/__tests__/views/kpi-grid.test.js +26 -0
  141. package/dist/__tests__/views/kpi-grid.test.js.map +1 -0
  142. package/dist/__tests__/views/mcp-connect.render.test.js +1 -0
  143. package/dist/__tests__/views/mcp-connect.render.test.js.map +1 -1
  144. package/dist/__tests__/views/post-section-editor.test.js +3 -1
  145. package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
  146. package/dist/__tests__/views/script-tags.render.test.js +25 -0
  147. package/dist/__tests__/views/script-tags.render.test.js.map +1 -1
  148. package/dist/__tests__/views/security-settings.render.test.js +9 -8
  149. package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
  150. package/dist/__tests__/views/seo-settings.render.test.js +7 -7
  151. package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
  152. package/dist/__tests__/views/settings-save-bar.render.test.d.ts +2 -0
  153. package/dist/__tests__/views/settings-save-bar.render.test.d.ts.map +1 -0
  154. package/dist/__tests__/views/settings-save-bar.render.test.js +68 -0
  155. package/dist/__tests__/views/settings-save-bar.render.test.js.map +1 -0
  156. package/dist/__tests__/views/settings-save-footer.render.test.js +23 -15
  157. package/dist/__tests__/views/settings-save-footer.render.test.js.map +1 -1
  158. package/dist/__tests__/views/settings-tabs.render.test.d.ts +2 -0
  159. package/dist/__tests__/views/settings-tabs.render.test.d.ts.map +1 -0
  160. package/dist/__tests__/views/settings-tabs.render.test.js +319 -0
  161. package/dist/__tests__/views/settings-tabs.render.test.js.map +1 -0
  162. package/dist/__tests__/views/updates-tab.render.test.js +4 -3
  163. package/dist/__tests__/views/updates-tab.render.test.js.map +1 -1
  164. package/dist/components/Breadcrumbs.d.ts +8 -1
  165. package/dist/components/Breadcrumbs.d.ts.map +1 -1
  166. package/dist/components/Breadcrumbs.js +6 -0
  167. package/dist/components/Breadcrumbs.js.map +1 -1
  168. package/dist/components/CommandPalette.d.ts.map +1 -1
  169. package/dist/components/CommandPalette.js +40 -4
  170. package/dist/components/CommandPalette.js.map +1 -1
  171. package/dist/components/LocaleSwitcher.d.ts.map +1 -1
  172. package/dist/components/LocaleSwitcher.js +1 -1
  173. package/dist/components/LocaleSwitcher.js.map +1 -1
  174. package/dist/components/NotificationBell.d.ts.map +1 -1
  175. package/dist/components/NotificationBell.js +44 -3
  176. package/dist/components/NotificationBell.js.map +1 -1
  177. package/dist/components/OfflineStatus.js +1 -1
  178. package/dist/components/OfflineStatus.js.map +1 -1
  179. package/dist/components/PresenceIndicator.d.ts.map +1 -1
  180. package/dist/components/PresenceIndicator.js +2 -1
  181. package/dist/components/PresenceIndicator.js.map +1 -1
  182. package/dist/components/ReflowMeter.js +2 -2
  183. package/dist/components/ReflowMeter.js.map +1 -1
  184. package/dist/components/SEOPanel.js +2 -2
  185. package/dist/components/SEOPanel.js.map +1 -1
  186. package/dist/components/ThemeProvider.d.ts +24 -4
  187. package/dist/components/ThemeProvider.d.ts.map +1 -1
  188. package/dist/components/ThemeProvider.js +80 -22
  189. package/dist/components/ThemeProvider.js.map +1 -1
  190. package/dist/components/TipTapEditor.d.ts.map +1 -1
  191. package/dist/components/TipTapEditor.js +3 -4
  192. package/dist/components/TipTapEditor.js.map +1 -1
  193. package/dist/components/forms/EmbedPanel.js +1 -1
  194. package/dist/components/forms/EmbedPanel.js.map +1 -1
  195. package/dist/components/forms/FormEntryDetailPane.js +6 -1
  196. package/dist/components/forms/FormEntryDetailPane.js.map +1 -1
  197. package/dist/components/forms/ShortcodeGlossary.d.ts.map +1 -1
  198. package/dist/components/forms/ShortcodeGlossary.js +6 -1
  199. package/dist/components/forms/ShortcodeGlossary.js.map +1 -1
  200. package/dist/components/forms/primitives.d.ts +15 -5
  201. package/dist/components/forms/primitives.d.ts.map +1 -1
  202. package/dist/components/forms/primitives.js +11 -11
  203. package/dist/components/forms/primitives.js.map +1 -1
  204. package/dist/components/pages/tag-badges.d.ts +2 -1
  205. package/dist/components/pages/tag-badges.d.ts.map +1 -1
  206. package/dist/components/pages/tag-badges.js +4 -3
  207. package/dist/components/pages/tag-badges.js.map +1 -1
  208. package/dist/components/seo/PageAuditPanel.js +2 -2
  209. package/dist/components/seo/PageAuditPanel.js.map +1 -1
  210. package/dist/components/seo/SerpPreview.d.ts +4 -4
  211. package/dist/components/seo/SerpPreview.js +1 -1
  212. package/dist/components/seo/SerpPreview.js.map +1 -1
  213. package/dist/components/seo/primitives.js +2 -2
  214. package/dist/components/seo/primitives.js.map +1 -1
  215. package/dist/components/ui/AuthorAvatar.d.ts +4 -4
  216. package/dist/components/ui/AuthorAvatar.d.ts.map +1 -1
  217. package/dist/components/ui/AuthorAvatar.js +11 -10
  218. package/dist/components/ui/AuthorAvatar.js.map +1 -1
  219. package/dist/components/ui/Badge.d.ts +1 -1
  220. package/dist/components/ui/Badge.js +1 -1
  221. package/dist/components/ui/Badge.js.map +1 -1
  222. package/dist/components/ui/Breadcrumb.d.ts +27 -0
  223. package/dist/components/ui/Breadcrumb.d.ts.map +1 -0
  224. package/dist/components/ui/Breadcrumb.js +41 -0
  225. package/dist/components/ui/Breadcrumb.js.map +1 -0
  226. package/dist/components/ui/Button.d.ts +2 -2
  227. package/dist/components/ui/Button.d.ts.map +1 -1
  228. package/dist/components/ui/Button.js +31 -16
  229. package/dist/components/ui/Button.js.map +1 -1
  230. package/dist/components/ui/Card.d.ts +8 -4
  231. package/dist/components/ui/Card.d.ts.map +1 -1
  232. package/dist/components/ui/Card.js +21 -13
  233. package/dist/components/ui/Card.js.map +1 -1
  234. package/dist/components/ui/CardListControls.d.ts +34 -0
  235. package/dist/components/ui/CardListControls.d.ts.map +1 -0
  236. package/dist/components/ui/CardListControls.js +25 -0
  237. package/dist/components/ui/CardListControls.js.map +1 -0
  238. package/dist/components/ui/CountChip.d.ts +20 -0
  239. package/dist/components/ui/CountChip.d.ts.map +1 -0
  240. package/dist/components/ui/CountChip.js +48 -0
  241. package/dist/components/ui/CountChip.js.map +1 -0
  242. package/dist/components/ui/FilterPill.d.ts +2 -1
  243. package/dist/components/ui/FilterPill.d.ts.map +1 -1
  244. package/dist/components/ui/FilterPill.js +3 -2
  245. package/dist/components/ui/FilterPill.js.map +1 -1
  246. package/dist/components/ui/Input.d.ts +1 -1
  247. package/dist/components/ui/InspectorPane.d.ts +1 -1
  248. package/dist/components/ui/InspectorPane.d.ts.map +1 -1
  249. package/dist/components/ui/InspectorPane.js +15 -4
  250. package/dist/components/ui/InspectorPane.js.map +1 -1
  251. package/dist/components/ui/Modal.d.ts.map +1 -1
  252. package/dist/components/ui/Modal.js +10 -2
  253. package/dist/components/ui/Modal.js.map +1 -1
  254. package/dist/components/ui/PageHeader.d.ts +28 -0
  255. package/dist/components/ui/PageHeader.d.ts.map +1 -0
  256. package/dist/components/ui/PageHeader.js +18 -0
  257. package/dist/components/ui/PageHeader.js.map +1 -0
  258. package/dist/components/ui/RowActions.d.ts +5 -0
  259. package/dist/components/ui/RowActions.d.ts.map +1 -1
  260. package/dist/components/ui/RowActions.js +7 -2
  261. package/dist/components/ui/RowActions.js.map +1 -1
  262. package/dist/components/ui/Segmented.d.ts +21 -0
  263. package/dist/components/ui/Segmented.d.ts.map +1 -0
  264. package/dist/components/ui/Segmented.js +41 -0
  265. package/dist/components/ui/Segmented.js.map +1 -0
  266. package/dist/components/ui/Toaster.d.ts +12 -0
  267. package/dist/components/ui/Toaster.d.ts.map +1 -0
  268. package/dist/components/ui/Toaster.js +103 -0
  269. package/dist/components/ui/Toaster.js.map +1 -0
  270. package/dist/components/ui/Toggle.d.ts +4 -3
  271. package/dist/components/ui/Toggle.d.ts.map +1 -1
  272. package/dist/components/ui/Toggle.js +16 -12
  273. package/dist/components/ui/Toggle.js.map +1 -1
  274. package/dist/components/ui/UnderlineTabs.d.ts +56 -0
  275. package/dist/components/ui/UnderlineTabs.d.ts.map +1 -0
  276. package/dist/components/ui/UnderlineTabs.js +205 -0
  277. package/dist/components/ui/UnderlineTabs.js.map +1 -0
  278. package/dist/components/ui/UnsavedBar.d.ts +25 -0
  279. package/dist/components/ui/UnsavedBar.d.ts.map +1 -0
  280. package/dist/components/ui/UnsavedBar.js +22 -0
  281. package/dist/components/ui/UnsavedBar.js.map +1 -0
  282. package/dist/components/ui/admin-links.d.ts +11 -0
  283. package/dist/components/ui/admin-links.d.ts.map +1 -0
  284. package/dist/components/ui/admin-links.js +10 -0
  285. package/dist/components/ui/admin-links.js.map +1 -0
  286. package/dist/components/ui/card-table.d.ts +43 -0
  287. package/dist/components/ui/card-table.d.ts.map +1 -0
  288. package/dist/components/ui/card-table.js +31 -0
  289. package/dist/components/ui/card-table.js.map +1 -0
  290. package/dist/components/virtual-window.d.ts +7 -0
  291. package/dist/components/virtual-window.d.ts.map +1 -1
  292. package/dist/components/virtual-window.js +5 -2
  293. package/dist/components/virtual-window.js.map +1 -1
  294. package/dist/fields/RelationshipField.js +1 -1
  295. package/dist/fields/RelationshipField.js.map +1 -1
  296. package/dist/files/dm-mono-latin-400-normal.woff +0 -0
  297. package/dist/files/dm-mono-latin-400-normal.woff2 +0 -0
  298. package/dist/files/dm-mono-latin-500-normal.woff +0 -0
  299. package/dist/files/dm-mono-latin-500-normal.woff2 +0 -0
  300. package/dist/files/dm-mono-latin-ext-400-normal.woff +0 -0
  301. package/dist/files/dm-mono-latin-ext-400-normal.woff2 +0 -0
  302. package/dist/files/dm-mono-latin-ext-500-normal.woff +0 -0
  303. package/dist/files/dm-mono-latin-ext-500-normal.woff2 +0 -0
  304. package/dist/files/dm-sans-latin-400-normal.woff +0 -0
  305. package/dist/files/dm-sans-latin-400-normal.woff2 +0 -0
  306. package/dist/files/dm-sans-latin-500-normal.woff +0 -0
  307. package/dist/files/dm-sans-latin-500-normal.woff2 +0 -0
  308. package/dist/files/dm-sans-latin-600-normal.woff +0 -0
  309. package/dist/files/dm-sans-latin-600-normal.woff2 +0 -0
  310. package/dist/files/dm-sans-latin-700-normal.woff +0 -0
  311. package/dist/files/dm-sans-latin-700-normal.woff2 +0 -0
  312. package/dist/files/dm-sans-latin-ext-400-normal.woff +0 -0
  313. package/dist/files/dm-sans-latin-ext-400-normal.woff2 +0 -0
  314. package/dist/files/dm-sans-latin-ext-500-normal.woff +0 -0
  315. package/dist/files/dm-sans-latin-ext-500-normal.woff2 +0 -0
  316. package/dist/files/dm-sans-latin-ext-600-normal.woff +0 -0
  317. package/dist/files/dm-sans-latin-ext-600-normal.woff2 +0 -0
  318. package/dist/files/dm-sans-latin-ext-700-normal.woff +0 -0
  319. package/dist/files/dm-sans-latin-ext-700-normal.woff2 +0 -0
  320. package/dist/hooks/useAppearanceBootstrap.d.ts.map +1 -1
  321. package/dist/hooks/useAppearanceBootstrap.js +9 -3
  322. package/dist/hooks/useAppearanceBootstrap.js.map +1 -1
  323. package/dist/index.d.ts +15 -0
  324. package/dist/index.d.ts.map +1 -1
  325. package/dist/index.js +8 -0
  326. package/dist/index.js.map +1 -1
  327. package/dist/layout/Header.d.ts +14 -5
  328. package/dist/layout/Header.d.ts.map +1 -1
  329. package/dist/layout/Header.js +78 -13
  330. package/dist/layout/Header.js.map +1 -1
  331. package/dist/layout/Layout.d.ts +5 -3
  332. package/dist/layout/Layout.d.ts.map +1 -1
  333. package/dist/layout/Layout.js +45 -54
  334. package/dist/layout/Layout.js.map +1 -1
  335. package/dist/layout/MobileTabBar.d.ts +20 -0
  336. package/dist/layout/MobileTabBar.d.ts.map +1 -0
  337. package/dist/layout/MobileTabBar.js +29 -0
  338. package/dist/layout/MobileTabBar.js.map +1 -0
  339. package/dist/layout/Sidebar.d.ts +8 -26
  340. package/dist/layout/Sidebar.d.ts.map +1 -1
  341. package/dist/layout/Sidebar.js +309 -345
  342. package/dist/layout/Sidebar.js.map +1 -1
  343. package/dist/layout/nav-config.d.ts +101 -0
  344. package/dist/layout/nav-config.d.ts.map +1 -0
  345. package/dist/layout/nav-config.js +297 -0
  346. package/dist/layout/nav-config.js.map +1 -0
  347. package/dist/layout/primitives/AdminShell.d.ts +10 -2
  348. package/dist/layout/primitives/AdminShell.d.ts.map +1 -1
  349. package/dist/layout/primitives/AdminShell.js +114 -6
  350. package/dist/layout/primitives/AdminShell.js.map +1 -1
  351. package/dist/layout/topbar-classes.d.ts +8 -0
  352. package/dist/layout/topbar-classes.d.ts.map +1 -0
  353. package/dist/layout/topbar-classes.js +8 -0
  354. package/dist/layout/topbar-classes.js.map +1 -0
  355. package/dist/layout/use-nav-items.d.ts +10 -0
  356. package/dist/layout/use-nav-items.d.ts.map +1 -0
  357. package/dist/layout/use-nav-items.js +39 -0
  358. package/dist/layout/use-nav-items.js.map +1 -0
  359. package/dist/lib/api.d.ts +6 -0
  360. package/dist/lib/api.d.ts.map +1 -1
  361. package/dist/lib/api.js +1 -1
  362. package/dist/lib/api.js.map +1 -1
  363. package/dist/lib/appearance-api.d.ts +3 -1
  364. package/dist/lib/appearance-api.d.ts.map +1 -1
  365. package/dist/lib/appearance-api.js +1 -0
  366. package/dist/lib/appearance-api.js.map +1 -1
  367. package/dist/lib/job-drawer.d.ts +128 -0
  368. package/dist/lib/job-drawer.d.ts.map +1 -0
  369. package/dist/lib/job-drawer.js +351 -0
  370. package/dist/lib/job-drawer.js.map +1 -0
  371. package/dist/lib/job-view.d.ts +187 -0
  372. package/dist/lib/job-view.d.ts.map +1 -0
  373. package/dist/lib/job-view.js +363 -0
  374. package/dist/lib/job-view.js.map +1 -0
  375. package/dist/lib/jobs-service.d.ts +134 -0
  376. package/dist/lib/jobs-service.d.ts.map +1 -0
  377. package/dist/lib/jobs-service.js +179 -0
  378. package/dist/lib/jobs-service.js.map +1 -0
  379. package/dist/lib/sheet-focus.d.ts +24 -0
  380. package/dist/lib/sheet-focus.d.ts.map +1 -0
  381. package/dist/lib/sheet-focus.js +61 -0
  382. package/dist/lib/sheet-focus.js.map +1 -0
  383. package/dist/lib/tiers.d.ts +30 -0
  384. package/dist/lib/tiers.d.ts.map +1 -0
  385. package/dist/lib/tiers.js +43 -0
  386. package/dist/lib/tiers.js.map +1 -0
  387. package/dist/prenta-admin.css +1 -1
  388. package/dist/router/admin-routes.d.ts.map +1 -1
  389. package/dist/router/admin-routes.js +8 -0
  390. package/dist/router/admin-routes.js.map +1 -1
  391. package/dist/router/index.d.ts +5 -1
  392. package/dist/router/index.d.ts.map +1 -1
  393. package/dist/router/index.js +7 -0
  394. package/dist/router/index.js.map +1 -1
  395. package/dist/views/ApiKeys.js +2 -2
  396. package/dist/views/ApiKeys.js.map +1 -1
  397. package/dist/views/AppearanceChrome.d.ts +6 -0
  398. package/dist/views/AppearanceChrome.d.ts.map +1 -0
  399. package/dist/views/AppearanceChrome.js +25 -0
  400. package/dist/views/AppearanceChrome.js.map +1 -0
  401. package/dist/views/CollectionList.d.ts.map +1 -1
  402. package/dist/views/CollectionList.js +73 -53
  403. package/dist/views/CollectionList.js.map +1 -1
  404. package/dist/views/Dashboard.d.ts.map +1 -1
  405. package/dist/views/Dashboard.js +14 -8
  406. package/dist/views/Dashboard.js.map +1 -1
  407. package/dist/views/DocumentEdit.d.ts +6 -1
  408. package/dist/views/DocumentEdit.d.ts.map +1 -1
  409. package/dist/views/DocumentEdit.js +44 -8
  410. package/dist/views/DocumentEdit.js.map +1 -1
  411. package/dist/views/FormDeliveries.d.ts.map +1 -1
  412. package/dist/views/FormDeliveries.js +7 -1
  413. package/dist/views/FormDeliveries.js.map +1 -1
  414. package/dist/views/FormEntries.d.ts.map +1 -1
  415. package/dist/views/FormEntries.js +11 -2
  416. package/dist/views/FormEntries.js.map +1 -1
  417. package/dist/views/FormSubmissions.d.ts +1 -1
  418. package/dist/views/FormSubmissions.d.ts.map +1 -1
  419. package/dist/views/FormSubmissions.js +15 -4
  420. package/dist/views/FormSubmissions.js.map +1 -1
  421. package/dist/views/Forms.d.ts.map +1 -1
  422. package/dist/views/Forms.js +6 -4
  423. package/dist/views/Forms.js.map +1 -1
  424. package/dist/views/FormsSettings.d.ts +2 -0
  425. package/dist/views/FormsSettings.d.ts.map +1 -0
  426. package/dist/views/FormsSettings.js +37 -0
  427. package/dist/views/FormsSettings.js.map +1 -0
  428. package/dist/views/Jobs/BusyButton.d.ts +15 -0
  429. package/dist/views/Jobs/BusyButton.d.ts.map +1 -0
  430. package/dist/views/Jobs/BusyButton.js +26 -0
  431. package/dist/views/Jobs/BusyButton.js.map +1 -0
  432. package/dist/views/Jobs/CancelJobDialog.d.ts +21 -0
  433. package/dist/views/Jobs/CancelJobDialog.d.ts.map +1 -0
  434. package/dist/views/Jobs/CancelJobDialog.js +66 -0
  435. package/dist/views/Jobs/CancelJobDialog.js.map +1 -0
  436. package/dist/views/Jobs/DrawerActivity.d.ts +16 -0
  437. package/dist/views/Jobs/DrawerActivity.d.ts.map +1 -0
  438. package/dist/views/Jobs/DrawerActivity.js +59 -0
  439. package/dist/views/Jobs/DrawerActivity.js.map +1 -0
  440. package/dist/views/Jobs/DrawerOverview.d.ts +28 -0
  441. package/dist/views/Jobs/DrawerOverview.d.ts.map +1 -0
  442. package/dist/views/Jobs/DrawerOverview.js +32 -0
  443. package/dist/views/Jobs/DrawerOverview.js.map +1 -0
  444. package/dist/views/Jobs/DrawerResults.d.ts +25 -0
  445. package/dist/views/Jobs/DrawerResults.d.ts.map +1 -0
  446. package/dist/views/Jobs/DrawerResults.js +50 -0
  447. package/dist/views/Jobs/DrawerResults.js.map +1 -0
  448. package/dist/views/Jobs/DrawerReview.d.ts +44 -0
  449. package/dist/views/Jobs/DrawerReview.d.ts.map +1 -0
  450. package/dist/views/Jobs/DrawerReview.js +110 -0
  451. package/dist/views/Jobs/DrawerReview.js.map +1 -0
  452. package/dist/views/Jobs/JobDrawer.d.ts +31 -0
  453. package/dist/views/Jobs/JobDrawer.d.ts.map +1 -0
  454. package/dist/views/Jobs/JobDrawer.js +353 -0
  455. package/dist/views/Jobs/JobDrawer.js.map +1 -0
  456. package/dist/views/Jobs/JobsTable.d.ts +19 -0
  457. package/dist/views/Jobs/JobsTable.d.ts.map +1 -0
  458. package/dist/views/Jobs/JobsTable.js +80 -0
  459. package/dist/views/Jobs/JobsTable.js.map +1 -0
  460. package/dist/views/Jobs/WorkerStrip.d.ts +22 -0
  461. package/dist/views/Jobs/WorkerStrip.d.ts.map +1 -0
  462. package/dist/views/Jobs/WorkerStrip.js +63 -0
  463. package/dist/views/Jobs/WorkerStrip.js.map +1 -0
  464. package/dist/views/Jobs/atoms.d.ts +19 -0
  465. package/dist/views/Jobs/atoms.d.ts.map +1 -0
  466. package/dist/views/Jobs/atoms.js +52 -0
  467. package/dist/views/Jobs/atoms.js.map +1 -0
  468. package/dist/views/Jobs/focus.d.ts +23 -0
  469. package/dist/views/Jobs/focus.d.ts.map +1 -0
  470. package/dist/views/Jobs/focus.js +54 -0
  471. package/dist/views/Jobs/focus.js.map +1 -0
  472. package/dist/views/Jobs/icons.d.ts +22 -0
  473. package/dist/views/Jobs/icons.d.ts.map +1 -0
  474. package/dist/views/Jobs/icons.js +23 -0
  475. package/dist/views/Jobs/icons.js.map +1 -0
  476. package/dist/views/Jobs/placeholders.d.ts +6 -0
  477. package/dist/views/Jobs/placeholders.d.ts.map +1 -0
  478. package/dist/views/Jobs/placeholders.js +20 -0
  479. package/dist/views/Jobs/placeholders.js.map +1 -0
  480. package/dist/views/Jobs/useJobDetail.d.ts +21 -0
  481. package/dist/views/Jobs/useJobDetail.d.ts.map +1 -0
  482. package/dist/views/Jobs/useJobDetail.js +39 -0
  483. package/dist/views/Jobs/useJobDetail.js.map +1 -0
  484. package/dist/views/Jobs/useJobsFeed.d.ts +35 -0
  485. package/dist/views/Jobs/useJobsFeed.d.ts.map +1 -0
  486. package/dist/views/Jobs/useJobsFeed.js +231 -0
  487. package/dist/views/Jobs/useJobsFeed.js.map +1 -0
  488. package/dist/views/Jobs/useReviewItems.d.ts +39 -0
  489. package/dist/views/Jobs/useReviewItems.d.ts.map +1 -0
  490. package/dist/views/Jobs/useReviewItems.js +140 -0
  491. package/dist/views/Jobs/useReviewItems.js.map +1 -0
  492. package/dist/views/Jobs.d.ts +10 -1
  493. package/dist/views/Jobs.d.ts.map +1 -1
  494. package/dist/views/Jobs.js +126 -35
  495. package/dist/views/Jobs.js.map +1 -1
  496. package/dist/views/MediaBrowser.d.ts.map +1 -1
  497. package/dist/views/MediaBrowser.js +37 -9
  498. package/dist/views/MediaBrowser.js.map +1 -1
  499. package/dist/views/Pages/PagesListView.d.ts +1 -1
  500. package/dist/views/Pages/PagesListView.d.ts.map +1 -1
  501. package/dist/views/Pages/PagesListView.js +34 -5
  502. package/dist/views/Pages/PagesListView.js.map +1 -1
  503. package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -1
  504. package/dist/views/Posts/NewPostTypeInstructions.js +12 -2
  505. package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -1
  506. package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
  507. package/dist/views/Posts/PostTypesView.js +10 -1
  508. package/dist/views/Posts/PostTypesView.js.map +1 -1
  509. package/dist/views/Posts/PostsListView.d.ts +1 -2
  510. package/dist/views/Posts/PostsListView.d.ts.map +1 -1
  511. package/dist/views/Posts/PostsListView.js +31 -3
  512. package/dist/views/Posts/PostsListView.js.map +1 -1
  513. package/dist/views/SEO.d.ts.map +1 -1
  514. package/dist/views/SEO.js +3 -3
  515. package/dist/views/SEO.js.map +1 -1
  516. package/dist/views/ScriptTags.d.ts.map +1 -1
  517. package/dist/views/ScriptTags.js +20 -15
  518. package/dist/views/ScriptTags.js.map +1 -1
  519. package/dist/views/Settings.d.ts.map +1 -1
  520. package/dist/views/Settings.js +115 -51
  521. package/dist/views/Settings.js.map +1 -1
  522. package/dist/views/SpamFolder.d.ts +2 -0
  523. package/dist/views/SpamFolder.d.ts.map +1 -0
  524. package/dist/views/SpamFolder.js +14 -0
  525. package/dist/views/SpamFolder.js.map +1 -0
  526. package/dist/views/Trash.d.ts +6 -1
  527. package/dist/views/Trash.d.ts.map +1 -1
  528. package/dist/views/Trash.js +7 -2
  529. package/dist/views/Trash.js.map +1 -1
  530. package/dist/views/Users.js +2 -2
  531. package/dist/views/Users.js.map +1 -1
  532. package/dist/views/WidgetsPlaceholder.d.ts +2 -0
  533. package/dist/views/WidgetsPlaceholder.d.ts.map +1 -0
  534. package/dist/views/WidgetsPlaceholder.js +15 -0
  535. package/dist/views/WidgetsPlaceholder.js.map +1 -0
  536. package/dist/views/dashboard/kpi-grid.d.ts +18 -0
  537. package/dist/views/dashboard/kpi-grid.d.ts.map +1 -0
  538. package/dist/views/dashboard/kpi-grid.js +36 -0
  539. package/dist/views/dashboard/kpi-grid.js.map +1 -0
  540. package/dist/views/lazy-views.d.ts +4 -0
  541. package/dist/views/lazy-views.d.ts.map +1 -1
  542. package/dist/views/lazy-views.js +4 -0
  543. package/dist/views/lazy-views.js.map +1 -1
  544. package/dist/views/page-editor/PageTab.js +1 -1
  545. package/dist/views/page-editor/PageTab.js.map +1 -1
  546. package/dist/views/page-editor/ReviewBar.js +1 -1
  547. package/dist/views/page-editor/ReviewBar.js.map +1 -1
  548. package/dist/views/page-editor/VersionHistoryPanel.js +1 -1
  549. package/dist/views/page-editor/VersionHistoryPanel.js.map +1 -1
  550. package/dist/views/seo/AuditTab.js +1 -1
  551. package/dist/views/seo/AuditTab.js.map +1 -1
  552. package/dist/views/seo/OverviewTab.d.ts.map +1 -1
  553. package/dist/views/seo/OverviewTab.js +2 -2
  554. package/dist/views/seo/OverviewTab.js.map +1 -1
  555. package/dist/views/seo/ProposalsTab.js +1 -1
  556. package/dist/views/seo/ProposalsTab.js.map +1 -1
  557. package/dist/views/settings/AISettingsTab.d.ts.map +1 -1
  558. package/dist/views/settings/AISettingsTab.js +4 -2
  559. package/dist/views/settings/AISettingsTab.js.map +1 -1
  560. package/dist/views/settings/AccessProtectionCard.js +1 -1
  561. package/dist/views/settings/AccessProtectionCard.js.map +1 -1
  562. package/dist/views/settings/AdminThemeCard.d.ts.map +1 -1
  563. package/dist/views/settings/AdminThemeCard.js +63 -15
  564. package/dist/views/settings/AdminThemeCard.js.map +1 -1
  565. package/dist/views/settings/AppearanceTab.d.ts.map +1 -1
  566. package/dist/views/settings/AppearanceTab.js +4 -2
  567. package/dist/views/settings/AppearanceTab.js.map +1 -1
  568. package/dist/views/settings/BrandPreviewCard.js +1 -1
  569. package/dist/views/settings/BrandPreviewCard.js.map +1 -1
  570. package/dist/views/settings/BrandVoiceCard.js +1 -1
  571. package/dist/views/settings/BrandVoiceCard.js.map +1 -1
  572. package/dist/views/settings/EmailSettingsTab.d.ts.map +1 -1
  573. package/dist/views/settings/EmailSettingsTab.js +5 -3
  574. package/dist/views/settings/EmailSettingsTab.js.map +1 -1
  575. package/dist/views/settings/GeneralSettingsTab.d.ts.map +1 -1
  576. package/dist/views/settings/GeneralSettingsTab.js +4 -2
  577. package/dist/views/settings/GeneralSettingsTab.js.map +1 -1
  578. package/dist/views/settings/McpConnectTab.d.ts.map +1 -1
  579. package/dist/views/settings/McpConnectTab.js +1 -1
  580. package/dist/views/settings/McpConnectTab.js.map +1 -1
  581. package/dist/views/settings/ProviderConnectionCard.js +1 -1
  582. package/dist/views/settings/ProviderConnectionCard.js.map +1 -1
  583. package/dist/views/settings/SearchDataCard.js +1 -1
  584. package/dist/views/settings/SearchDataCard.js.map +1 -1
  585. package/dist/views/settings/SecurityStatusCard.js +2 -2
  586. package/dist/views/settings/SecurityStatusCard.js.map +1 -1
  587. package/dist/views/settings/SecurityTab.d.ts.map +1 -1
  588. package/dist/views/settings/SecurityTab.js +4 -2
  589. package/dist/views/settings/SecurityTab.js.map +1 -1
  590. package/dist/views/settings/SeoAutopilotCard.js +2 -2
  591. package/dist/views/settings/SeoAutopilotCard.js.map +1 -1
  592. package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
  593. package/dist/views/settings/SeoSettingsTab.js +5 -3
  594. package/dist/views/settings/SeoSettingsTab.js.map +1 -1
  595. package/dist/views/settings/SiteIndexStatusCard.js +1 -1
  596. package/dist/views/settings/SiteIndexStatusCard.js.map +1 -1
  597. package/dist/views/settings/SiteThemeCard.js +1 -1
  598. package/dist/views/settings/UpdatesTab.d.ts.map +1 -1
  599. package/dist/views/settings/UpdatesTab.js +6 -5
  600. package/dist/views/settings/UpdatesTab.js.map +1 -1
  601. package/dist/views/settings/components.d.ts +7 -11
  602. package/dist/views/settings/components.d.ts.map +1 -1
  603. package/dist/views/settings/components.js +18 -12
  604. package/dist/views/settings/components.js.map +1 -1
  605. package/dist/views/settings/useAppearanceSettings.d.ts +7 -1
  606. package/dist/views/settings/useAppearanceSettings.d.ts.map +1 -1
  607. package/dist/views/settings/useAppearanceSettings.js +89 -22
  608. package/dist/views/settings/useAppearanceSettings.js.map +1 -1
  609. package/dist/views/users/UserDetailDrawer.js +1 -1
  610. package/dist/views/users/UserDetailDrawer.js.map +1 -1
  611. package/package.json +11 -9
  612. package/src/AdminRoot.tsx +88 -30
  613. package/src/LoginRoot.tsx +2 -1
  614. package/src/__tests__/components/author-avatar.test.tsx +3 -20
  615. package/src/__tests__/components/notification-bell.test.tsx +146 -14
  616. package/src/__tests__/components/responsive-tiers.render.test.tsx +461 -0
  617. package/src/__tests__/components/serp-preview.render.test.tsx +6 -3
  618. package/src/__tests__/components/shell-primitives.render.test.tsx +951 -0
  619. package/src/__tests__/components/theme-provider-accent.test.tsx +83 -8
  620. package/src/__tests__/components/toggle.test.tsx +11 -3
  621. package/src/__tests__/components/ui-primitives.test.tsx +5 -8
  622. package/src/__tests__/layout/build-nav-items.test.ts +1 -1
  623. package/src/__tests__/layout/shell-nav.render.test.tsx +983 -0
  624. package/src/__tests__/layout/sidebar-forms-submenu.render.test.tsx +8 -4
  625. package/src/__tests__/layout/sidebar-groups.render.test.tsx +116 -0
  626. package/src/__tests__/layout/sidebar-menus.render.test.tsx +116 -0
  627. package/src/__tests__/layout/sidebar-plugin-nav.test.ts +1 -1
  628. package/src/__tests__/layout/sidebar-posts.test.ts +48 -40
  629. package/src/__tests__/layout/sidebar-submenu.render.test.tsx +35 -10
  630. package/src/__tests__/layout/topbar.render.test.tsx +464 -0
  631. package/src/__tests__/lib/api.test.ts +2 -0
  632. package/src/__tests__/lib/job-drawer.test.ts +493 -0
  633. package/src/__tests__/lib/job-view.test.ts +437 -0
  634. package/src/__tests__/lib/jobs-service.test.ts +98 -0
  635. package/src/__tests__/router/admin-route-order.test.ts +39 -0
  636. package/src/__tests__/styles/ai-gradient-contrast.test.ts +135 -0
  637. package/src/__tests__/styles/breakpoint-order.test.ts +68 -0
  638. package/src/__tests__/styles/responsive-layer.test.ts +367 -12
  639. package/src/__tests__/styles/tailwind-preset.test.ts +154 -0
  640. package/src/__tests__/styles/theme-token-scope.test.ts +10 -3
  641. package/src/__tests__/styles/tiers.test.ts +136 -0
  642. package/src/__tests__/styles/tokens-drift.test.ts +67 -6
  643. package/src/__tests__/views/admin-lazy-route.render.test.tsx +41 -1
  644. package/src/__tests__/views/ai-settings.render.test.tsx +4 -4
  645. package/src/__tests__/views/appearance-menus-route.render.test.tsx +58 -0
  646. package/src/__tests__/views/appearance-settings.render.test.tsx +10 -8
  647. package/src/__tests__/views/appearance-theme-provider.render.test.tsx +347 -0
  648. package/src/__tests__/views/dashboard.test.tsx +48 -0
  649. package/src/__tests__/views/document-edit-navigation.render.test.tsx +130 -0
  650. package/src/__tests__/views/document-edit-routes.render.test.tsx +130 -0
  651. package/src/__tests__/views/email-settings.render.test.tsx +2 -2
  652. package/src/__tests__/views/form-entries.render.test.tsx +22 -0
  653. package/src/__tests__/views/form-submissions.render.test.tsx +22 -0
  654. package/src/__tests__/views/forms-list.render.test.tsx +13 -0
  655. package/src/__tests__/views/general-settings.render.test.tsx +38 -11
  656. package/src/__tests__/views/jobs-drawer.render.test.tsx +1047 -0
  657. package/src/__tests__/views/jobs-list.render.test.tsx +850 -0
  658. package/src/__tests__/views/kpi-grid.test.ts +29 -0
  659. package/src/__tests__/views/mcp-connect.render.test.tsx +1 -0
  660. package/src/__tests__/views/post-section-editor.test.tsx +5 -1
  661. package/src/__tests__/views/script-tags.render.test.tsx +29 -0
  662. package/src/__tests__/views/security-settings.render.test.tsx +9 -10
  663. package/src/__tests__/views/seo-settings.render.test.tsx +7 -9
  664. package/src/__tests__/views/settings-save-bar.render.test.tsx +91 -0
  665. package/src/__tests__/views/settings-save-footer.render.test.tsx +25 -15
  666. package/src/__tests__/views/settings-tabs.render.test.tsx +371 -0
  667. package/src/__tests__/views/updates-tab.render.test.tsx +4 -3
  668. package/src/components/Breadcrumbs.tsx +7 -1
  669. package/src/components/CommandPalette.tsx +51 -3
  670. package/src/components/LocaleSwitcher.tsx +5 -6
  671. package/src/components/NotificationBell.tsx +74 -18
  672. package/src/components/OfflineStatus.tsx +1 -1
  673. package/src/components/PresenceIndicator.tsx +2 -1
  674. package/src/components/ReflowMeter.tsx +3 -3
  675. package/src/components/SEOPanel.tsx +2 -2
  676. package/src/components/ThemeProvider.tsx +122 -42
  677. package/src/components/TipTapEditor.tsx +3 -4
  678. package/src/components/forms/EmbedPanel.tsx +1 -1
  679. package/src/components/forms/FormEntryDetailPane.tsx +7 -1
  680. package/src/components/forms/ShortcodeGlossary.tsx +7 -1
  681. package/src/components/forms/primitives.tsx +37 -21
  682. package/src/components/pages/tag-badges.tsx +4 -3
  683. package/src/components/seo/PageAuditPanel.tsx +2 -2
  684. package/src/components/seo/SerpPreview.tsx +5 -5
  685. package/src/components/seo/primitives.tsx +2 -2
  686. package/src/components/ui/AuthorAvatar.tsx +11 -10
  687. package/src/components/ui/Badge.tsx +1 -1
  688. package/src/components/ui/Breadcrumb.tsx +109 -0
  689. package/src/components/ui/Button.tsx +40 -15
  690. package/src/components/ui/Card.tsx +35 -32
  691. package/src/components/ui/CardListControls.tsx +90 -0
  692. package/src/components/ui/CountChip.tsx +90 -0
  693. package/src/components/ui/FilterPill.tsx +5 -3
  694. package/src/components/ui/InspectorPane.tsx +21 -7
  695. package/src/components/ui/Modal.tsx +20 -6
  696. package/src/components/ui/PageHeader.tsx +63 -0
  697. package/src/components/ui/RowActions.tsx +7 -2
  698. package/src/components/ui/Segmented.tsx +97 -0
  699. package/src/components/ui/Toaster.tsx +139 -0
  700. package/src/components/ui/Toggle.tsx +18 -14
  701. package/src/components/ui/UnderlineTabs.tsx +373 -0
  702. package/src/components/ui/UnsavedBar.tsx +119 -0
  703. package/src/components/ui/admin-links.tsx +20 -0
  704. package/src/components/ui/card-table.ts +34 -0
  705. package/src/components/virtual-window.tsx +13 -2
  706. package/src/fields/RelationshipField.tsx +1 -1
  707. package/src/hooks/useAppearanceBootstrap.ts +9 -3
  708. package/src/index.ts +19 -0
  709. package/src/layout/Header.tsx +210 -135
  710. package/src/layout/Layout.tsx +61 -108
  711. package/src/layout/MobileTabBar.tsx +92 -0
  712. package/src/layout/Sidebar.tsx +555 -593
  713. package/src/layout/nav-config.tsx +425 -0
  714. package/src/layout/primitives/AdminShell.tsx +154 -6
  715. package/src/layout/topbar-classes.ts +8 -0
  716. package/src/layout/use-nav-items.ts +40 -0
  717. package/src/lib/api.ts +7 -1
  718. package/src/lib/appearance-api.ts +4 -0
  719. package/src/lib/job-drawer.ts +424 -0
  720. package/src/lib/job-view.ts +456 -0
  721. package/src/lib/jobs-service.ts +290 -0
  722. package/src/lib/sheet-focus.ts +71 -0
  723. package/src/lib/tiers.ts +43 -0
  724. package/src/router/admin-routes.ts +8 -0
  725. package/src/router/index.ts +9 -1
  726. package/src/styles/prenta-tokens.css +263 -82
  727. package/src/styles/responsive.css +460 -22
  728. package/src/styles/theme.css +177 -28
  729. package/src/views/ApiKeys.tsx +2 -2
  730. package/src/views/AppearanceChrome.tsx +74 -0
  731. package/src/views/CollectionList.tsx +264 -205
  732. package/src/views/Dashboard.tsx +26 -13
  733. package/src/views/DocumentEdit.tsx +111 -56
  734. package/src/views/FormDeliveries.tsx +30 -17
  735. package/src/views/FormEntries.tsx +28 -20
  736. package/src/views/FormSubmissions.tsx +32 -34
  737. package/src/views/Forms.tsx +55 -34
  738. package/src/views/FormsSettings.tsx +94 -0
  739. package/src/views/Jobs/BusyButton.tsx +56 -0
  740. package/src/views/Jobs/CancelJobDialog.tsx +127 -0
  741. package/src/views/Jobs/DrawerActivity.tsx +142 -0
  742. package/src/views/Jobs/DrawerOverview.tsx +139 -0
  743. package/src/views/Jobs/DrawerResults.tsx +155 -0
  744. package/src/views/Jobs/DrawerReview.tsx +464 -0
  745. package/src/views/Jobs/JobDrawer.tsx +556 -0
  746. package/src/views/Jobs/JobsTable.tsx +260 -0
  747. package/src/views/Jobs/WorkerStrip.tsx +144 -0
  748. package/src/views/Jobs/atoms.tsx +93 -0
  749. package/src/views/Jobs/focus.ts +56 -0
  750. package/src/views/Jobs/icons.tsx +111 -0
  751. package/src/views/Jobs/placeholders.tsx +66 -0
  752. package/src/views/Jobs/useJobDetail.ts +64 -0
  753. package/src/views/Jobs/useJobsFeed.ts +249 -0
  754. package/src/views/Jobs/useReviewItems.ts +187 -0
  755. package/src/views/Jobs.tsx +282 -105
  756. package/src/views/MediaBrowser.tsx +71 -21
  757. package/src/views/Pages/PagesListView.tsx +115 -39
  758. package/src/views/Posts/NewPostTypeInstructions.tsx +15 -10
  759. package/src/views/Posts/PostTypesView.tsx +28 -6
  760. package/src/views/Posts/PostsListView.tsx +130 -40
  761. package/src/views/SEO.tsx +23 -8
  762. package/src/views/ScriptTags.tsx +233 -153
  763. package/src/views/Settings.tsx +215 -256
  764. package/src/views/SpamFolder.tsx +32 -0
  765. package/src/views/Trash.tsx +12 -1
  766. package/src/views/Users.tsx +2 -2
  767. package/src/views/WidgetsPlaceholder.tsx +33 -0
  768. package/src/views/dashboard/kpi-grid.ts +38 -0
  769. package/src/views/lazy-views.tsx +12 -0
  770. package/src/views/page-editor/PageTab.tsx +2 -2
  771. package/src/views/page-editor/ReviewBar.tsx +1 -1
  772. package/src/views/page-editor/VersionHistoryPanel.tsx +1 -1
  773. package/src/views/seo/AuditTab.tsx +1 -1
  774. package/src/views/seo/OverviewTab.tsx +6 -5
  775. package/src/views/seo/ProposalsTab.tsx +1 -1
  776. package/src/views/settings/AISettingsTab.tsx +10 -6
  777. package/src/views/settings/AccessProtectionCard.tsx +1 -1
  778. package/src/views/settings/AdminThemeCard.tsx +228 -35
  779. package/src/views/settings/AppearanceTab.tsx +10 -6
  780. package/src/views/settings/BrandPreviewCard.tsx +1 -1
  781. package/src/views/settings/BrandVoiceCard.tsx +1 -1
  782. package/src/views/settings/EmailSettingsTab.tsx +11 -7
  783. package/src/views/settings/GeneralSettingsTab.tsx +10 -6
  784. package/src/views/settings/McpConnectTab.tsx +2 -1
  785. package/src/views/settings/ProviderConnectionCard.tsx +1 -1
  786. package/src/views/settings/SearchDataCard.tsx +1 -1
  787. package/src/views/settings/SecurityStatusCard.tsx +2 -2
  788. package/src/views/settings/SecurityTab.tsx +10 -6
  789. package/src/views/settings/SeoAutopilotCard.tsx +2 -2
  790. package/src/views/settings/SeoSettingsTab.tsx +13 -9
  791. package/src/views/settings/SiteIndexStatusCard.tsx +1 -1
  792. package/src/views/settings/SiteThemeCard.tsx +1 -1
  793. package/src/views/settings/UpdatesTab.tsx +107 -101
  794. package/src/views/settings/components.tsx +26 -62
  795. package/src/views/settings/useAppearanceSettings.ts +116 -25
  796. package/src/views/users/UserDetailDrawer.tsx +1 -1
  797. package/dist/components/ui/StatPill.d.ts +0 -18
  798. package/dist/components/ui/StatPill.d.ts.map +0 -1
  799. package/dist/components/ui/StatPill.js +0 -5
  800. package/dist/components/ui/StatPill.js.map +0 -1
  801. package/src/components/ui/StatPill.tsx +0 -37
@@ -1,58 +1,16 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useRef, useState, type ReactNode } from 'react'
3
+ import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'
4
4
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
5
+ import { accentPickCssVars, generateAdminThemeTokens } from '@prenta/core/appearance'
6
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
5
7
  import { adminPortalContainer } from '../lib/portal-container.js'
8
+ import { TIER_MEDIA } from '../lib/tiers.js'
6
9
  import { useTheme } from '../components/ThemeProvider.js'
7
- import { fetchFormsSidebarCounts, type FormsSidebarCount } from '../lib/forms-service.js'
8
- import { onFormsChanged } from '../lib/forms-events.js'
9
- import {
10
- LayoutDashboard,
11
- FileText,
12
- File,
13
- Image,
14
- Settings,
15
- ChevronLeft,
16
- ChevronDown,
17
- ClipboardList,
18
- Inbox,
19
- AlertTriangle,
20
- Search as SearchIcon,
21
- Briefcase,
22
- FolderOpen,
23
- BookOpen,
24
- HelpCircle,
25
- Newspaper,
26
- PanelTop,
27
- PanelBottom,
28
- Layers,
29
- Code2,
30
- ListTree,
31
- Menu as MenuIcon,
32
- PanelLeftClose,
33
- PanelLeftOpen,
34
- Trash2,
35
- ListTodo,
36
- } from 'lucide-react'
37
- import type { LucideIcon } from 'lucide-react'
38
10
  import { PrentaBrandLogo, PrentaMark } from '../assets/prenta-logo.js'
39
11
  import { LogoPlaceholder } from '../components/LogoPlaceholder.js'
40
-
41
- const ICON_MAP: Record<string, LucideIcon> = {
42
- file: File,
43
- 'file-text': FileText,
44
- briefcase: Briefcase,
45
- folder: FolderOpen,
46
- book: BookOpen,
47
- help: HelpCircle,
48
- newspaper: Newspaper,
49
- navigation: ListTree,
50
- menu: MenuIcon,
51
- 'list-tree': ListTree,
52
- PanelTop: PanelTop,
53
- PanelBottom: PanelBottom,
54
- Layers: Layers,
55
- }
12
+ import { activeChildPath, hasActiveDescendant, isPathActive, type NavItem } from './nav-config.js'
13
+ import { useNavItems } from './use-nav-items.js'
56
14
 
57
15
  /**
58
16
  * Sidebar brand lockup.
@@ -64,7 +22,8 @@ const ICON_MAP: Record<string, LucideIcon> = {
64
22
  * un-branded installs. The placeholder replaces the old initials badge, which
65
23
  * read as a user avatar (the real account avatar already sits top-right).
66
24
  * Dark-mode uses the dark logo when one is configured. Logos are never
67
- * stretched/cropped (object-contain).
25
+ * stretched/cropped (object-contain). Kit sizes (cms-responsive.css): the
26
+ * full logo is 24px tall and at most 150px wide; the rail mark is 26px.
68
27
  */
69
28
  function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean }) {
70
29
  const { adminBrand, resolvedTheme } = useTheme()
@@ -94,26 +53,32 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
94
53
  const cfg = usable(configLogo)
95
54
  const alt = brandName ?? 'Admin'
96
55
 
97
- // Wide horizontal logos must scale down to fit the narrow rail / sidebar
98
- // header instead of overflowing — height-capped, width auto, never wider than
99
- // the available space.
100
- const logoClass = 'h-8 w-auto max-w-full object-contain'
56
+ // Wide horizontal logos scale down to fit the rail / sidebar header instead
57
+ // of overflowing: height-capped, width auto, never wider than the slot.
58
+ const logoClass = 'h-6 w-auto max-w-full object-contain'
101
59
 
102
60
  if (collapsed) {
103
61
  // Collapsed rail prefers a dedicated square mark; otherwise it shows the
104
62
  // logo (scaled to fit) — never the favicon.
105
63
  if (mark)
106
- return <img src={mark} alt={alt} className="h-8 w-8 object-contain" onError={onErr(mark)} />
64
+ return (
65
+ <img
66
+ src={mark}
67
+ alt={alt}
68
+ className="h-[26px] w-[26px] object-contain"
69
+ onError={onErr(mark)}
70
+ />
71
+ )
107
72
  if (primary)
108
73
  return <img src={primary} alt={alt} className={logoClass} onError={onErr(primary)} />
109
74
  if (cfg) return <img src={cfg} alt={alt} className={logoClass} onError={onErr(cfg)} />
110
75
  if (hasInitials) return <LogoPlaceholder tone="sidebar" />
111
- return <PrentaMark className="h-8 w-8" />
76
+ return <PrentaMark className="h-[26px] w-[26px]" />
112
77
  }
113
78
 
114
79
  if (primary) {
115
80
  return (
116
- <div className="flex min-w-0 items-center gap-2.5">
81
+ <div className="flex max-w-[150px] min-w-0 items-center gap-2.5">
117
82
  <img src={primary} alt={alt} className={logoClass} onError={onErr(primary)} />
118
83
  </div>
119
84
  )
@@ -121,7 +86,7 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
121
86
 
122
87
  if (cfg) {
123
88
  return (
124
- <div className="flex min-w-0 items-center gap-2.5">
89
+ <div className="flex max-w-[150px] min-w-0 items-center gap-2.5">
125
90
  <img src={cfg} alt={alt} className={logoClass} onError={onErr(cfg)} />
126
91
  </div>
127
92
  )
@@ -133,12 +98,10 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
133
98
  <img
134
99
  src={mark}
135
100
  alt={alt}
136
- className="h-8 w-8 shrink-0 object-contain"
101
+ className="h-[26px] w-[26px] shrink-0 object-contain"
137
102
  onError={onErr(mark)}
138
103
  />
139
- <span className="text-sidebar-foreground truncate text-sm font-medium">
140
- {brandName ?? alt}
141
- </span>
104
+ <span className="text-body text-txt truncate font-medium">{brandName ?? alt}</span>
142
105
  </div>
143
106
  )
144
107
  }
@@ -150,445 +113,556 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
150
113
  if (brandName) {
151
114
  return (
152
115
  <div className="flex min-w-0 items-center gap-2.5">
153
- <PrentaMark className="h-7 w-7 shrink-0" />
154
- <span className="text-sidebar-foreground truncate text-sm font-medium">{brandName}</span>
116
+ <PrentaMark className="h-[26px] w-[26px] shrink-0" />
117
+ <span className="text-body text-txt truncate font-medium">{brandName}</span>
155
118
  </div>
156
119
  )
157
120
  }
158
121
 
159
- return <PrentaBrandLogo className="h-8 w-auto" />
122
+ return <PrentaBrandLogo className="h-6 w-auto" />
160
123
  }
161
124
 
162
- /**
163
- * Children of the Posts parent. Same shape regardless of how many
164
- * post-type collections are registered — Post Types are accessed via
165
- * the `/posts/types` cards page, not as individual sidebar entries.
166
- */
167
- const POSTS_CHILDREN: NavItem[] = [
168
- { path: '/posts', label: 'All Posts', icon: FileText },
169
- { path: '/posts/types', label: 'Post Types', icon: Layers },
170
- ]
171
-
172
- /**
173
- * Nav tree for the default (config-less) install. Dashboard sits on its
174
- * own at the top, then a CONTENT group holding Pages (no children) above
175
- * Posts (with All Posts + Post Types), followed by Media / Forms / SEO /
176
- * Script Tags, then a SETTINGS group (Users / API Keys / Settings).
177
- */
178
- const defaultNavItems: NavItem[] = [
179
- { path: '/', label: 'Dashboard', icon: LayoutDashboard },
180
- { path: '/pages', label: 'Pages', icon: File, group: 'Content' },
181
- { path: '/posts', label: 'Posts', icon: FileText, group: 'Content', children: POSTS_CHILDREN },
182
- { path: '/media', label: 'Media', icon: Image, group: 'Content' },
183
- { path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
184
- { path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
185
- { path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
186
- { path: '/trash', label: 'Trash', icon: Trash2, group: 'Content' },
187
- { path: '/jobs', label: 'Jobs', icon: ListTodo, group: 'Settings' },
188
- { path: '/settings', label: 'Settings', icon: Settings, group: 'Settings' },
189
- ]
190
-
191
125
  export interface SidebarProps {
192
126
  collapsed: boolean
193
127
  onToggleCollapse: () => void
194
128
  currentPath: string
195
129
  onNavigate: (path: string) => void
196
130
  config?: any
131
+ /** Nav tree from `useNavItems`; built (and Forms counts fetched) here when omitted. */
132
+ items?: NavItem[]
197
133
  }
198
134
 
135
+ /**
136
+ * The admin sidebar (SHELL.md §2). Expanded: section labels, rows and
137
+ * collapsible sub-groups. Collapsed or at ≤1024px: a 60px icon rail with no
138
+ * sub-items, the parent active for any child page, children in a flyout and
139
+ * labels kept for screen readers. The nav itself comes from `nav-config`.
140
+ */
199
141
  export function Sidebar({
200
142
  collapsed,
201
143
  onToggleCollapse,
202
144
  currentPath,
203
145
  onNavigate,
204
146
  config,
147
+ items,
205
148
  }: SidebarProps) {
206
- const formsSidebar = useFormsSidebar()
207
- const navItems = applyFormsNav(buildNavItems(config), formsSidebar)
208
- const { density } = useTheme()
209
- const compact = density === 'compact'
149
+ const ownItems = useNavItems(config, !items)
150
+ const navItems = items ?? ownItems
210
151
 
211
152
  return (
212
153
  <aside
213
- className={`bg-sidebar border-sidebar-border flex h-full flex-col border-r transition-[width] duration-[var(--motion-layout)] ${
154
+ className={`bg-sb border-bdr flex h-full flex-col border-r transition-[width] duration-[var(--mo-slow)] ease-[cubic-bezier(.4,0,.2,1)] ${
214
155
  collapsed ? 'w-[var(--sbw-col)]' : 'w-[var(--sbw)]'
215
156
  }`}
216
157
  >
217
- {/* Brand header — no toggle button here anymore; it now lives in the footer. */}
218
158
  <div
219
- className={`border-sidebar-border flex h-[var(--tbh)] shrink-0 items-center border-b px-4 ${
220
- collapsed ? 'justify-center' : ''
159
+ className={`border-bdr flex h-[var(--tbh)] shrink-0 items-center gap-2.5 overflow-hidden border-b ${
160
+ // Below 769px the drawer's Close button (36px, 44px on touch, at
161
+ // right 10px) sits in this row; keep a long brand name clear of it.
162
+ collapsed ? 'justify-center px-0' : 'mobile:pr-14 px-[14px]'
221
163
  }`}
222
164
  >
223
165
  <BrandLogo config={config} collapsed={collapsed} />
224
166
  </div>
225
167
 
168
+ {/* `relative`: the rail's `sr-only` labels (absolutely positioned)
169
+ would otherwise size the fixed admin root and scroll the shell. */}
226
170
  <nav
227
- className={`flex-1 overflow-y-auto ${compact ? 'space-y-0.5 p-2' : 'space-y-1 p-3'}`}
171
+ className="[&::-webkit-scrollbar-thumb]:rounded-pill [&::-webkit-scrollbar-thumb]:bg-bdr relative flex-1 overflow-x-hidden overflow-y-auto py-2 [&::-webkit-scrollbar]:w-[3px]"
228
172
  aria-label="Primary"
229
173
  >
230
- {renderNavTree(navItems, { collapsed, currentPath, onNavigate, compact })}
174
+ <NavTree items={navItems} ctx={{ collapsed, currentPath, onNavigate }} />
231
175
  </nav>
232
176
 
233
- {/* Footer collapse control. Sits at the bottom of the sidebar so it
234
- mirrors the reference design and stays out of the way during
235
- everyday nav. Hidden below md: the mobile sheet has its own
236
- dismiss affordance in <AdminShell>. */}
237
- <div className="border-sidebar-border hidden shrink-0 border-t p-3 md:block">
177
+ {/* Only at >=1025px: the 769-1024px rail can't expand, and the mobile
178
+ drawer has its own dismiss. */}
179
+ <div className="border-bdr not-narrow:block hidden shrink-0 border-t p-2">
238
180
  <button
181
+ type="button"
239
182
  onClick={onToggleCollapse}
240
- className={`text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-sm transition-colors ${
183
+ className={`rounded-7 text-meta text-sub hover:bg-acc-l hover:text-acc flex w-full cursor-pointer items-center gap-[9px] overflow-hidden border-0 bg-transparent px-2.5 py-[7px] leading-[normal] font-normal whitespace-nowrap ${
241
184
  collapsed ? 'justify-center' : ''
242
185
  }`}
186
+ aria-expanded={!collapsed}
243
187
  aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
244
- title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
245
188
  >
246
- {collapsed ? (
247
- <PanelLeftOpen className="h-4 w-4 shrink-0" aria-hidden />
248
- ) : (
249
- <>
250
- <PanelLeftClose className="h-4 w-4 shrink-0" aria-hidden />
251
- <span className="font-medium">Collapse sidebar</span>
252
- </>
253
- )}
189
+ {/* 19px: the kit's inline chevron sits on the 12px text baseline, so the button is 33px. */}
190
+ <span className="flex h-[19px] shrink-0 items-center">
191
+ {collapsed ? (
192
+ <ChevronRight className="h-[15px] w-[15px]" strokeWidth={1.75} aria-hidden />
193
+ ) : (
194
+ <ChevronLeft className="h-[15px] w-[15px]" strokeWidth={1.75} aria-hidden />
195
+ )}
196
+ </span>
197
+ <span className={collapsed ? 'sr-only' : ''}>Collapse sidebar</span>
254
198
  </button>
255
199
  </div>
256
200
  </aside>
257
201
  )
258
202
  }
259
203
 
260
- // ─── Dynamic Forms unread badge ───────────────────────────────────────────
204
+ // ─── Rendering ──────────────────────────────────────────────────────────────
261
205
 
262
- interface FormsSidebarState {
263
- /** Total unread submissions across active forms (parent badge). */
264
- totalUnread: number
265
- /** Per-form unread counts, used to build the Forms submenu children. */
266
- forms: FormsSidebarCount[]
206
+ interface NavRenderContext {
207
+ collapsed: boolean
208
+ currentPath: string
209
+ onNavigate: (path: string) => void
267
210
  }
268
211
 
212
+ /** Kit `.ni`: 13px --sub, 7px 10px, margin 1px 8px, radius 7, gap 9. */
213
+ const ROW =
214
+ 'rounded-7 text-body flex w-full cursor-pointer items-center gap-[9px] overflow-hidden border-0 leading-[normal] whitespace-nowrap'
215
+ const ROW_IDLE = 'bg-transparent font-[450] text-sub hover:bg-acc-l hover:text-acc'
216
+ // Forced colors drop the fill, so the current row also gets an outline there.
217
+ const FORCED_ACTIVE =
218
+ 'forced-colors:outline-solid forced-colors:outline-2 forced-colors:-outline-offset-2'
219
+ const ROW_ACTIVE = `bg-acc-l font-[550] text-acc ${FORCED_ACTIVE}`
220
+
269
221
  /**
270
- * Live unread counts for the Forms submenu: a total surfaced on the Forms nav
271
- * item plus per-form counts that drive the "All Entries" + per-form children.
272
- * Refreshes on mount and whenever a forms/entries mutation fires the shared
273
- * event (mark read, archive, notify toggle, …). Best-effort: a failed fetch
274
- * keeps the last known value rather than surfacing an error in nav.
222
+ * At >=1025px a parent row toggles its group (the kit's `onClickCapture`);
223
+ * below that, in the mobile drawer, it opens the parent page.
224
+ *
225
+ * Starts wide and corrects after mount, so a server render and the browser's
226
+ * first render agree (the row's `aria-expanded` depends on it).
275
227
  */
276
- function useFormsSidebar(): FormsSidebarState {
277
- const [state, setState] = useState<FormsSidebarState>({ totalUnread: 0, forms: [] })
278
-
228
+ function useWideNav(): boolean {
229
+ const [wide, setWide] = useState(true)
279
230
  useEffect(() => {
280
- let active = true
281
- const load = () => {
282
- fetchFormsSidebarCounts()
283
- .then((counts) => {
284
- if (!active) return
285
- setState({
286
- totalUnread: counts.reduce((sum, c) => sum + (c.unread ?? 0), 0),
287
- forms: counts,
288
- })
289
- })
290
- .catch(() => {
291
- /* keep the last known value; nav must never break on a fetch error */
292
- })
293
- }
294
- load()
295
- const off = onFormsChanged(load)
296
- return () => {
297
- active = false
298
- off()
299
- }
231
+ if (typeof window.matchMedia !== 'function') return
232
+ const mq = window.matchMedia(TIER_MEDIA.narrow)
233
+ const apply = () => setWide(!mq.matches)
234
+ apply()
235
+ mq.addEventListener?.('change', apply)
236
+ return () => mq.removeEventListener?.('change', apply)
300
237
  }, [])
301
-
302
- return state
238
+ return wide
303
239
  }
304
240
 
305
- /**
306
- * Return a copy of the nav tree with the Forms item expanded into a submenu:
307
- * a total-unread badge on the parent, an "All Entries" child, and one child
308
- * per active form carrying its own unread badge.
309
- */
310
- function applyFormsNav(items: NavItem[], state: FormsSidebarState): NavItem[] {
311
- return items.map((item) => {
312
- if (item.path !== '/forms') return item
313
- const children: NavItem[] = [
314
- { path: '/forms/entries', label: 'All Entries', icon: Inbox },
315
- { path: '/forms/deliveries', label: 'Deliveries', icon: AlertTriangle },
316
- ...state.forms.map((f) => ({
317
- path: `/forms/${f.formId}/submissions`,
318
- label: f.name,
319
- icon: ClipboardList,
320
- badge: f.unread,
321
- })),
322
- ]
323
- return {
324
- ...item,
325
- badge: state.totalUnread > 0 ? state.totalUnread : undefined,
326
- children,
327
- }
328
- })
241
+ /** A parent is current for any page under its path or any of its children. */
242
+ function isSectionActive(currentPath: string, item: NavItem): boolean {
243
+ const path = currentPath.split(/[?#]/)[0]!
244
+ return (
245
+ path === item.path ||
246
+ (item.path !== '/' && path.startsWith(item.path + '/')) ||
247
+ hasActiveDescendant(item.children ?? [], currentPath)
248
+ )
329
249
  }
330
250
 
331
- // ─── Rendering ──────────────────────────────────────────────────────────────
332
-
333
- interface NavRenderContext {
334
- collapsed: boolean
335
- currentPath: string
336
- onNavigate: (path: string) => void
337
- /** Compact density tightens nav row vertical padding. */
338
- compact?: boolean
251
+ /** 16px for top-level rows; sub-items default to the kit's 14px. */
252
+ function NavIcon({ item, size: fallback = 16 }: { item: NavItem; size?: number }) {
253
+ const Icon = item.icon
254
+ const size = item.iconSize ?? fallback
255
+ return (
256
+ <span className="flex h-4 w-4 shrink-0 items-center justify-center" aria-hidden>
257
+ <Icon className="shrink-0" size={size} strokeWidth={1.75} />
258
+ </span>
259
+ )
339
260
  }
340
261
 
341
- function isPathActive(currentPath: string, itemPath: string): boolean {
342
- // Ignore query string / hash so state-bearing routes (e.g. `/seo?tab=audit`)
343
- // still highlight their nav item.
344
- currentPath = currentPath.split('?')[0]!.split('#')[0]!
345
- if (itemPath === '/') return currentPath === '/'
346
-
347
- // Special case: /posts is a prefix of /posts/types — without this
348
- // guard, "All Posts" (/posts) would stay highlighted while the user
349
- // is on /posts/types or /posts/types/new. Treat /posts as exact-only
350
- // when a sibling /posts/* route exists.
351
- if (itemPath === '/posts') {
352
- return currentPath === '/posts' || currentPath === '/posts/new'
353
- }
262
+ /** Kit `.ni-badge`: 16px pill, --err fill, 9.5px / 700. */
263
+ function NavBadge({ count, className = '' }: { count?: number; className?: string }) {
264
+ if (!count || count <= 0) return null
265
+ return (
266
+ <span
267
+ className={`rounded-pill bg-err text-badge text-acc-on flex h-4 min-w-4 shrink-0 items-center justify-center px-1 leading-none font-bold tabular-nums ${className}`}
268
+ aria-label={`${count} unread`}
269
+ >
270
+ {count > 99 ? '99+' : count}
271
+ </span>
272
+ )
273
+ }
354
274
 
355
- // Use boundary match so /pages doesn't match /pages-something. Exact, or
356
- // followed by a `/` separator.
357
- return currentPath === itemPath || currentPath.startsWith(itemPath + '/')
275
+ function SectionLabel({ label, collapsed }: { label: string; collapsed: boolean }) {
276
+ // Kit: the collapsed sidebar keeps the label's space but hides it; the
277
+ // 769–1024px rail drops it entirely. The mobile drawer shows it.
278
+ return (
279
+ <div
280
+ className={`text-label text-faint tablet:hidden overflow-hidden px-4 pt-2.5 pb-1 leading-[normal] font-semibold tracking-[0.8px] whitespace-nowrap uppercase ${
281
+ collapsed ? 'pointer-events-none opacity-0' : ''
282
+ }`}
283
+ >
284
+ {label}
285
+ </div>
286
+ )
358
287
  }
359
288
 
360
- function renderNavTree(items: NavItem[], ctx: NavRenderContext): ReactNode {
289
+ function NavTree({ items, ctx }: { items: NavItem[]; ctx: NavRenderContext }) {
361
290
  const nodes: ReactNode[] = []
362
291
  let lastGroup: string | undefined = undefined
363
292
 
364
293
  for (const item of items) {
365
294
  if (item.group && item.group !== lastGroup) {
366
295
  nodes.push(
367
- ctx.collapsed ? (
368
- <div key={`g-${item.group}`} className="flex justify-center pt-3 pb-1">
369
- <span className="border-sidebar-foreground/20 w-4 border-t" aria-hidden />
370
- </div>
371
- ) : (
372
- <div key={`g-${item.group}`} className="px-3 pt-4 pb-1">
373
- <span className="text-sidebar-foreground/80 text-[10px] font-medium tracking-wider uppercase">
374
- {item.group}
375
- </span>
376
- </div>
377
- ),
296
+ <SectionLabel key={`g-${item.group}`} label={item.group} collapsed={ctx.collapsed} />,
378
297
  )
379
- lastGroup = item.group
380
- } else if (!item.group) {
381
- lastGroup = undefined
382
298
  }
299
+ lastGroup = item.group
383
300
 
384
301
  const hasChildren = !!item.children && item.children.length > 0
385
-
386
- // Collapsed parents reveal their children in a hover/focus flyout instead
387
- // of inline (there's no room for indented rows in the 80px rail).
388
- if (hasChildren && ctx.collapsed) {
389
- nodes.push(<CollapsedFlyout key={`flyout:${item.path}`} item={item} ctx={ctx} />)
390
- continue
302
+ // Keys are namespaced: a parent and its first child can share a path.
303
+ if (ctx.collapsed) {
304
+ nodes.push(<RailItem key={`rail:${item.label}:${item.path}`} item={item} ctx={ctx} />)
305
+ } else if (hasChildren) {
306
+ nodes.push(<NavGroup key={`group:${item.label}:${item.path}`} item={item} ctx={ctx} />)
307
+ } else {
308
+ nodes.push(<NavRow key={`item:${item.path}`} item={item} ctx={ctx} />)
391
309
  }
392
-
393
- // Expanded parents with children render as a collapsible section
394
- // (chevron toggle to contract/expand the submenu).
395
- if (hasChildren) {
396
- nodes.push(<NavSection key={`section:${item.path}`} item={item} ctx={ctx} />)
397
- continue
398
- }
399
-
400
- // Keys are namespaced because a parent and its first child can share
401
- // the same path (e.g. the Posts parent and its "All Posts" child are
402
- // both `/posts`). Using the raw path as the key collided and made
403
- // React warn about — and risk dropping — duplicate siblings.
404
- nodes.push(<NavLink key={`item:${item.path}`} item={item} ctx={ctx} />)
405
310
  }
406
311
 
407
- return nodes
312
+ return <>{nodes}</>
313
+ }
314
+
315
+ function NavRow({ item, ctx }: { item: NavItem; ctx: NavRenderContext }) {
316
+ const active = isPathActive(ctx.currentPath, item.path)
317
+ return (
318
+ <div className="mx-2 my-px">
319
+ <button
320
+ type="button"
321
+ onClick={() => ctx.onNavigate(item.path)}
322
+ className={`${ROW} px-2.5 py-[7px] ${active ? ROW_ACTIVE : ROW_IDLE}`}
323
+ aria-current={active ? 'page' : undefined}
324
+ >
325
+ <NavIcon item={item} />
326
+ <span className="truncate">{item.label}</span>
327
+ <NavBadge count={item.badge} className="ml-auto" />
328
+ </button>
329
+ </div>
330
+ )
408
331
  }
409
332
 
410
333
  /**
411
- * Expanded parent with a collapsible submenu. The label/icon navigates to the
412
- * parent destination; a trailing chevron toggles the child list open/closed.
413
- * Defaults to open, and auto-opens when the parent or one of its children is
414
- * the active route so the current page is never hidden behind a collapsed
415
- * section.
334
+ * Expanded parent with a sub-group. Groups start closed and open on their
335
+ * own when the current page is one of their children; a manual toggle holds
336
+ * until the route changes. At ≥1025px clicking the row toggles the group, as
337
+ * in the kit; below that (the mobile drawer) it opens the parent page. The
338
+ * parent stays filled while its section holds the current page, and the
339
+ * current child is filled too (Inbox.html).
416
340
  */
417
- function NavSection({ item, ctx }: { item: NavItem; ctx: NavRenderContext }) {
418
- const Icon = item.icon
419
- const isActive = isPathActive(ctx.currentPath, item.path)
420
- const childActive = item.children!.some((c) => isPathActive(ctx.currentPath, c.path))
421
- const [open, setOpen] = useState(true)
422
- const childListId = `nav-section-${item.path.replace(/[^a-z0-9]+/gi, '-')}`
341
+ function NavGroup({ item, ctx }: { item: NavItem; ctx: NavRenderContext }) {
342
+ const children = item.children!
343
+ const sectionActive = isSectionActive(ctx.currentPath, item)
344
+ const childActive = hasActiveDescendant(children, ctx.currentPath)
345
+ const [open, setOpen] = useState(childActive)
346
+ // Re-sync on navigation. Adjusting state during render (rather than in an
347
+ // effect) avoids painting the old open state for a frame. Query/hash-only
348
+ // changes (filters, tabs) keep the user's toggle.
349
+ const route = ctx.currentPath.split(/[?#]/)[0]!
350
+ const [syncedRoute, setSyncedRoute] = useState(route)
351
+ if (syncedRoute !== route) {
352
+ setSyncedRoute(route)
353
+ setOpen(childActive)
354
+ }
355
+ const listId = `nav-group-${item.label.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`
356
+ const current = activeChildPath(children, ctx.currentPath)
423
357
 
424
- // Keep the active section visible: if the user is on this section's route
425
- // (parent or child) it stays expanded regardless of the manual toggle.
426
- const expanded = open || isActive || childActive
358
+ const wide = useWideNav()
359
+ const onRowClick = () => {
360
+ if (wide) setOpen((value) => !value)
361
+ else ctx.onNavigate(item.path)
362
+ }
427
363
 
364
+ // Kit `.ni` with `.ni-tog`: the 22px toggle sits inside the 7px padding, so
365
+ // parent rows are 36px tall; the toggle ends 10px from the row's edge.
428
366
  return (
429
367
  <div>
430
368
  <div
431
- className={`flex items-center rounded-lg transition-colors ${
432
- isActive
433
- ? 'bg-sidebar-accent text-sidebar-primary'
434
- : 'text-sidebar-foreground hover:bg-sidebar-accent'
369
+ className={`rounded-7 mx-2 my-px flex h-9 items-center pr-2.5 ${
370
+ sectionActive
371
+ ? `bg-acc-l text-acc ${FORCED_ACTIVE}`
372
+ : 'text-sub hover:bg-acc-l hover:text-acc'
435
373
  }`}
436
374
  >
437
375
  <button
438
- onClick={() => ctx.onNavigate(item.path)}
439
- className="flex min-w-0 flex-1 items-center gap-3 rounded-lg py-2.5 pr-1 pl-3 text-left"
440
- aria-current={isActive ? 'page' : undefined}
376
+ type="button"
377
+ onClick={onRowClick}
378
+ className={`${ROW} h-full min-w-0 flex-1 bg-transparent py-0 pl-2.5 text-inherit ${
379
+ sectionActive ? 'font-[550]' : 'font-[450]'
380
+ }`}
381
+ aria-current={sectionActive && !open ? 'page' : undefined}
382
+ // When the row itself toggles, it reports the group like the chevron.
383
+ aria-expanded={wide ? open : undefined}
384
+ aria-controls={wide ? listId : undefined}
441
385
  >
442
- <Icon className="h-5 w-5 shrink-0" aria-hidden />
443
- <span className="truncate text-sm font-medium">{item.label}</span>
386
+ <NavIcon item={item} />
387
+ <span className="truncate">{item.label}</span>
388
+ <NavBadge count={item.badge} className="ml-1.5" />
444
389
  </button>
445
- {!!item.badge && item.badge > 0 && (
446
- <span
447
- className="bg-destructive text-destructive-foreground mr-1 inline-flex min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 py-0.5 text-xs font-medium tabular-nums"
448
- aria-label={`${item.badge} unread`}
449
- >
450
- {item.badge > 99 ? '99+' : item.badge}
451
- </span>
452
- )}
390
+ {/* 24px (kit 22) for WCAG 2.5.8. On touch screens the hit area is
391
+ 44px wide and exactly the 36px row tall, so it doesn't reach into
392
+ the rows above and below. */}
453
393
  <button
454
- onClick={() => setOpen((v) => !v)}
455
- className="text-sidebar-foreground/55 hover:text-sidebar-foreground mr-1 flex shrink-0 items-center rounded-md p-1.5 transition-colors"
456
- aria-label={`${expanded ? 'Collapse' : 'Expand'} ${item.label}`}
457
- aria-expanded={expanded}
458
- aria-controls={childListId}
394
+ type="button"
395
+ onClick={() => setOpen((value) => !value)}
396
+ className="prenta-touch-hit rounded-5 text-faint hover:text-txt relative ml-auto flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent p-0 [--touch-hit-block:-6px] [--touch-hit-inline:-10px] hover:bg-[color-mix(in_srgb,var(--txt)_8%,transparent)]"
397
+ aria-label={`${open ? 'Collapse' : 'Expand'} ${item.label}`}
398
+ aria-expanded={open}
399
+ aria-controls={listId}
459
400
  >
460
- <ChevronDown
461
- className={`h-4 w-4 transition-transform duration-200 ${expanded ? '' : '-rotate-90'}`}
401
+ <ChevronRight
402
+ className={`h-3 w-3 motion-safe:transition-transform motion-safe:duration-[var(--mo-base)] ${open ? 'rotate-90' : ''}`}
403
+ strokeWidth={2.2}
462
404
  aria-hidden
463
405
  />
464
406
  </button>
465
407
  </div>
466
408
 
467
- {/* Tight (gap-less) connector container so the vertical tree guides of
468
- adjacent rows touch and read as one continuous line with curved
469
- elbows into each child. */}
470
- {expanded && (
471
- <div id={childListId} className="relative">
472
- {item.children!.map((child, i) => (
473
- <NavLink
474
- key={`child:${item.path}:${child.path}`}
475
- item={child}
476
- ctx={ctx}
477
- nested
478
- isLast={i === item.children!.length - 1}
479
- />
480
- ))}
409
+ {open && (
410
+ // Kit `.sub-group`: margin 2px 8px 4px 18px, a 1.5px guide on the left
411
+ // and a 13px connector into each row.
412
+ <div
413
+ id={listId}
414
+ className="before:rounded-pill before:bg-bdr relative mt-0.5 mr-2 mb-1 ml-[18px] before:absolute before:top-1 before:bottom-[18px] before:left-0 before:w-[1.5px]"
415
+ >
416
+ {children.map((child) =>
417
+ child.children?.length ? (
418
+ <NestedSubGroup
419
+ key={`nest:${child.label}:${child.path}`}
420
+ item={child}
421
+ ctx={ctx}
422
+ current={current}
423
+ />
424
+ ) : (
425
+ <SubRow
426
+ key={`child:${child.label}:${child.path}`}
427
+ item={child}
428
+ ctx={ctx}
429
+ active={child.path === current}
430
+ />
431
+ ),
432
+ )}
481
433
  </div>
482
434
  )}
483
435
  </div>
484
436
  )
485
437
  }
486
438
 
487
- function NavLink({
439
+ /** Kit `.sub-group .ni`: 12.5px, inset 20px past the guide, with a connector. */
440
+ function SubRow({
488
441
  item,
489
442
  ctx,
490
- nested = false,
491
- isLast = false,
443
+ active,
444
+ kid = false,
445
+ className = '',
492
446
  }: {
493
447
  item: NavItem
494
448
  ctx: NavRenderContext
495
- nested?: boolean
496
- /** Last child in its group — the elbow stops here (no continuation line). */
497
- isLast?: boolean
449
+ active: boolean
450
+ /** Third-level row (`a[data-nest-k]`): inset 30px, icon at half opacity. */
451
+ kid?: boolean
452
+ className?: string
498
453
  }) {
499
- const Icon = item.icon
500
- const isActive = isPathActive(ctx.currentPath, item.path)
501
-
502
- const badge = item.badge && item.badge > 0 ? item.badge : 0
503
-
504
- const button = (
454
+ return (
505
455
  <button
456
+ type="button"
506
457
  onClick={() => ctx.onNavigate(item.path)}
507
- className={`flex w-full items-center gap-3 rounded-lg text-left transition-colors ${
508
- nested
509
- ? ctx.compact
510
- ? 'py-1 pr-3 pl-9'
511
- : 'py-1.5 pr-3 pl-9'
512
- : ctx.compact
513
- ? 'px-3 py-1.5'
514
- : 'px-3 py-2.5'
458
+ className={`${ROW} text-sm2 relative my-px py-[7px] pr-2.5 before:absolute before:top-1/2 before:left-0 before:h-[1.5px] before:w-[13px] before:-translate-y-1/2 ${
459
+ kid ? 'pl-[30px]' : 'pl-5'
515
460
  } ${
516
- isActive
517
- ? 'bg-sidebar-accent text-sidebar-primary'
518
- : 'text-sidebar-foreground hover:bg-sidebar-accent'
519
- } ${ctx.collapsed ? 'justify-center px-3' : ''}`}
520
- title={ctx.collapsed ? item.label : ''}
521
- aria-current={isActive ? 'page' : undefined}
461
+ active ? `${ROW_ACTIVE} before:bg-acc-m` : `${ROW_IDLE} before:bg-bdr hover:before:bg-acc-m`
462
+ } ${className}`}
463
+ aria-current={active ? 'page' : undefined}
522
464
  >
523
- <span className="relative flex shrink-0 items-center">
524
- {!nested && <Icon className="h-5 w-5 shrink-0" aria-hidden />}
525
- {nested && !ctx.collapsed && (
526
- <Icon className="text-sidebar-foreground/60 h-4 w-4 shrink-0" aria-hidden />
527
- )}
528
- {/* Collapsed rail: a dot signals unread without room for a number. */}
529
- {ctx.collapsed && badge > 0 && (
530
- <span
531
- className="bg-destructive absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full"
532
- aria-hidden
533
- />
534
- )}
465
+ <span className={kid ? 'flex opacity-50' : 'flex'}>
466
+ <NavIcon item={item} size={14} />
535
467
  </span>
536
- {!ctx.collapsed && (
537
- <span className={`truncate font-medium ${nested ? 'text-[13px]' : 'text-sm'}`}>
538
- {item.label}
539
- </span>
540
- )}
541
- {!ctx.collapsed && badge > 0 && (
542
- <span
543
- className="bg-destructive text-destructive-foreground ml-auto inline-flex min-w-5 items-center justify-center rounded-full px-1.5 py-0.5 text-xs font-medium tabular-nums"
544
- aria-label={`${badge} unread`}
468
+ <span className="truncate">{item.label}</span>
469
+ <NavBadge count={item.badge} className="ml-auto" />
470
+ </button>
471
+ )
472
+ }
473
+
474
+ /**
475
+ * Third level (`cms-layouts.js` "3rd-level nav nesting"): All Entries and All
476
+ * Forms open one row per form. The row itself navigates; the chevron at its
477
+ * right edge toggles the rows, which start closed and open when one of them
478
+ * is the current page.
479
+ */
480
+ function NestedSubGroup({
481
+ item,
482
+ ctx,
483
+ current,
484
+ }: {
485
+ item: NavItem
486
+ ctx: NavRenderContext
487
+ current: string | undefined
488
+ }) {
489
+ const kids = item.children!
490
+ const kidActive = kids.some((kid) => kid.path === current)
491
+ const [open, setOpen] = useState(kidActive)
492
+ const route = ctx.currentPath.split(/[?#]/)[0]!
493
+ const [syncedRoute, setSyncedRoute] = useState(route)
494
+ if (syncedRoute !== route) {
495
+ setSyncedRoute(route)
496
+ setOpen(kidActive)
497
+ }
498
+ const listId = `nav-nest-${item.label.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`
499
+
500
+ return (
501
+ <>
502
+ <div className="relative">
503
+ <SubRow item={item} ctx={ctx} active={item.path === current} className="pr-[30px]!" />
504
+ <button
505
+ type="button"
506
+ onClick={() => setOpen((value) => !value)}
507
+ // Touch hit area: 44px wide, the 30px row tall.
508
+ className={`prenta-touch-hit rounded-5 absolute top-1/2 right-1 flex h-6 w-6 -translate-y-1/2 cursor-pointer items-center justify-center border-0 bg-transparent p-0 [--touch-hit-block:-3px] [--touch-hit-inline:-10px] ${
509
+ item.path === current ? 'text-acc' : 'text-sub'
510
+ }`}
511
+ aria-label={`${open ? 'Collapse' : 'Expand'} ${item.label}`}
512
+ aria-expanded={open}
513
+ aria-controls={listId}
545
514
  >
546
- {badge > 99 ? '99+' : badge}
547
- </span>
515
+ <ChevronRight
516
+ className={`h-[14px] w-[14px] opacity-55 motion-safe:transition-transform motion-safe:duration-[var(--mo-base)] ${open ? 'rotate-90' : ''}`}
517
+ strokeWidth={2.4}
518
+ aria-hidden
519
+ />
520
+ </button>
521
+ </div>
522
+ {open && (
523
+ <div id={listId}>
524
+ {kids.map((kid) => (
525
+ <SubRow
526
+ key={`kid:${kid.label}:${kid.path}`}
527
+ item={kid}
528
+ ctx={ctx}
529
+ active={kid.path === current}
530
+ kid
531
+ />
532
+ ))}
533
+ </div>
548
534
  )}
535
+ </>
536
+ )
537
+ }
538
+
539
+ /**
540
+ * Rail item (collapsed sidebar or ≤1024px). The label stays in the
541
+ * accessibility tree (`sr-only`). Parents never render sub-items here: the
542
+ * parent is active for any child page and the children open in a flyout.
543
+ */
544
+ function RailItem({ item, ctx }: { item: NavItem; ctx: NavRenderContext }) {
545
+ const hasChildren = !!item.children && item.children.length > 0
546
+ const active = hasChildren
547
+ ? isSectionActive(ctx.currentPath, item)
548
+ : isPathActive(ctx.currentPath, item.path)
549
+
550
+ const trigger = (
551
+ <button
552
+ type="button"
553
+ onClick={() => ctx.onNavigate(item.path)}
554
+ className={`${ROW} justify-center px-0 py-[9px] ${active ? ROW_ACTIVE : ROW_IDLE}`}
555
+ aria-current={active ? 'page' : undefined}
556
+ >
557
+ <NavIcon item={item} />
558
+ <span className="sr-only">{item.label}</span>
559
+ <NavBadge count={item.badge} />
549
560
  </button>
550
561
  )
551
562
 
552
- if (!nested || ctx.collapsed) return button
563
+ if (hasChildren) return <RailFlyout item={item} ctx={ctx} trigger={trigger} />
564
+ return <RailLabel label={item.label}>{trigger}</RailLabel>
565
+ }
553
566
 
554
- // Curved-elbow tree connector (expanded only). The elbow draws the vertical
555
- // run from the row top to its vertical center, curves at the bottom-left
556
- // corner, then runs horizontally into the row. Non-last rows continue the
557
- // vertical line to the row bottom so it meets the next sibling's elbow.
558
- // The guide is centered (~21px) under the parent item's 20px icon.
567
+ /** Kit `.flyout-panel` chrome, shared by the label tip and the menu. */
568
+ const FLYOUT_PANEL =
569
+ 'bg-card border-bdr rounded-12 shadow-shd2 min-w-[200px] overflow-hidden border pb-2 motion-safe:animate-in motion-safe:fade-in'
570
+ const FLYOUT_LABEL =
571
+ 'text-body text-txt flex items-center gap-2 px-[14px] py-2.5 leading-[normal] font-medium'
572
+
573
+ /**
574
+ * Hover/focus label for a childless rail item (the kit's label-only flyout).
575
+ * Visual only: the button already carries its label for screen readers.
576
+ * WCAG 1.4.13: Esc dismisses it, the pointer can move onto it, and it shows
577
+ * for mouse and keyboard only, so a tap on a touch tablet doesn't leave it
578
+ * stuck after navigating.
579
+ */
580
+ function RailLabel({ label, children }: { label: string; children: ReactNode }) {
581
+ const [top, setTop] = useState<number | null>(null)
582
+ const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
583
+ const cancelHide = () => {
584
+ if (hideTimer.current) clearTimeout(hideTimer.current)
585
+ hideTimer.current = null
586
+ }
587
+ const show = (el: HTMLElement) => {
588
+ cancelHide()
589
+ setTop(el.getBoundingClientRect().top)
590
+ }
591
+ const hide = () => {
592
+ cancelHide()
593
+ setTop(null)
594
+ }
595
+ // Crossing the 6px gap to the label briefly leaves the trigger.
596
+ const scheduleHide = () => {
597
+ cancelHide()
598
+ hideTimer.current = setTimeout(() => setTop(null), 120)
599
+ }
600
+ const visible = top !== null
601
+ useEffect(() => {
602
+ if (!visible) return
603
+ const onKey = (e: KeyboardEvent) => {
604
+ if (e.key === 'Escape') setTop(null)
605
+ }
606
+ document.addEventListener('keydown', onKey)
607
+ return () => document.removeEventListener('keydown', onKey)
608
+ }, [visible])
609
+ useEffect(() => () => cancelHide(), [])
559
610
  return (
560
- <div className="relative">
561
- <span
562
- aria-hidden
563
- className="border-sidebar-border absolute top-0 left-[21px] h-1/2 w-[15px] rounded-bl-lg border-b border-l"
564
- />
565
- {!isLast && (
566
- <span
611
+ <div
612
+ className="mx-1.5 my-px"
613
+ onPointerEnter={(e) => {
614
+ if (e.pointerType === 'mouse') show(e.currentTarget)
615
+ }}
616
+ onPointerLeave={scheduleHide}
617
+ onFocus={(e) => {
618
+ if (e.currentTarget.querySelector(':focus-visible')) show(e.currentTarget)
619
+ }}
620
+ onBlur={hide}
621
+ onClick={hide}
622
+ >
623
+ {children}
624
+ {visible && (
625
+ <div
567
626
  aria-hidden
568
- className="border-sidebar-border absolute top-1/2 bottom-0 left-[21px] border-l"
569
- />
627
+ className={`${FLYOUT_PANEL} fixed left-[var(--sbw-col)] z-[var(--z-menu)] min-w-0 pb-0`}
628
+ style={{ top }}
629
+ onPointerEnter={cancelHide}
630
+ onPointerLeave={scheduleHide}
631
+ >
632
+ <div className={FLYOUT_LABEL}>{label}</div>
633
+ </div>
570
634
  )}
571
- {button}
572
635
  </div>
573
636
  )
574
637
  }
575
638
 
576
639
  /**
577
- * Collapsed-rail parent: shows the icon button and, on hover or keyboard
578
- * focus, a flyout listing the parent's children. Built on Radix
579
- * DropdownMenu so the panel is portaled (escaping the nav's `overflow`
580
- * clip), positioned, dismissable, and keyboard-navigable. Hover-opening is
581
- * controlled manually; we suppress Radix's auto-focus when opened by pointer
582
- * so hovering doesn't steal focus, while keyboard opening still moves focus
583
- * into the menu.
640
+ * Rail parent with a flyout listing its children. Built on Radix
641
+ * DropdownMenu so the panel is portaled (escaping the nav's `overflow` clip),
642
+ * dismissable and keyboard-navigable. Hover opens it; Radix's auto-focus is
643
+ * suppressed for pointer opens so hovering doesn't steal focus, while
644
+ * keyboard opens still move focus into the menu.
584
645
  */
585
- function CollapsedFlyout({ item, ctx }: { item: NavItem; ctx: NavRenderContext }) {
586
- const Icon = item.icon
587
- const isActive = isPathActive(ctx.currentPath, item.path)
588
- const { resolvedTheme } = useTheme()
646
+ function RailFlyout({
647
+ item,
648
+ ctx,
649
+ trigger,
650
+ }: {
651
+ item: NavItem
652
+ ctx: NavRenderContext
653
+ trigger: ReactNode
654
+ }) {
655
+ const children = item.children!
656
+ const { resolvedTheme, lightBase, accent } = useTheme()
589
657
  const [open, setOpen] = useState(false)
590
658
  const openedByPointer = useRef(false)
659
+ const focusEntered = useRef(false)
660
+ const closingByHover = useRef(false)
661
+ const pointerSelect = useRef(false)
662
+ const wrapRef = useRef<HTMLDivElement>(null)
663
+ const contentRef = useRef<HTMLDivElement>(null)
591
664
  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
665
+ const current = activeChildPath(children, ctx.currentPath)
592
666
 
593
667
  const cancelClose = () => {
594
668
  if (closeTimer.current) {
@@ -598,98 +672,148 @@ function CollapsedFlyout({ item, ctx }: { item: NavItem; ctx: NavRenderContext }
598
672
  }
599
673
  const scheduleClose = () => {
600
674
  cancelClose()
601
- closeTimer.current = setTimeout(() => setOpen(false), 120)
675
+ closeTimer.current = setTimeout(() => {
676
+ closingByHover.current = true
677
+ setOpen(false)
678
+ }, 120)
679
+ }
680
+ // Only a hover-opened menu closes when the pointer leaves; a keyboard user
681
+ // inside a menu keeps it while the mouse happens to cross the rail.
682
+ const hoverLeave = () => {
683
+ if (openedByPointer.current) scheduleClose()
684
+ }
685
+ const onOpenChange = (next: boolean) => {
686
+ if (next) closingByHover.current = false
687
+ setOpen(next)
602
688
  }
603
689
 
690
+ // `openedByPointer` records how the menu last opened, set by the opening
691
+ // input itself: a mouse hover on a closed menu sets it, while a key or a
692
+ // touch/pen press on the trigger clears it before Radix toggles. Hovering an
693
+ // already open menu leaves it alone, and so do close paths, so the close
694
+ // focus handling below still knows how the closing menu was opened. (Esc
695
+ // needs no listener here: Radix dismisses the top layer on Esc wherever
696
+ // focus is.)
604
697
  return (
605
- <DropdownMenu.Root
606
- open={open}
607
- onOpenChange={(next) => {
608
- if (!next) openedByPointer.current = false
609
- setOpen(next)
610
- }}
611
- modal={false}
612
- >
698
+ <DropdownMenu.Root open={open} onOpenChange={onOpenChange} modal={false}>
613
699
  <div
700
+ ref={wrapRef}
701
+ className="mx-1.5 my-px"
614
702
  onMouseEnter={() => {
615
- openedByPointer.current = true
703
+ if (!open) {
704
+ openedByPointer.current = true
705
+ closingByHover.current = false
706
+ }
616
707
  cancelClose()
617
708
  setOpen(true)
618
709
  }}
619
- onMouseLeave={scheduleClose}
620
- >
621
- <DropdownMenu.Trigger asChild>
622
- <button
623
- onClick={() => ctx.onNavigate(item.path)}
624
- className={`flex w-full items-center justify-center rounded-lg px-3 py-2.5 transition-colors ${
625
- isActive
626
- ? 'bg-sidebar-accent text-sidebar-primary'
627
- : 'text-sidebar-foreground hover:bg-sidebar-accent'
628
- }`}
629
- aria-label={
630
- item.badge && item.badge > 0 ? `${item.label} (${item.badge} unread)` : item.label
710
+ onMouseLeave={hoverLeave}
711
+ onKeyDownCapture={(e) => {
712
+ openedByPointer.current = false
713
+ // The mouse already opened the menu while focus sat on the trigger:
714
+ // Radix would treat these keys as a no-op (ArrowDown) or a toggle
715
+ // closed (Enter/Space), so take the keyboard user into the menu.
716
+ if (open && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) {
717
+ const first = contentRef.current?.querySelector<HTMLElement>(
718
+ '[role="menuitem"]:not([data-disabled])',
719
+ )
720
+ if (first) {
721
+ e.preventDefault()
722
+ e.stopPropagation()
723
+ first.focus()
631
724
  }
632
- title={item.label}
633
- aria-current={isActive ? 'page' : undefined}
634
- >
635
- <span className="relative flex shrink-0 items-center">
636
- <Icon className="h-5 w-5 shrink-0" aria-hidden />
637
- {!!item.badge && item.badge > 0 && (
638
- <span
639
- className="bg-destructive absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full"
640
- aria-hidden
641
- />
642
- )}
643
- </span>
644
- </button>
645
- </DropdownMenu.Trigger>
725
+ }
726
+ }}
727
+ onPointerDownCapture={(e) => {
728
+ if (e.pointerType !== 'mouse') openedByPointer.current = false
729
+ }}
730
+ >
731
+ <DropdownMenu.Trigger asChild>{trigger}</DropdownMenu.Trigger>
646
732
  </div>
647
733
 
648
734
  <DropdownMenu.Portal container={adminPortalContainer()}>
649
735
  <DropdownMenu.Content
736
+ ref={contentRef}
650
737
  side="right"
651
738
  align="start"
652
- sideOffset={10}
739
+ // The trigger ends 6px inside the 60px rail; the kit panel starts at 60px.
740
+ sideOffset={6}
653
741
  onMouseEnter={cancelClose}
654
- onMouseLeave={scheduleClose}
742
+ onMouseLeave={hoverLeave}
743
+ // A hover open must not pull focus out of what the user is typing
744
+ // in (typeahead would then eat their keys). Keyboard opens focus
745
+ // the first item as usual. DropdownMenu forwards Menu's
746
+ // `onOpenAutoFocus` but leaves it out of its types; the shell-nav
747
+ // render test pins the behaviour across Radix upgrades.
748
+ {...({
749
+ onOpenAutoFocus: (e: Event) => {
750
+ focusEntered.current = false
751
+ pointerSelect.current = false
752
+ if (openedByPointer.current) e.preventDefault()
753
+ },
754
+ } as object)}
755
+ onFocusCapture={() => {
756
+ focusEntered.current = true
757
+ }}
655
758
  onCloseAutoFocus={(e) => {
656
- // After a pointer-driven open, don't yank focus back to the rail
657
- // trigger (it would leave a stray focus ring once the cursor moves
658
- // away). Keyboard-driven opens keep the default focus-return.
659
- if (openedByPointer.current) e.preventDefault()
759
+ // After a pointer-driven open that never took focus, leave focus
760
+ // where the user had it (e.g. a field they're typing in) instead
761
+ // of yanking it to the rail trigger. Once focus has been inside
762
+ // the menu (keyboard open, or Radix focusing items under the
763
+ // pointer), Esc or picking an item returns it to the trigger;
764
+ // otherwise it would fall to <body> as the focused item unmounts.
765
+ // A close because the pointer left never moves focus: focusing
766
+ // this trigger would dismiss the next flyout the pointer opened.
767
+ if (openedByPointer.current && (!focusEntered.current || closingByHover.current)) {
768
+ e.preventDefault()
769
+ } else if (pointerSelect.current) {
770
+ // A mouse pick still returns focus to the trigger, but without
771
+ // the keyboard focus ring (Radix's item focus under the pointer
772
+ // would otherwise make Chromium show it).
773
+ e.preventDefault()
774
+ wrapRef.current
775
+ ?.querySelector<HTMLElement>('button')
776
+ ?.focus({ preventScroll: true, focusVisible: false } as FocusOptions)
777
+ }
660
778
  }}
661
- // Radix portals to <body>, outside the `.prenta-admin` scope where
662
- // the theme tokens (`--popover`, `--radius`, …) live — without the
663
- // scope class the panel renders transparent with square corners.
664
- // Re-assert the scope (and current dark state) on the panel itself so
665
- // it inherits the tokens regardless of portal position.
666
- className={`prenta-admin ${resolvedTheme === 'dark' ? 'dark' : ''} bg-popover text-popover-foreground border-border motion-safe:animate-in motion-safe:fade-in z-50 min-w-44 overflow-hidden rounded-lg border p-1 shadow-lg`}
779
+ // Radix portals outside the `.prenta-admin` scope where the theme
780
+ // tokens live; re-assert the scope, dark state, light base and
781
+ // accent on the panel so it paints with the admin's tokens.
782
+ data-light={lightBase}
783
+ style={
784
+ accentPickCssVars(
785
+ generateAdminThemeTokens({ adminAccentColor: accent }, resolvedTheme),
786
+ ) as CSSProperties
787
+ }
788
+ className={`prenta-admin ${resolvedTheme === 'dark' ? 'dark' : ''} ${FLYOUT_PANEL} z-[var(--z-menu)]`}
667
789
  >
668
- <div className="text-muted-foreground border-border mb-1 border-b px-3 py-2 text-xs font-medium">
790
+ <div className={`${FLYOUT_LABEL} border-bdr mb-1 border-b pb-2`}>
791
+ <span className="text-acc flex">
792
+ <NavIcon item={item} />
793
+ </span>
669
794
  {item.label}
795
+ <NavBadge count={item.badge} />
670
796
  </div>
671
- {item.children!.map((child) => {
672
- const ChildIcon = child.icon
673
- const childActive = isPathActive(ctx.currentPath, child.path)
797
+ {children.map((child) => {
798
+ const active = child.path === current
674
799
  return (
675
800
  <DropdownMenu.Item
676
- key={`flyout-item:${item.path}:${child.path}`}
801
+ key={`flyout:${child.label}:${child.path}`}
802
+ // Radix's Enter/Space select clicks the item with detail 0.
803
+ onClick={(e) => {
804
+ pointerSelect.current = e.detail !== 0
805
+ }}
677
806
  onSelect={() => ctx.onNavigate(child.path)}
678
- className={`data-highlighted:bg-accent hover:bg-accent flex cursor-pointer items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none select-none ${
679
- childActive ? 'text-primary' : 'text-foreground'
807
+ className={`text-body rounded-6 data-highlighted:bg-acc-l data-highlighted:text-acc mx-1 my-px flex cursor-pointer items-center gap-2 px-[14px] py-1.5 leading-[normal] outline-none select-none ${
808
+ active ? `bg-acc-l text-acc font-[550] ${FORCED_ACTIVE}` : 'text-sub'
680
809
  }`}
681
- aria-current={childActive ? 'page' : undefined}
810
+ aria-current={active ? 'page' : undefined}
682
811
  >
683
- <ChildIcon className="text-muted-foreground h-4 w-4 shrink-0" aria-hidden />
684
- <span className="truncate">{child.label}</span>
685
- {!!child.badge && child.badge > 0 && (
686
- <span
687
- className="bg-destructive text-destructive-foreground ml-auto inline-flex min-w-5 items-center justify-center rounded-full px-1.5 py-0.5 text-xs font-medium tabular-nums"
688
- aria-label={`${child.badge} unread`}
689
- >
690
- {child.badge > 99 ? '99+' : child.badge}
691
- </span>
692
- )}
812
+ <span className="text-faint flex">
813
+ <NavIcon item={child} size={14} />
814
+ </span>
815
+ <span className="flex-1 truncate">{child.label}</span>
816
+ <NavBadge count={child.badge} />
693
817
  </DropdownMenu.Item>
694
818
  )
695
819
  })}
@@ -698,165 +822,3 @@ function CollapsedFlyout({ item, ctx }: { item: NavItem; ctx: NavRenderContext }
698
822
  </DropdownMenu.Root>
699
823
  )
700
824
  }
701
-
702
- // ─── Config-driven nav builder ──────────────────────────────────────────────
703
-
704
- function resolveIcon(collection: any): LucideIcon {
705
- const mapped = collection.admin?.icon ? ICON_MAP[collection.admin.icon] : undefined
706
- if (mapped) return mapped
707
- return collection.type === 'page' ? File : FileText
708
- }
709
-
710
- export interface NavItem {
711
- path: string
712
- label: string
713
- icon: LucideIcon
714
- /**
715
- * Section header. Items sharing the same `group` are grouped together
716
- * under a header rendered on first encounter. Items without a `group`
717
- * render at the top of the nav in document order.
718
- */
719
- group?: string
720
- /** Indented children rendered immediately after this item. */
721
- children?: NavItem[]
722
- /**
723
- * Optional unread/notification count. Rendered as a red pill (expanded)
724
- * or a dot (collapsed). Falsy / zero values render nothing.
725
- */
726
- badge?: number
727
- }
728
-
729
- /**
730
- * Build the sidebar nav tree from the CMS config. If no collections are
731
- * configured, returns the curated `defaultNavItems` tree. When
732
- * collections ARE configured, page-type collections render as Content
733
- * entries, and every static destination (Media, Forms, SEO, Script Tags,
734
- * Users, API Keys, Settings) is anchored under the right group.
735
- */
736
- export function buildNavItems(config: any): NavItem[] {
737
- if (!config?.collections) return defaultNavItems
738
-
739
- const raw = config.collections
740
- const collectionsList: any[] = Array.isArray(raw) ? raw : Object.values(raw)
741
- const visible = collectionsList.filter((c) => !c.admin?.hidden)
742
-
743
- // Treat `admin.group: 'Posts'` (case-insensitive) as a synonym for
744
- // `type: 'post'` so consumers can tag arbitrary collections into
745
- // the Posts area without changing the discriminator they already
746
- // use elsewhere (sitemaps, schema.org, etc.).
747
- const isPostsGroup = (c: any) =>
748
- typeof c.admin?.group === 'string' && c.admin.group.toLowerCase() === 'posts'
749
- const pages = visible.filter((c) => c.type === 'page' && !c.admin?.group)
750
- const posts = visible.filter((c) => c.type === 'post' || isPostsGroup(c))
751
- const otherUngrouped = visible.filter(
752
- (c) => c.type !== 'page' && c.type !== 'post' && !c.admin?.group,
753
- )
754
- // Anything explicitly grouped that isn't a posts collection.
755
- const grouped = visible.filter((c) => c.admin?.group && !isPostsGroup(c) && c.type !== 'post')
756
-
757
- const items: NavItem[] = [{ path: '/', label: 'Dashboard', icon: LayoutDashboard }]
758
-
759
- // Ungrouped collections that are neither page- nor post-type stay at
760
- // the top level, directly under Dashboard, before the CONTENT group.
761
- for (const collection of otherUngrouped) {
762
- items.push({
763
- label: collection.labels?.plural ?? collection.slug,
764
- path: `/${collection.slug}`,
765
- icon: resolveIcon(collection),
766
- })
767
- }
768
-
769
- // CONTENT group. Everything below shares `group: 'Content'`, so the
770
- // renderer emits the "CONTENT" header once — above Pages. Order:
771
- // Pages → Posts → static content destinations (Media / Forms / SEO /
772
- // Script Tags).
773
-
774
- // Pages-style collections render as plain Content entries (no nested
775
- // children).
776
- pages.forEach((collection) => {
777
- items.push({
778
- label: collection.labels?.plural ?? collection.slug,
779
- path: `/${collection.slug}`,
780
- icon: resolveIcon(collection),
781
- group: 'Content',
782
- })
783
- })
784
-
785
- // Posts area — collapses every post-type collection under a single
786
- // "Posts" parent. The aggregated table at /posts and the type cards
787
- // at /posts/types are the entry points; individual post-type
788
- // collections are not enumerated in the sidebar to avoid the
789
- // duplicate-nav problem when a project has many types.
790
- if (posts.length > 0) {
791
- items.push({
792
- path: '/posts',
793
- label: 'Posts',
794
- icon: FileText,
795
- group: 'Content',
796
- children: POSTS_CHILDREN,
797
- })
798
- }
799
-
800
- // Standard Content destinations (always present — they map to CMS
801
- // features, not collections).
802
- items.push(
803
- { path: '/media', label: 'Media', icon: Image, group: 'Content' },
804
- { path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
805
- { path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
806
- { path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
807
- { path: '/trash', label: 'Trash', icon: Trash2, group: 'Content' },
808
- )
809
-
810
- // Plugin-contributed custom nav links (PluginDefinition.adminNavItems,
811
- // collected onto `_pluginNavItems` by cms-core's applyPlugins). Deduped by
812
- // path against everything already present so a plugin can't double a built-in
813
- // destination or one of its own collections. Emitted HERE — inside the Content
814
- // block, before the custom collection groups below — so their `group: 'Content'`
815
- // stays contiguous and doesn't start a second "CONTENT" header after the groups.
816
- const pluginNav: Array<{ label?: string; icon?: string; path?: string }> = Array.isArray(
817
- config?._pluginNavItems,
818
- )
819
- ? config._pluginNavItems
820
- : []
821
- for (const item of pluginNav) {
822
- if (!item?.path || items.some((n) => n.path === item.path)) continue
823
- items.push({
824
- label: item.label ?? item.path,
825
- path: item.path,
826
- icon: (item.icon && ICON_MAP[item.icon]) || FileText,
827
- group: 'Content',
828
- })
829
- }
830
-
831
- // Config-supplied custom groups land after the standard Content block and
832
- // before Settings. Each renders under its own header. Skip any collection
833
- // whose path a built-in Content destination (or an already-added item) already
834
- // provides — otherwise a grouped collection like plugin-forms' `forms`
835
- // (admin.group: 'Forms', path `/forms`) duplicates the static Forms entry, and
836
- // `applyFormsNav` then attaches "All Entries" to BOTH.
837
- const groupBuckets = new Map<string, NavItem[]>()
838
- for (const col of grouped) {
839
- const path = `/${col.slug}`
840
- if (items.some((n) => n.path === path)) continue
841
- const group = col.admin.group as string
842
- const bucket = groupBuckets.get(group) ?? []
843
- bucket.push({
844
- label: col.labels?.plural ?? col.slug,
845
- path,
846
- icon: resolveIcon(col),
847
- group,
848
- })
849
- groupBuckets.set(group, bucket)
850
- }
851
- for (const bucket of groupBuckets.values()) items.push(...bucket)
852
-
853
- // Settings destinations — always last. Users + API Keys now live inside
854
- // the Settings view as tabs (see Settings.tsx), so only Settings is anchored
855
- // here.
856
- items.push(
857
- { path: '/jobs', label: 'Jobs', icon: ListTodo, group: 'Settings' },
858
- { path: '/settings', label: 'Settings', icon: Settings, group: 'Settings' },
859
- )
860
-
861
- return items
862
- }