@prenta/admin 2.6.1 → 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 (794) hide show
  1. package/CHANGELOG.md +98 -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/post-section-editor.test.js +3 -1
  143. package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
  144. package/dist/__tests__/views/script-tags.render.test.js +25 -0
  145. package/dist/__tests__/views/script-tags.render.test.js.map +1 -1
  146. package/dist/__tests__/views/security-settings.render.test.js +9 -8
  147. package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
  148. package/dist/__tests__/views/seo-settings.render.test.js +7 -7
  149. package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
  150. package/dist/__tests__/views/settings-save-bar.render.test.d.ts +2 -0
  151. package/dist/__tests__/views/settings-save-bar.render.test.d.ts.map +1 -0
  152. package/dist/__tests__/views/settings-save-bar.render.test.js +68 -0
  153. package/dist/__tests__/views/settings-save-bar.render.test.js.map +1 -0
  154. package/dist/__tests__/views/settings-save-footer.render.test.js +23 -15
  155. package/dist/__tests__/views/settings-save-footer.render.test.js.map +1 -1
  156. package/dist/__tests__/views/settings-tabs.render.test.d.ts +2 -0
  157. package/dist/__tests__/views/settings-tabs.render.test.d.ts.map +1 -0
  158. package/dist/__tests__/views/settings-tabs.render.test.js +319 -0
  159. package/dist/__tests__/views/settings-tabs.render.test.js.map +1 -0
  160. package/dist/__tests__/views/updates-tab.render.test.js +4 -3
  161. package/dist/__tests__/views/updates-tab.render.test.js.map +1 -1
  162. package/dist/components/Breadcrumbs.d.ts +8 -1
  163. package/dist/components/Breadcrumbs.d.ts.map +1 -1
  164. package/dist/components/Breadcrumbs.js +6 -0
  165. package/dist/components/Breadcrumbs.js.map +1 -1
  166. package/dist/components/CommandPalette.d.ts.map +1 -1
  167. package/dist/components/CommandPalette.js +40 -4
  168. package/dist/components/CommandPalette.js.map +1 -1
  169. package/dist/components/LocaleSwitcher.d.ts.map +1 -1
  170. package/dist/components/LocaleSwitcher.js +1 -1
  171. package/dist/components/LocaleSwitcher.js.map +1 -1
  172. package/dist/components/NotificationBell.d.ts.map +1 -1
  173. package/dist/components/NotificationBell.js +44 -3
  174. package/dist/components/NotificationBell.js.map +1 -1
  175. package/dist/components/OfflineStatus.js +1 -1
  176. package/dist/components/OfflineStatus.js.map +1 -1
  177. package/dist/components/PresenceIndicator.d.ts.map +1 -1
  178. package/dist/components/PresenceIndicator.js +2 -1
  179. package/dist/components/PresenceIndicator.js.map +1 -1
  180. package/dist/components/ReflowMeter.js +2 -2
  181. package/dist/components/ReflowMeter.js.map +1 -1
  182. package/dist/components/SEOPanel.js +2 -2
  183. package/dist/components/SEOPanel.js.map +1 -1
  184. package/dist/components/ThemeProvider.d.ts +24 -4
  185. package/dist/components/ThemeProvider.d.ts.map +1 -1
  186. package/dist/components/ThemeProvider.js +80 -22
  187. package/dist/components/ThemeProvider.js.map +1 -1
  188. package/dist/components/TipTapEditor.d.ts.map +1 -1
  189. package/dist/components/TipTapEditor.js +3 -4
  190. package/dist/components/TipTapEditor.js.map +1 -1
  191. package/dist/components/forms/EmbedPanel.js +1 -1
  192. package/dist/components/forms/EmbedPanel.js.map +1 -1
  193. package/dist/components/forms/FormEntryDetailPane.js +6 -1
  194. package/dist/components/forms/FormEntryDetailPane.js.map +1 -1
  195. package/dist/components/forms/ShortcodeGlossary.d.ts.map +1 -1
  196. package/dist/components/forms/ShortcodeGlossary.js +6 -1
  197. package/dist/components/forms/ShortcodeGlossary.js.map +1 -1
  198. package/dist/components/forms/primitives.d.ts +15 -5
  199. package/dist/components/forms/primitives.d.ts.map +1 -1
  200. package/dist/components/forms/primitives.js +11 -11
  201. package/dist/components/forms/primitives.js.map +1 -1
  202. package/dist/components/pages/tag-badges.d.ts +2 -1
  203. package/dist/components/pages/tag-badges.d.ts.map +1 -1
  204. package/dist/components/pages/tag-badges.js +4 -3
  205. package/dist/components/pages/tag-badges.js.map +1 -1
  206. package/dist/components/seo/PageAuditPanel.js +2 -2
  207. package/dist/components/seo/PageAuditPanel.js.map +1 -1
  208. package/dist/components/seo/SerpPreview.d.ts +4 -4
  209. package/dist/components/seo/SerpPreview.js +1 -1
  210. package/dist/components/seo/SerpPreview.js.map +1 -1
  211. package/dist/components/seo/primitives.js +2 -2
  212. package/dist/components/seo/primitives.js.map +1 -1
  213. package/dist/components/ui/AuthorAvatar.d.ts +4 -4
  214. package/dist/components/ui/AuthorAvatar.d.ts.map +1 -1
  215. package/dist/components/ui/AuthorAvatar.js +11 -10
  216. package/dist/components/ui/AuthorAvatar.js.map +1 -1
  217. package/dist/components/ui/Badge.d.ts +1 -1
  218. package/dist/components/ui/Badge.js +1 -1
  219. package/dist/components/ui/Badge.js.map +1 -1
  220. package/dist/components/ui/Breadcrumb.d.ts +27 -0
  221. package/dist/components/ui/Breadcrumb.d.ts.map +1 -0
  222. package/dist/components/ui/Breadcrumb.js +41 -0
  223. package/dist/components/ui/Breadcrumb.js.map +1 -0
  224. package/dist/components/ui/Button.d.ts +2 -2
  225. package/dist/components/ui/Button.d.ts.map +1 -1
  226. package/dist/components/ui/Button.js +31 -16
  227. package/dist/components/ui/Button.js.map +1 -1
  228. package/dist/components/ui/Card.d.ts +8 -4
  229. package/dist/components/ui/Card.d.ts.map +1 -1
  230. package/dist/components/ui/Card.js +21 -13
  231. package/dist/components/ui/Card.js.map +1 -1
  232. package/dist/components/ui/CardListControls.d.ts +34 -0
  233. package/dist/components/ui/CardListControls.d.ts.map +1 -0
  234. package/dist/components/ui/CardListControls.js +25 -0
  235. package/dist/components/ui/CardListControls.js.map +1 -0
  236. package/dist/components/ui/CountChip.d.ts +20 -0
  237. package/dist/components/ui/CountChip.d.ts.map +1 -0
  238. package/dist/components/ui/CountChip.js +48 -0
  239. package/dist/components/ui/CountChip.js.map +1 -0
  240. package/dist/components/ui/FilterPill.d.ts +2 -1
  241. package/dist/components/ui/FilterPill.d.ts.map +1 -1
  242. package/dist/components/ui/FilterPill.js +3 -2
  243. package/dist/components/ui/FilterPill.js.map +1 -1
  244. package/dist/components/ui/Input.d.ts +1 -1
  245. package/dist/components/ui/InspectorPane.d.ts +1 -1
  246. package/dist/components/ui/InspectorPane.d.ts.map +1 -1
  247. package/dist/components/ui/InspectorPane.js +15 -4
  248. package/dist/components/ui/InspectorPane.js.map +1 -1
  249. package/dist/components/ui/Modal.d.ts.map +1 -1
  250. package/dist/components/ui/Modal.js +10 -2
  251. package/dist/components/ui/Modal.js.map +1 -1
  252. package/dist/components/ui/PageHeader.d.ts +28 -0
  253. package/dist/components/ui/PageHeader.d.ts.map +1 -0
  254. package/dist/components/ui/PageHeader.js +18 -0
  255. package/dist/components/ui/PageHeader.js.map +1 -0
  256. package/dist/components/ui/RowActions.d.ts +5 -0
  257. package/dist/components/ui/RowActions.d.ts.map +1 -1
  258. package/dist/components/ui/RowActions.js +7 -2
  259. package/dist/components/ui/RowActions.js.map +1 -1
  260. package/dist/components/ui/Segmented.d.ts +21 -0
  261. package/dist/components/ui/Segmented.d.ts.map +1 -0
  262. package/dist/components/ui/Segmented.js +41 -0
  263. package/dist/components/ui/Segmented.js.map +1 -0
  264. package/dist/components/ui/Toaster.d.ts +12 -0
  265. package/dist/components/ui/Toaster.d.ts.map +1 -0
  266. package/dist/components/ui/Toaster.js +103 -0
  267. package/dist/components/ui/Toaster.js.map +1 -0
  268. package/dist/components/ui/Toggle.d.ts +4 -3
  269. package/dist/components/ui/Toggle.d.ts.map +1 -1
  270. package/dist/components/ui/Toggle.js +16 -12
  271. package/dist/components/ui/Toggle.js.map +1 -1
  272. package/dist/components/ui/UnderlineTabs.d.ts +56 -0
  273. package/dist/components/ui/UnderlineTabs.d.ts.map +1 -0
  274. package/dist/components/ui/UnderlineTabs.js +205 -0
  275. package/dist/components/ui/UnderlineTabs.js.map +1 -0
  276. package/dist/components/ui/UnsavedBar.d.ts +25 -0
  277. package/dist/components/ui/UnsavedBar.d.ts.map +1 -0
  278. package/dist/components/ui/UnsavedBar.js +22 -0
  279. package/dist/components/ui/UnsavedBar.js.map +1 -0
  280. package/dist/components/ui/admin-links.d.ts +11 -0
  281. package/dist/components/ui/admin-links.d.ts.map +1 -0
  282. package/dist/components/ui/admin-links.js +10 -0
  283. package/dist/components/ui/admin-links.js.map +1 -0
  284. package/dist/components/ui/card-table.d.ts +43 -0
  285. package/dist/components/ui/card-table.d.ts.map +1 -0
  286. package/dist/components/ui/card-table.js +31 -0
  287. package/dist/components/ui/card-table.js.map +1 -0
  288. package/dist/components/virtual-window.d.ts +7 -0
  289. package/dist/components/virtual-window.d.ts.map +1 -1
  290. package/dist/components/virtual-window.js +5 -2
  291. package/dist/components/virtual-window.js.map +1 -1
  292. package/dist/fields/RelationshipField.js +1 -1
  293. package/dist/fields/RelationshipField.js.map +1 -1
  294. package/dist/files/dm-mono-latin-400-normal.woff +0 -0
  295. package/dist/files/dm-mono-latin-400-normal.woff2 +0 -0
  296. package/dist/files/dm-mono-latin-500-normal.woff +0 -0
  297. package/dist/files/dm-mono-latin-500-normal.woff2 +0 -0
  298. package/dist/files/dm-mono-latin-ext-400-normal.woff +0 -0
  299. package/dist/files/dm-mono-latin-ext-400-normal.woff2 +0 -0
  300. package/dist/files/dm-mono-latin-ext-500-normal.woff +0 -0
  301. package/dist/files/dm-mono-latin-ext-500-normal.woff2 +0 -0
  302. package/dist/files/dm-sans-latin-400-normal.woff +0 -0
  303. package/dist/files/dm-sans-latin-400-normal.woff2 +0 -0
  304. package/dist/files/dm-sans-latin-500-normal.woff +0 -0
  305. package/dist/files/dm-sans-latin-500-normal.woff2 +0 -0
  306. package/dist/files/dm-sans-latin-600-normal.woff +0 -0
  307. package/dist/files/dm-sans-latin-600-normal.woff2 +0 -0
  308. package/dist/files/dm-sans-latin-700-normal.woff +0 -0
  309. package/dist/files/dm-sans-latin-700-normal.woff2 +0 -0
  310. package/dist/files/dm-sans-latin-ext-400-normal.woff +0 -0
  311. package/dist/files/dm-sans-latin-ext-400-normal.woff2 +0 -0
  312. package/dist/files/dm-sans-latin-ext-500-normal.woff +0 -0
  313. package/dist/files/dm-sans-latin-ext-500-normal.woff2 +0 -0
  314. package/dist/files/dm-sans-latin-ext-600-normal.woff +0 -0
  315. package/dist/files/dm-sans-latin-ext-600-normal.woff2 +0 -0
  316. package/dist/files/dm-sans-latin-ext-700-normal.woff +0 -0
  317. package/dist/files/dm-sans-latin-ext-700-normal.woff2 +0 -0
  318. package/dist/hooks/useAppearanceBootstrap.d.ts.map +1 -1
  319. package/dist/hooks/useAppearanceBootstrap.js +9 -3
  320. package/dist/hooks/useAppearanceBootstrap.js.map +1 -1
  321. package/dist/index.d.ts +15 -0
  322. package/dist/index.d.ts.map +1 -1
  323. package/dist/index.js +8 -0
  324. package/dist/index.js.map +1 -1
  325. package/dist/layout/Header.d.ts +14 -5
  326. package/dist/layout/Header.d.ts.map +1 -1
  327. package/dist/layout/Header.js +78 -13
  328. package/dist/layout/Header.js.map +1 -1
  329. package/dist/layout/Layout.d.ts +5 -3
  330. package/dist/layout/Layout.d.ts.map +1 -1
  331. package/dist/layout/Layout.js +45 -54
  332. package/dist/layout/Layout.js.map +1 -1
  333. package/dist/layout/MobileTabBar.d.ts +20 -0
  334. package/dist/layout/MobileTabBar.d.ts.map +1 -0
  335. package/dist/layout/MobileTabBar.js +29 -0
  336. package/dist/layout/MobileTabBar.js.map +1 -0
  337. package/dist/layout/Sidebar.d.ts +8 -26
  338. package/dist/layout/Sidebar.d.ts.map +1 -1
  339. package/dist/layout/Sidebar.js +309 -345
  340. package/dist/layout/Sidebar.js.map +1 -1
  341. package/dist/layout/nav-config.d.ts +101 -0
  342. package/dist/layout/nav-config.d.ts.map +1 -0
  343. package/dist/layout/nav-config.js +297 -0
  344. package/dist/layout/nav-config.js.map +1 -0
  345. package/dist/layout/primitives/AdminShell.d.ts +10 -2
  346. package/dist/layout/primitives/AdminShell.d.ts.map +1 -1
  347. package/dist/layout/primitives/AdminShell.js +114 -6
  348. package/dist/layout/primitives/AdminShell.js.map +1 -1
  349. package/dist/layout/topbar-classes.d.ts +8 -0
  350. package/dist/layout/topbar-classes.d.ts.map +1 -0
  351. package/dist/layout/topbar-classes.js +8 -0
  352. package/dist/layout/topbar-classes.js.map +1 -0
  353. package/dist/layout/use-nav-items.d.ts +10 -0
  354. package/dist/layout/use-nav-items.d.ts.map +1 -0
  355. package/dist/layout/use-nav-items.js +39 -0
  356. package/dist/layout/use-nav-items.js.map +1 -0
  357. package/dist/lib/api.d.ts +6 -0
  358. package/dist/lib/api.d.ts.map +1 -1
  359. package/dist/lib/api.js +1 -1
  360. package/dist/lib/api.js.map +1 -1
  361. package/dist/lib/appearance-api.d.ts +3 -1
  362. package/dist/lib/appearance-api.d.ts.map +1 -1
  363. package/dist/lib/appearance-api.js +1 -0
  364. package/dist/lib/appearance-api.js.map +1 -1
  365. package/dist/lib/job-drawer.d.ts +128 -0
  366. package/dist/lib/job-drawer.d.ts.map +1 -0
  367. package/dist/lib/job-drawer.js +351 -0
  368. package/dist/lib/job-drawer.js.map +1 -0
  369. package/dist/lib/job-view.d.ts +187 -0
  370. package/dist/lib/job-view.d.ts.map +1 -0
  371. package/dist/lib/job-view.js +363 -0
  372. package/dist/lib/job-view.js.map +1 -0
  373. package/dist/lib/jobs-service.d.ts +134 -0
  374. package/dist/lib/jobs-service.d.ts.map +1 -0
  375. package/dist/lib/jobs-service.js +179 -0
  376. package/dist/lib/jobs-service.js.map +1 -0
  377. package/dist/lib/sheet-focus.d.ts +24 -0
  378. package/dist/lib/sheet-focus.d.ts.map +1 -0
  379. package/dist/lib/sheet-focus.js +61 -0
  380. package/dist/lib/sheet-focus.js.map +1 -0
  381. package/dist/lib/tiers.d.ts +30 -0
  382. package/dist/lib/tiers.d.ts.map +1 -0
  383. package/dist/lib/tiers.js +43 -0
  384. package/dist/lib/tiers.js.map +1 -0
  385. package/dist/prenta-admin.css +1 -1
  386. package/dist/router/admin-routes.d.ts.map +1 -1
  387. package/dist/router/admin-routes.js +8 -0
  388. package/dist/router/admin-routes.js.map +1 -1
  389. package/dist/router/index.d.ts +5 -1
  390. package/dist/router/index.d.ts.map +1 -1
  391. package/dist/router/index.js +7 -0
  392. package/dist/router/index.js.map +1 -1
  393. package/dist/views/ApiKeys.js +2 -2
  394. package/dist/views/ApiKeys.js.map +1 -1
  395. package/dist/views/AppearanceChrome.d.ts +6 -0
  396. package/dist/views/AppearanceChrome.d.ts.map +1 -0
  397. package/dist/views/AppearanceChrome.js +25 -0
  398. package/dist/views/AppearanceChrome.js.map +1 -0
  399. package/dist/views/CollectionList.d.ts.map +1 -1
  400. package/dist/views/CollectionList.js +73 -53
  401. package/dist/views/CollectionList.js.map +1 -1
  402. package/dist/views/Dashboard.d.ts.map +1 -1
  403. package/dist/views/Dashboard.js +14 -8
  404. package/dist/views/Dashboard.js.map +1 -1
  405. package/dist/views/DocumentEdit.d.ts +6 -1
  406. package/dist/views/DocumentEdit.d.ts.map +1 -1
  407. package/dist/views/DocumentEdit.js +44 -8
  408. package/dist/views/DocumentEdit.js.map +1 -1
  409. package/dist/views/FormDeliveries.d.ts.map +1 -1
  410. package/dist/views/FormDeliveries.js +7 -1
  411. package/dist/views/FormDeliveries.js.map +1 -1
  412. package/dist/views/FormEntries.d.ts.map +1 -1
  413. package/dist/views/FormEntries.js +11 -2
  414. package/dist/views/FormEntries.js.map +1 -1
  415. package/dist/views/FormSubmissions.d.ts +1 -1
  416. package/dist/views/FormSubmissions.d.ts.map +1 -1
  417. package/dist/views/FormSubmissions.js +15 -4
  418. package/dist/views/FormSubmissions.js.map +1 -1
  419. package/dist/views/Forms.d.ts.map +1 -1
  420. package/dist/views/Forms.js +6 -4
  421. package/dist/views/Forms.js.map +1 -1
  422. package/dist/views/FormsSettings.d.ts +2 -0
  423. package/dist/views/FormsSettings.d.ts.map +1 -0
  424. package/dist/views/FormsSettings.js +37 -0
  425. package/dist/views/FormsSettings.js.map +1 -0
  426. package/dist/views/Jobs/BusyButton.d.ts +15 -0
  427. package/dist/views/Jobs/BusyButton.d.ts.map +1 -0
  428. package/dist/views/Jobs/BusyButton.js +26 -0
  429. package/dist/views/Jobs/BusyButton.js.map +1 -0
  430. package/dist/views/Jobs/CancelJobDialog.d.ts +21 -0
  431. package/dist/views/Jobs/CancelJobDialog.d.ts.map +1 -0
  432. package/dist/views/Jobs/CancelJobDialog.js +66 -0
  433. package/dist/views/Jobs/CancelJobDialog.js.map +1 -0
  434. package/dist/views/Jobs/DrawerActivity.d.ts +16 -0
  435. package/dist/views/Jobs/DrawerActivity.d.ts.map +1 -0
  436. package/dist/views/Jobs/DrawerActivity.js +59 -0
  437. package/dist/views/Jobs/DrawerActivity.js.map +1 -0
  438. package/dist/views/Jobs/DrawerOverview.d.ts +28 -0
  439. package/dist/views/Jobs/DrawerOverview.d.ts.map +1 -0
  440. package/dist/views/Jobs/DrawerOverview.js +32 -0
  441. package/dist/views/Jobs/DrawerOverview.js.map +1 -0
  442. package/dist/views/Jobs/DrawerResults.d.ts +25 -0
  443. package/dist/views/Jobs/DrawerResults.d.ts.map +1 -0
  444. package/dist/views/Jobs/DrawerResults.js +50 -0
  445. package/dist/views/Jobs/DrawerResults.js.map +1 -0
  446. package/dist/views/Jobs/DrawerReview.d.ts +44 -0
  447. package/dist/views/Jobs/DrawerReview.d.ts.map +1 -0
  448. package/dist/views/Jobs/DrawerReview.js +110 -0
  449. package/dist/views/Jobs/DrawerReview.js.map +1 -0
  450. package/dist/views/Jobs/JobDrawer.d.ts +31 -0
  451. package/dist/views/Jobs/JobDrawer.d.ts.map +1 -0
  452. package/dist/views/Jobs/JobDrawer.js +353 -0
  453. package/dist/views/Jobs/JobDrawer.js.map +1 -0
  454. package/dist/views/Jobs/JobsTable.d.ts +19 -0
  455. package/dist/views/Jobs/JobsTable.d.ts.map +1 -0
  456. package/dist/views/Jobs/JobsTable.js +80 -0
  457. package/dist/views/Jobs/JobsTable.js.map +1 -0
  458. package/dist/views/Jobs/WorkerStrip.d.ts +22 -0
  459. package/dist/views/Jobs/WorkerStrip.d.ts.map +1 -0
  460. package/dist/views/Jobs/WorkerStrip.js +63 -0
  461. package/dist/views/Jobs/WorkerStrip.js.map +1 -0
  462. package/dist/views/Jobs/atoms.d.ts +19 -0
  463. package/dist/views/Jobs/atoms.d.ts.map +1 -0
  464. package/dist/views/Jobs/atoms.js +52 -0
  465. package/dist/views/Jobs/atoms.js.map +1 -0
  466. package/dist/views/Jobs/focus.d.ts +23 -0
  467. package/dist/views/Jobs/focus.d.ts.map +1 -0
  468. package/dist/views/Jobs/focus.js +54 -0
  469. package/dist/views/Jobs/focus.js.map +1 -0
  470. package/dist/views/Jobs/icons.d.ts +22 -0
  471. package/dist/views/Jobs/icons.d.ts.map +1 -0
  472. package/dist/views/Jobs/icons.js +23 -0
  473. package/dist/views/Jobs/icons.js.map +1 -0
  474. package/dist/views/Jobs/placeholders.d.ts +6 -0
  475. package/dist/views/Jobs/placeholders.d.ts.map +1 -0
  476. package/dist/views/Jobs/placeholders.js +20 -0
  477. package/dist/views/Jobs/placeholders.js.map +1 -0
  478. package/dist/views/Jobs/useJobDetail.d.ts +21 -0
  479. package/dist/views/Jobs/useJobDetail.d.ts.map +1 -0
  480. package/dist/views/Jobs/useJobDetail.js +39 -0
  481. package/dist/views/Jobs/useJobDetail.js.map +1 -0
  482. package/dist/views/Jobs/useJobsFeed.d.ts +35 -0
  483. package/dist/views/Jobs/useJobsFeed.d.ts.map +1 -0
  484. package/dist/views/Jobs/useJobsFeed.js +231 -0
  485. package/dist/views/Jobs/useJobsFeed.js.map +1 -0
  486. package/dist/views/Jobs/useReviewItems.d.ts +39 -0
  487. package/dist/views/Jobs/useReviewItems.d.ts.map +1 -0
  488. package/dist/views/Jobs/useReviewItems.js +140 -0
  489. package/dist/views/Jobs/useReviewItems.js.map +1 -0
  490. package/dist/views/Jobs.d.ts +10 -1
  491. package/dist/views/Jobs.d.ts.map +1 -1
  492. package/dist/views/Jobs.js +126 -35
  493. package/dist/views/Jobs.js.map +1 -1
  494. package/dist/views/MediaBrowser.d.ts.map +1 -1
  495. package/dist/views/MediaBrowser.js +37 -9
  496. package/dist/views/MediaBrowser.js.map +1 -1
  497. package/dist/views/Pages/PagesListView.d.ts +1 -1
  498. package/dist/views/Pages/PagesListView.d.ts.map +1 -1
  499. package/dist/views/Pages/PagesListView.js +34 -5
  500. package/dist/views/Pages/PagesListView.js.map +1 -1
  501. package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -1
  502. package/dist/views/Posts/NewPostTypeInstructions.js +12 -2
  503. package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -1
  504. package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
  505. package/dist/views/Posts/PostTypesView.js +10 -1
  506. package/dist/views/Posts/PostTypesView.js.map +1 -1
  507. package/dist/views/Posts/PostsListView.d.ts +1 -2
  508. package/dist/views/Posts/PostsListView.d.ts.map +1 -1
  509. package/dist/views/Posts/PostsListView.js +31 -3
  510. package/dist/views/Posts/PostsListView.js.map +1 -1
  511. package/dist/views/SEO.d.ts.map +1 -1
  512. package/dist/views/SEO.js +3 -3
  513. package/dist/views/SEO.js.map +1 -1
  514. package/dist/views/ScriptTags.d.ts.map +1 -1
  515. package/dist/views/ScriptTags.js +20 -15
  516. package/dist/views/ScriptTags.js.map +1 -1
  517. package/dist/views/Settings.d.ts.map +1 -1
  518. package/dist/views/Settings.js +115 -51
  519. package/dist/views/Settings.js.map +1 -1
  520. package/dist/views/SpamFolder.d.ts +2 -0
  521. package/dist/views/SpamFolder.d.ts.map +1 -0
  522. package/dist/views/SpamFolder.js +14 -0
  523. package/dist/views/SpamFolder.js.map +1 -0
  524. package/dist/views/Trash.d.ts +6 -1
  525. package/dist/views/Trash.d.ts.map +1 -1
  526. package/dist/views/Trash.js +7 -2
  527. package/dist/views/Trash.js.map +1 -1
  528. package/dist/views/Users.js +2 -2
  529. package/dist/views/Users.js.map +1 -1
  530. package/dist/views/WidgetsPlaceholder.d.ts +2 -0
  531. package/dist/views/WidgetsPlaceholder.d.ts.map +1 -0
  532. package/dist/views/WidgetsPlaceholder.js +15 -0
  533. package/dist/views/WidgetsPlaceholder.js.map +1 -0
  534. package/dist/views/dashboard/kpi-grid.d.ts +18 -0
  535. package/dist/views/dashboard/kpi-grid.d.ts.map +1 -0
  536. package/dist/views/dashboard/kpi-grid.js +36 -0
  537. package/dist/views/dashboard/kpi-grid.js.map +1 -0
  538. package/dist/views/lazy-views.d.ts +4 -0
  539. package/dist/views/lazy-views.d.ts.map +1 -1
  540. package/dist/views/lazy-views.js +4 -0
  541. package/dist/views/lazy-views.js.map +1 -1
  542. package/dist/views/page-editor/PageTab.js +1 -1
  543. package/dist/views/page-editor/PageTab.js.map +1 -1
  544. package/dist/views/page-editor/ReviewBar.js +1 -1
  545. package/dist/views/page-editor/ReviewBar.js.map +1 -1
  546. package/dist/views/page-editor/VersionHistoryPanel.js +1 -1
  547. package/dist/views/page-editor/VersionHistoryPanel.js.map +1 -1
  548. package/dist/views/seo/AuditTab.js +1 -1
  549. package/dist/views/seo/AuditTab.js.map +1 -1
  550. package/dist/views/seo/OverviewTab.d.ts.map +1 -1
  551. package/dist/views/seo/OverviewTab.js +2 -2
  552. package/dist/views/seo/OverviewTab.js.map +1 -1
  553. package/dist/views/seo/ProposalsTab.js +1 -1
  554. package/dist/views/seo/ProposalsTab.js.map +1 -1
  555. package/dist/views/settings/AISettingsTab.d.ts.map +1 -1
  556. package/dist/views/settings/AISettingsTab.js +4 -2
  557. package/dist/views/settings/AISettingsTab.js.map +1 -1
  558. package/dist/views/settings/AccessProtectionCard.js +1 -1
  559. package/dist/views/settings/AccessProtectionCard.js.map +1 -1
  560. package/dist/views/settings/AdminThemeCard.d.ts.map +1 -1
  561. package/dist/views/settings/AdminThemeCard.js +63 -15
  562. package/dist/views/settings/AdminThemeCard.js.map +1 -1
  563. package/dist/views/settings/AppearanceTab.d.ts.map +1 -1
  564. package/dist/views/settings/AppearanceTab.js +4 -2
  565. package/dist/views/settings/AppearanceTab.js.map +1 -1
  566. package/dist/views/settings/BrandPreviewCard.js +1 -1
  567. package/dist/views/settings/BrandPreviewCard.js.map +1 -1
  568. package/dist/views/settings/BrandVoiceCard.js +1 -1
  569. package/dist/views/settings/BrandVoiceCard.js.map +1 -1
  570. package/dist/views/settings/EmailSettingsTab.d.ts.map +1 -1
  571. package/dist/views/settings/EmailSettingsTab.js +5 -3
  572. package/dist/views/settings/EmailSettingsTab.js.map +1 -1
  573. package/dist/views/settings/GeneralSettingsTab.d.ts.map +1 -1
  574. package/dist/views/settings/GeneralSettingsTab.js +4 -2
  575. package/dist/views/settings/GeneralSettingsTab.js.map +1 -1
  576. package/dist/views/settings/ProviderConnectionCard.js +1 -1
  577. package/dist/views/settings/ProviderConnectionCard.js.map +1 -1
  578. package/dist/views/settings/SearchDataCard.js +1 -1
  579. package/dist/views/settings/SearchDataCard.js.map +1 -1
  580. package/dist/views/settings/SecurityStatusCard.js +2 -2
  581. package/dist/views/settings/SecurityStatusCard.js.map +1 -1
  582. package/dist/views/settings/SecurityTab.d.ts.map +1 -1
  583. package/dist/views/settings/SecurityTab.js +4 -2
  584. package/dist/views/settings/SecurityTab.js.map +1 -1
  585. package/dist/views/settings/SeoAutopilotCard.js +2 -2
  586. package/dist/views/settings/SeoAutopilotCard.js.map +1 -1
  587. package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
  588. package/dist/views/settings/SeoSettingsTab.js +5 -3
  589. package/dist/views/settings/SeoSettingsTab.js.map +1 -1
  590. package/dist/views/settings/SiteIndexStatusCard.js +1 -1
  591. package/dist/views/settings/SiteIndexStatusCard.js.map +1 -1
  592. package/dist/views/settings/SiteThemeCard.js +1 -1
  593. package/dist/views/settings/UpdatesTab.d.ts.map +1 -1
  594. package/dist/views/settings/UpdatesTab.js +6 -5
  595. package/dist/views/settings/UpdatesTab.js.map +1 -1
  596. package/dist/views/settings/components.d.ts +7 -11
  597. package/dist/views/settings/components.d.ts.map +1 -1
  598. package/dist/views/settings/components.js +18 -12
  599. package/dist/views/settings/components.js.map +1 -1
  600. package/dist/views/settings/useAppearanceSettings.d.ts +7 -1
  601. package/dist/views/settings/useAppearanceSettings.d.ts.map +1 -1
  602. package/dist/views/settings/useAppearanceSettings.js +89 -22
  603. package/dist/views/settings/useAppearanceSettings.js.map +1 -1
  604. package/dist/views/users/UserDetailDrawer.js +1 -1
  605. package/dist/views/users/UserDetailDrawer.js.map +1 -1
  606. package/package.json +10 -8
  607. package/src/AdminRoot.tsx +88 -30
  608. package/src/LoginRoot.tsx +2 -1
  609. package/src/__tests__/components/author-avatar.test.tsx +3 -20
  610. package/src/__tests__/components/notification-bell.test.tsx +146 -14
  611. package/src/__tests__/components/responsive-tiers.render.test.tsx +461 -0
  612. package/src/__tests__/components/serp-preview.render.test.tsx +6 -3
  613. package/src/__tests__/components/shell-primitives.render.test.tsx +951 -0
  614. package/src/__tests__/components/theme-provider-accent.test.tsx +83 -8
  615. package/src/__tests__/components/toggle.test.tsx +11 -3
  616. package/src/__tests__/components/ui-primitives.test.tsx +5 -8
  617. package/src/__tests__/layout/build-nav-items.test.ts +1 -1
  618. package/src/__tests__/layout/shell-nav.render.test.tsx +983 -0
  619. package/src/__tests__/layout/sidebar-forms-submenu.render.test.tsx +8 -4
  620. package/src/__tests__/layout/sidebar-groups.render.test.tsx +116 -0
  621. package/src/__tests__/layout/sidebar-menus.render.test.tsx +116 -0
  622. package/src/__tests__/layout/sidebar-plugin-nav.test.ts +1 -1
  623. package/src/__tests__/layout/sidebar-posts.test.ts +48 -40
  624. package/src/__tests__/layout/sidebar-submenu.render.test.tsx +35 -10
  625. package/src/__tests__/layout/topbar.render.test.tsx +464 -0
  626. package/src/__tests__/lib/api.test.ts +2 -0
  627. package/src/__tests__/lib/job-drawer.test.ts +493 -0
  628. package/src/__tests__/lib/job-view.test.ts +437 -0
  629. package/src/__tests__/lib/jobs-service.test.ts +98 -0
  630. package/src/__tests__/router/admin-route-order.test.ts +39 -0
  631. package/src/__tests__/styles/ai-gradient-contrast.test.ts +135 -0
  632. package/src/__tests__/styles/breakpoint-order.test.ts +68 -0
  633. package/src/__tests__/styles/responsive-layer.test.ts +367 -12
  634. package/src/__tests__/styles/tailwind-preset.test.ts +154 -0
  635. package/src/__tests__/styles/theme-token-scope.test.ts +10 -3
  636. package/src/__tests__/styles/tiers.test.ts +136 -0
  637. package/src/__tests__/styles/tokens-drift.test.ts +67 -6
  638. package/src/__tests__/views/admin-lazy-route.render.test.tsx +41 -1
  639. package/src/__tests__/views/ai-settings.render.test.tsx +4 -4
  640. package/src/__tests__/views/appearance-menus-route.render.test.tsx +58 -0
  641. package/src/__tests__/views/appearance-settings.render.test.tsx +10 -8
  642. package/src/__tests__/views/appearance-theme-provider.render.test.tsx +347 -0
  643. package/src/__tests__/views/dashboard.test.tsx +48 -0
  644. package/src/__tests__/views/document-edit-navigation.render.test.tsx +130 -0
  645. package/src/__tests__/views/document-edit-routes.render.test.tsx +130 -0
  646. package/src/__tests__/views/email-settings.render.test.tsx +2 -2
  647. package/src/__tests__/views/form-entries.render.test.tsx +22 -0
  648. package/src/__tests__/views/form-submissions.render.test.tsx +22 -0
  649. package/src/__tests__/views/forms-list.render.test.tsx +13 -0
  650. package/src/__tests__/views/general-settings.render.test.tsx +38 -11
  651. package/src/__tests__/views/jobs-drawer.render.test.tsx +1047 -0
  652. package/src/__tests__/views/jobs-list.render.test.tsx +850 -0
  653. package/src/__tests__/views/kpi-grid.test.ts +29 -0
  654. package/src/__tests__/views/post-section-editor.test.tsx +5 -1
  655. package/src/__tests__/views/script-tags.render.test.tsx +29 -0
  656. package/src/__tests__/views/security-settings.render.test.tsx +9 -10
  657. package/src/__tests__/views/seo-settings.render.test.tsx +7 -9
  658. package/src/__tests__/views/settings-save-bar.render.test.tsx +91 -0
  659. package/src/__tests__/views/settings-save-footer.render.test.tsx +25 -15
  660. package/src/__tests__/views/settings-tabs.render.test.tsx +371 -0
  661. package/src/__tests__/views/updates-tab.render.test.tsx +4 -3
  662. package/src/components/Breadcrumbs.tsx +7 -1
  663. package/src/components/CommandPalette.tsx +51 -3
  664. package/src/components/LocaleSwitcher.tsx +5 -6
  665. package/src/components/NotificationBell.tsx +74 -18
  666. package/src/components/OfflineStatus.tsx +1 -1
  667. package/src/components/PresenceIndicator.tsx +2 -1
  668. package/src/components/ReflowMeter.tsx +3 -3
  669. package/src/components/SEOPanel.tsx +2 -2
  670. package/src/components/ThemeProvider.tsx +122 -42
  671. package/src/components/TipTapEditor.tsx +3 -4
  672. package/src/components/forms/EmbedPanel.tsx +1 -1
  673. package/src/components/forms/FormEntryDetailPane.tsx +7 -1
  674. package/src/components/forms/ShortcodeGlossary.tsx +7 -1
  675. package/src/components/forms/primitives.tsx +37 -21
  676. package/src/components/pages/tag-badges.tsx +4 -3
  677. package/src/components/seo/PageAuditPanel.tsx +2 -2
  678. package/src/components/seo/SerpPreview.tsx +5 -5
  679. package/src/components/seo/primitives.tsx +2 -2
  680. package/src/components/ui/AuthorAvatar.tsx +11 -10
  681. package/src/components/ui/Badge.tsx +1 -1
  682. package/src/components/ui/Breadcrumb.tsx +109 -0
  683. package/src/components/ui/Button.tsx +40 -15
  684. package/src/components/ui/Card.tsx +35 -32
  685. package/src/components/ui/CardListControls.tsx +90 -0
  686. package/src/components/ui/CountChip.tsx +90 -0
  687. package/src/components/ui/FilterPill.tsx +5 -3
  688. package/src/components/ui/InspectorPane.tsx +21 -7
  689. package/src/components/ui/Modal.tsx +20 -6
  690. package/src/components/ui/PageHeader.tsx +63 -0
  691. package/src/components/ui/RowActions.tsx +7 -2
  692. package/src/components/ui/Segmented.tsx +97 -0
  693. package/src/components/ui/Toaster.tsx +139 -0
  694. package/src/components/ui/Toggle.tsx +18 -14
  695. package/src/components/ui/UnderlineTabs.tsx +373 -0
  696. package/src/components/ui/UnsavedBar.tsx +119 -0
  697. package/src/components/ui/admin-links.tsx +20 -0
  698. package/src/components/ui/card-table.ts +34 -0
  699. package/src/components/virtual-window.tsx +13 -2
  700. package/src/fields/RelationshipField.tsx +1 -1
  701. package/src/hooks/useAppearanceBootstrap.ts +9 -3
  702. package/src/index.ts +19 -0
  703. package/src/layout/Header.tsx +210 -135
  704. package/src/layout/Layout.tsx +61 -108
  705. package/src/layout/MobileTabBar.tsx +92 -0
  706. package/src/layout/Sidebar.tsx +555 -593
  707. package/src/layout/nav-config.tsx +425 -0
  708. package/src/layout/primitives/AdminShell.tsx +154 -6
  709. package/src/layout/topbar-classes.ts +8 -0
  710. package/src/layout/use-nav-items.ts +40 -0
  711. package/src/lib/api.ts +7 -1
  712. package/src/lib/appearance-api.ts +4 -0
  713. package/src/lib/job-drawer.ts +424 -0
  714. package/src/lib/job-view.ts +456 -0
  715. package/src/lib/jobs-service.ts +290 -0
  716. package/src/lib/sheet-focus.ts +71 -0
  717. package/src/lib/tiers.ts +43 -0
  718. package/src/router/admin-routes.ts +8 -0
  719. package/src/router/index.ts +9 -1
  720. package/src/styles/prenta-tokens.css +263 -82
  721. package/src/styles/responsive.css +460 -22
  722. package/src/styles/theme.css +177 -28
  723. package/src/views/ApiKeys.tsx +2 -2
  724. package/src/views/AppearanceChrome.tsx +74 -0
  725. package/src/views/CollectionList.tsx +264 -205
  726. package/src/views/Dashboard.tsx +26 -13
  727. package/src/views/DocumentEdit.tsx +111 -56
  728. package/src/views/FormDeliveries.tsx +30 -17
  729. package/src/views/FormEntries.tsx +28 -20
  730. package/src/views/FormSubmissions.tsx +32 -34
  731. package/src/views/Forms.tsx +55 -34
  732. package/src/views/FormsSettings.tsx +94 -0
  733. package/src/views/Jobs/BusyButton.tsx +56 -0
  734. package/src/views/Jobs/CancelJobDialog.tsx +127 -0
  735. package/src/views/Jobs/DrawerActivity.tsx +142 -0
  736. package/src/views/Jobs/DrawerOverview.tsx +139 -0
  737. package/src/views/Jobs/DrawerResults.tsx +155 -0
  738. package/src/views/Jobs/DrawerReview.tsx +464 -0
  739. package/src/views/Jobs/JobDrawer.tsx +556 -0
  740. package/src/views/Jobs/JobsTable.tsx +260 -0
  741. package/src/views/Jobs/WorkerStrip.tsx +144 -0
  742. package/src/views/Jobs/atoms.tsx +93 -0
  743. package/src/views/Jobs/focus.ts +56 -0
  744. package/src/views/Jobs/icons.tsx +111 -0
  745. package/src/views/Jobs/placeholders.tsx +66 -0
  746. package/src/views/Jobs/useJobDetail.ts +64 -0
  747. package/src/views/Jobs/useJobsFeed.ts +249 -0
  748. package/src/views/Jobs/useReviewItems.ts +187 -0
  749. package/src/views/Jobs.tsx +282 -105
  750. package/src/views/MediaBrowser.tsx +71 -21
  751. package/src/views/Pages/PagesListView.tsx +115 -39
  752. package/src/views/Posts/NewPostTypeInstructions.tsx +15 -10
  753. package/src/views/Posts/PostTypesView.tsx +28 -6
  754. package/src/views/Posts/PostsListView.tsx +130 -40
  755. package/src/views/SEO.tsx +23 -8
  756. package/src/views/ScriptTags.tsx +233 -153
  757. package/src/views/Settings.tsx +215 -256
  758. package/src/views/SpamFolder.tsx +32 -0
  759. package/src/views/Trash.tsx +12 -1
  760. package/src/views/Users.tsx +2 -2
  761. package/src/views/WidgetsPlaceholder.tsx +33 -0
  762. package/src/views/dashboard/kpi-grid.ts +38 -0
  763. package/src/views/lazy-views.tsx +12 -0
  764. package/src/views/page-editor/PageTab.tsx +2 -2
  765. package/src/views/page-editor/ReviewBar.tsx +1 -1
  766. package/src/views/page-editor/VersionHistoryPanel.tsx +1 -1
  767. package/src/views/seo/AuditTab.tsx +1 -1
  768. package/src/views/seo/OverviewTab.tsx +6 -5
  769. package/src/views/seo/ProposalsTab.tsx +1 -1
  770. package/src/views/settings/AISettingsTab.tsx +10 -6
  771. package/src/views/settings/AccessProtectionCard.tsx +1 -1
  772. package/src/views/settings/AdminThemeCard.tsx +228 -35
  773. package/src/views/settings/AppearanceTab.tsx +10 -6
  774. package/src/views/settings/BrandPreviewCard.tsx +1 -1
  775. package/src/views/settings/BrandVoiceCard.tsx +1 -1
  776. package/src/views/settings/EmailSettingsTab.tsx +11 -7
  777. package/src/views/settings/GeneralSettingsTab.tsx +10 -6
  778. package/src/views/settings/ProviderConnectionCard.tsx +1 -1
  779. package/src/views/settings/SearchDataCard.tsx +1 -1
  780. package/src/views/settings/SecurityStatusCard.tsx +2 -2
  781. package/src/views/settings/SecurityTab.tsx +10 -6
  782. package/src/views/settings/SeoAutopilotCard.tsx +2 -2
  783. package/src/views/settings/SeoSettingsTab.tsx +13 -9
  784. package/src/views/settings/SiteIndexStatusCard.tsx +1 -1
  785. package/src/views/settings/SiteThemeCard.tsx +1 -1
  786. package/src/views/settings/UpdatesTab.tsx +107 -101
  787. package/src/views/settings/components.tsx +26 -62
  788. package/src/views/settings/useAppearanceSettings.ts +116 -25
  789. package/src/views/users/UserDetailDrawer.tsx +1 -1
  790. package/dist/components/ui/StatPill.d.ts +0 -18
  791. package/dist/components/ui/StatPill.d.ts.map +0 -1
  792. package/dist/components/ui/StatPill.js +0 -5
  793. package/dist/components/ui/StatPill.js.map +0 -1
  794. package/src/components/ui/StatPill.tsx +0 -37
@@ -0,0 +1,951 @@
1
+ // @vitest-environment happy-dom
2
+ import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react'
3
+ import { useState } from 'react'
4
+ import { afterEach, describe, expect, it, vi } from 'vitest'
5
+ import { toast } from 'sonner'
6
+
7
+ import { Breadcrumb } from '../../components/ui/Breadcrumb.js'
8
+ import { Button } from '../../components/ui/Button.js'
9
+ import {
10
+ Card,
11
+ CardContent,
12
+ CardDescription,
13
+ CardHeader,
14
+ CardTitle,
15
+ } from '../../components/ui/Card.js'
16
+ import { CountChip } from '../../components/ui/CountChip.js'
17
+ import { PageHeader } from '../../components/ui/PageHeader.js'
18
+ import { Segmented } from '../../components/ui/Segmented.js'
19
+ import { Switch, Toggle } from '../../components/ui/Toggle.js'
20
+ import { Toaster } from '../../components/ui/Toaster.js'
21
+ import { UnderlineTabPanel, UnderlineTabs } from '../../components/ui/UnderlineTabs.js'
22
+ import { UnsavedBar } from '../../components/ui/UnsavedBar.js'
23
+ import { AdminLinksProvider } from '../../components/ui/admin-links.js'
24
+ import { AdminShell } from '../../layout/primitives/AdminShell.js'
25
+
26
+ afterEach(cleanup)
27
+
28
+ /**
29
+ * P-03 shared primitives (docs/design/prenta/specs/SHELL.md §4–7). This file is
30
+ * the "test page": it renders every primitive and every variant, and pins the
31
+ * kit values each one must carry.
32
+ */
33
+
34
+ const classOf = (el: Element | null) => el?.getAttribute('class') ?? ''
35
+
36
+ describe('PageHeader', () => {
37
+ it('renders title, subtitle, actions and an optional breadcrumb', () => {
38
+ render(
39
+ <PageHeader
40
+ title="Post Types"
41
+ subtitle="Define content models and their fields."
42
+ actions={<Button>New Post Type</Button>}
43
+ breadcrumb={
44
+ <Breadcrumb items={[{ label: 'Dashboard', href: '/' }, { label: 'Post Types' }]} />
45
+ }
46
+ />,
47
+ )
48
+ const heading = screen.getByRole('heading', { level: 1, name: 'Post Types' })
49
+ expect(classOf(heading)).toContain('text-page')
50
+ expect(screen.getByText('Define content models and their fields.')).toBeTruthy()
51
+ expect(screen.getByRole('button', { name: 'New Post Type' })).toBeTruthy()
52
+ expect(screen.getByRole('navigation', { name: 'Breadcrumb' })).toBeTruthy()
53
+ })
54
+
55
+ it('pads to the page gutter unless flush', () => {
56
+ const { container, rerender } = render(<PageHeader title="Jobs" />)
57
+ expect(classOf(container.querySelector('header'))).toContain('px-[var(--gut)] pt-3.5')
58
+ rerender(<PageHeader title="Jobs" flush />)
59
+ expect(classOf(container.querySelector('header'))).not.toContain('px-[var(--gut)]')
60
+ expect(screen.queryByRole('navigation')).toBeNull()
61
+ })
62
+ })
63
+
64
+ describe('Breadcrumb', () => {
65
+ it('marks the current page and hides the separators', () => {
66
+ const onPosts = vi.fn()
67
+ render(
68
+ <Breadcrumb
69
+ items={[
70
+ { label: 'Dashboard', href: '/admin' },
71
+ { label: 'Posts', onClick: onPosts },
72
+ { label: 'Post Types' },
73
+ ]}
74
+ />,
75
+ )
76
+ const nav = screen.getByRole('navigation', { name: 'Breadcrumb' })
77
+ expect(screen.getByRole('link', { name: 'Dashboard' }).getAttribute('href')).toBe('/admin')
78
+ fireEvent.click(screen.getByRole('button', { name: 'Posts' }))
79
+ expect(onPosts).toHaveBeenCalledOnce()
80
+ const current = within(nav).getByText('Post Types')
81
+ expect(current.getAttribute('aria-current')).toBe('page')
82
+ expect(classOf(current)).toContain('font-[550]')
83
+ const separators = nav.querySelectorAll('li[aria-hidden="true"]')
84
+ expect(separators).toHaveLength(2)
85
+ expect(separators[0]?.textContent).toBe('/')
86
+ })
87
+
88
+ it('routes plain clicks on a link with onClick in-app, but leaves modified clicks alone', () => {
89
+ const onClick = vi.fn()
90
+ render(
91
+ <Breadcrumb items={[{ label: 'Forms', href: '/admin/forms', onClick }, { label: 'Spam' }]} />,
92
+ )
93
+ const link = screen.getByRole('link', { name: 'Forms' })
94
+ fireEvent.click(link)
95
+ expect(onClick).toHaveBeenCalledOnce()
96
+ fireEvent.click(link, { metaKey: true })
97
+ expect(onClick).toHaveBeenCalledOnce()
98
+ })
99
+
100
+ it('renders a plain ancestor when it has nowhere to go, and nothing for no items', () => {
101
+ const { container, rerender } = render(
102
+ <Breadcrumb items={[{ label: 'Appearance' }, { label: 'Widgets' }]} />,
103
+ )
104
+ expect(screen.queryByRole('link')).toBeNull()
105
+ expect(screen.queryByRole('button')).toBeNull()
106
+ rerender(<Breadcrumb items={[]} />)
107
+ expect(container.innerHTML).toBe('')
108
+ })
109
+ })
110
+
111
+ describe('CountChip', () => {
112
+ it('renders static, link and button chips with the kit look', () => {
113
+ const onClick = vi.fn()
114
+ render(
115
+ <>
116
+ <CountChip n={12} label="total" />
117
+ <CountChip n={3} label="unread" onClick={onClick} selected />
118
+ <CountChip n={4} label="active" tone="ok" href="/admin/script-tags" />
119
+ <CountChip n={2} label="failed" tone="bad" />
120
+ </>,
121
+ )
122
+ const total = screen.getByText('total')
123
+ expect(classOf(total)).toContain('font-[450]')
124
+ expect(classOf(total)).toContain('rounded-pill')
125
+ expect(classOf(total)).toContain('whitespace-nowrap')
126
+ expect(classOf(total.querySelector('b'))).toContain('tabular-nums')
127
+
128
+ const unread = screen.getByRole('button', { name: '3 unread' })
129
+ expect(unread.getAttribute('aria-pressed')).toBe('true')
130
+ expect(classOf(unread)).toContain('bg-acc-l')
131
+ fireEvent.click(unread)
132
+ expect(onClick).toHaveBeenCalledOnce()
133
+
134
+ const active = screen.getByRole('link', { name: '4 active' })
135
+ expect(classOf(active.querySelector('b'))).toContain('text-suc')
136
+ expect(classOf(screen.getByText('failed'))).toContain('text-err')
137
+ })
138
+
139
+ // P-06: Media's and Script Tags' header counts moved onto CountChip.
140
+ it('takes a formatted value, a muted number and a leading icon', () => {
141
+ render(
142
+ <>
143
+ <CountChip n="2.4 GB" label="Storage used" />
144
+ <CountChip n={1} label="Inactive" tone="muted" />
145
+ <CountChip n={5} label="Consent-gated" icon={<svg data-testid="shield" />} />
146
+ </>,
147
+ )
148
+ expect(screen.getByText('2.4 GB').tagName).toBe('B')
149
+ expect(classOf(screen.getByText('Inactive').querySelector('b'))).toContain('text-faint')
150
+ const icon = screen.getByTestId('shield').parentElement!
151
+ expect(icon.getAttribute('aria-hidden')).toBe('true')
152
+ expect(classOf(icon)).toContain('[&>svg]:h-3')
153
+ expect(screen.getByText('Consent-gated').textContent).toBe('5Consent-gated')
154
+ })
155
+
156
+ it('keeps a selected chip visible in forced-colors mode', () => {
157
+ render(
158
+ <>
159
+ <CountChip n={3} label="unread" onClick={() => {}} selected />
160
+ <CountChip n={2} label="starred" onClick={() => {}} selected={false} />
161
+ </>,
162
+ )
163
+ expect(classOf(screen.getByRole('button', { name: '3 unread' }))).toContain(
164
+ 'forced-colors:outline-2',
165
+ )
166
+ expect(classOf(screen.getByRole('button', { name: '2 starred' }))).not.toContain(
167
+ 'forced-colors:',
168
+ )
169
+ })
170
+ })
171
+
172
+ describe('UnderlineTabs', () => {
173
+ const TABS = [
174
+ { id: 'general', label: 'General', icon: <svg data-testid="icon" /> },
175
+ { id: 'users', label: 'Users', count: 6 },
176
+ { id: 'api-keys', label: 'API Keys', count: 4, badge: <b>Live</b> },
177
+ ]
178
+
179
+ function Harness({ initial = 'general' }: { initial?: string }) {
180
+ const [tab, setTab] = useState(initial)
181
+ return (
182
+ <>
183
+ <UnderlineTabs
184
+ label="Settings"
185
+ idBase="settings"
186
+ tabs={TABS}
187
+ value={tab}
188
+ onChange={setTab}
189
+ />
190
+ {TABS.map((t) =>
191
+ t.id === tab ? (
192
+ <UnderlineTabPanel key={t.id} idBase="settings" tabId={t.id}>
193
+ {t.label} panel
194
+ </UnderlineTabPanel>
195
+ ) : null,
196
+ )}
197
+ </>
198
+ )
199
+ }
200
+
201
+ it('marks the active tab with the inset box-shadow, never a border', () => {
202
+ render(<Harness />)
203
+ const general = screen.getByRole('tab', { name: 'General' })
204
+ expect(general.getAttribute('aria-selected')).toBe('true')
205
+ expect(classOf(general)).toContain('shadow-[inset_0_-2px_0_var(--acc)]')
206
+ expect(classOf(general)).not.toContain('border-b')
207
+ expect(classOf(screen.getByRole('tablist'))).toContain('shadow-[inset_0_-1px_0_var(--bdr)]')
208
+ expect(screen.getByTestId('icon')).toBeTruthy()
209
+ expect(screen.getByRole('tab', { name: 'Users 6' })).toBeTruthy()
210
+ expect(screen.getByRole('tab', { name: 'API Keys 4 Live' })).toBeTruthy()
211
+ })
212
+
213
+ it('wires each tab to its panel', () => {
214
+ render(<Harness />)
215
+ const tab = screen.getByRole('tab', { name: 'General' })
216
+ const panel = screen.getByRole('tabpanel')
217
+ expect(tab.getAttribute('aria-controls')).toBe(panel.id)
218
+ expect(panel.getAttribute('aria-labelledby')).toBe(tab.id)
219
+ })
220
+
221
+ it('moves with arrow keys, Home and End, with a roving tab stop', () => {
222
+ render(<Harness />)
223
+ const list = screen.getByRole('tablist')
224
+ fireEvent.keyDown(list, { key: 'ArrowRight' })
225
+ const users = screen.getByRole('tab', { name: 'Users 6' })
226
+ expect(users.getAttribute('aria-selected')).toBe('true')
227
+ expect(users.getAttribute('tabindex')).toBe('0')
228
+ expect(document.activeElement).toBe(users)
229
+ expect(screen.getByRole('tab', { name: 'General' }).getAttribute('tabindex')).toBe('-1')
230
+ fireEvent.keyDown(list, { key: 'End' })
231
+ expect(screen.getByRole('tabpanel').textContent).toBe('API Keys panel')
232
+ fireEvent.keyDown(list, { key: 'ArrowRight' })
233
+ expect(screen.getByRole('tabpanel').textContent).toBe('General panel')
234
+ fireEvent.keyDown(list, { key: 'ArrowLeft' })
235
+ expect(screen.getByRole('tabpanel').textContent).toBe('API Keys panel')
236
+ fireEvent.keyDown(list, { key: 'Home' })
237
+ expect(screen.getByRole('tabpanel').textContent).toBe('General panel')
238
+ })
239
+
240
+ /**
241
+ * Gives the strip a box: `scrollWidth` of content in a `clientWidth` window,
242
+ * tabs laid out at `tabWidth` each. happy-dom has no layout of its own.
243
+ */
244
+ function stubStrip(scrollWidth: number, initialWidth: number, tabWidth = 200) {
245
+ const list = screen.getByRole('tablist')
246
+ let left = 0
247
+ let clientWidth = initialWidth
248
+ const clamp = (v: number) => Math.max(0, Math.min(v, scrollWidth - clientWidth))
249
+ Object.defineProperty(list, 'scrollWidth', { configurable: true, get: () => scrollWidth })
250
+ Object.defineProperty(list, 'clientWidth', { configurable: true, get: () => clientWidth })
251
+ Object.defineProperty(list, 'scrollLeft', {
252
+ configurable: true,
253
+ get: () => left,
254
+ set: (v: number) => {
255
+ left = clamp(v)
256
+ },
257
+ })
258
+ // Called from a click handler, already inside act: a plain event, not fireEvent.
259
+ const scrollBy = vi.fn((options: ScrollToOptions) => {
260
+ left = clamp(left + (options.left ?? 0))
261
+ list.dispatchEvent(new Event('scroll'))
262
+ })
263
+ list.scrollBy = scrollBy as unknown as typeof list.scrollBy
264
+ screen.getAllByRole('tab').forEach((tab, index) => {
265
+ Object.defineProperty(tab, 'offsetLeft', { configurable: true, get: () => index * tabWidth })
266
+ Object.defineProperty(tab, 'offsetWidth', { configurable: true, get: () => tabWidth })
267
+ })
268
+ const scrollTo = (v: number) => {
269
+ left = clamp(v)
270
+ fireEvent.scroll(list)
271
+ }
272
+ /** Narrow or widen the strip's window (the observer is the caller's to fire). */
273
+ const resize = (width: number) => {
274
+ clientWidth = width
275
+ }
276
+ return { list, scrollBy, scrollTo, resize }
277
+ }
278
+
279
+ const END_FADE = '[mask-image:linear-gradient(90deg,black_calc(100%-48px),transparent)]'
280
+ const START_FADE = '[mask-image:linear-gradient(90deg,transparent,black_48px)]'
281
+ const BOTH_FADES =
282
+ '[mask-image:linear-gradient(90deg,transparent,black_48px,black_calc(100%-48px),transparent)]'
283
+ const scrollButton = (side: 'start' | 'end') =>
284
+ document.querySelector<HTMLButtonElement>(`[data-tabs-scroll="${side}"]`)
285
+
286
+ it('shows no fade and no scroll buttons while every tab fits', () => {
287
+ render(<Harness />)
288
+ expect(classOf(screen.getByRole('tablist'))).not.toContain('mask-image')
289
+ expect(scrollButton('start')).toBeNull()
290
+ expect(scrollButton('end')).toBeNull()
291
+ })
292
+
293
+ it('fades only the edges that hide tabs, each with a scroll button for the mouse', () => {
294
+ render(<Harness />)
295
+ const { list, scrollBy, scrollTo } = stubStrip(600, 300)
296
+ fireEvent.scroll(list)
297
+ expect(classOf(list)).toContain(END_FADE)
298
+ expect(list.getAttribute('data-overflow-end')).toBe('true')
299
+ expect(scrollButton('start')).toBeNull()
300
+
301
+ // Hidden from the keyboard and screen readers, which reach every tab
302
+ // through the tab list; a click never takes focus.
303
+ const end = scrollButton('end')!
304
+ expect(end.getAttribute('aria-hidden')).toBe('true')
305
+ expect(end.getAttribute('tabindex')).toBe('-1')
306
+ expect(fireEvent.mouseDown(end)).toBe(false)
307
+
308
+ // One page minus the two fades.
309
+ fireEvent.click(end)
310
+ expect(scrollBy).toHaveBeenCalledWith({ left: 204 })
311
+ expect(classOf(list)).toContain(BOTH_FADES)
312
+ expect(scrollButton('start')).toBeTruthy()
313
+ expect(scrollButton('end')).toBeTruthy()
314
+
315
+ scrollTo(300)
316
+ expect(classOf(list)).toContain(START_FADE)
317
+ expect(scrollButton('end')).toBeNull()
318
+ fireEvent.click(scrollButton('start')!)
319
+ expect(scrollBy).toHaveBeenLastCalledWith({ left: -204 })
320
+ expect(list.scrollLeft).toBe(96)
321
+ })
322
+
323
+ it('scrolls a tab chosen from the keyboard clear of the fades, never the page', () => {
324
+ render(<Harness />)
325
+ const { list } = stubStrip(600, 300)
326
+ const original = Element.prototype.scrollIntoView
327
+ const scrollIntoView = vi.fn()
328
+ Element.prototype.scrollIntoView = scrollIntoView
329
+ try {
330
+ fireEvent.scroll(list)
331
+
332
+ fireEvent.keyDown(list, { key: 'End' })
333
+ expect(list.scrollLeft).toBe(300)
334
+ expect(classOf(list)).toContain(START_FADE)
335
+ expect(list.hasAttribute('data-overflow-end')).toBe(false)
336
+
337
+ fireEvent.keyDown(list, { key: 'Home' })
338
+ expect(list.scrollLeft).toBe(0)
339
+ expect(classOf(list)).toContain(END_FADE)
340
+ expect(scrollIntoView).not.toHaveBeenCalled()
341
+ } finally {
342
+ Element.prototype.scrollIntoView = original
343
+ }
344
+ })
345
+
346
+ it('opens scrolled to a selected tab that starts out hidden, without a glide', () => {
347
+ // Layout before the first render: the strip is 300px of 600, tabs 200px each.
348
+ const proto = HTMLElement.prototype
349
+ const saved = (
350
+ ['scrollWidth', 'clientWidth', 'scrollLeft', 'offsetLeft', 'offsetWidth'] as const
351
+ ).map((key) => [key, Object.getOwnPropertyDescriptor(proto, key)] as const)
352
+ const lefts = new WeakMap<HTMLElement, number>()
353
+ const behaviours: string[] = []
354
+ const isList = (el: HTMLElement) => el.getAttribute('role') === 'tablist'
355
+ const isTab = (el: HTMLElement) => el.getAttribute('role') === 'tab'
356
+ Object.defineProperties(proto, {
357
+ scrollWidth: { configurable: true, get: () => 600 },
358
+ clientWidth: { configurable: true, get: () => 300 },
359
+ scrollLeft: {
360
+ configurable: true,
361
+ get(this: HTMLElement) {
362
+ return lefts.get(this) ?? 0
363
+ },
364
+ set(this: HTMLElement, v: number) {
365
+ if (isList(this)) behaviours.push(this.style.scrollBehavior)
366
+ lefts.set(this, Math.max(0, Math.min(v, 300)))
367
+ },
368
+ },
369
+ offsetLeft: {
370
+ configurable: true,
371
+ get(this: HTMLElement) {
372
+ return isTab(this) ? [...this.parentElement!.children].indexOf(this) * 200 : 0
373
+ },
374
+ },
375
+ offsetWidth: {
376
+ configurable: true,
377
+ get(this: HTMLElement) {
378
+ return isTab(this) ? 200 : 0
379
+ },
380
+ },
381
+ })
382
+ try {
383
+ render(<Harness initial="api-keys" />)
384
+ const list = screen.getByRole('tablist')
385
+ expect(list.scrollLeft).toBe(300)
386
+ expect(behaviours).toEqual(['auto'])
387
+ expect(list.style.scrollBehavior).toBe('')
388
+ expect(classOf(list)).toContain(START_FADE)
389
+ } finally {
390
+ for (const [key, descriptor] of saved) {
391
+ if (descriptor) Object.defineProperty(proto, key, descriptor)
392
+ else delete (proto as unknown as Record<string, unknown>)[key]
393
+ }
394
+ }
395
+ })
396
+
397
+ // Fixes from the independent review of P-06.
398
+ it('treats the open tab as no change, from a click or Home/End', () => {
399
+ const onChange = vi.fn()
400
+ render(<UnderlineTabs label="Tabs" tabs={TABS} value="general" onChange={onChange} />)
401
+ fireEvent.click(screen.getByRole('tab', { name: 'General' }))
402
+ fireEvent.keyDown(screen.getByRole('tablist'), { key: 'Home' })
403
+ expect(onChange).not.toHaveBeenCalled()
404
+ fireEvent.keyDown(screen.getByRole('tablist'), { key: 'End' })
405
+ expect(onChange).toHaveBeenCalledWith('api-keys')
406
+ })
407
+
408
+ it('leaves modified arrows to the browser (Alt+Left is Back)', () => {
409
+ const onChange = vi.fn()
410
+ render(<UnderlineTabs label="Tabs" tabs={TABS} value="users" onChange={onChange} />)
411
+ const list = screen.getByRole('tablist')
412
+ for (const modifier of ['altKey', 'ctrlKey', 'metaKey', 'shiftKey']) {
413
+ expect(fireEvent.keyDown(list, { key: 'ArrowLeft', [modifier]: true })).toBe(true)
414
+ }
415
+ expect(onChange).not.toHaveBeenCalled()
416
+ })
417
+
418
+ it('brings the selected tab back into view when the strip changes width', () => {
419
+ const callbacks: ResizeObserverCallback[] = []
420
+ const Original = globalThis.ResizeObserver
421
+ globalThis.ResizeObserver = class {
422
+ constructor(callback: ResizeObserverCallback) {
423
+ callbacks.push(callback)
424
+ }
425
+ observe() {}
426
+ unobserve() {}
427
+ disconnect() {}
428
+ } as unknown as typeof ResizeObserver
429
+ try {
430
+ render(<Harness initial="api-keys" />)
431
+ const { list, resize, scrollTo } = stubStrip(600, 300)
432
+ // The strip gets its 300px box: API Keys is revealed.
433
+ act(() => callbacks.at(-1)!([], {} as ResizeObserver))
434
+ expect(list.scrollLeft).toBe(300)
435
+ // Scrolled back by hand, then a tab alone resizes (a count arriving):
436
+ // same strip width, so nothing moves.
437
+ scrollTo(0)
438
+ act(() => callbacks.at(-1)!([], {} as ResizeObserver))
439
+ expect(list.scrollLeft).toBe(0)
440
+ // The strip narrows: API Keys (400–600px) comes back into view.
441
+ resize(250)
442
+ act(() => callbacks.at(-1)!([], {} as ResizeObserver))
443
+ // 400 + 200 + 48 − 250 = 398, clamped to the end of the strip (350).
444
+ expect(list.scrollLeft).toBe(350)
445
+ expect(classOf(list)).toContain(START_FADE)
446
+ } finally {
447
+ globalThis.ResizeObserver = Original
448
+ }
449
+ })
450
+
451
+ it('keeps a spent scroll button under the pointer, inert, so a double click stays put', () => {
452
+ render(<Harness />)
453
+ const { list, scrollBy } = stubStrip(600, 300)
454
+ fireEvent.scroll(list)
455
+ const end = scrollButton('end')!
456
+ fireEvent.pointerEnter(end)
457
+ fireEvent.click(end)
458
+ expect(list.scrollLeft).toBe(204)
459
+ // The second click reaches the end; the button stays where the pointer is.
460
+ fireEvent.click(scrollButton('end')!)
461
+ expect(list.scrollLeft).toBe(300)
462
+ const spent = scrollButton('end')!
463
+ expect(spent).toBe(end)
464
+ expect(spent.hasAttribute('data-inactive')).toBe(true)
465
+ expect(classOf(spent)).toContain('opacity-0')
466
+ fireEvent.click(spent)
467
+ expect(scrollBy).toHaveBeenCalledTimes(2)
468
+ expect(screen.getByRole('tab', { selected: true }).textContent).toBe('General')
469
+ fireEvent.pointerLeave(spent)
470
+ expect(scrollButton('end')).toBeNull()
471
+ })
472
+
473
+ it('outlines the selected tab and divides the strip in forced-colors mode', () => {
474
+ render(<UnderlineTabs label="Tabs" tabs={TABS} value="users" onChange={() => {}} />)
475
+ expect(classOf(screen.getByRole('tab', { name: 'Users 6' }))).toContain(
476
+ 'forced-colors:outline-2',
477
+ )
478
+ expect(classOf(screen.getByRole('tab', { name: 'General' }))).not.toContain('forced-colors:')
479
+ expect(classOf(screen.getByRole('tablist'))).toContain('forced-colors:border-b')
480
+ })
481
+
482
+ it("draws the page variant as Settings.html's .pg-tab", () => {
483
+ render(
484
+ <UnderlineTabs variant="page" label="Tabs" tabs={TABS} value="users" onChange={() => {}} />,
485
+ )
486
+ const general = screen.getByRole('tab', { name: 'General' })
487
+ expect(classOf(general)).toContain('font-medium')
488
+ expect(classOf(general)).toContain('text-faint')
489
+ expect(classOf(general)).toContain('px-4')
490
+ expect(classOf(screen.getByRole('tablist'))).not.toContain('gap-0.5')
491
+ const users = screen.getByRole('tab', { name: 'Users 6' })
492
+ expect(classOf(users)).toContain('text-acc')
493
+ expect(classOf(users)).toContain('shadow-[inset_0_-2px_0_var(--acc)]')
494
+ })
495
+
496
+ it("draws the drawer variant as the Jobs drawer's .jd-tabs, with no divider", () => {
497
+ render(
498
+ <UnderlineTabs variant="drawer" label="Tabs" tabs={TABS} value="users" onChange={() => {}} />,
499
+ )
500
+ const general = screen.getByRole('tab', { name: 'General' })
501
+ expect(classOf(general)).toContain('px-[11px]')
502
+ expect(classOf(general)).toContain('py-[9px]')
503
+ expect(classOf(general)).toContain('gap-1.5')
504
+ expect(classOf(general)).toContain('font-semibold')
505
+ expect(classOf(general)).toContain('text-sub')
506
+ const list = classOf(screen.getByRole('tablist'))
507
+ expect(list).toContain('gap-0.5')
508
+ expect(list).not.toContain('shadow-[inset_0_-1px_0_var(--bdr)]')
509
+ expect(list).not.toContain('forced-colors:border-b')
510
+ expect(classOf(screen.getByRole('tab', { name: 'Users 6' }))).toContain(
511
+ 'shadow-[inset_0_-2px_0_var(--acc)]',
512
+ )
513
+ })
514
+
515
+ it('leaves aria-controls off when the caller renders no panels', () => {
516
+ render(<UnderlineTabs label="Tabs" tabs={TABS} value="general" onChange={() => {}} />)
517
+ expect(screen.getByRole('tab', { name: 'General' }).hasAttribute('aria-controls')).toBe(false)
518
+ })
519
+ })
520
+
521
+ describe('Segmented', () => {
522
+ it('raises the active segment and shows counts from either source', () => {
523
+ render(
524
+ <Segmented
525
+ label="Status"
526
+ value="all"
527
+ onChange={() => {}}
528
+ counts={{ active: 2 }}
529
+ options={[
530
+ { id: 'all', label: 'All', count: 4 },
531
+ { id: 'active', label: 'Active' },
532
+ { id: 'failed', label: 'Failed' },
533
+ ]}
534
+ />,
535
+ )
536
+ const all = screen.getByRole('tab', { name: 'All 4' })
537
+ expect(classOf(all)).toContain('bg-card')
538
+ expect(classOf(all)).toContain('shadow-shd')
539
+ expect(classOf(all.querySelector('b'))).toContain('text-acc')
540
+ const active = screen.getByRole('tab', { name: 'Active 2' })
541
+ expect(classOf(active.querySelector('b'))).toContain('text-faint')
542
+ expect(screen.getByRole('tab', { name: 'Failed' })).toBeTruthy()
543
+ })
544
+
545
+ it('moves with the arrow keys', () => {
546
+ const onChange = vi.fn()
547
+ render(
548
+ <Segmented
549
+ label="Density"
550
+ value="comfortable"
551
+ onChange={onChange}
552
+ options={[
553
+ { id: 'comfortable', label: 'Comfortable' },
554
+ { id: 'compact', label: 'Compact' },
555
+ ]}
556
+ />,
557
+ )
558
+ fireEvent.keyDown(screen.getByRole('tablist'), { key: 'ArrowRight' })
559
+ expect(onChange).toHaveBeenCalledWith('compact')
560
+ fireEvent.keyDown(screen.getByRole('tablist'), { key: 'ArrowLeft' })
561
+ expect(onChange).toHaveBeenLastCalledWith('compact')
562
+ })
563
+ })
564
+
565
+ describe('Switch', () => {
566
+ it('is a 34×19 role="switch" with aria-checked, in both variants', () => {
567
+ const onChange = vi.fn()
568
+ render(
569
+ <>
570
+ <Switch checked={false} onCheckedChange={onChange} aria-label="Accent" />
571
+ <Toggle checked variant="enabled" onCheckedChange={() => {}} aria-label="Enabled" />
572
+ </>,
573
+ )
574
+ const accent = screen.getByRole('switch', { name: 'Accent' })
575
+ expect(accent.getAttribute('aria-checked')).toBe('false')
576
+ expect(classOf(accent)).toContain('h-[19px] w-[34px]')
577
+ fireEvent.click(accent)
578
+ expect(onChange).toHaveBeenCalledWith(true)
579
+ const enabled = screen.getByRole('switch', { name: 'Enabled' })
580
+ expect(enabled.getAttribute('aria-checked')).toBe('true')
581
+ expect(classOf(enabled)).toContain('data-[state=checked]:bg-suc')
582
+ })
583
+ })
584
+
585
+ describe('UnsavedBar', () => {
586
+ it('shows no bar until dirty, then floats as the dark pill', () => {
587
+ const { rerender } = render(<UnsavedBar dirty={false} onDiscard={() => {}} onSave={() => {}} />)
588
+ expect(screen.queryByRole('region', { name: 'Unsaved changes' })).toBeNull()
589
+ expect(screen.queryByRole('button')).toBeNull()
590
+
591
+ const onSave = vi.fn()
592
+ const onDiscard = vi.fn()
593
+ rerender(<UnsavedBar dirty onDiscard={onDiscard} onSave={onSave} />)
594
+ const bar = screen.getByRole('region', { name: 'Unsaved changes' })
595
+ for (const cls of [
596
+ 'sticky',
597
+ 'bottom-4',
598
+ 'ml-auto',
599
+ 'w-[min(520px,100%)]',
600
+ 'bg-txt',
601
+ 'text-bg',
602
+ 'rounded-12',
603
+ 'shadow-bar',
604
+ // Focus rings on the dark pill are drawn in --bg (responsive.css).
605
+ 'prenta-inverse',
606
+ ]) {
607
+ expect(classOf(bar)).toContain(cls)
608
+ }
609
+ expect(within(bar).getByText('Unsaved changes')).toBeTruthy()
610
+ // Discard is dimmed at rest but not while focused.
611
+ expect(classOf(screen.getByRole('button', { name: 'Discard' }))).toContain(
612
+ 'focus-visible:opacity-100',
613
+ )
614
+ fireEvent.click(screen.getByRole('button', { name: 'Save' }))
615
+ fireEvent.click(screen.getByRole('button', { name: 'Discard' }))
616
+ expect(onSave).toHaveBeenCalledOnce()
617
+ expect(onDiscard).toHaveBeenCalledOnce()
618
+ })
619
+
620
+ it('announces through a live region that exists before the form gets dirty', () => {
621
+ const { rerender } = render(<UnsavedBar dirty={false} onDiscard={() => {}} onSave={() => {}} />)
622
+ const live = screen.getByRole('status')
623
+ expect(live.getAttribute('aria-live')).toBe('polite')
624
+ // Fixed, so it can't stretch the shell's scroll area from the end of a long tab.
625
+ expect(classOf(live)).toContain('fixed!')
626
+ expect(live.textContent).toBe('')
627
+ rerender(<UnsavedBar dirty onDiscard={() => {}} onSave={() => {}} />)
628
+ // Same node, new text: the region was already in the tree.
629
+ expect(screen.getByRole('status')).toBe(live)
630
+ expect(live.textContent).toBe('Unsaved changes')
631
+ })
632
+
633
+ it('takes custom copy', () => {
634
+ render(
635
+ <UnsavedBar
636
+ dirty
637
+ label="Unsaved menu changes"
638
+ saveLabel="Save menus"
639
+ onDiscard={() => {}}
640
+ onSave={() => {}}
641
+ />,
642
+ )
643
+ const bar = screen.getByRole('region', { name: 'Unsaved changes' })
644
+ expect(within(bar).getByText('Unsaved menu changes')).toBeTruthy()
645
+ expect(screen.getByRole('button', { name: 'Save menus' })).toBeTruthy()
646
+ })
647
+
648
+ it('has the Settings card look (Settings.html .ap-save)', () => {
649
+ render(
650
+ <UnsavedBar
651
+ dirty
652
+ variant="card"
653
+ discardLabel="Reset"
654
+ saveLabel="Save changes"
655
+ onDiscard={() => {}}
656
+ onSave={() => {}}
657
+ />,
658
+ )
659
+ const bar = screen.getByRole('region', { name: 'Unsaved changes' })
660
+ for (const cls of [
661
+ 'sticky',
662
+ 'bottom-0',
663
+ 'bg-card',
664
+ 'border-bdr',
665
+ 'rounded-12',
666
+ 'shadow-save',
667
+ ]) {
668
+ expect(classOf(bar)).toContain(cls)
669
+ }
670
+ expect(classOf(bar)).not.toContain('bg-txt')
671
+ expect(bar.querySelector('.bg-warn')).toBeTruthy()
672
+ expect(classOf(screen.getByRole('button', { name: 'Reset' }))).toContain('prenta-touch-target')
673
+ expect(classOf(screen.getByRole('button', { name: 'Save changes' }))).toContain(
674
+ 'prenta-touch-target',
675
+ )
676
+ expect(screen.getByRole('button', { name: 'Reset' })).toBeTruthy()
677
+ expect(screen.getByRole('button', { name: 'Save changes' })).toBeTruthy()
678
+ })
679
+ })
680
+
681
+ describe('Card', () => {
682
+ it('uses radius 12, the resting shadow and a 13px 16px header row', () => {
683
+ render(
684
+ <Card data-testid="card">
685
+ <CardHeader data-testid="head">
686
+ <CardTitle as="h2">Variant</CardTitle>
687
+ <CardDescription>Site default.</CardDescription>
688
+ </CardHeader>
689
+ <CardContent data-testid="body">Body</CardContent>
690
+ </Card>,
691
+ )
692
+ expect(classOf(screen.getByTestId('card'))).toContain('rounded-12')
693
+ expect(classOf(screen.getByTestId('card'))).toContain('shadow-shd')
694
+ expect(classOf(screen.getByTestId('head'))).toContain('px-4 py-[13px]')
695
+ expect(classOf(screen.getByTestId('head'))).toContain('border-b')
696
+ const title = screen.getByRole('heading', { level: 2, name: 'Variant' })
697
+ expect(classOf(title)).toContain('font-[650]')
698
+ expect(classOf(screen.getByTestId('body'))).toContain('px-4 py-3.5')
699
+ })
700
+ })
701
+
702
+ describe('Button', () => {
703
+ it('renders every kit variant', () => {
704
+ render(
705
+ <>
706
+ <Button variant="primary">Primary</Button>
707
+ <Button variant="secondary">Secondary</Button>
708
+ <Button variant="secondary" size="sm">
709
+ Small
710
+ </Button>
711
+ <Button variant="ghost">Ghost</Button>
712
+ <Button variant="danger">Danger</Button>
713
+ <Button variant="ai">AI Copilot</Button>
714
+ <Button variant="icon" size="icon" aria-label="Row action" />
715
+ <Button variant="icon" size="icon-lg" aria-label="Topbar action" />
716
+ </>,
717
+ )
718
+ const cls = (name: string) => classOf(screen.getByRole('button', { name }))
719
+ expect(cls('Primary')).toContain('h-[34px]')
720
+ expect(cls('Primary')).toContain('text-body')
721
+ expect(cls('Primary')).toContain('font-semibold')
722
+ expect(cls('Primary')).toContain('bg-acc')
723
+ expect(cls('Secondary')).toContain('bg-card')
724
+ expect(cls('Secondary')).toContain('border-bdr')
725
+ expect(cls('Small')).toContain('h-7')
726
+ expect(cls('Small')).toContain('text-meta')
727
+ expect(cls('Ghost')).toContain('border-transparent')
728
+ expect(cls('Danger')).toContain('text-err')
729
+ expect(cls('Danger')).toContain('color-mix(in_srgb,var(--err)_35%,var(--bdr))')
730
+ expect(cls('AI Copilot')).toContain('linear-gradient(135deg,var(--ai1),var(--ai2))')
731
+ expect(cls('Row action')).toContain('h-[30px] w-[30px]')
732
+ expect(cls('Topbar action')).toContain('h-[34px] w-[34px]')
733
+ })
734
+
735
+ it('marks AI buttons so the responsive collapse keeps their label', () => {
736
+ render(
737
+ <>
738
+ <Button variant="ai">AI Copilot</Button>
739
+ <Button variant="secondary">Export</Button>
740
+ </>,
741
+ )
742
+ expect(screen.getByRole('button', { name: 'AI Copilot' }).hasAttribute('data-keep-label')).toBe(
743
+ true,
744
+ )
745
+ expect(screen.getByRole('button', { name: 'Export' }).hasAttribute('data-keep-label')).toBe(
746
+ false,
747
+ )
748
+ })
749
+
750
+ it('is busy and disabled while loading', () => {
751
+ render(<Button loading>Save</Button>)
752
+ const button = screen.getByRole('button', { name: 'Save' }) as HTMLButtonElement
753
+ expect(button.disabled).toBe(true)
754
+ expect(button.getAttribute('aria-busy')).toBe('true')
755
+ })
756
+ })
757
+
758
+ describe('Toaster', () => {
759
+ it('paints sonner toasts as the kit toast, bottom-centre', async () => {
760
+ const { container } = render(<Toaster />)
761
+ act(() => {
762
+ toast('Saved · site revalidated')
763
+ })
764
+ const message = await screen.findByText('Saved · site revalidated')
765
+ const item = message.closest('[data-sonner-toast]')
766
+ expect(item).toBeTruthy()
767
+ for (const cls of [
768
+ 'bg-txt',
769
+ 'text-bg',
770
+ 'rounded-9',
771
+ 'px-4',
772
+ 'py-2.5',
773
+ 'text-body',
774
+ 'font-medium',
775
+ ]) {
776
+ expect(classOf(item)).toContain(cls)
777
+ }
778
+ const list = container.querySelector('[data-sonner-toaster]')
779
+ expect(list?.getAttribute('data-y-position')).toBe('bottom')
780
+ expect(list?.getAttribute('data-x-position')).toBe('center')
781
+ })
782
+
783
+ it('drops the icon on success toasts and keeps it on errors', async () => {
784
+ render(<Toaster />)
785
+ act(() => {
786
+ toast.success('Menus saved')
787
+ toast.error('Save failed')
788
+ })
789
+ const ok = (await screen.findByText('Menus saved')).closest('[data-sonner-toast]')
790
+ const bad = (await screen.findByText('Save failed')).closest('[data-sonner-toast]')
791
+ expect(classOf(ok)).toContain('[&_[data-icon]]:hidden!')
792
+ expect(classOf(bad)).not.toContain('[&_[data-icon]]:hidden!')
793
+ })
794
+ })
795
+
796
+ /** Fixes from the independent review of the P-03 commit. */
797
+ describe('P-03 review fixes', () => {
798
+ it('keeps the 44px touch hit area on the 34×19 switch', () => {
799
+ render(<Switch checked={false} onCheckedChange={() => {}} aria-label="Hit" />)
800
+ expect(classOf(screen.getByRole('switch', { name: 'Hit' }))).toContain('prenta-touch-hit')
801
+ })
802
+
803
+ it('selects nothing when the value matches no tab, but keeps a tab stop', () => {
804
+ render(
805
+ <>
806
+ <UnderlineTabs
807
+ label="Tabs"
808
+ value="missing"
809
+ onChange={() => {}}
810
+ tabs={[
811
+ { id: 'a', label: 'A' },
812
+ { id: 'b', label: 'B' },
813
+ ]}
814
+ />
815
+ <Segmented
816
+ label="Seg"
817
+ value="missing"
818
+ onChange={() => {}}
819
+ options={[
820
+ { id: 'x', label: 'X' },
821
+ { id: 'y', label: 'Y' },
822
+ ]}
823
+ />
824
+ </>,
825
+ )
826
+ for (const name of ['A', 'B', 'X', 'Y']) {
827
+ expect(screen.getByRole('tab', { name }).getAttribute('aria-selected')).toBe('false')
828
+ }
829
+ expect(screen.getByRole('tab', { name: 'A' }).getAttribute('tabindex')).toBe('0')
830
+ expect(screen.getByRole('tab', { name: 'X' }).getAttribute('tabindex')).toBe('0')
831
+ expect(classOf(screen.getByRole('tab', { name: 'A' }))).not.toContain('shadow-[inset_0_-2px')
832
+ })
833
+
834
+ it('lays the card header out as the kit row', () => {
835
+ render(
836
+ <CardHeader data-testid="head">
837
+ <CardTitle>Branding</CardTitle>
838
+ </CardHeader>,
839
+ )
840
+ expect(classOf(screen.getByTestId('head'))).toContain('flex items-center gap-2.5')
841
+ expect(classOf(screen.getByRole('heading', { name: 'Branding' }))).toContain('flex-1')
842
+ })
843
+
844
+ it('colors a selected chip number with the accent', () => {
845
+ render(<CountChip n={6} label="unread" onClick={() => {}} selected />)
846
+ const chip = screen.getByRole('button', { name: '6 unread' })
847
+ expect(classOf(chip.querySelector('b'))).toContain('text-acc')
848
+ })
849
+
850
+ it('gives the toast icon slot a box and keeps stacked toasts quiet', async () => {
851
+ render(<Toaster />)
852
+ act(() => {
853
+ toast.loading('Uploading')
854
+ })
855
+ const item = (await screen.findByText('Uploading')).closest('[data-sonner-toast]')
856
+ expect(classOf(item)).toContain('prenta-inverse')
857
+ expect(classOf(item)).toContain('[&[data-expanded=false][data-front=false]>*]:opacity-0')
858
+ expect(classOf(item?.querySelector('[data-icon]') ?? null)).toContain('size-4')
859
+ })
860
+
861
+ it('keeps errors and warnings up longer while the admin toaster is mounted', () => {
862
+ const error = vi.spyOn(toast, 'error')
863
+ const warning = vi.spyOn(toast, 'warning')
864
+ const { unmount } = render(<Toaster />)
865
+ toast.error('Server said no')
866
+ toast.warning('Careful', { duration: 1000 })
867
+ expect(error).toHaveBeenCalledWith('Server said no', { duration: 6000 })
868
+ expect(warning).toHaveBeenCalledWith('Careful', { duration: 1000 })
869
+ unmount()
870
+ expect(toast.error).toBe(error)
871
+ error.mockRestore()
872
+ warning.mockRestore()
873
+ })
874
+
875
+ it('gives toast.promise errors the longer lifetime too', async () => {
876
+ const promise = vi.spyOn(toast, 'promise')
877
+ const { unmount } = render(<Toaster />)
878
+ toast.promise(Promise.resolve(), { loading: 'Saving', success: 'Saved', error: 'Failed' })
879
+ const data = promise.mock.calls[0]![1]!
880
+ expect(data.success).toBe('Saved')
881
+ const error = data.error as (reason: unknown) => Promise<unknown>
882
+ await expect(error(new Error('boom'))).resolves.toEqual({ message: 'Failed', duration: 6000 })
883
+ // A duration the caller set for the whole promise toast still wins.
884
+ toast.promise(Promise.resolve(), { loading: 'Saving', error: 'Failed', duration: 1500 })
885
+ const own = promise.mock.calls[1]![1]!.error as (reason: unknown) => Promise<unknown>
886
+ await expect(own(new Error('boom'))).resolves.toEqual({ message: 'Failed', duration: 1500 })
887
+ unmount()
888
+ promise.mockRestore()
889
+ })
890
+
891
+ it('installs once for several toasters and restores only its own wrappers', () => {
892
+ const original = toast.error
893
+ const first = render(<Toaster />)
894
+ const wrapped = toast.error
895
+ expect(wrapped).not.toBe(original)
896
+ const second = render(<Toaster />)
897
+ expect(toast.error).toBe(wrapped)
898
+ first.unmount()
899
+ expect(toast.error).toBe(wrapped)
900
+ // Someone else replaced it meanwhile: leave theirs in place.
901
+ const theirs = vi.fn() as unknown as typeof toast.error
902
+ toast.error = theirs
903
+ second.unmount()
904
+ expect(toast.error).toBe(theirs)
905
+ toast.error = original
906
+ })
907
+
908
+ it('renders path crumbs as real links that navigate in-app', () => {
909
+ const navigate = vi.fn()
910
+ render(
911
+ <AdminLinksProvider value={{ href: (path) => `/admin${path === '/' ? '' : path}`, navigate }}>
912
+ <Breadcrumb
913
+ items={[
914
+ { label: 'Dashboard', path: '/' },
915
+ { label: 'Posts', path: '/posts' },
916
+ { label: 'Trash' },
917
+ ]}
918
+ />
919
+ </AdminLinksProvider>,
920
+ )
921
+ const posts = screen.getByRole('link', { name: 'Posts' })
922
+ expect(posts.getAttribute('href')).toBe('/admin/posts')
923
+ expect(screen.getByRole('link', { name: 'Dashboard' }).getAttribute('href')).toBe('/admin')
924
+ fireEvent.click(posts)
925
+ expect(navigate).toHaveBeenCalledWith('/posts')
926
+ expect(screen.queryByRole('button')).toBeNull()
927
+ })
928
+
929
+ it('puts a rich subtitle in a block wrapper, not a paragraph', () => {
930
+ render(
931
+ <PageHeader
932
+ title="Edit Hello"
933
+ subtitle={
934
+ <>
935
+ <span>DRAFT</span>
936
+ <div>Presence</div>
937
+ </>
938
+ }
939
+ />,
940
+ )
941
+ expect(screen.getByText('Presence').parentElement?.tagName).toBe('DIV')
942
+ })
943
+
944
+ it('pads the scroll area so a focused field clears the floating bar', () => {
945
+ render(<AdminShell sidebar={null}>Body</AdminShell>)
946
+ const main = document.getElementById('prenta-main')
947
+ expect(classOf(main)).toContain('scroll-pb-[84px]')
948
+ // Positioned, so absolutely positioned sr-only text stays in main's scroll area.
949
+ expect(classOf(main).split(/\s+/)).toContain('relative')
950
+ })
951
+ })