@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
@@ -0,0 +1,983 @@
1
+ // @vitest-environment happy-dom
2
+ import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
3
+ import type { ReactNode } from 'react'
4
+ import { createPortal } from 'react-dom'
5
+ import { hydrateRoot, type Root } from 'react-dom/client'
6
+ import { renderToString } from 'react-dom/server'
7
+ import { afterEach, describe, expect, it, vi } from 'vitest'
8
+
9
+ const fetchFormsSidebarCounts = vi.fn(async () => [
10
+ { formId: 'f1', name: 'Contact Us', slug: 'contact-us', status: 'active', unread: 4 },
11
+ { formId: 'f2', name: 'Free RFP', slug: 'free-rfp', status: 'active', unread: 2 },
12
+ ])
13
+ vi.mock('../../lib/forms-service.js', () => ({
14
+ fetchFormsSidebarCounts: () => fetchFormsSidebarCounts(),
15
+ }))
16
+
17
+ vi.mock('../../components/ThemeProvider.js', () => ({
18
+ useTheme: () => ({
19
+ adminBrand: { primaryUrl: null, markUrl: null, darkUrl: null, initials: 'BW' },
20
+ resolvedTheme: 'light',
21
+ density: 'default',
22
+ }),
23
+ }))
24
+
25
+ // The real Header pulls in the command palette, notifications and locale
26
+ // switcher; the Layout tests only need its menu button.
27
+ vi.mock('../../layout/Header.js', () => ({
28
+ Header: ({ onToggleSidebar }: { onToggleSidebar: () => void }) => (
29
+ <button type="button" onClick={onToggleSidebar}>
30
+ Menu
31
+ </button>
32
+ ),
33
+ }))
34
+
35
+ const { Sidebar } = await import('../../layout/Sidebar.js')
36
+ const { MobileTabBar } = await import('../../layout/MobileTabBar.js')
37
+ const { Layout } = await import('../../layout/Layout.js')
38
+ const { AdminShell, DRAWER_ID } = await import('../../layout/primitives/AdminShell.js')
39
+ const { Toaster } = await import('../../components/ui/Toaster.js')
40
+ const { toast } = await import('sonner')
41
+ const {
42
+ activeChildPath,
43
+ applyFormsNav,
44
+ buildNavItems,
45
+ isPathActive,
46
+ SETTINGS_TABS,
47
+ withSettingsTabs,
48
+ } = await import('../../layout/nav-config.js')
49
+
50
+ afterEach(() => {
51
+ cleanup()
52
+ vi.unstubAllGlobals()
53
+ })
54
+
55
+ /**
56
+ * happy-dom has no real media queries; answer them from a fixed width. Reads
57
+ * `min-width` / `max-width` and range syntax (`width < 769px`,
58
+ * `769px <= width < 1025px`), joined by `and`.
59
+ */
60
+ function matchesWidth(query: string, width: number): boolean {
61
+ const compare = (a: number, op: string, b: number) =>
62
+ op === '<' ? a < b : op === '<=' ? a <= b : op === '>' ? a > b : a >= b
63
+ return query.split(/\)\s*and\s*\(/).every((raw) => {
64
+ const part = raw.replace(/[()\s]/g, '')
65
+ const min = /^min-width:(\d+(?:\.\d+)?)px$/.exec(part)
66
+ if (min) return width >= Number(min[1])
67
+ const max = /^max-width:(\d+(?:\.\d+)?)px$/.exec(part)
68
+ if (max) return width <= Number(max[1])
69
+ const range = /^(?:(\d+(?:\.\d+)?)px(<=?|>=?))?width(?:(<=?|>=?)(\d+(?:\.\d+)?)px)?$/.exec(part)
70
+ if (!range) throw new Error(`stubWidth can't read ${query}`)
71
+ const [, lo, loOp, hiOp, hi] = range
72
+ return (
73
+ (lo === undefined || compare(Number(lo), loOp!, width)) &&
74
+ (hi === undefined || compare(width, hiOp!, Number(hi)))
75
+ )
76
+ })
77
+ }
78
+
79
+ function stubWidth(width: number) {
80
+ vi.stubGlobal('matchMedia', (query: string) => {
81
+ const matches = matchesWidth(query, width)
82
+ return {
83
+ matches,
84
+ media: query,
85
+ addEventListener: () => {},
86
+ removeEventListener: () => {},
87
+ }
88
+ })
89
+ }
90
+
91
+ /**
92
+ * Whether a rail flyout is open, read from its trigger. (Radix hides the
93
+ * menu with `visibility: hidden` once happy-dom's zero-size layout puts it
94
+ * "out of view", so role queries can't tell open from closed.)
95
+ */
96
+ const isOpen = (trigger: HTMLElement) => trigger.getAttribute('aria-expanded') === 'true'
97
+
98
+ function renderSidebar(currentPath: string, collapsed = false, onNavigate = vi.fn()) {
99
+ render(
100
+ <Sidebar
101
+ collapsed={collapsed}
102
+ onToggleCollapse={() => {}}
103
+ currentPath={currentPath}
104
+ onNavigate={onNavigate}
105
+ />,
106
+ )
107
+ return onNavigate
108
+ }
109
+
110
+ /**
111
+ * P-02 shell & nav (docs/design/prenta/TRACKING.md, specs/SHELL.md §2).
112
+ */
113
+ describe('nav config', () => {
114
+ it('lists the Forms children in the kit order, with per-form rows one level down', () => {
115
+ const forms = applyFormsNav(buildNavItems({}), [
116
+ { formId: 'f1', name: 'Contact Us', slug: 'contact-us', status: 'active', unread: 4 },
117
+ { formId: 'f2', name: 'Free RFP', slug: 'free-rfp', status: 'active', unread: 2 },
118
+ ] as never)
119
+ const item = forms.find((i) => i.label === 'Forms')
120
+ const label = (c: { label: string; badge?: number }) =>
121
+ `${c.label}${c.badge ? ` (${c.badge})` : ''}`
122
+ expect(item?.badge).toBe(6)
123
+ expect(item?.children?.map(label)).toEqual([
124
+ 'All Entries (6)',
125
+ 'All Forms',
126
+ 'Spam',
127
+ 'SMTP Log',
128
+ 'Form Settings',
129
+ ])
130
+ // cms-layouts.js "3rd-level nav nesting": Entries and Forms → per-form.
131
+ expect(item?.children?.[0]?.children?.map(label)).toEqual(['Contact Us (4)', 'Free RFP (2)'])
132
+ expect(item?.children?.[0]?.children?.[0]?.path).toBe('/forms/f1/submissions')
133
+ expect(item?.children?.[1]?.children?.map((c) => c.path)).toEqual([
134
+ '/forms/f1/edit',
135
+ '/forms/f2/edit',
136
+ ])
137
+ })
138
+
139
+ it('lets the most specific sibling own the page', () => {
140
+ const pages = buildNavItems({}).find((i) => i.label === 'Pages')!
141
+ expect(activeChildPath(pages.children!, '/pages/trash')).toBe('/pages/trash')
142
+ expect(activeChildPath(pages.children!, '/pages/abc/edit')).toBe('/pages')
143
+ })
144
+
145
+ it('keeps config and plugin entries out of the tab bar set', () => {
146
+ const items = buildNavItems({
147
+ collections: {
148
+ pages: { slug: 'pages', type: 'page' },
149
+ faqs: { slug: 'faqs', labels: { plural: 'FAQs' } },
150
+ team: { slug: 'team', labels: { plural: 'Team' }, admin: { group: 'Company' } },
151
+ },
152
+ _pluginNavItems: [{ label: 'Redirect log', path: '/redirect-log' }],
153
+ })
154
+ expect(items.filter((i) => i.extra).map((i) => `${i.group}:${i.label}`)).toEqual([
155
+ 'Content:FAQs',
156
+ 'Content:Redirect log',
157
+ 'Company:Team',
158
+ ])
159
+ // Config groups sit after CONTENT and before SETTINGS.
160
+ const labels = items.map((i) => i.label)
161
+ expect(labels.indexOf('Team')).toBeGreaterThan(labels.indexOf('Script Tags'))
162
+ expect(labels.indexOf('Team')).toBeLessThan(labels.indexOf('Jobs'))
163
+ })
164
+ })
165
+
166
+ describe('Sidebar', () => {
167
+ it('puts Jobs above Settings under the SETTINGS label', () => {
168
+ stubWidth(1280)
169
+ renderSidebar('/')
170
+ const nav = screen.getByRole('navigation', { name: 'Primary' })
171
+ const text = nav.textContent ?? ''
172
+ expect(text.indexOf('Settings')).toBeLessThan(text.indexOf('Jobs'))
173
+ expect(text.indexOf('Jobs')).toBeLessThan(text.lastIndexOf('Settings'))
174
+ // Pages, Posts and Appearance come before the CONTENT label.
175
+ expect(text.indexOf('Appearance')).toBeLessThan(text.indexOf('Content'))
176
+ expect(text.indexOf('Content')).toBeLessThan(text.indexOf('Media'))
177
+ })
178
+
179
+ it('toggles a parent on click at ≥1025px instead of navigating', () => {
180
+ stubWidth(1280)
181
+ const onNavigate = renderSidebar('/', false)
182
+ fireEvent.click(screen.getByRole('button', { name: 'Pages' }))
183
+ expect(onNavigate).not.toHaveBeenCalled()
184
+ expect(screen.getByRole('button', { name: 'All Pages' })).toBeTruthy()
185
+ expect(
186
+ screen.getByRole('button', { name: 'Collapse Pages' }).getAttribute('aria-expanded'),
187
+ ).toBe('true')
188
+ })
189
+
190
+ it('navigates from a parent in the mobile drawer (below 1025px)', () => {
191
+ stubWidth(600)
192
+ const onNavigate = renderSidebar('/', false)
193
+ fireEvent.click(screen.getByRole('button', { name: 'Pages' }))
194
+ expect(onNavigate).toHaveBeenCalledWith('/pages')
195
+ })
196
+
197
+ it('shows the Forms unread total as a kit badge, in the rail too', async () => {
198
+ stubWidth(924)
199
+ renderSidebar('/', true)
200
+ const forms = await screen.findByRole('button', { name: /Forms/ })
201
+ await waitFor(() => expect(within(forms).getByLabelText('6 unread')).toBeTruthy())
202
+ const badge = within(forms).getByLabelText('6 unread')
203
+ expect(badge.className).toContain('bg-err')
204
+ expect(badge.className).toContain('text-badge')
205
+ // Rail labels stay in the accessibility tree.
206
+ expect(within(forms).getByText('Forms').className).toContain('sr-only')
207
+ })
208
+
209
+ it('hides section labels in the ≤1024px rail', () => {
210
+ stubWidth(924)
211
+ renderSidebar('/', true)
212
+ const label = screen.getByText('Content')
213
+ expect(label.className).toContain('tablet:hidden')
214
+ expect(label.className).toContain('opacity-0')
215
+ })
216
+ })
217
+
218
+ describe('Forms third level', () => {
219
+ it('opens the per-form rows when one is the current page, and toggles them', async () => {
220
+ stubWidth(1280)
221
+ renderSidebar('/forms/f2/submissions')
222
+ const rfp = await screen.findByRole('button', { name: /^Free RFP/ })
223
+ expect(rfp.getAttribute('aria-current')).toBe('page')
224
+ expect(rfp.className).toContain('pl-[30px]')
225
+ // All Forms' rows stay closed; All Entries' are open.
226
+ const allForms = screen.getByRole('button', { name: 'Expand All Forms' })
227
+ expect(allForms.getAttribute('aria-expanded')).toBe('false')
228
+ const entries = screen.getByRole('button', { name: 'Collapse All Entries' })
229
+ fireEvent.click(entries)
230
+ expect(screen.queryByRole('button', { name: /^Free RFP/ })).toBeNull()
231
+ })
232
+
233
+ it('leaves per-form rows out of the rail flyout', async () => {
234
+ stubWidth(924)
235
+ renderSidebar('/', true)
236
+ const forms = await screen.findByRole('button', { name: /^Forms/ })
237
+ fireEvent.mouseEnter(forms.parentElement as HTMLElement)
238
+ await waitFor(() => expect(screen.getByText('All Entries')).toBeTruthy())
239
+ expect(screen.getByText('Form Settings')).toBeTruthy()
240
+ expect(screen.queryByText('Contact Us')).toBeNull()
241
+ })
242
+ })
243
+
244
+ describe('MobileTabBar', () => {
245
+ it('shows one tab per kit destination plus More, and leaves extras to the drawer', () => {
246
+ const items = applyFormsNav(
247
+ buildNavItems({
248
+ collections: {
249
+ pages: { slug: 'pages', type: 'page' },
250
+ blog: { slug: 'blog', type: 'post' },
251
+ faqs: { slug: 'faqs', labels: { plural: 'FAQs' } },
252
+ },
253
+ }),
254
+ [
255
+ { formId: 'f1', name: 'Contact Us', slug: 'contact-us', status: 'active', unread: 3 },
256
+ ] as never,
257
+ )
258
+ const onNavigate = vi.fn()
259
+ const onMore = vi.fn()
260
+ render(
261
+ <MobileTabBar
262
+ items={items}
263
+ currentPath="/forms/entries"
264
+ onNavigate={onNavigate}
265
+ onMore={onMore}
266
+ moreOpen={false}
267
+ />,
268
+ )
269
+ const bar = screen.getByRole('navigation', { name: 'Tab bar' })
270
+ const tabs = within(bar)
271
+ .getAllByRole('button')
272
+ .map((b) => b.textContent)
273
+ expect(tabs).toEqual([
274
+ 'Dashboard',
275
+ 'Pages',
276
+ 'Posts',
277
+ 'Appearance',
278
+ 'Media',
279
+ 'Forms3',
280
+ 'SEO',
281
+ 'Script Tags',
282
+ 'Jobs',
283
+ 'Settings',
284
+ 'More',
285
+ ])
286
+ // A child page marks its parent tab.
287
+ expect(
288
+ within(bar)
289
+ .getByRole('button', { name: /^Forms/ })
290
+ .getAttribute('aria-current'),
291
+ ).toBe('page')
292
+ fireEvent.click(within(bar).getByRole('button', { name: 'Pages' }))
293
+ expect(onNavigate).toHaveBeenCalledWith('/pages')
294
+ const more = within(bar).getByRole('button', { name: 'More' })
295
+ expect(more.getAttribute('aria-expanded')).toBe('false')
296
+ fireEvent.click(more)
297
+ expect(onMore).toHaveBeenCalledOnce()
298
+ })
299
+ })
300
+
301
+ describe('AdminShell', () => {
302
+ it('renders the tab bar on mobile only and keeps the closed drawer inert', async () => {
303
+ stubWidth(600)
304
+ const { rerender } = render(
305
+ <AdminShell
306
+ desktopBreakpoint={769}
307
+ sidebar={<button type="button">Drawer link</button>}
308
+ tabBar={<div>Tab bar</div>}
309
+ >
310
+ Body
311
+ </AdminShell>,
312
+ )
313
+ expect(screen.getByText('Tab bar')).toBeTruthy()
314
+ const drawer = screen.getByText('Drawer link').parentElement as HTMLElement & {
315
+ inert?: boolean
316
+ }
317
+ await waitFor(() => expect(drawer.inert).toBe(true))
318
+ // (happy-dom drops the `calc(… env(safe-area-inset-bottom))` padding, so it
319
+ // isn't asserted here; the tab bar test checks the safe-area classes.)
320
+
321
+ rerender(
322
+ <AdminShell
323
+ desktopBreakpoint={769}
324
+ mobileSidebarOpen
325
+ sidebar={<button type="button">Drawer link</button>}
326
+ tabBar={<div>Tab bar</div>}
327
+ >
328
+ Body
329
+ </AdminShell>,
330
+ )
331
+ await waitFor(() => expect(drawer.inert).toBe(false))
332
+
333
+ cleanup()
334
+ stubWidth(1280)
335
+ await act(async () => {
336
+ render(
337
+ <AdminShell
338
+ desktopBreakpoint={769}
339
+ sidebar={<button type="button">Drawer link</button>}
340
+ tabBar={<div>Tab bar</div>}
341
+ >
342
+ Body
343
+ </AdminShell>,
344
+ )
345
+ })
346
+ expect(screen.queryByText('Tab bar')).toBeNull()
347
+ })
348
+ })
349
+
350
+ /** Fixes from the second review of the P-02 commit. */
351
+ describe('P-02 review fixes', () => {
352
+ it('reads every tier query the same way at fractional widths', () => {
353
+ // Layout's and Sidebar's queries: exactly one tier matches at any width.
354
+ const tiers = ['(width < 769px)', '(769px <= width < 1025px)', '(min-width: 1025px)']
355
+ for (const width of [320, 768, 768.5, 769, 924, 1024, 1024.5, 1025, 1440]) {
356
+ expect(tiers.filter((q) => matchesWidth(q, width))).toHaveLength(1)
357
+ // AdminShell's desktop query is the mobile tier's complement.
358
+ expect(matchesWidth('(min-width: 769px)', width)).toBe(!matchesWidth(tiers[0]!, width))
359
+ }
360
+ })
361
+
362
+ it('leaves focus where it was when a rail flyout opens on hover', async () => {
363
+ stubWidth(924)
364
+ render(
365
+ <>
366
+ <input aria-label="Title" />
367
+ <Sidebar collapsed onToggleCollapse={() => {}} currentPath="/" onNavigate={vi.fn()} />
368
+ </>,
369
+ )
370
+ const input = screen.getByRole('textbox', { name: 'Title' })
371
+ input.focus()
372
+ expect(document.activeElement).toBe(input)
373
+ const pages = screen.getByRole('button', { name: 'Pages' })
374
+ fireEvent.mouseEnter(pages.parentElement as HTMLElement)
375
+ await waitFor(() => expect(isOpen(pages)).toBe(true))
376
+ expect(document.activeElement).toBe(input)
377
+ // Esc still closes it, though focus isn't in it.
378
+ fireEvent.keyDown(input, { key: 'Escape' })
379
+ await waitFor(() => expect(isOpen(pages)).toBe(false))
380
+ })
381
+
382
+ it('reports the group state on a parent row that toggles it (≥1025px)', () => {
383
+ stubWidth(1280)
384
+ renderSidebar('/')
385
+ const row = screen.getByRole('button', { name: 'Pages' })
386
+ const chevron = screen.getByRole('button', { name: 'Expand Pages' })
387
+ expect(row.getAttribute('aria-expanded')).toBe('false')
388
+ expect(row.getAttribute('aria-controls')).toBe(chevron.getAttribute('aria-controls'))
389
+ fireEvent.click(row)
390
+ expect(row.getAttribute('aria-expanded')).toBe('true')
391
+ expect(document.getElementById(row.getAttribute('aria-controls')!)).toBeTruthy()
392
+ // 24px chevrons that grow to the touch target on coarse pointers.
393
+ const collapse = screen.getByRole('button', { name: 'Collapse Pages' })
394
+ expect(collapse.className).toContain('prenta-touch-hit')
395
+ expect(collapse.className).toContain('h-6 w-6')
396
+ })
397
+
398
+ it('leaves aria-expanded off a parent row that navigates (drawer)', () => {
399
+ stubWidth(600)
400
+ renderSidebar('/')
401
+ expect(screen.getByRole('button', { name: 'Pages' }).hasAttribute('aria-expanded')).toBe(false)
402
+ })
403
+
404
+ it('outlines the current row for forced colors', () => {
405
+ stubWidth(1280)
406
+ renderSidebar('/media')
407
+ const media = screen.getByRole('button', { name: 'Media' })
408
+ expect(media.getAttribute('aria-current')).toBe('page')
409
+ expect(media.className).toContain('forced-colors:outline-solid')
410
+ })
411
+
412
+ it('names the Header & Footer row as the kit does', () => {
413
+ stubWidth(1280)
414
+ renderSidebar('/appearance/header')
415
+ expect(screen.getByRole('button', { name: 'Header & Footer' })).toBeTruthy()
416
+ })
417
+
418
+ it('shows the rail label for mouse and keyboard, dismisses it on Esc, not for touch', () => {
419
+ stubWidth(924)
420
+ renderSidebar('/', true)
421
+ const wrapper = screen.getByRole('button', { name: 'Dashboard' }).parentElement as HTMLElement
422
+ const labels = () => screen.queryAllByText('Dashboard').length
423
+ expect(labels()).toBe(1) // the button's own sr-only label
424
+ fireEvent.pointerEnter(wrapper, { pointerType: 'touch' })
425
+ expect(labels()).toBe(1)
426
+ fireEvent.pointerEnter(wrapper, { pointerType: 'mouse' })
427
+ expect(labels()).toBe(2)
428
+ fireEvent.keyDown(document, { key: 'Escape' })
429
+ expect(labels()).toBe(1)
430
+ })
431
+
432
+ it('hides Expand sidebar in the 769–1024px rail', () => {
433
+ stubWidth(924)
434
+ renderSidebar('/', true)
435
+ const toggle = screen.getByRole('button', { name: /expand sidebar/i })
436
+ expect(toggle.closest('div')?.className).toContain('not-narrow:block')
437
+ })
438
+
439
+ it('lists the Settings tabs under Settings, each active on its own tab', () => {
440
+ const items = withSettingsTabs(buildNavItems({}), {})
441
+ const settings = items.find((i) => i.path === '/settings')!
442
+ expect(settings.children?.map((c) => c.label)).toEqual([
443
+ 'General',
444
+ 'Admin appearance',
445
+ 'Security',
446
+ 'Users',
447
+ 'MCP',
448
+ 'API Keys',
449
+ 'SEO',
450
+ 'Email (SMTP)',
451
+ 'Webhooks',
452
+ 'AI',
453
+ 'Updates',
454
+ ])
455
+ // The same list as the Settings strip (P-06), so each tab reads the same.
456
+ expect(settings.children?.map((c) => [c.label, c.icon])).toEqual(
457
+ SETTINGS_TABS.filter((t) => t.id !== 'layout').map((t) => [t.label, t.icon]),
458
+ )
459
+ expect(settings.children?.every((c) => c.iconSize === 14)).toBe(true)
460
+ expect(activeChildPath(settings.children!, '/settings')).toBe('/settings?tab=general')
461
+ expect(activeChildPath(settings.children!, '/settings?tab=security')).toBe(
462
+ '/settings?tab=security',
463
+ )
464
+ expect(isPathActive('/settings/other', '/settings?tab=general')).toBe(false)
465
+ // Layout only exists when the site defines regions.
466
+ const withLayout = withSettingsTabs(buildNavItems({}), { layout: { regions: { header: {} } } })
467
+ expect(withLayout.find((i) => i.path === '/settings')?.children?.map((c) => c.label)).toContain(
468
+ 'Layout',
469
+ )
470
+ })
471
+
472
+ it('keeps 44px tabs and offers the drawer from More', () => {
473
+ render(
474
+ <MobileTabBar
475
+ items={buildNavItems({})}
476
+ currentPath="/"
477
+ onNavigate={vi.fn()}
478
+ onMore={vi.fn()}
479
+ moreOpen={false}
480
+ drawerId="drawer"
481
+ />,
482
+ )
483
+ const bar = screen.getByRole('navigation', { name: 'Tab bar' })
484
+ for (const tab of within(bar).getAllByRole('button')) {
485
+ expect(tab.className).toContain('min-h-11')
486
+ expect(tab.className).toContain('min-w-[60px]')
487
+ }
488
+ expect(within(bar).getByRole('button', { name: 'More' }).getAttribute('aria-controls')).toBe(
489
+ 'drawer',
490
+ )
491
+ // Below every overlay layer, and clear of the home indicator.
492
+ expect(bar.className).toContain('z-[var(--z-tabbar)]')
493
+ expect(bar.className).toContain('pb-[env(safe-area-inset-bottom)]')
494
+ })
495
+
496
+ it('makes the open drawer a modal dialog: focus in, trapped, Esc, focus back', async () => {
497
+ stubWidth(600)
498
+ const onClose = vi.fn()
499
+ const shell = (open: boolean) => (
500
+ <AdminShell
501
+ desktopBreakpoint={769}
502
+ mobileSidebarOpen={open}
503
+ onMobileSidebarClose={onClose}
504
+ sidebar={
505
+ <>
506
+ <button type="button">First link</button>
507
+ <button type="button">Last link</button>
508
+ </>
509
+ }
510
+ tabBar={
511
+ <button type="button" aria-controls={DRAWER_ID}>
512
+ More
513
+ </button>
514
+ }
515
+ >
516
+ <button type="button">Body button</button>
517
+ </AdminShell>
518
+ )
519
+ const { rerender } = render(shell(false))
520
+ const more = await screen.findByRole('button', { name: 'More' })
521
+ expect(screen.queryByRole('dialog')).toBeNull()
522
+ more.focus()
523
+
524
+ rerender(shell(true))
525
+ const dialog = await screen.findByRole('dialog', { name: 'Navigation' })
526
+ expect(dialog.id).toBe(DRAWER_ID)
527
+ expect(dialog.getAttribute('aria-modal')).toBe('true')
528
+ // The drawer's own close button comes first and takes focus.
529
+ const first = screen.getByRole('button', { name: 'Close navigation' })
530
+ const last = screen.getByRole('button', { name: 'Last link' })
531
+ await waitFor(() => expect(document.activeElement).toBe(first))
532
+ type Inert = HTMLElement & { inert?: boolean }
533
+ const content = document.getElementById('prenta-main')!.parentElement as Inert
534
+ expect(content.inert).toBe(true)
535
+ expect((more.parentElement as Inert).inert).toBe(true)
536
+
537
+ // Tab wraps at both ends.
538
+ last.focus()
539
+ fireEvent.keyDown(last, { key: 'Tab' })
540
+ expect(document.activeElement).toBe(first)
541
+ fireEvent.keyDown(first, { key: 'Tab', shiftKey: true })
542
+ expect(document.activeElement).toBe(last)
543
+
544
+ fireEvent.keyDown(last, { key: 'Escape' })
545
+ expect(onClose).toHaveBeenCalledOnce()
546
+ rerender(shell(false))
547
+ await waitFor(() => expect(document.activeElement).toBe(more))
548
+ expect(content.inert).toBe(false)
549
+ expect(screen.queryByRole('dialog')).toBeNull()
550
+ })
551
+
552
+ it('slides the drawer with a motion-safe class the Reduce motion setting can stop', () => {
553
+ stubWidth(600)
554
+ render(
555
+ <AdminShell desktopBreakpoint={769} sidebar={<span>Nav</span>}>
556
+ Body
557
+ </AdminShell>,
558
+ )
559
+ const drawer = document.getElementById(DRAWER_ID)!
560
+ expect(drawer.style.transition).toBe('')
561
+ expect(drawer.className).toContain('motion-safe:transition-transform')
562
+ })
563
+
564
+ it('wires the drawer to More and lists Settings tabs in it on mobile', async () => {
565
+ stubWidth(600)
566
+ render(
567
+ <Layout config={{}} session={null} currentPath="/settings?tab=security" onNavigate={vi.fn()}>
568
+ Body
569
+ </Layout>,
570
+ )
571
+ const bar = await screen.findByRole('navigation', { name: 'Tab bar' })
572
+ const more = within(bar).getByRole('button', { name: 'More' })
573
+ expect(more.getAttribute('aria-controls')).toBe(DRAWER_ID)
574
+ more.focus()
575
+ fireEvent.click(more)
576
+ const dialog = await screen.findByRole('dialog', { name: 'Navigation' })
577
+ const security = within(dialog).getByRole('button', { name: 'Security' })
578
+ expect(security.getAttribute('aria-current')).toBe('page')
579
+ fireEvent.keyDown(security, { key: 'Escape' })
580
+ await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
581
+ await waitFor(() => expect(document.activeElement).toBe(more))
582
+ })
583
+
584
+ it('keeps Settings a single row in the desktop sidebar', async () => {
585
+ stubWidth(1280)
586
+ render(
587
+ <Layout config={{}} session={null} currentPath="/settings" onNavigate={vi.fn()}>
588
+ Body
589
+ </Layout>,
590
+ )
591
+ await screen.findByRole('navigation', { name: 'Primary' })
592
+ expect(screen.queryByRole('button', { name: 'Security' })).toBeNull()
593
+ })
594
+
595
+ it('lifts toasts above the tab bar', async () => {
596
+ const { container } = render(<Toaster aboveTabBar />)
597
+ act(() => {
598
+ toast('Saved')
599
+ })
600
+ await screen.findByText('Saved')
601
+ const list = container.querySelector<HTMLElement>('[data-sonner-toaster]')!
602
+ expect(list.style.getPropertyValue('--offset-bottom')).toContain('var(--tabbar-h)')
603
+ expect(list.style.getPropertyValue('--mobile-offset-bottom')).toContain(
604
+ 'env(safe-area-inset-bottom)',
605
+ )
606
+ })
607
+ })
608
+
609
+ /**
610
+ * happy-dom applies no stylesheet, so Tailwind's `hidden` (display: none)
611
+ * leaves boxes. Give elements under it no client rects, as a browser does.
612
+ */
613
+ function hideTailwindHidden() {
614
+ return vi.spyOn(HTMLElement.prototype, 'getClientRects').mockImplementation(function (
615
+ this: HTMLElement,
616
+ ) {
617
+ return (this.closest('.hidden') ? [] : [{}]) as unknown as DOMRectList
618
+ })
619
+ }
620
+
621
+ /** `stubWidth` whose width can change, firing the queries' change listeners. */
622
+ function stubResizableWidth(initial: number) {
623
+ let width = initial
624
+ const listeners = new Set<() => void>()
625
+ vi.stubGlobal('matchMedia', (query: string) => ({
626
+ media: query,
627
+ get matches() {
628
+ return matchesWidth(query, width)
629
+ },
630
+ addEventListener: (_: string, listener: () => void) => listeners.add(listener),
631
+ removeEventListener: (_: string, listener: () => void) => listeners.delete(listener),
632
+ }))
633
+ return (next: number) => {
634
+ width = next
635
+ act(() => {
636
+ for (const listener of [...listeners]) listener()
637
+ })
638
+ }
639
+ }
640
+
641
+ /** Fixes from the third review of the P-02 commits. */
642
+ describe('P-02 third review', () => {
643
+ it('traps Tab among the visible rows of the real sidebar, with a close button first', async () => {
644
+ stubWidth(600)
645
+ const rects = hideTailwindHidden()
646
+ const onClose = vi.fn()
647
+ render(
648
+ <AdminShell
649
+ desktopBreakpoint={769}
650
+ mobileSidebarOpen
651
+ onMobileSidebarClose={onClose}
652
+ sidebar={
653
+ <Sidebar
654
+ collapsed={false}
655
+ onToggleCollapse={() => {}}
656
+ currentPath="/"
657
+ onNavigate={vi.fn()}
658
+ />
659
+ }
660
+ tabBar={<button type="button">More</button>}
661
+ >
662
+ Body
663
+ </AdminShell>,
664
+ )
665
+ const dialog = await screen.findByRole('dialog', { name: 'Navigation' })
666
+ const close = within(dialog).getByRole('button', { name: 'Close navigation' })
667
+ await waitFor(() => expect(document.activeElement).toBe(close))
668
+ // The collapse footer is in the DOM but not rendered below 1025px.
669
+ const visible = within(dialog)
670
+ .getAllByRole('button')
671
+ .filter((b) => !b.closest('.hidden'))
672
+ const last = visible[visible.length - 1]!
673
+ expect(last.textContent).toBe('Settings')
674
+
675
+ last.focus()
676
+ fireEvent.keyDown(last, { key: 'Tab' })
677
+ expect(document.activeElement).toBe(close)
678
+ fireEvent.keyDown(close, { key: 'Tab', shiftKey: true })
679
+ expect(document.activeElement).toBe(last)
680
+
681
+ // Nothing behind the modal takes focus, the skip link included.
682
+ const skip = document.querySelector('.prenta-skip-link') as HTMLElement & { inert?: boolean }
683
+ expect(skip.inert).toBe(true)
684
+
685
+ fireEvent.click(close)
686
+ expect(onClose).toHaveBeenCalledOnce()
687
+ rects.mockRestore()
688
+ })
689
+
690
+ it('leaves Esc from a layer portaled out of the drawer to that layer', async () => {
691
+ stubWidth(600)
692
+ const onClose = vi.fn()
693
+ render(
694
+ <AdminShell
695
+ desktopBreakpoint={769}
696
+ mobileSidebarOpen
697
+ onMobileSidebarClose={onClose}
698
+ sidebar={
699
+ <>
700
+ <button type="button">Row</button>
701
+ {createPortal(<button type="button">Menu item</button>, document.body)}
702
+ </>
703
+ }
704
+ >
705
+ Body
706
+ </AdminShell>,
707
+ )
708
+ await screen.findByRole('dialog', { name: 'Navigation' })
709
+ fireEvent.keyDown(screen.getByRole('button', { name: 'Menu item' }), { key: 'Escape' })
710
+ expect(onClose).not.toHaveBeenCalled()
711
+ // Handled already inside the drawer (default prevented): not ours either.
712
+ const row = screen.getByRole('button', { name: 'Row' })
713
+ const claim = (e: Event) => e.preventDefault()
714
+ row.addEventListener('keydown', claim)
715
+ fireEvent.keyDown(row, { key: 'Escape' })
716
+ expect(onClose).not.toHaveBeenCalled()
717
+ row.removeEventListener('keydown', claim)
718
+ fireEvent.keyDown(row, { key: 'Escape' })
719
+ expect(onClose).toHaveBeenCalledOnce()
720
+ })
721
+
722
+ it('closes the drawer when the window widens past the mobile tier', async () => {
723
+ const resize = stubResizableWidth(600)
724
+ render(
725
+ <Layout config={{}} session={null} currentPath="/" onNavigate={vi.fn()}>
726
+ Body
727
+ </Layout>,
728
+ )
729
+ const bar = await screen.findByRole('navigation', { name: 'Tab bar' })
730
+ fireEvent.click(within(bar).getByRole('button', { name: 'More' }))
731
+ await screen.findByRole('dialog', { name: 'Navigation' })
732
+ resize(1280)
733
+ expect(screen.queryByRole('dialog')).toBeNull()
734
+ resize(600)
735
+ const again = await screen.findByRole('navigation', { name: 'Tab bar' })
736
+ expect(screen.queryByRole('dialog')).toBeNull()
737
+ expect(within(again).getByRole('button', { name: 'More' }).getAttribute('aria-expanded')).toBe(
738
+ 'false',
739
+ )
740
+ })
741
+
742
+ it('keeps Ctrl/Cmd+K from opening the command palette behind the open drawer', async () => {
743
+ stubWidth(600)
744
+ const palette = vi.fn()
745
+ const listen = (e: KeyboardEvent) => {
746
+ if (e.key === 'k' && (e.metaKey || e.ctrlKey)) palette()
747
+ }
748
+ document.addEventListener('keydown', listen)
749
+ const shell = (open: boolean) => (
750
+ <AdminShell
751
+ desktopBreakpoint={769}
752
+ mobileSidebarOpen={open}
753
+ onMobileSidebarClose={() => {}}
754
+ sidebar={<button type="button">Row</button>}
755
+ >
756
+ Body
757
+ </AdminShell>
758
+ )
759
+ const { rerender } = render(shell(true))
760
+ await screen.findByRole('dialog', { name: 'Navigation' })
761
+ const row = screen.getByRole('button', { name: 'Row' })
762
+ // fireEvent returns false when the default action was prevented.
763
+ expect(fireEvent.keyDown(row, { key: 'k', ctrlKey: true })).toBe(false)
764
+ expect(fireEvent.keyDown(row, { key: 'k', metaKey: true })).toBe(false)
765
+ expect(fireEvent.keyDown(row, { key: 'K', ctrlKey: true, shiftKey: true })).toBe(true)
766
+ expect(palette).not.toHaveBeenCalled()
767
+ rerender(shell(false))
768
+ await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
769
+ fireEvent.keyDown(document.body, { key: 'k', ctrlKey: true })
770
+ expect(palette).toHaveBeenCalledOnce()
771
+ document.removeEventListener('keydown', listen)
772
+ })
773
+
774
+ it('moves focus into a flyout opened from the keyboard after an earlier hover', async () => {
775
+ stubWidth(924)
776
+ renderSidebar('/', true)
777
+ const pages = screen.getByRole('button', { name: 'Pages' })
778
+ const wrapper = pages.parentElement as HTMLElement
779
+ fireEvent.mouseEnter(wrapper)
780
+ await waitFor(() => expect(isOpen(pages)).toBe(true))
781
+ fireEvent.mouseLeave(wrapper)
782
+ await waitFor(() => expect(isOpen(pages)).toBe(false)) // the 120ms hover close
783
+ pages.focus()
784
+ // happy-dom delivers the focusin a task later; let it land first.
785
+ await act(() => new Promise((resolve) => setTimeout(resolve, 0)))
786
+ fireEvent.keyDown(pages, { key: 'Enter' })
787
+ await waitFor(() => expect(isOpen(pages)).toBe(true))
788
+ const menu = document.querySelector('[role="menu"]')!
789
+ await waitFor(() => expect(menu.contains(document.activeElement)).toBe(true))
790
+ })
791
+
792
+ it('leaves focus in the field when Esc closes a hover-opened flyout', async () => {
793
+ stubWidth(924)
794
+ render(
795
+ <>
796
+ <input aria-label="Title" />
797
+ <Sidebar collapsed onToggleCollapse={() => {}} currentPath="/" onNavigate={vi.fn()} />
798
+ </>,
799
+ )
800
+ const input = screen.getByRole('textbox', { name: 'Title' })
801
+ input.focus()
802
+ const pages = screen.getByRole('button', { name: 'Pages' })
803
+ fireEvent.mouseEnter(pages.parentElement as HTMLElement)
804
+ await waitFor(() => expect(isOpen(pages)).toBe(true))
805
+ fireEvent.keyDown(input, { key: 'Escape' })
806
+ await waitFor(() => expect(isOpen(pages)).toBe(false))
807
+ expect(document.activeElement).toBe(input)
808
+ })
809
+
810
+ // These two mount inside a marked admin root, as AdminRoot does, so the
811
+ // flyout portals into a stable container. (Without one, the portal target
812
+ // falls back to the first `.prenta-admin` in the document, which is the
813
+ // open flyout panel itself, and the panel ends up detached.)
814
+ const railInAdminRoot = (extra?: ReactNode) =>
815
+ render(
816
+ <div data-prenta-admin-root="" className="prenta-admin">
817
+ {extra}
818
+ <Sidebar collapsed onToggleCollapse={() => {}} currentPath="/" onNavigate={vi.fn()} />
819
+ </div>,
820
+ )
821
+
822
+ it('returns focus to the trigger when a hover-opened flyout closes after focus moved into it', async () => {
823
+ stubWidth(924)
824
+ railInAdminRoot(<input aria-label="Title" />)
825
+ screen.getByRole('textbox', { name: 'Title' }).focus()
826
+ const pages = screen.getByRole('button', { name: 'Pages' })
827
+ fireEvent.mouseEnter(pages.parentElement as HTMLElement)
828
+ await waitFor(() => expect(isOpen(pages)).toBe(true))
829
+ await waitFor(() => expect(document.querySelector('[role="menuitem"]')).not.toBeNull())
830
+ // Radix focuses menu items under the pointer; stand in for that move.
831
+ const item = document.querySelector<HTMLElement>('[role="menuitem"]')!
832
+ act(() => item.focus())
833
+ await act(() => new Promise((resolve) => setTimeout(resolve, 0)))
834
+ fireEvent.keyDown(item, { key: 'Escape' })
835
+ await waitFor(() => expect(isOpen(pages)).toBe(false))
836
+ await waitFor(() => expect(document.activeElement).toBe(pages))
837
+ })
838
+
839
+ it('keeps the keyboard focus return when the pointer crosses an open flyout trigger', async () => {
840
+ stubWidth(924)
841
+ railInAdminRoot()
842
+ const pages = screen.getByRole('button', { name: 'Pages' })
843
+ pages.focus()
844
+ await act(() => new Promise((resolve) => setTimeout(resolve, 0)))
845
+ fireEvent.keyDown(pages, { key: 'Enter' })
846
+ await waitFor(() => expect(isOpen(pages)).toBe(true))
847
+ const menu = document.querySelector('[role="menu"]')!
848
+ await waitFor(() => expect(menu.contains(document.activeElement)).toBe(true))
849
+ // The mouse passes over the trigger while the keyboard-opened menu is up.
850
+ fireEvent.mouseEnter(pages.parentElement as HTMLElement)
851
+ fireEvent.keyDown(document.activeElement as HTMLElement, { key: 'Escape' })
852
+ await waitFor(() => expect(isOpen(pages)).toBe(false))
853
+ await waitFor(() => expect(document.activeElement).toBe(pages))
854
+ })
855
+
856
+ it('keeps the next hover flyout open when the pointer moves on from one that held focus', async () => {
857
+ stubWidth(924)
858
+ railInAdminRoot()
859
+ const pages = screen.getByRole('button', { name: 'Pages' })
860
+ const posts = screen.getByRole('button', { name: 'Posts' })
861
+ fireEvent.mouseEnter(pages.parentElement as HTMLElement)
862
+ await waitFor(() => expect(document.querySelector('[role="menuitem"]')).not.toBeNull())
863
+ act(() => document.querySelector<HTMLElement>('[role="menuitem"]')!.focus())
864
+ await act(() => new Promise((resolve) => setTimeout(resolve, 0)))
865
+ fireEvent.mouseLeave(pages.parentElement as HTMLElement)
866
+ fireEvent.mouseEnter(posts.parentElement as HTMLElement)
867
+ await waitFor(() => expect(isOpen(posts)).toBe(true))
868
+ // Past the 120ms hover close of Pages, plus a tick for Radix's focus return.
869
+ await act(() => new Promise((resolve) => setTimeout(resolve, 250)))
870
+ // Documents the intended behaviour. happy-dom doesn't route Radix's
871
+ // close focus-return to the trigger on this path, so this can't fail on
872
+ // the regression by itself; the close-cause guard in RailFlyout is what
873
+ // prevents the trigger focus that dismissed the next flyout in browsers.
874
+ expect(isOpen(pages)).toBe(false)
875
+ expect(isOpen(posts)).toBe(true)
876
+ expect(document.activeElement).not.toBe(pages)
877
+ })
878
+
879
+ it('keeps a keyboard-opened flyout open while the pointer crosses the rail', async () => {
880
+ stubWidth(924)
881
+ railInAdminRoot()
882
+ const pages = screen.getByRole('button', { name: 'Pages' })
883
+ pages.focus()
884
+ await act(() => new Promise((resolve) => setTimeout(resolve, 0)))
885
+ fireEvent.keyDown(pages, { key: 'Enter' })
886
+ await waitFor(() => expect(isOpen(pages)).toBe(true))
887
+ fireEvent.mouseEnter(pages.parentElement as HTMLElement)
888
+ fireEvent.mouseLeave(pages.parentElement as HTMLElement)
889
+ await act(() => new Promise((resolve) => setTimeout(resolve, 250)))
890
+ expect(isOpen(pages)).toBe(true)
891
+ })
892
+
893
+ it('moves focus into a flyout opened by a touch tap, even after an earlier mouse hover', async () => {
894
+ stubWidth(924)
895
+ railInAdminRoot()
896
+ const pages = screen.getByRole('button', { name: 'Pages' })
897
+ fireEvent.mouseEnter(pages.parentElement as HTMLElement)
898
+ await waitFor(() => expect(isOpen(pages)).toBe(true))
899
+ fireEvent.mouseLeave(pages.parentElement as HTMLElement)
900
+ await waitFor(() => expect(isOpen(pages)).toBe(false))
901
+ fireEvent.pointerDown(pages, { pointerType: 'touch', button: 0, ctrlKey: false })
902
+ await waitFor(() => expect(isOpen(pages)).toBe(true))
903
+ const menu = document.querySelector('[role="menu"]')!
904
+ await waitFor(() => expect(menu.contains(document.activeElement)).toBe(true))
905
+ })
906
+
907
+ it('takes the keyboard into a flyout the mouse already opened', async () => {
908
+ stubWidth(924)
909
+ railInAdminRoot()
910
+ const pages = screen.getByRole('button', { name: 'Pages' })
911
+ pages.focus()
912
+ await act(() => new Promise((resolve) => setTimeout(resolve, 0)))
913
+ fireEvent.mouseEnter(pages.parentElement as HTMLElement)
914
+ await waitFor(() => expect(document.querySelector('[role="menuitem"]')).not.toBeNull())
915
+ expect(document.activeElement).toBe(pages)
916
+ fireEvent.keyDown(pages, { key: 'ArrowDown' })
917
+ expect(isOpen(pages)).toBe(true)
918
+ expect(document.activeElement).toBe(document.querySelector('[role="menuitem"]'))
919
+ })
920
+
921
+ it('returns focus to the trigger without a focus ring after a mouse pick', async () => {
922
+ stubWidth(924)
923
+ const onNavigate = vi.fn()
924
+ render(
925
+ <div data-prenta-admin-root="" className="prenta-admin">
926
+ <Sidebar collapsed onToggleCollapse={() => {}} currentPath="/" onNavigate={onNavigate} />
927
+ </div>,
928
+ )
929
+ const pages = screen.getByRole('button', { name: 'Pages' })
930
+ const focus = vi.spyOn(pages, 'focus')
931
+ fireEvent.mouseEnter(pages.parentElement as HTMLElement)
932
+ await waitFor(() => expect(document.querySelector('[role="menuitem"]')).not.toBeNull())
933
+ const item = document.querySelector<HTMLElement>('[role="menuitem"]')!
934
+ act(() => item.focus())
935
+ await act(() => new Promise((resolve) => setTimeout(resolve, 0)))
936
+ fireEvent.click(item, { detail: 1 })
937
+ expect(onNavigate).toHaveBeenCalled()
938
+ await waitFor(() => expect(document.activeElement).toBe(pages))
939
+ expect(focus).toHaveBeenCalledWith(expect.objectContaining({ focusVisible: false }))
940
+ })
941
+
942
+ it('gives the rail labels a positioned ancestor inside the sidebar', () => {
943
+ stubWidth(924)
944
+ renderSidebar('/', true)
945
+ expect(screen.getByRole('navigation', { name: 'Primary' }).className).toMatch(/\brelative\b/)
946
+ })
947
+ })
948
+
949
+ describe('Sidebar hydration', () => {
950
+ it('renders parent rows the same on the server and in the first client render', async () => {
951
+ // Below 1025px the row navigates and reports no group state, but only
952
+ // after mount: the server can't know the width. happy-dom gives
953
+ // renderToString a window, so take matchMedia away as a server has none.
954
+ const tree = (
955
+ <Sidebar
956
+ collapsed={false}
957
+ onToggleCollapse={() => {}}
958
+ currentPath="/"
959
+ onNavigate={vi.fn()}
960
+ items={buildNavItems({})}
961
+ />
962
+ )
963
+ vi.stubGlobal('matchMedia', undefined)
964
+ const html = renderToString(tree)
965
+ stubWidth(600)
966
+ const container = document.createElement('div')
967
+ container.innerHTML = html
968
+ document.body.appendChild(container)
969
+ const errors = vi.spyOn(console, 'error').mockImplementation(() => {})
970
+ const recoverable = vi.fn()
971
+ let root: Root | undefined
972
+ await act(async () => {
973
+ root = hydrateRoot(container, tree, { onRecoverableError: recoverable })
974
+ })
975
+ expect(errors).not.toHaveBeenCalled()
976
+ expect(recoverable).not.toHaveBeenCalled()
977
+ const pages = within(container).getByRole('button', { name: 'Pages' })
978
+ expect(pages.hasAttribute('aria-expanded')).toBe(false)
979
+ act(() => root!.unmount())
980
+ container.remove()
981
+ errors.mockRestore()
982
+ })
983
+ })