@prenta/admin 2.6.1 → 2.8.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 (864) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/design-system/tokens.css +196 -85
  3. package/design-system/tokens.json +310 -85
  4. package/design-system/tokens.scss +163 -44
  5. package/design-system/tokens.tailwind.cjs +64 -1
  6. package/dist/AdminRoot.d.ts.map +1 -1
  7. package/dist/AdminRoot.js +43 -21
  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/batch-wizard.test.d.ts +2 -0
  61. package/dist/__tests__/lib/batch-wizard.test.d.ts.map +1 -0
  62. package/dist/__tests__/lib/batch-wizard.test.js +144 -0
  63. package/dist/__tests__/lib/batch-wizard.test.js.map +1 -0
  64. package/dist/__tests__/lib/job-drawer.test.d.ts +2 -0
  65. package/dist/__tests__/lib/job-drawer.test.d.ts.map +1 -0
  66. package/dist/__tests__/lib/job-drawer.test.js +395 -0
  67. package/dist/__tests__/lib/job-drawer.test.js.map +1 -0
  68. package/dist/__tests__/lib/job-view.test.d.ts +2 -0
  69. package/dist/__tests__/lib/job-view.test.d.ts.map +1 -0
  70. package/dist/__tests__/lib/job-view.test.js +484 -0
  71. package/dist/__tests__/lib/job-view.test.js.map +1 -0
  72. package/dist/__tests__/lib/jobs-service.test.d.ts +2 -0
  73. package/dist/__tests__/lib/jobs-service.test.d.ts.map +1 -0
  74. package/dist/__tests__/lib/jobs-service.test.js +142 -0
  75. package/dist/__tests__/lib/jobs-service.test.js.map +1 -0
  76. package/dist/__tests__/router/admin-route-order.test.d.ts +2 -0
  77. package/dist/__tests__/router/admin-route-order.test.d.ts.map +1 -0
  78. package/dist/__tests__/router/admin-route-order.test.js +36 -0
  79. package/dist/__tests__/router/admin-route-order.test.js.map +1 -0
  80. package/dist/__tests__/styles/ai-gradient-contrast.test.d.ts +2 -0
  81. package/dist/__tests__/styles/ai-gradient-contrast.test.d.ts.map +1 -0
  82. package/dist/__tests__/styles/ai-gradient-contrast.test.js +110 -0
  83. package/dist/__tests__/styles/ai-gradient-contrast.test.js.map +1 -0
  84. package/dist/__tests__/styles/breakpoint-order.test.d.ts +2 -0
  85. package/dist/__tests__/styles/breakpoint-order.test.d.ts.map +1 -0
  86. package/dist/__tests__/styles/breakpoint-order.test.js +52 -0
  87. package/dist/__tests__/styles/breakpoint-order.test.js.map +1 -0
  88. package/dist/__tests__/styles/responsive-layer.test.js +303 -12
  89. package/dist/__tests__/styles/responsive-layer.test.js.map +1 -1
  90. package/dist/__tests__/styles/tailwind-preset.test.d.ts +2 -0
  91. package/dist/__tests__/styles/tailwind-preset.test.d.ts.map +1 -0
  92. package/dist/__tests__/styles/tailwind-preset.test.js +147 -0
  93. package/dist/__tests__/styles/tailwind-preset.test.js.map +1 -0
  94. package/dist/__tests__/styles/theme-token-scope.test.js +9 -3
  95. package/dist/__tests__/styles/theme-token-scope.test.js.map +1 -1
  96. package/dist/__tests__/styles/tiers.test.d.ts +2 -0
  97. package/dist/__tests__/styles/tiers.test.d.ts.map +1 -0
  98. package/dist/__tests__/styles/tiers.test.js +121 -0
  99. package/dist/__tests__/styles/tiers.test.js.map +1 -0
  100. package/dist/__tests__/styles/tokens-drift.test.js +57 -6
  101. package/dist/__tests__/styles/tokens-drift.test.js.map +1 -1
  102. package/dist/__tests__/views/admin-lazy-route.render.test.js +27 -1
  103. package/dist/__tests__/views/admin-lazy-route.render.test.js.map +1 -1
  104. package/dist/__tests__/views/ai-settings.render.test.js +4 -4
  105. package/dist/__tests__/views/appearance-chrome.render.test.d.ts +2 -0
  106. package/dist/__tests__/views/appearance-chrome.render.test.d.ts.map +1 -0
  107. package/dist/__tests__/views/appearance-chrome.render.test.js +688 -0
  108. package/dist/__tests__/views/appearance-chrome.render.test.js.map +1 -0
  109. package/dist/__tests__/views/appearance-menus-route.render.test.d.ts +2 -0
  110. package/dist/__tests__/views/appearance-menus-route.render.test.d.ts.map +1 -0
  111. package/dist/__tests__/views/appearance-menus-route.render.test.js +50 -0
  112. package/dist/__tests__/views/appearance-menus-route.render.test.js.map +1 -0
  113. package/dist/__tests__/views/appearance-settings.render.test.js +8 -6
  114. package/dist/__tests__/views/appearance-settings.render.test.js.map +1 -1
  115. package/dist/__tests__/views/appearance-theme-provider.render.test.d.ts +2 -0
  116. package/dist/__tests__/views/appearance-theme-provider.render.test.d.ts.map +1 -0
  117. package/dist/__tests__/views/appearance-theme-provider.render.test.js +286 -0
  118. package/dist/__tests__/views/appearance-theme-provider.render.test.js.map +1 -0
  119. package/dist/__tests__/views/dashboard.test.js +37 -0
  120. package/dist/__tests__/views/dashboard.test.js.map +1 -1
  121. package/dist/__tests__/views/document-edit-navigation.render.test.d.ts +2 -0
  122. package/dist/__tests__/views/document-edit-navigation.render.test.d.ts.map +1 -0
  123. package/dist/__tests__/views/document-edit-navigation.render.test.js +101 -0
  124. package/dist/__tests__/views/document-edit-navigation.render.test.js.map +1 -0
  125. package/dist/__tests__/views/document-edit-routes.render.test.d.ts +2 -0
  126. package/dist/__tests__/views/document-edit-routes.render.test.d.ts.map +1 -0
  127. package/dist/__tests__/views/document-edit-routes.render.test.js +113 -0
  128. package/dist/__tests__/views/document-edit-routes.render.test.js.map +1 -0
  129. package/dist/__tests__/views/email-settings.render.test.js +2 -2
  130. package/dist/__tests__/views/form-entries.render.test.js +15 -0
  131. package/dist/__tests__/views/form-entries.render.test.js.map +1 -1
  132. package/dist/__tests__/views/form-submissions.render.test.js +18 -0
  133. package/dist/__tests__/views/form-submissions.render.test.js.map +1 -1
  134. package/dist/__tests__/views/forms-list.render.test.js +12 -0
  135. package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
  136. package/dist/__tests__/views/general-settings.render.test.js +26 -10
  137. package/dist/__tests__/views/general-settings.render.test.js.map +1 -1
  138. package/dist/__tests__/views/header-footer-form.test.d.ts +2 -0
  139. package/dist/__tests__/views/header-footer-form.test.d.ts.map +1 -0
  140. package/dist/__tests__/views/header-footer-form.test.js +266 -0
  141. package/dist/__tests__/views/header-footer-form.test.js.map +1 -0
  142. package/dist/__tests__/views/jobs-drawer.render.test.d.ts +2 -0
  143. package/dist/__tests__/views/jobs-drawer.render.test.d.ts.map +1 -0
  144. package/dist/__tests__/views/jobs-drawer.render.test.js +1348 -0
  145. package/dist/__tests__/views/jobs-drawer.render.test.js.map +1 -0
  146. package/dist/__tests__/views/jobs-list.render.test.d.ts +2 -0
  147. package/dist/__tests__/views/jobs-list.render.test.d.ts.map +1 -0
  148. package/dist/__tests__/views/jobs-list.render.test.js +697 -0
  149. package/dist/__tests__/views/jobs-list.render.test.js.map +1 -0
  150. package/dist/__tests__/views/jobs-wizard.render.test.d.ts +2 -0
  151. package/dist/__tests__/views/jobs-wizard.render.test.d.ts.map +1 -0
  152. package/dist/__tests__/views/jobs-wizard.render.test.js +588 -0
  153. package/dist/__tests__/views/jobs-wizard.render.test.js.map +1 -0
  154. package/dist/__tests__/views/kpi-grid.test.d.ts +2 -0
  155. package/dist/__tests__/views/kpi-grid.test.d.ts.map +1 -0
  156. package/dist/__tests__/views/kpi-grid.test.js +26 -0
  157. package/dist/__tests__/views/kpi-grid.test.js.map +1 -0
  158. package/dist/__tests__/views/post-section-editor.test.js +3 -1
  159. package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
  160. package/dist/__tests__/views/script-tags.render.test.js +25 -0
  161. package/dist/__tests__/views/script-tags.render.test.js.map +1 -1
  162. package/dist/__tests__/views/security-settings.render.test.js +9 -8
  163. package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
  164. package/dist/__tests__/views/seo-settings.render.test.js +7 -7
  165. package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
  166. package/dist/__tests__/views/settings-save-bar.render.test.d.ts +2 -0
  167. package/dist/__tests__/views/settings-save-bar.render.test.d.ts.map +1 -0
  168. package/dist/__tests__/views/settings-save-bar.render.test.js +68 -0
  169. package/dist/__tests__/views/settings-save-bar.render.test.js.map +1 -0
  170. package/dist/__tests__/views/settings-save-footer.render.test.js +23 -15
  171. package/dist/__tests__/views/settings-save-footer.render.test.js.map +1 -1
  172. package/dist/__tests__/views/settings-tabs.render.test.d.ts +2 -0
  173. package/dist/__tests__/views/settings-tabs.render.test.d.ts.map +1 -0
  174. package/dist/__tests__/views/settings-tabs.render.test.js +319 -0
  175. package/dist/__tests__/views/settings-tabs.render.test.js.map +1 -0
  176. package/dist/__tests__/views/updates-tab.render.test.js +4 -3
  177. package/dist/__tests__/views/updates-tab.render.test.js.map +1 -1
  178. package/dist/components/Breadcrumbs.d.ts +8 -1
  179. package/dist/components/Breadcrumbs.d.ts.map +1 -1
  180. package/dist/components/Breadcrumbs.js +6 -0
  181. package/dist/components/Breadcrumbs.js.map +1 -1
  182. package/dist/components/CommandPalette.d.ts.map +1 -1
  183. package/dist/components/CommandPalette.js +40 -4
  184. package/dist/components/CommandPalette.js.map +1 -1
  185. package/dist/components/LocaleSwitcher.d.ts.map +1 -1
  186. package/dist/components/LocaleSwitcher.js +1 -1
  187. package/dist/components/LocaleSwitcher.js.map +1 -1
  188. package/dist/components/NotificationBell.d.ts.map +1 -1
  189. package/dist/components/NotificationBell.js +44 -3
  190. package/dist/components/NotificationBell.js.map +1 -1
  191. package/dist/components/OfflineStatus.js +1 -1
  192. package/dist/components/OfflineStatus.js.map +1 -1
  193. package/dist/components/PresenceIndicator.d.ts.map +1 -1
  194. package/dist/components/PresenceIndicator.js +2 -1
  195. package/dist/components/PresenceIndicator.js.map +1 -1
  196. package/dist/components/ReflowMeter.js +2 -2
  197. package/dist/components/ReflowMeter.js.map +1 -1
  198. package/dist/components/SEOPanel.js +2 -2
  199. package/dist/components/SEOPanel.js.map +1 -1
  200. package/dist/components/ThemeProvider.d.ts +24 -4
  201. package/dist/components/ThemeProvider.d.ts.map +1 -1
  202. package/dist/components/ThemeProvider.js +80 -22
  203. package/dist/components/ThemeProvider.js.map +1 -1
  204. package/dist/components/TipTapEditor.d.ts.map +1 -1
  205. package/dist/components/TipTapEditor.js +3 -4
  206. package/dist/components/TipTapEditor.js.map +1 -1
  207. package/dist/components/forms/EmbedPanel.js +1 -1
  208. package/dist/components/forms/EmbedPanel.js.map +1 -1
  209. package/dist/components/forms/FormEntryDetailPane.js +6 -1
  210. package/dist/components/forms/FormEntryDetailPane.js.map +1 -1
  211. package/dist/components/forms/ShortcodeGlossary.d.ts.map +1 -1
  212. package/dist/components/forms/ShortcodeGlossary.js +6 -1
  213. package/dist/components/forms/ShortcodeGlossary.js.map +1 -1
  214. package/dist/components/forms/primitives.d.ts +15 -5
  215. package/dist/components/forms/primitives.d.ts.map +1 -1
  216. package/dist/components/forms/primitives.js +11 -11
  217. package/dist/components/forms/primitives.js.map +1 -1
  218. package/dist/components/pages/tag-badges.d.ts +2 -1
  219. package/dist/components/pages/tag-badges.d.ts.map +1 -1
  220. package/dist/components/pages/tag-badges.js +4 -3
  221. package/dist/components/pages/tag-badges.js.map +1 -1
  222. package/dist/components/seo/PageAuditPanel.js +2 -2
  223. package/dist/components/seo/PageAuditPanel.js.map +1 -1
  224. package/dist/components/seo/SerpPreview.d.ts +4 -4
  225. package/dist/components/seo/SerpPreview.js +1 -1
  226. package/dist/components/seo/SerpPreview.js.map +1 -1
  227. package/dist/components/seo/primitives.js +2 -2
  228. package/dist/components/seo/primitives.js.map +1 -1
  229. package/dist/components/ui/AuthorAvatar.d.ts +4 -4
  230. package/dist/components/ui/AuthorAvatar.d.ts.map +1 -1
  231. package/dist/components/ui/AuthorAvatar.js +11 -10
  232. package/dist/components/ui/AuthorAvatar.js.map +1 -1
  233. package/dist/components/ui/Badge.d.ts +1 -1
  234. package/dist/components/ui/Badge.js +1 -1
  235. package/dist/components/ui/Badge.js.map +1 -1
  236. package/dist/components/ui/Breadcrumb.d.ts +27 -0
  237. package/dist/components/ui/Breadcrumb.d.ts.map +1 -0
  238. package/dist/components/ui/Breadcrumb.js +41 -0
  239. package/dist/components/ui/Breadcrumb.js.map +1 -0
  240. package/dist/components/ui/Button.d.ts +2 -2
  241. package/dist/components/ui/Button.d.ts.map +1 -1
  242. package/dist/components/ui/Button.js +31 -16
  243. package/dist/components/ui/Button.js.map +1 -1
  244. package/dist/components/ui/Card.d.ts +8 -4
  245. package/dist/components/ui/Card.d.ts.map +1 -1
  246. package/dist/components/ui/Card.js +21 -13
  247. package/dist/components/ui/Card.js.map +1 -1
  248. package/dist/components/ui/CardListControls.d.ts +34 -0
  249. package/dist/components/ui/CardListControls.d.ts.map +1 -0
  250. package/dist/components/ui/CardListControls.js +25 -0
  251. package/dist/components/ui/CardListControls.js.map +1 -0
  252. package/dist/components/ui/CountChip.d.ts +20 -0
  253. package/dist/components/ui/CountChip.d.ts.map +1 -0
  254. package/dist/components/ui/CountChip.js +48 -0
  255. package/dist/components/ui/CountChip.js.map +1 -0
  256. package/dist/components/ui/FilterPill.d.ts +2 -1
  257. package/dist/components/ui/FilterPill.d.ts.map +1 -1
  258. package/dist/components/ui/FilterPill.js +3 -2
  259. package/dist/components/ui/FilterPill.js.map +1 -1
  260. package/dist/components/ui/Input.d.ts +1 -1
  261. package/dist/components/ui/InspectorPane.d.ts +1 -1
  262. package/dist/components/ui/InspectorPane.d.ts.map +1 -1
  263. package/dist/components/ui/InspectorPane.js +15 -4
  264. package/dist/components/ui/InspectorPane.js.map +1 -1
  265. package/dist/components/ui/Modal.d.ts.map +1 -1
  266. package/dist/components/ui/Modal.js +10 -2
  267. package/dist/components/ui/Modal.js.map +1 -1
  268. package/dist/components/ui/PageHeader.d.ts +28 -0
  269. package/dist/components/ui/PageHeader.d.ts.map +1 -0
  270. package/dist/components/ui/PageHeader.js +18 -0
  271. package/dist/components/ui/PageHeader.js.map +1 -0
  272. package/dist/components/ui/RowActions.d.ts +5 -0
  273. package/dist/components/ui/RowActions.d.ts.map +1 -1
  274. package/dist/components/ui/RowActions.js +7 -2
  275. package/dist/components/ui/RowActions.js.map +1 -1
  276. package/dist/components/ui/Segmented.d.ts +21 -0
  277. package/dist/components/ui/Segmented.d.ts.map +1 -0
  278. package/dist/components/ui/Segmented.js +41 -0
  279. package/dist/components/ui/Segmented.js.map +1 -0
  280. package/dist/components/ui/Toaster.d.ts +12 -0
  281. package/dist/components/ui/Toaster.d.ts.map +1 -0
  282. package/dist/components/ui/Toaster.js +103 -0
  283. package/dist/components/ui/Toaster.js.map +1 -0
  284. package/dist/components/ui/Toggle.d.ts +4 -3
  285. package/dist/components/ui/Toggle.d.ts.map +1 -1
  286. package/dist/components/ui/Toggle.js +16 -12
  287. package/dist/components/ui/Toggle.js.map +1 -1
  288. package/dist/components/ui/UnderlineTabs.d.ts +56 -0
  289. package/dist/components/ui/UnderlineTabs.d.ts.map +1 -0
  290. package/dist/components/ui/UnderlineTabs.js +205 -0
  291. package/dist/components/ui/UnderlineTabs.js.map +1 -0
  292. package/dist/components/ui/UnsavedBar.d.ts +25 -0
  293. package/dist/components/ui/UnsavedBar.d.ts.map +1 -0
  294. package/dist/components/ui/UnsavedBar.js +22 -0
  295. package/dist/components/ui/UnsavedBar.js.map +1 -0
  296. package/dist/components/ui/admin-links.d.ts +11 -0
  297. package/dist/components/ui/admin-links.d.ts.map +1 -0
  298. package/dist/components/ui/admin-links.js +10 -0
  299. package/dist/components/ui/admin-links.js.map +1 -0
  300. package/dist/components/ui/card-table.d.ts +43 -0
  301. package/dist/components/ui/card-table.d.ts.map +1 -0
  302. package/dist/components/ui/card-table.js +31 -0
  303. package/dist/components/ui/card-table.js.map +1 -0
  304. package/dist/components/virtual-window.d.ts +7 -0
  305. package/dist/components/virtual-window.d.ts.map +1 -1
  306. package/dist/components/virtual-window.js +5 -2
  307. package/dist/components/virtual-window.js.map +1 -1
  308. package/dist/fields/RelationshipField.js +1 -1
  309. package/dist/fields/RelationshipField.js.map +1 -1
  310. package/dist/files/dm-mono-latin-400-normal.woff +0 -0
  311. package/dist/files/dm-mono-latin-400-normal.woff2 +0 -0
  312. package/dist/files/dm-mono-latin-500-normal.woff +0 -0
  313. package/dist/files/dm-mono-latin-500-normal.woff2 +0 -0
  314. package/dist/files/dm-mono-latin-ext-400-normal.woff +0 -0
  315. package/dist/files/dm-mono-latin-ext-400-normal.woff2 +0 -0
  316. package/dist/files/dm-mono-latin-ext-500-normal.woff +0 -0
  317. package/dist/files/dm-mono-latin-ext-500-normal.woff2 +0 -0
  318. package/dist/files/dm-sans-latin-400-normal.woff +0 -0
  319. package/dist/files/dm-sans-latin-400-normal.woff2 +0 -0
  320. package/dist/files/dm-sans-latin-500-normal.woff +0 -0
  321. package/dist/files/dm-sans-latin-500-normal.woff2 +0 -0
  322. package/dist/files/dm-sans-latin-600-normal.woff +0 -0
  323. package/dist/files/dm-sans-latin-600-normal.woff2 +0 -0
  324. package/dist/files/dm-sans-latin-700-normal.woff +0 -0
  325. package/dist/files/dm-sans-latin-700-normal.woff2 +0 -0
  326. package/dist/files/dm-sans-latin-ext-400-normal.woff +0 -0
  327. package/dist/files/dm-sans-latin-ext-400-normal.woff2 +0 -0
  328. package/dist/files/dm-sans-latin-ext-500-normal.woff +0 -0
  329. package/dist/files/dm-sans-latin-ext-500-normal.woff2 +0 -0
  330. package/dist/files/dm-sans-latin-ext-600-normal.woff +0 -0
  331. package/dist/files/dm-sans-latin-ext-600-normal.woff2 +0 -0
  332. package/dist/files/dm-sans-latin-ext-700-normal.woff +0 -0
  333. package/dist/files/dm-sans-latin-ext-700-normal.woff2 +0 -0
  334. package/dist/hooks/useAppearanceBootstrap.d.ts.map +1 -1
  335. package/dist/hooks/useAppearanceBootstrap.js +9 -3
  336. package/dist/hooks/useAppearanceBootstrap.js.map +1 -1
  337. package/dist/index.d.ts +15 -0
  338. package/dist/index.d.ts.map +1 -1
  339. package/dist/index.js +8 -0
  340. package/dist/index.js.map +1 -1
  341. package/dist/layout/Header.d.ts +14 -5
  342. package/dist/layout/Header.d.ts.map +1 -1
  343. package/dist/layout/Header.js +78 -13
  344. package/dist/layout/Header.js.map +1 -1
  345. package/dist/layout/Layout.d.ts +5 -3
  346. package/dist/layout/Layout.d.ts.map +1 -1
  347. package/dist/layout/Layout.js +45 -54
  348. package/dist/layout/Layout.js.map +1 -1
  349. package/dist/layout/MobileTabBar.d.ts +20 -0
  350. package/dist/layout/MobileTabBar.d.ts.map +1 -0
  351. package/dist/layout/MobileTabBar.js +29 -0
  352. package/dist/layout/MobileTabBar.js.map +1 -0
  353. package/dist/layout/Sidebar.d.ts +8 -26
  354. package/dist/layout/Sidebar.d.ts.map +1 -1
  355. package/dist/layout/Sidebar.js +309 -345
  356. package/dist/layout/Sidebar.js.map +1 -1
  357. package/dist/layout/nav-config.d.ts +101 -0
  358. package/dist/layout/nav-config.d.ts.map +1 -0
  359. package/dist/layout/nav-config.js +297 -0
  360. package/dist/layout/nav-config.js.map +1 -0
  361. package/dist/layout/primitives/AdminShell.d.ts +10 -2
  362. package/dist/layout/primitives/AdminShell.d.ts.map +1 -1
  363. package/dist/layout/primitives/AdminShell.js +114 -6
  364. package/dist/layout/primitives/AdminShell.js.map +1 -1
  365. package/dist/layout/topbar-classes.d.ts +8 -0
  366. package/dist/layout/topbar-classes.d.ts.map +1 -0
  367. package/dist/layout/topbar-classes.js +8 -0
  368. package/dist/layout/topbar-classes.js.map +1 -0
  369. package/dist/layout/use-nav-items.d.ts +10 -0
  370. package/dist/layout/use-nav-items.d.ts.map +1 -0
  371. package/dist/layout/use-nav-items.js +39 -0
  372. package/dist/layout/use-nav-items.js.map +1 -0
  373. package/dist/lib/api.d.ts +8 -0
  374. package/dist/lib/api.d.ts.map +1 -1
  375. package/dist/lib/api.js +8 -1
  376. package/dist/lib/api.js.map +1 -1
  377. package/dist/lib/appearance-api.d.ts +3 -1
  378. package/dist/lib/appearance-api.d.ts.map +1 -1
  379. package/dist/lib/appearance-api.js +1 -0
  380. package/dist/lib/appearance-api.js.map +1 -1
  381. package/dist/lib/batch-wizard.d.ts +84 -0
  382. package/dist/lib/batch-wizard.d.ts.map +1 -0
  383. package/dist/lib/batch-wizard.js +122 -0
  384. package/dist/lib/batch-wizard.js.map +1 -0
  385. package/dist/lib/chrome-service.d.ts +69 -0
  386. package/dist/lib/chrome-service.d.ts.map +1 -0
  387. package/dist/lib/chrome-service.js +108 -0
  388. package/dist/lib/chrome-service.js.map +1 -0
  389. package/dist/lib/job-drawer.d.ts +128 -0
  390. package/dist/lib/job-drawer.d.ts.map +1 -0
  391. package/dist/lib/job-drawer.js +351 -0
  392. package/dist/lib/job-drawer.js.map +1 -0
  393. package/dist/lib/job-view.d.ts +230 -0
  394. package/dist/lib/job-view.d.ts.map +1 -0
  395. package/dist/lib/job-view.js +419 -0
  396. package/dist/lib/job-view.js.map +1 -0
  397. package/dist/lib/jobs-service.d.ts +152 -0
  398. package/dist/lib/jobs-service.d.ts.map +1 -0
  399. package/dist/lib/jobs-service.js +220 -0
  400. package/dist/lib/jobs-service.js.map +1 -0
  401. package/dist/lib/sheet-focus.d.ts +24 -0
  402. package/dist/lib/sheet-focus.d.ts.map +1 -0
  403. package/dist/lib/sheet-focus.js +61 -0
  404. package/dist/lib/sheet-focus.js.map +1 -0
  405. package/dist/lib/tiers.d.ts +30 -0
  406. package/dist/lib/tiers.d.ts.map +1 -0
  407. package/dist/lib/tiers.js +43 -0
  408. package/dist/lib/tiers.js.map +1 -0
  409. package/dist/prenta-admin.css +1 -1
  410. package/dist/router/admin-routes.d.ts.map +1 -1
  411. package/dist/router/admin-routes.js +8 -0
  412. package/dist/router/admin-routes.js.map +1 -1
  413. package/dist/router/index.d.ts +5 -1
  414. package/dist/router/index.d.ts.map +1 -1
  415. package/dist/router/index.js +7 -0
  416. package/dist/router/index.js.map +1 -1
  417. package/dist/views/ApiKeys.js +2 -2
  418. package/dist/views/ApiKeys.js.map +1 -1
  419. package/dist/views/AppearanceChrome.d.ts +24 -0
  420. package/dist/views/AppearanceChrome.d.ts.map +1 -0
  421. package/dist/views/AppearanceChrome.js +461 -0
  422. package/dist/views/AppearanceChrome.js.map +1 -0
  423. package/dist/views/CollectionList.d.ts.map +1 -1
  424. package/dist/views/CollectionList.js +73 -53
  425. package/dist/views/CollectionList.js.map +1 -1
  426. package/dist/views/Dashboard.d.ts.map +1 -1
  427. package/dist/views/Dashboard.js +14 -8
  428. package/dist/views/Dashboard.js.map +1 -1
  429. package/dist/views/DocumentEdit.d.ts +6 -1
  430. package/dist/views/DocumentEdit.d.ts.map +1 -1
  431. package/dist/views/DocumentEdit.js +44 -8
  432. package/dist/views/DocumentEdit.js.map +1 -1
  433. package/dist/views/FormDeliveries.d.ts.map +1 -1
  434. package/dist/views/FormDeliveries.js +7 -1
  435. package/dist/views/FormDeliveries.js.map +1 -1
  436. package/dist/views/FormEntries.d.ts.map +1 -1
  437. package/dist/views/FormEntries.js +11 -2
  438. package/dist/views/FormEntries.js.map +1 -1
  439. package/dist/views/FormSubmissions.d.ts +1 -1
  440. package/dist/views/FormSubmissions.d.ts.map +1 -1
  441. package/dist/views/FormSubmissions.js +15 -4
  442. package/dist/views/FormSubmissions.js.map +1 -1
  443. package/dist/views/Forms.d.ts.map +1 -1
  444. package/dist/views/Forms.js +6 -4
  445. package/dist/views/Forms.js.map +1 -1
  446. package/dist/views/FormsSettings.d.ts +2 -0
  447. package/dist/views/FormsSettings.d.ts.map +1 -0
  448. package/dist/views/FormsSettings.js +37 -0
  449. package/dist/views/FormsSettings.js.map +1 -0
  450. package/dist/views/Jobs/BusyButton.d.ts +15 -0
  451. package/dist/views/Jobs/BusyButton.d.ts.map +1 -0
  452. package/dist/views/Jobs/BusyButton.js +26 -0
  453. package/dist/views/Jobs/BusyButton.js.map +1 -0
  454. package/dist/views/Jobs/CancelJobDialog.d.ts +27 -0
  455. package/dist/views/Jobs/CancelJobDialog.d.ts.map +1 -0
  456. package/dist/views/Jobs/CancelJobDialog.js +76 -0
  457. package/dist/views/Jobs/CancelJobDialog.js.map +1 -0
  458. package/dist/views/Jobs/DrawerActivity.d.ts +16 -0
  459. package/dist/views/Jobs/DrawerActivity.d.ts.map +1 -0
  460. package/dist/views/Jobs/DrawerActivity.js +59 -0
  461. package/dist/views/Jobs/DrawerActivity.js.map +1 -0
  462. package/dist/views/Jobs/DrawerOverview.d.ts +28 -0
  463. package/dist/views/Jobs/DrawerOverview.d.ts.map +1 -0
  464. package/dist/views/Jobs/DrawerOverview.js +32 -0
  465. package/dist/views/Jobs/DrawerOverview.js.map +1 -0
  466. package/dist/views/Jobs/DrawerResults.d.ts +25 -0
  467. package/dist/views/Jobs/DrawerResults.d.ts.map +1 -0
  468. package/dist/views/Jobs/DrawerResults.js +50 -0
  469. package/dist/views/Jobs/DrawerResults.js.map +1 -0
  470. package/dist/views/Jobs/DrawerReview.d.ts +51 -0
  471. package/dist/views/Jobs/DrawerReview.d.ts.map +1 -0
  472. package/dist/views/Jobs/DrawerReview.js +110 -0
  473. package/dist/views/Jobs/DrawerReview.js.map +1 -0
  474. package/dist/views/Jobs/JobDrawer.d.ts +51 -0
  475. package/dist/views/Jobs/JobDrawer.d.ts.map +1 -0
  476. package/dist/views/Jobs/JobDrawer.js +378 -0
  477. package/dist/views/Jobs/JobDrawer.js.map +1 -0
  478. package/dist/views/Jobs/JobsTable.d.ts +21 -0
  479. package/dist/views/Jobs/JobsTable.d.ts.map +1 -0
  480. package/dist/views/Jobs/JobsTable.js +81 -0
  481. package/dist/views/Jobs/JobsTable.js.map +1 -0
  482. package/dist/views/Jobs/NewBatchJob.d.ts +34 -0
  483. package/dist/views/Jobs/NewBatchJob.d.ts.map +1 -0
  484. package/dist/views/Jobs/NewBatchJob.js +282 -0
  485. package/dist/views/Jobs/NewBatchJob.js.map +1 -0
  486. package/dist/views/Jobs/WorkerStrip.d.ts +24 -0
  487. package/dist/views/Jobs/WorkerStrip.d.ts.map +1 -0
  488. package/dist/views/Jobs/WorkerStrip.js +63 -0
  489. package/dist/views/Jobs/WorkerStrip.js.map +1 -0
  490. package/dist/views/Jobs/atoms.d.ts +19 -0
  491. package/dist/views/Jobs/atoms.d.ts.map +1 -0
  492. package/dist/views/Jobs/atoms.js +52 -0
  493. package/dist/views/Jobs/atoms.js.map +1 -0
  494. package/dist/views/Jobs/focus.d.ts +29 -0
  495. package/dist/views/Jobs/focus.d.ts.map +1 -0
  496. package/dist/views/Jobs/focus.js +64 -0
  497. package/dist/views/Jobs/focus.js.map +1 -0
  498. package/dist/views/Jobs/icons.d.ts +22 -0
  499. package/dist/views/Jobs/icons.d.ts.map +1 -0
  500. package/dist/views/Jobs/icons.js +23 -0
  501. package/dist/views/Jobs/icons.js.map +1 -0
  502. package/dist/views/Jobs/useJobDetail.d.ts +21 -0
  503. package/dist/views/Jobs/useJobDetail.d.ts.map +1 -0
  504. package/dist/views/Jobs/useJobDetail.js +39 -0
  505. package/dist/views/Jobs/useJobDetail.js.map +1 -0
  506. package/dist/views/Jobs/useJobsFeed.d.ts +35 -0
  507. package/dist/views/Jobs/useJobsFeed.d.ts.map +1 -0
  508. package/dist/views/Jobs/useJobsFeed.js +231 -0
  509. package/dist/views/Jobs/useJobsFeed.js.map +1 -0
  510. package/dist/views/Jobs/useReviewItems.d.ts +39 -0
  511. package/dist/views/Jobs/useReviewItems.d.ts.map +1 -0
  512. package/dist/views/Jobs/useReviewItems.js +140 -0
  513. package/dist/views/Jobs/useReviewItems.js.map +1 -0
  514. package/dist/views/Jobs.d.ts +19 -1
  515. package/dist/views/Jobs.d.ts.map +1 -1
  516. package/dist/views/Jobs.js +204 -35
  517. package/dist/views/Jobs.js.map +1 -1
  518. package/dist/views/MediaBrowser.d.ts.map +1 -1
  519. package/dist/views/MediaBrowser.js +37 -9
  520. package/dist/views/MediaBrowser.js.map +1 -1
  521. package/dist/views/Pages/PagesListView.d.ts +1 -1
  522. package/dist/views/Pages/PagesListView.d.ts.map +1 -1
  523. package/dist/views/Pages/PagesListView.js +34 -5
  524. package/dist/views/Pages/PagesListView.js.map +1 -1
  525. package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -1
  526. package/dist/views/Posts/NewPostTypeInstructions.js +12 -2
  527. package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -1
  528. package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
  529. package/dist/views/Posts/PostTypesView.js +10 -1
  530. package/dist/views/Posts/PostTypesView.js.map +1 -1
  531. package/dist/views/Posts/PostsListView.d.ts +1 -2
  532. package/dist/views/Posts/PostsListView.d.ts.map +1 -1
  533. package/dist/views/Posts/PostsListView.js +31 -3
  534. package/dist/views/Posts/PostsListView.js.map +1 -1
  535. package/dist/views/SEO.d.ts.map +1 -1
  536. package/dist/views/SEO.js +3 -3
  537. package/dist/views/SEO.js.map +1 -1
  538. package/dist/views/ScriptTags.d.ts.map +1 -1
  539. package/dist/views/ScriptTags.js +20 -15
  540. package/dist/views/ScriptTags.js.map +1 -1
  541. package/dist/views/Settings.d.ts.map +1 -1
  542. package/dist/views/Settings.js +115 -51
  543. package/dist/views/Settings.js.map +1 -1
  544. package/dist/views/SpamFolder.d.ts +2 -0
  545. package/dist/views/SpamFolder.d.ts.map +1 -0
  546. package/dist/views/SpamFolder.js +14 -0
  547. package/dist/views/SpamFolder.js.map +1 -0
  548. package/dist/views/Trash.d.ts +6 -1
  549. package/dist/views/Trash.d.ts.map +1 -1
  550. package/dist/views/Trash.js +7 -2
  551. package/dist/views/Trash.js.map +1 -1
  552. package/dist/views/Users.js +2 -2
  553. package/dist/views/Users.js.map +1 -1
  554. package/dist/views/WidgetsPlaceholder.d.ts +2 -0
  555. package/dist/views/WidgetsPlaceholder.d.ts.map +1 -0
  556. package/dist/views/WidgetsPlaceholder.js +15 -0
  557. package/dist/views/WidgetsPlaceholder.js.map +1 -0
  558. package/dist/views/dashboard/kpi-grid.d.ts +18 -0
  559. package/dist/views/dashboard/kpi-grid.d.ts.map +1 -0
  560. package/dist/views/dashboard/kpi-grid.js +36 -0
  561. package/dist/views/dashboard/kpi-grid.js.map +1 -0
  562. package/dist/views/header-footer/AnnouncementTab.d.ts +15 -0
  563. package/dist/views/header-footer/AnnouncementTab.d.ts.map +1 -0
  564. package/dist/views/header-footer/AnnouncementTab.js +43 -0
  565. package/dist/views/header-footer/AnnouncementTab.js.map +1 -0
  566. package/dist/views/header-footer/ChromeField.d.ts +25 -0
  567. package/dist/views/header-footer/ChromeField.d.ts.map +1 -0
  568. package/dist/views/header-footer/ChromeField.js +99 -0
  569. package/dist/views/header-footer/ChromeField.js.map +1 -0
  570. package/dist/views/header-footer/ChromePreview.d.ts +31 -0
  571. package/dist/views/header-footer/ChromePreview.d.ts.map +1 -0
  572. package/dist/views/header-footer/ChromePreview.js +195 -0
  573. package/dist/views/header-footer/ChromePreview.js.map +1 -0
  574. package/dist/views/header-footer/DeveloperTab.d.ts +17 -0
  575. package/dist/views/header-footer/DeveloperTab.d.ts.map +1 -0
  576. package/dist/views/header-footer/DeveloperTab.js +98 -0
  577. package/dist/views/header-footer/DeveloperTab.js.map +1 -0
  578. package/dist/views/header-footer/RegionTab.d.ts +27 -0
  579. package/dist/views/header-footer/RegionTab.d.ts.map +1 -0
  580. package/dist/views/header-footer/RegionTab.js +80 -0
  581. package/dist/views/header-footer/RegionTab.js.map +1 -0
  582. package/dist/views/header-footer/chrome-form.d.ts +137 -0
  583. package/dist/views/header-footer/chrome-form.d.ts.map +1 -0
  584. package/dist/views/header-footer/chrome-form.js +518 -0
  585. package/dist/views/header-footer/chrome-form.js.map +1 -0
  586. package/dist/views/header-footer/code-samples.d.ts +30 -0
  587. package/dist/views/header-footer/code-samples.d.ts.map +1 -0
  588. package/dist/views/header-footer/code-samples.js +127 -0
  589. package/dist/views/header-footer/code-samples.js.map +1 -0
  590. package/dist/views/header-footer/controls.d.ts +57 -0
  591. package/dist/views/header-footer/controls.d.ts.map +1 -0
  592. package/dist/views/header-footer/controls.js +43 -0
  593. package/dist/views/header-footer/controls.js.map +1 -0
  594. package/dist/views/lazy-views.d.ts +4 -0
  595. package/dist/views/lazy-views.d.ts.map +1 -1
  596. package/dist/views/lazy-views.js +4 -0
  597. package/dist/views/lazy-views.js.map +1 -1
  598. package/dist/views/page-editor/PageTab.js +1 -1
  599. package/dist/views/page-editor/PageTab.js.map +1 -1
  600. package/dist/views/page-editor/ReviewBar.js +1 -1
  601. package/dist/views/page-editor/ReviewBar.js.map +1 -1
  602. package/dist/views/page-editor/VersionHistoryPanel.js +1 -1
  603. package/dist/views/page-editor/VersionHistoryPanel.js.map +1 -1
  604. package/dist/views/seo/AuditTab.js +1 -1
  605. package/dist/views/seo/AuditTab.js.map +1 -1
  606. package/dist/views/seo/OverviewTab.d.ts.map +1 -1
  607. package/dist/views/seo/OverviewTab.js +2 -2
  608. package/dist/views/seo/OverviewTab.js.map +1 -1
  609. package/dist/views/seo/ProposalsTab.js +1 -1
  610. package/dist/views/seo/ProposalsTab.js.map +1 -1
  611. package/dist/views/settings/AISettingsTab.d.ts.map +1 -1
  612. package/dist/views/settings/AISettingsTab.js +4 -2
  613. package/dist/views/settings/AISettingsTab.js.map +1 -1
  614. package/dist/views/settings/AccessProtectionCard.js +1 -1
  615. package/dist/views/settings/AccessProtectionCard.js.map +1 -1
  616. package/dist/views/settings/AdminThemeCard.d.ts.map +1 -1
  617. package/dist/views/settings/AdminThemeCard.js +63 -15
  618. package/dist/views/settings/AdminThemeCard.js.map +1 -1
  619. package/dist/views/settings/AppearanceTab.d.ts.map +1 -1
  620. package/dist/views/settings/AppearanceTab.js +4 -2
  621. package/dist/views/settings/AppearanceTab.js.map +1 -1
  622. package/dist/views/settings/BrandPreviewCard.js +1 -1
  623. package/dist/views/settings/BrandPreviewCard.js.map +1 -1
  624. package/dist/views/settings/BrandVoiceCard.js +1 -1
  625. package/dist/views/settings/BrandVoiceCard.js.map +1 -1
  626. package/dist/views/settings/EmailSettingsTab.d.ts.map +1 -1
  627. package/dist/views/settings/EmailSettingsTab.js +5 -3
  628. package/dist/views/settings/EmailSettingsTab.js.map +1 -1
  629. package/dist/views/settings/GeneralSettingsTab.d.ts.map +1 -1
  630. package/dist/views/settings/GeneralSettingsTab.js +4 -2
  631. package/dist/views/settings/GeneralSettingsTab.js.map +1 -1
  632. package/dist/views/settings/ProviderConnectionCard.js +1 -1
  633. package/dist/views/settings/ProviderConnectionCard.js.map +1 -1
  634. package/dist/views/settings/SearchDataCard.js +1 -1
  635. package/dist/views/settings/SearchDataCard.js.map +1 -1
  636. package/dist/views/settings/SecurityStatusCard.js +2 -2
  637. package/dist/views/settings/SecurityStatusCard.js.map +1 -1
  638. package/dist/views/settings/SecurityTab.d.ts.map +1 -1
  639. package/dist/views/settings/SecurityTab.js +4 -2
  640. package/dist/views/settings/SecurityTab.js.map +1 -1
  641. package/dist/views/settings/SeoAutopilotCard.js +2 -2
  642. package/dist/views/settings/SeoAutopilotCard.js.map +1 -1
  643. package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
  644. package/dist/views/settings/SeoSettingsTab.js +5 -3
  645. package/dist/views/settings/SeoSettingsTab.js.map +1 -1
  646. package/dist/views/settings/SiteIndexStatusCard.js +1 -1
  647. package/dist/views/settings/SiteIndexStatusCard.js.map +1 -1
  648. package/dist/views/settings/SiteThemeCard.js +1 -1
  649. package/dist/views/settings/UpdatesTab.d.ts.map +1 -1
  650. package/dist/views/settings/UpdatesTab.js +6 -5
  651. package/dist/views/settings/UpdatesTab.js.map +1 -1
  652. package/dist/views/settings/components.d.ts +7 -11
  653. package/dist/views/settings/components.d.ts.map +1 -1
  654. package/dist/views/settings/components.js +18 -12
  655. package/dist/views/settings/components.js.map +1 -1
  656. package/dist/views/settings/useAppearanceSettings.d.ts +7 -1
  657. package/dist/views/settings/useAppearanceSettings.d.ts.map +1 -1
  658. package/dist/views/settings/useAppearanceSettings.js +89 -22
  659. package/dist/views/settings/useAppearanceSettings.js.map +1 -1
  660. package/dist/views/users/UserDetailDrawer.js +1 -1
  661. package/dist/views/users/UserDetailDrawer.js.map +1 -1
  662. package/package.json +10 -8
  663. package/src/AdminRoot.tsx +103 -31
  664. package/src/LoginRoot.tsx +2 -1
  665. package/src/__tests__/components/author-avatar.test.tsx +3 -20
  666. package/src/__tests__/components/notification-bell.test.tsx +146 -14
  667. package/src/__tests__/components/responsive-tiers.render.test.tsx +461 -0
  668. package/src/__tests__/components/serp-preview.render.test.tsx +6 -3
  669. package/src/__tests__/components/shell-primitives.render.test.tsx +951 -0
  670. package/src/__tests__/components/theme-provider-accent.test.tsx +83 -8
  671. package/src/__tests__/components/toggle.test.tsx +11 -3
  672. package/src/__tests__/components/ui-primitives.test.tsx +5 -8
  673. package/src/__tests__/layout/build-nav-items.test.ts +1 -1
  674. package/src/__tests__/layout/shell-nav.render.test.tsx +983 -0
  675. package/src/__tests__/layout/sidebar-forms-submenu.render.test.tsx +8 -4
  676. package/src/__tests__/layout/sidebar-groups.render.test.tsx +116 -0
  677. package/src/__tests__/layout/sidebar-menus.render.test.tsx +116 -0
  678. package/src/__tests__/layout/sidebar-plugin-nav.test.ts +1 -1
  679. package/src/__tests__/layout/sidebar-posts.test.ts +48 -40
  680. package/src/__tests__/layout/sidebar-submenu.render.test.tsx +35 -10
  681. package/src/__tests__/layout/topbar.render.test.tsx +464 -0
  682. package/src/__tests__/lib/api.test.ts +2 -0
  683. package/src/__tests__/lib/batch-wizard.test.ts +199 -0
  684. package/src/__tests__/lib/job-drawer.test.ts +495 -0
  685. package/src/__tests__/lib/job-view.test.ts +558 -0
  686. package/src/__tests__/lib/jobs-service.test.ts +167 -0
  687. package/src/__tests__/router/admin-route-order.test.ts +39 -0
  688. package/src/__tests__/styles/ai-gradient-contrast.test.ts +135 -0
  689. package/src/__tests__/styles/breakpoint-order.test.ts +68 -0
  690. package/src/__tests__/styles/responsive-layer.test.ts +367 -12
  691. package/src/__tests__/styles/tailwind-preset.test.ts +154 -0
  692. package/src/__tests__/styles/theme-token-scope.test.ts +10 -3
  693. package/src/__tests__/styles/tiers.test.ts +136 -0
  694. package/src/__tests__/styles/tokens-drift.test.ts +67 -6
  695. package/src/__tests__/views/admin-lazy-route.render.test.tsx +41 -1
  696. package/src/__tests__/views/ai-settings.render.test.tsx +4 -4
  697. package/src/__tests__/views/appearance-chrome.render.test.tsx +818 -0
  698. package/src/__tests__/views/appearance-menus-route.render.test.tsx +58 -0
  699. package/src/__tests__/views/appearance-settings.render.test.tsx +10 -8
  700. package/src/__tests__/views/appearance-theme-provider.render.test.tsx +347 -0
  701. package/src/__tests__/views/dashboard.test.tsx +48 -0
  702. package/src/__tests__/views/document-edit-navigation.render.test.tsx +130 -0
  703. package/src/__tests__/views/document-edit-routes.render.test.tsx +130 -0
  704. package/src/__tests__/views/email-settings.render.test.tsx +2 -2
  705. package/src/__tests__/views/form-entries.render.test.tsx +22 -0
  706. package/src/__tests__/views/form-submissions.render.test.tsx +22 -0
  707. package/src/__tests__/views/forms-list.render.test.tsx +13 -0
  708. package/src/__tests__/views/general-settings.render.test.tsx +38 -11
  709. package/src/__tests__/views/header-footer-form.test.ts +333 -0
  710. package/src/__tests__/views/jobs-drawer.render.test.tsx +1553 -0
  711. package/src/__tests__/views/jobs-list.render.test.tsx +853 -0
  712. package/src/__tests__/views/jobs-wizard.render.test.tsx +707 -0
  713. package/src/__tests__/views/kpi-grid.test.ts +29 -0
  714. package/src/__tests__/views/post-section-editor.test.tsx +5 -1
  715. package/src/__tests__/views/script-tags.render.test.tsx +29 -0
  716. package/src/__tests__/views/security-settings.render.test.tsx +9 -10
  717. package/src/__tests__/views/seo-settings.render.test.tsx +7 -9
  718. package/src/__tests__/views/settings-save-bar.render.test.tsx +91 -0
  719. package/src/__tests__/views/settings-save-footer.render.test.tsx +25 -15
  720. package/src/__tests__/views/settings-tabs.render.test.tsx +371 -0
  721. package/src/__tests__/views/updates-tab.render.test.tsx +4 -3
  722. package/src/components/Breadcrumbs.tsx +7 -1
  723. package/src/components/CommandPalette.tsx +51 -3
  724. package/src/components/LocaleSwitcher.tsx +5 -6
  725. package/src/components/NotificationBell.tsx +74 -18
  726. package/src/components/OfflineStatus.tsx +1 -1
  727. package/src/components/PresenceIndicator.tsx +2 -1
  728. package/src/components/ReflowMeter.tsx +3 -3
  729. package/src/components/SEOPanel.tsx +2 -2
  730. package/src/components/ThemeProvider.tsx +122 -42
  731. package/src/components/TipTapEditor.tsx +3 -4
  732. package/src/components/forms/EmbedPanel.tsx +1 -1
  733. package/src/components/forms/FormEntryDetailPane.tsx +7 -1
  734. package/src/components/forms/ShortcodeGlossary.tsx +7 -1
  735. package/src/components/forms/primitives.tsx +37 -21
  736. package/src/components/pages/tag-badges.tsx +4 -3
  737. package/src/components/seo/PageAuditPanel.tsx +2 -2
  738. package/src/components/seo/SerpPreview.tsx +5 -5
  739. package/src/components/seo/primitives.tsx +2 -2
  740. package/src/components/ui/AuthorAvatar.tsx +11 -10
  741. package/src/components/ui/Badge.tsx +1 -1
  742. package/src/components/ui/Breadcrumb.tsx +109 -0
  743. package/src/components/ui/Button.tsx +40 -15
  744. package/src/components/ui/Card.tsx +35 -32
  745. package/src/components/ui/CardListControls.tsx +90 -0
  746. package/src/components/ui/CountChip.tsx +90 -0
  747. package/src/components/ui/FilterPill.tsx +5 -3
  748. package/src/components/ui/InspectorPane.tsx +21 -7
  749. package/src/components/ui/Modal.tsx +20 -6
  750. package/src/components/ui/PageHeader.tsx +63 -0
  751. package/src/components/ui/RowActions.tsx +7 -2
  752. package/src/components/ui/Segmented.tsx +97 -0
  753. package/src/components/ui/Toaster.tsx +139 -0
  754. package/src/components/ui/Toggle.tsx +18 -14
  755. package/src/components/ui/UnderlineTabs.tsx +373 -0
  756. package/src/components/ui/UnsavedBar.tsx +119 -0
  757. package/src/components/ui/admin-links.tsx +20 -0
  758. package/src/components/ui/card-table.ts +34 -0
  759. package/src/components/virtual-window.tsx +13 -2
  760. package/src/fields/RelationshipField.tsx +1 -1
  761. package/src/hooks/useAppearanceBootstrap.ts +9 -3
  762. package/src/index.ts +19 -0
  763. package/src/layout/Header.tsx +210 -135
  764. package/src/layout/Layout.tsx +61 -108
  765. package/src/layout/MobileTabBar.tsx +92 -0
  766. package/src/layout/Sidebar.tsx +555 -593
  767. package/src/layout/nav-config.tsx +425 -0
  768. package/src/layout/primitives/AdminShell.tsx +154 -6
  769. package/src/layout/topbar-classes.ts +8 -0
  770. package/src/layout/use-nav-items.ts +40 -0
  771. package/src/lib/api.ts +18 -1
  772. package/src/lib/appearance-api.ts +4 -0
  773. package/src/lib/batch-wizard.ts +188 -0
  774. package/src/lib/chrome-service.ts +200 -0
  775. package/src/lib/job-drawer.ts +424 -0
  776. package/src/lib/job-view.ts +550 -0
  777. package/src/lib/jobs-service.ts +345 -0
  778. package/src/lib/sheet-focus.ts +71 -0
  779. package/src/lib/tiers.ts +43 -0
  780. package/src/router/admin-routes.ts +8 -0
  781. package/src/router/index.ts +9 -1
  782. package/src/styles/prenta-tokens.css +280 -82
  783. package/src/styles/responsive.css +460 -22
  784. package/src/styles/theme.css +192 -28
  785. package/src/views/ApiKeys.tsx +2 -2
  786. package/src/views/AppearanceChrome.tsx +732 -0
  787. package/src/views/CollectionList.tsx +264 -205
  788. package/src/views/Dashboard.tsx +26 -13
  789. package/src/views/DocumentEdit.tsx +111 -56
  790. package/src/views/FormDeliveries.tsx +30 -17
  791. package/src/views/FormEntries.tsx +28 -20
  792. package/src/views/FormSubmissions.tsx +32 -34
  793. package/src/views/Forms.tsx +55 -34
  794. package/src/views/FormsSettings.tsx +94 -0
  795. package/src/views/Jobs/BusyButton.tsx +56 -0
  796. package/src/views/Jobs/CancelJobDialog.tsx +145 -0
  797. package/src/views/Jobs/DrawerActivity.tsx +142 -0
  798. package/src/views/Jobs/DrawerOverview.tsx +139 -0
  799. package/src/views/Jobs/DrawerResults.tsx +155 -0
  800. package/src/views/Jobs/DrawerReview.tsx +464 -0
  801. package/src/views/Jobs/JobDrawer.tsx +636 -0
  802. package/src/views/Jobs/JobsTable.tsx +267 -0
  803. package/src/views/Jobs/NewBatchJob.tsx +857 -0
  804. package/src/views/Jobs/WorkerStrip.tsx +149 -0
  805. package/src/views/Jobs/atoms.tsx +93 -0
  806. package/src/views/Jobs/focus.ts +67 -0
  807. package/src/views/Jobs/icons.tsx +111 -0
  808. package/src/views/Jobs/useJobDetail.ts +64 -0
  809. package/src/views/Jobs/useJobsFeed.ts +249 -0
  810. package/src/views/Jobs/useReviewItems.ts +187 -0
  811. package/src/views/Jobs.tsx +396 -105
  812. package/src/views/MediaBrowser.tsx +71 -21
  813. package/src/views/Pages/PagesListView.tsx +115 -39
  814. package/src/views/Posts/NewPostTypeInstructions.tsx +15 -10
  815. package/src/views/Posts/PostTypesView.tsx +28 -6
  816. package/src/views/Posts/PostsListView.tsx +130 -40
  817. package/src/views/SEO.tsx +23 -8
  818. package/src/views/ScriptTags.tsx +233 -153
  819. package/src/views/Settings.tsx +215 -256
  820. package/src/views/SpamFolder.tsx +32 -0
  821. package/src/views/Trash.tsx +12 -1
  822. package/src/views/Users.tsx +2 -2
  823. package/src/views/WidgetsPlaceholder.tsx +33 -0
  824. package/src/views/dashboard/kpi-grid.ts +38 -0
  825. package/src/views/header-footer/AnnouncementTab.tsx +231 -0
  826. package/src/views/header-footer/ChromeField.tsx +358 -0
  827. package/src/views/header-footer/ChromePreview.tsx +497 -0
  828. package/src/views/header-footer/DeveloperTab.tsx +331 -0
  829. package/src/views/header-footer/RegionTab.tsx +398 -0
  830. package/src/views/header-footer/chrome-form.ts +603 -0
  831. package/src/views/header-footer/code-samples.ts +146 -0
  832. package/src/views/header-footer/controls.tsx +166 -0
  833. package/src/views/lazy-views.tsx +12 -0
  834. package/src/views/page-editor/PageTab.tsx +2 -2
  835. package/src/views/page-editor/ReviewBar.tsx +1 -1
  836. package/src/views/page-editor/VersionHistoryPanel.tsx +1 -1
  837. package/src/views/seo/AuditTab.tsx +1 -1
  838. package/src/views/seo/OverviewTab.tsx +6 -5
  839. package/src/views/seo/ProposalsTab.tsx +1 -1
  840. package/src/views/settings/AISettingsTab.tsx +10 -6
  841. package/src/views/settings/AccessProtectionCard.tsx +1 -1
  842. package/src/views/settings/AdminThemeCard.tsx +228 -35
  843. package/src/views/settings/AppearanceTab.tsx +10 -6
  844. package/src/views/settings/BrandPreviewCard.tsx +1 -1
  845. package/src/views/settings/BrandVoiceCard.tsx +1 -1
  846. package/src/views/settings/EmailSettingsTab.tsx +11 -7
  847. package/src/views/settings/GeneralSettingsTab.tsx +10 -6
  848. package/src/views/settings/ProviderConnectionCard.tsx +1 -1
  849. package/src/views/settings/SearchDataCard.tsx +1 -1
  850. package/src/views/settings/SecurityStatusCard.tsx +2 -2
  851. package/src/views/settings/SecurityTab.tsx +10 -6
  852. package/src/views/settings/SeoAutopilotCard.tsx +2 -2
  853. package/src/views/settings/SeoSettingsTab.tsx +13 -9
  854. package/src/views/settings/SiteIndexStatusCard.tsx +1 -1
  855. package/src/views/settings/SiteThemeCard.tsx +1 -1
  856. package/src/views/settings/UpdatesTab.tsx +107 -101
  857. package/src/views/settings/components.tsx +26 -62
  858. package/src/views/settings/useAppearanceSettings.ts +116 -25
  859. package/src/views/users/UserDetailDrawer.tsx +1 -1
  860. package/dist/components/ui/StatPill.d.ts +0 -18
  861. package/dist/components/ui/StatPill.d.ts.map +0 -1
  862. package/dist/components/ui/StatPill.js +0 -5
  863. package/dist/components/ui/StatPill.js.map +0 -1
  864. package/src/components/ui/StatPill.tsx +0 -37
@@ -0,0 +1,853 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
+ import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
4
+ import { StrictMode } from 'react'
5
+ import type { Job, JobWorkerStatus } from '@prenta/core'
6
+
7
+ /*
8
+ * The Jobs list (P-17): kit copy and states, client-side filters and counts,
9
+ * the Live refresh (cadence, backoff, timeout, clock offset), a stable
10
+ * progress cell, the empty and error states, and focus around the list's
11
+ * actions. The refresh's GETs are answered by the `fetch` stub, the actions'
12
+ * POSTs by the `cmsApi` mock, both from `server` below.
13
+ */
14
+
15
+ const MIN = 60_000
16
+
17
+ interface Reply {
18
+ data?: unknown
19
+ error?: string
20
+ status: number
21
+ }
22
+
23
+ interface Server {
24
+ jobs: Job[]
25
+ worker: JobWorkerStatus
26
+ /** `GET /jobs` fails with this status and message. */
27
+ listError: { status: number; message: string; retryAfter?: string } | null
28
+ /** `GET /jobs` never answers until its request is aborted. */
29
+ listHangs: boolean
30
+ /** `GET /jobs/worker` fails with this message. */
31
+ workerError: string | null
32
+ /** The server's clock minus the browser's, for the `Date` header. */
33
+ clockOffsetMs: number
34
+ calls: Array<{ endpoint: string; method: string }>
35
+ post: (endpoint: string) => Reply | Promise<Reply>
36
+ }
37
+
38
+ const server: Server = {
39
+ jobs: [],
40
+ // Both are set by resetServer() before each test.
41
+ worker: {} as JobWorkerStatus,
42
+ listError: null,
43
+ listHangs: false,
44
+ workerError: null,
45
+ clockOffsetMs: 0,
46
+ calls: [],
47
+ post: () => ({ error: 'not stubbed', status: 500 }),
48
+ }
49
+
50
+ vi.mock('../../lib/api.js', () => ({
51
+ getApiBase: () => '/api/cms',
52
+ cmsApi: vi.fn(async (endpoint: string, init: RequestInit = {}) => {
53
+ server.calls.push({ endpoint, method: (init.method ?? 'GET').toUpperCase() })
54
+ return server.post(endpoint)
55
+ }),
56
+ }))
57
+
58
+ const toast = vi.hoisted(() => Object.assign(vi.fn(), { error: vi.fn() }))
59
+ vi.mock('sonner', () => ({ toast }))
60
+
61
+ /** The parts of a `Response` the Jobs service reads. */
62
+ function reply(status: number, body: unknown, headers: Record<string, string> = {}) {
63
+ const all: Record<string, string> = {
64
+ date: new Date(Date.now() + server.clockOffsetMs).toUTCString(),
65
+ ...headers,
66
+ }
67
+ return {
68
+ ok: status >= 200 && status < 300,
69
+ status,
70
+ headers: { get: (name: string) => all[name.toLowerCase()] ?? null },
71
+ json: async () => body,
72
+ }
73
+ }
74
+
75
+ function fetchStub(url: string, init: RequestInit = {}) {
76
+ const endpoint = url.replace(/^\/api\/cms/, '')
77
+ server.calls.push({ endpoint, method: 'GET' })
78
+ if (endpoint === '/jobs?limit=50') {
79
+ if (server.listHangs) {
80
+ return new Promise((_, reject) => {
81
+ init.signal?.addEventListener('abort', () =>
82
+ reject(new DOMException('This operation was aborted', 'AbortError')),
83
+ )
84
+ })
85
+ }
86
+ const failure = server.listError
87
+ if (failure) {
88
+ const headers: Record<string, string> = failure.retryAfter
89
+ ? { 'retry-after': failure.retryAfter }
90
+ : {}
91
+ return Promise.resolve(reply(failure.status, { error: failure.message }, headers))
92
+ }
93
+ return Promise.resolve(reply(200, { data: { jobs: server.jobs } }))
94
+ }
95
+ if (endpoint === '/jobs/worker') {
96
+ return Promise.resolve(
97
+ server.workerError
98
+ ? reply(500, { error: server.workerError })
99
+ : reply(200, { data: server.worker }),
100
+ )
101
+ }
102
+ return Promise.resolve(reply(404, { error: `unexpected ${endpoint}` }))
103
+ }
104
+
105
+ const { Jobs } = await import('../../views/Jobs.js')
106
+
107
+ /** Someone who may run, cancel and retry any job. */
108
+ const ADMIN = { role: 'ADMIN', userId: 'u1' }
109
+
110
+ /** A time on the server's clock, `offsetMs` from now. */
111
+ function iso(offsetMs: number): string {
112
+ return new Date(Date.now() + server.clockOffsetMs + offsetMs).toISOString()
113
+ }
114
+
115
+ function workerStatus(): JobWorkerStatus {
116
+ return {
117
+ lastTickAt: iso(-24_000),
118
+ lastTickMs: 1800,
119
+ nextTickAt: iso(36_000),
120
+ budgetMs: 20_000,
121
+ schedules: [
122
+ { type: 'seo-scan', cron: '0 4 * * 1', nextAt: iso(7 * 24 * 60 * MIN), activeJobId: null },
123
+ { type: 'content-health', cron: '30 2 * * *', nextAt: iso(12 * 60 * MIN), activeJobId: null },
124
+ ],
125
+ }
126
+ }
127
+
128
+ function batch(overrides: Partial<Job> & { done?: number; total?: number }): Job {
129
+ const { done = 0, total = 42, ...rest } = overrides
130
+ return {
131
+ id: 'job',
132
+ type: 'batch-ai',
133
+ status: 'queued',
134
+ title: 'Batch',
135
+ scope: '42 posts',
136
+ runAt: iso(0),
137
+ attempts: 0,
138
+ maxAttempts: 3,
139
+ progress: { done, total, unit: 'documents', cursor: null, completed: done },
140
+ lastError: null,
141
+ explain: null,
142
+ summary: null,
143
+ lockedAt: null,
144
+ cancelRequestedAt: null,
145
+ cancelledBy: null,
146
+ createdBy: { id: 'u1', name: 'Brandon West', initials: 'BW' },
147
+ createdAt: iso(-19 * MIN),
148
+ updatedAt: iso(0),
149
+ finishedAt: null,
150
+ usage: { tokensIn: 0, tokensOut: 0 },
151
+ payload: {
152
+ operation: 'meta',
153
+ instruction: null,
154
+ source: 'posts',
155
+ statusFilter: 'all',
156
+ documentIds: [],
157
+ review: 'review',
158
+ onApprove: 'draft',
159
+ },
160
+ review: { pending: 0, approved: 0, rejected: 0 },
161
+ ...rest,
162
+ }
163
+ }
164
+
165
+ function scan(type: 'seo-scan' | 'content-health', overrides: Partial<Job>): Job {
166
+ const job = batch({
167
+ type,
168
+ title: type === 'seo-scan' ? 'SEO scan' : 'Content health',
169
+ scope: type === 'seo-scan' ? 'All indexable pages' : 'All published documents',
170
+ createdBy: { kind: 'cron', schedule: type === 'seo-scan' ? 'weekly-seo' : 'nightly-health' },
171
+ payload: { trigger: 'cron' },
172
+ ...overrides,
173
+ })
174
+ delete job.review
175
+ return job
176
+ }
177
+
178
+ /** The kit's nine jobs (jobs-data.jsx `JOBS`), as the API sends them. */
179
+ function kitJobs(): Job[] {
180
+ return [
181
+ batch({
182
+ id: 'job_418',
183
+ title: 'Generate alt text',
184
+ scope: '64 images with no alt text',
185
+ done: 9,
186
+ total: 64,
187
+ attempts: 1,
188
+ lastError: 'ImageFetchError: img_2291.webp returned 404',
189
+ runAt: iso(30_000),
190
+ createdBy: { id: 'u2', name: 'Dana Cole', initials: 'DC' },
191
+ createdAt: iso(-34 * MIN),
192
+ review: { pending: 9, approved: 0, rejected: 0 },
193
+ }),
194
+ batch({
195
+ id: 'job_417',
196
+ title: 'Rewrite meta descriptions',
197
+ status: 'running',
198
+ done: 18,
199
+ total: 42,
200
+ attempts: 1,
201
+ lockedAt: iso(-5_000),
202
+ review: { pending: 6, approved: 12, rejected: 0 },
203
+ }),
204
+ scan('content-health', {
205
+ id: 'job_416',
206
+ status: 'succeeded',
207
+ progress: { done: 412, total: 412, unit: 'documents', cursor: null, completed: 412 },
208
+ createdAt: iso(-60 * MIN),
209
+ finishedAt: iso(-43 * MIN),
210
+ }),
211
+ batch({
212
+ id: 'job_415',
213
+ title: 'Translate excerpts to Spanish',
214
+ scope: '30 case studies',
215
+ status: 'dead',
216
+ done: 7,
217
+ total: 30,
218
+ attempts: 3,
219
+ lastError: 'ProviderError: 429 Too Many Requests',
220
+ createdBy: { id: 'u3', name: 'Maya Shah', initials: 'MS' },
221
+ review: { pending: 7, approved: 0, rejected: 0 },
222
+ }),
223
+ scan('seo-scan', {
224
+ id: 'job_414',
225
+ status: 'succeeded',
226
+ progress: { done: 340, total: 340, unit: 'pages', cursor: null, completed: 340 },
227
+ createdAt: iso(-180 * MIN),
228
+ finishedAt: iso(-68 * MIN),
229
+ }),
230
+ batch({
231
+ id: 'job_413',
232
+ title: 'Add internal links',
233
+ status: 'succeeded',
234
+ done: 7,
235
+ total: 7,
236
+ finishedAt: iso(-60 * MIN),
237
+ createdAt: iso(-68 * MIN),
238
+ review: { pending: 0, approved: 6, rejected: 1 },
239
+ }),
240
+ batch({
241
+ id: 'job_412',
242
+ title: 'Shorten long titles',
243
+ status: 'cancelled',
244
+ done: 12,
245
+ total: 30,
246
+ cancelledBy: { id: 'u2', name: 'Dana Cole', initials: 'DC' },
247
+ }),
248
+ scan('content-health', { id: 'job_411', status: 'succeeded', attempts: 2 }),
249
+ scan('content-health', { id: 'job_410', status: 'succeeded' }),
250
+ ]
251
+ }
252
+
253
+ let visibility: DocumentVisibilityState = 'visible'
254
+
255
+ function resetServer() {
256
+ server.clockOffsetMs = 0
257
+ server.jobs = kitJobs()
258
+ server.worker = workerStatus()
259
+ server.listError = null
260
+ server.listHangs = false
261
+ server.workerError = null
262
+ server.calls = []
263
+ server.post = () => ({ error: 'not stubbed', status: 500 })
264
+ }
265
+
266
+ beforeEach(() => {
267
+ resetServer()
268
+ vi.stubGlobal('fetch', vi.fn(fetchStub))
269
+ toast.mockClear()
270
+ toast.error.mockClear()
271
+ visibility = 'visible'
272
+ Object.defineProperty(document, 'visibilityState', {
273
+ configurable: true,
274
+ get: () => visibility,
275
+ })
276
+ })
277
+
278
+ afterEach(() => {
279
+ cleanup()
280
+ vi.useRealTimers()
281
+ vi.unstubAllGlobals()
282
+ })
283
+
284
+ const count = (endpoint: string) =>
285
+ server.calls.filter((call) => call.endpoint === endpoint && call.method === 'GET').length
286
+ const listCalls = () => count('/jobs?limit=50')
287
+ const workerCalls = () => count('/jobs/worker')
288
+
289
+ /** Fake every timer and the clock, from a whole second, and rebuild the fixtures on it. */
290
+ function useClock() {
291
+ vi.useFakeTimers({ now: new Date('2026-10-19T14:00:00.000Z') })
292
+ const offset = server.clockOffsetMs
293
+ resetServer()
294
+ server.clockOffsetMs = offset
295
+ server.jobs = kitJobs()
296
+ server.worker = workerStatus()
297
+ }
298
+
299
+ /** Run timers (and the promises they start) up to `ms` from now. */
300
+ async function advance(ms: number) {
301
+ await act(async () => {
302
+ await vi.advanceTimersByTimeAsync(ms)
303
+ })
304
+ }
305
+
306
+ async function renderLoaded() {
307
+ render(<Jobs onNavigate={() => {}} {...ADMIN} />)
308
+ await screen.findByRole('list', { name: 'Jobs' })
309
+ }
310
+
311
+ /** With fake timers: render and let the first load land. */
312
+ async function renderOnClock() {
313
+ render(<Jobs onNavigate={() => {}} {...ADMIN} />)
314
+ await advance(0)
315
+ expect(screen.getByRole('list', { name: 'Jobs' })).toBeTruthy()
316
+ }
317
+
318
+ function rowFor(title: string): HTMLElement {
319
+ const item = screen.getByRole('button', { name: title }).closest('li')
320
+ if (!item) throw new Error(`no row for ${title}`)
321
+ return item
322
+ }
323
+
324
+ function note(): string {
325
+ return screen.getByRole('status').textContent ?? ''
326
+ }
327
+
328
+ function setVisibility(next: DocumentVisibilityState) {
329
+ visibility = next
330
+ act(() => {
331
+ document.dispatchEvent(new Event('visibilitychange'))
332
+ })
333
+ }
334
+
335
+ describe('Jobs list', () => {
336
+ it('shows the kit’s header, worker strip, alerts and filter counts', async () => {
337
+ await renderLoaded()
338
+ expect(screen.getByRole('heading', { level: 1, name: 'Jobs' })).toBeTruthy()
339
+ expect(screen.getByRole('button', { name: 'Live', pressed: true })).toBeTruthy()
340
+ expect(screen.getByRole('button', { name: 'New batch AI job' })).toBeTruthy()
341
+ expect(screen.getByText(/^Ran 2\ds ago/)).toBeTruthy()
342
+ expect(screen.getByText('Mon 04:00 UTC', { exact: false })).toBeTruthy()
343
+ expect(screen.getByText('Daily 02:30 UTC', { exact: false })).toBeTruthy()
344
+ expect(screen.queryAllByText('· running now')).toHaveLength(0)
345
+
346
+ const failed = screen.getByRole('alert')
347
+ expect(failed.textContent).toBe(
348
+ '1 job failed after 3 attempts. Translate excerpts to Spanish: ProviderError: 429 Too Many Requests. Finished work is kept.See whyRetry',
349
+ )
350
+ expect(
351
+ screen.getByText(
352
+ (_, el) =>
353
+ el?.tagName === 'SPAN' &&
354
+ /^15 AI changes are waiting for review across 2 jobs\./.test(el.textContent ?? ''),
355
+ ),
356
+ ).toBeTruthy()
357
+
358
+ const tabs = within(screen.getByRole('tablist', { name: 'Filter jobs' }))
359
+ .getAllByRole('tab')
360
+ .map((tab) => tab.textContent)
361
+ expect(tabs).toEqual([
362
+ 'All9',
363
+ 'Active2',
364
+ 'Needs review3',
365
+ 'Succeeded5',
366
+ 'Failed1',
367
+ 'Cancelled1',
368
+ ])
369
+ expect(note()).toBe('Latest 50 jobs · updates every 5 seconds')
370
+
371
+ const running = rowFor('Rewrite meta descriptions')
372
+ expect(within(running).getByText('Running')).toBeTruthy()
373
+ expect(within(running).getByText('Slice 19 · working')).toBeTruthy()
374
+ expect(within(running).getByText('18 / 42 documents')).toBeTruthy()
375
+ expect(
376
+ within(running).getByRole('button', { name: 'Review: Rewrite meta descriptions' }),
377
+ ).toBeTruthy()
378
+ expect(
379
+ within(running).getByRole('button', { name: 'Cancel Rewrite meta descriptions' }),
380
+ ).toBeTruthy()
381
+
382
+ const retrying = rowFor('Generate alt text')
383
+ expect(within(retrying).getByText('Retrying')).toBeTruthy()
384
+ expect(within(retrying).getByText(/^Attempt 2 of 3 · in \d+s$/)).toBeTruthy()
385
+ expect(within(retrying).getByText('Dana Cole')).toBeTruthy()
386
+ expect(within(retrying).getByText('34 min ago')).toBeTruthy()
387
+
388
+ expect(within(rowFor('Shorten long titles')).getByText('by Dana Cole')).toBeTruthy()
389
+ const dead = rowFor('Translate excerpts to Spanish')
390
+ expect(
391
+ within(dead).getByRole('button', { name: 'Retry Translate excerpts to Spanish' }),
392
+ ).toBeTruthy()
393
+ expect(within(dead).getByText('After 3 attempts')).toBeTruthy()
394
+ const scanRow = screen.getAllByRole('button', { name: 'SEO scan' })[0]?.closest('li')
395
+ expect(scanRow && within(scanRow).getByText('Weekly cron')).toBeTruthy()
396
+ expect(scanRow && within(scanRow).getByText('340 / 340 pages')).toBeTruthy()
397
+ })
398
+
399
+ it('filters and counts on the client, within the picked type', async () => {
400
+ await renderLoaded()
401
+ const before = listCalls()
402
+ fireEvent.click(screen.getByRole('tab', { name: /^Failed/ }))
403
+ expect(screen.getByRole('tab', { name: /^Failed/ }).getAttribute('aria-selected')).toBe('true')
404
+ let rows = within(screen.getByRole('list', { name: 'Jobs' })).getAllByRole('listitem')
405
+ expect(rows.map((row) => within(row).getAllByRole('button')[0]?.textContent)).toEqual([
406
+ 'Translate excerpts to Spanish',
407
+ ])
408
+
409
+ fireEvent.click(screen.getByRole('tab', { name: /^Needs review/ }))
410
+ rows = within(screen.getByRole('list', { name: 'Jobs' })).getAllByRole('listitem')
411
+ expect(rows).toHaveLength(3)
412
+
413
+ fireEvent.change(screen.getByRole('combobox', { name: 'Job type' }), {
414
+ target: { value: 'content-health' },
415
+ })
416
+ expect(
417
+ within(screen.getByRole('tablist', { name: 'Filter jobs' }))
418
+ .getAllByRole('tab')
419
+ .map((tab) => tab.textContent),
420
+ ).toEqual(['All3', 'Active0', 'Needs review0', 'Succeeded3', 'Failed0', 'Cancelled0'])
421
+ expect(screen.queryByRole('list', { name: 'Jobs' })).toBeNull()
422
+ expect(screen.getByText('No jobs here. Every AI change has been reviewed.')).toBeTruthy()
423
+
424
+ fireEvent.click(screen.getByRole('tab', { name: /^All/ }))
425
+ expect(
426
+ within(screen.getByRole('list', { name: 'Jobs' })).getAllByRole('listitem'),
427
+ ).toHaveLength(3)
428
+ // Filtering never asks the server.
429
+ expect(listCalls()).toBe(before)
430
+ })
431
+
432
+ it('shows the empty state', async () => {
433
+ server.jobs = []
434
+ render(<Jobs onNavigate={() => {}} {...ADMIN} />)
435
+ expect(await screen.findByText('No jobs here.')).toBeTruthy()
436
+ expect(screen.queryByRole('alert')).toBeNull()
437
+ expect(
438
+ within(screen.getByRole('tablist', { name: 'Filter jobs' }))
439
+ .getAllByRole('tab')
440
+ .map((tab) => tab.textContent),
441
+ ).toEqual(['All0', 'Active0', 'Needs review0', 'Succeeded0', 'Failed0', 'Cancelled0'])
442
+ fireEvent.click(screen.getByRole('tab', { name: /^Failed/ }))
443
+ expect(screen.getByText('No jobs here. Nothing has failed.')).toBeTruthy()
444
+ })
445
+ })
446
+
447
+ describe('Jobs list refresh', () => {
448
+ it('refreshes the list every 5 seconds while Live is on and the tab is visible', async () => {
449
+ useClock()
450
+ await renderOnClock()
451
+ expect(listCalls()).toBe(1)
452
+
453
+ await advance(5000)
454
+ expect(listCalls()).toBe(2)
455
+ await advance(4999)
456
+ expect(listCalls()).toBe(2)
457
+ await advance(1)
458
+ expect(listCalls()).toBe(3)
459
+
460
+ // Hidden: no requests, however long.
461
+ setVisibility('hidden')
462
+ await advance(30_000)
463
+ expect(listCalls()).toBe(3)
464
+ // Visible again: refreshes at once, then every 5 seconds.
465
+ setVisibility('visible')
466
+ await advance(0)
467
+ expect(listCalls()).toBe(4)
468
+ await advance(5000)
469
+ expect(listCalls()).toBe(5)
470
+
471
+ // Paused: no requests.
472
+ fireEvent.click(screen.getByRole('button', { name: 'Live' }))
473
+ expect(screen.getByRole('button', { name: 'Paused', pressed: false })).toBeTruthy()
474
+ await advance(30_000)
475
+ expect(listCalls()).toBe(5)
476
+ // Live again: refreshes at once.
477
+ fireEvent.click(screen.getByRole('button', { name: 'Paused' }))
478
+ await advance(0)
479
+ expect(listCalls()).toBe(6)
480
+ await advance(5000)
481
+ expect(listCalls()).toBe(7)
482
+ })
483
+
484
+ it('asks for the worker status after the tick it expects, not with every refresh', async () => {
485
+ useClock()
486
+ await renderOnClock()
487
+ expect([listCalls(), workerCalls()]).toEqual([1, 1])
488
+
489
+ // The next tick is due at 36s; the strip asks a few seconds after it.
490
+ await advance(40_000)
491
+ expect([listCalls(), workerCalls()]).toEqual([9, 1])
492
+ // That tick ran: its stamp is what the server now has.
493
+ server.worker = { ...server.worker, lastTickAt: iso(-4_000), nextTickAt: iso(56_000) }
494
+ await advance(5000)
495
+ expect([listCalls(), workerCalls()]).toEqual([10, 2])
496
+ expect(screen.getByText(/^Ran [4-9]s ago/)).toBeTruthy()
497
+
498
+ // Nothing until a few seconds after the next tick (96s).
499
+ await advance(55_000)
500
+ expect([listCalls(), workerCalls()]).toEqual([21, 2])
501
+ await advance(5000)
502
+ expect(workerCalls()).toBe(3)
503
+
504
+ // A late tick: asked again 30 seconds later, not every 5.
505
+ await advance(29_999)
506
+ expect(workerCalls()).toBe(3)
507
+ await advance(1)
508
+ expect(workerCalls()).toBe(4)
509
+ // 135 seconds: 28 list refreshes and 4 status requests, about 14 a minute instead of 24.
510
+ expect([listCalls(), workerCalls()]).toEqual([28, 4])
511
+ })
512
+
513
+ it('backs off while refreshes fail and returns to 5 seconds after one works', async () => {
514
+ useClock()
515
+ await renderOnClock()
516
+ server.listError = { status: 500, message: 'Database unavailable' }
517
+
518
+ const at: Array<[number, number]> = []
519
+ for (const step of [5000, 9999, 1, 19_999, 1, 39_999, 1, 59_999, 1]) {
520
+ await advance(step)
521
+ at.push([step, listCalls()])
522
+ }
523
+ // Failures at 5s, 15s, 35s, 75s and 135s: waits of 10, 20, 40, then 60.
524
+ expect(at.map(([, calls]) => calls)).toEqual([2, 2, 3, 3, 4, 4, 5, 5, 6])
525
+ expect(note()).toBe('Couldn’t refresh: Database unavailable. Showing the last update.')
526
+ expect(
527
+ within(screen.getByRole('list', { name: 'Jobs' })).getAllByRole('listitem'),
528
+ ).toHaveLength(9)
529
+
530
+ server.listError = null
531
+ await advance(60_000)
532
+ expect(listCalls()).toBe(7)
533
+ expect(note()).toBe('Latest 50 jobs · updates every 5 seconds')
534
+ await advance(5000)
535
+ expect(listCalls()).toBe(8)
536
+ })
537
+
538
+ it('waits as long as a 429 asks', async () => {
539
+ useClock()
540
+ await renderOnClock()
541
+ server.listError = { status: 429, message: 'Too many requests', retryAfter: '30' }
542
+ await advance(5000)
543
+ expect(listCalls()).toBe(2)
544
+ await advance(29_999)
545
+ expect(listCalls()).toBe(2)
546
+ server.listError = null
547
+ await advance(1)
548
+ expect(listCalls()).toBe(3)
549
+ await advance(5000)
550
+ expect(listCalls()).toBe(4)
551
+ })
552
+
553
+ it('times out a refresh that never answers and keeps refreshing', async () => {
554
+ useClock()
555
+ await renderOnClock()
556
+ server.listHangs = true
557
+ await advance(5000)
558
+ expect(listCalls()).toBe(2)
559
+ await advance(9999)
560
+ expect(note()).toBe('Latest 50 jobs · updates every 5 seconds')
561
+ await advance(1)
562
+ expect(note()).toBe(
563
+ 'Couldn’t refresh: The server didn’t answer within 10 seconds. Showing the last update.',
564
+ )
565
+ server.listHangs = false
566
+ // The timeout counts as a failure: the next refresh is 10 seconds later.
567
+ await advance(9999)
568
+ expect(listCalls()).toBe(2)
569
+ await advance(1)
570
+ expect(listCalls()).toBe(3)
571
+ expect(note()).toBe('Latest 50 jobs · updates every 5 seconds')
572
+ await advance(5000)
573
+ expect(listCalls()).toBe(4)
574
+ })
575
+
576
+ it('loads the worker status through a development remount', async () => {
577
+ // StrictMode mounts, unmounts and remounts, aborting the first requests.
578
+ render(
579
+ <StrictMode>
580
+ <Jobs onNavigate={() => {}} {...ADMIN} />
581
+ </StrictMode>,
582
+ )
583
+ expect(await screen.findByText(/^Ran 2\ds ago/)).toBeTruthy()
584
+ expect(screen.queryByText('Status unavailable')).toBeNull()
585
+ })
586
+
587
+ it('shows a worker status failure in the strip only', async () => {
588
+ server.workerError = 'Worker status unavailable'
589
+ await renderLoaded()
590
+ expect(note()).toBe('Latest 50 jobs · updates every 5 seconds')
591
+ expect(screen.getByText('Status unavailable')).toBeTruthy()
592
+ expect(screen.queryByText(/^Ran /)).toBeNull()
593
+ })
594
+
595
+ it('reads times on the server’s clock when the browser’s is wrong', async () => {
596
+ // The browser is 11 minutes fast.
597
+ server.clockOffsetMs = -11 * MIN
598
+ useClock()
599
+ server.jobs = [
600
+ batch({
601
+ id: 'job_417',
602
+ title: 'Rewrite meta descriptions',
603
+ status: 'running',
604
+ done: 18,
605
+ lockedAt: iso(-30_000),
606
+ }),
607
+ batch({ id: 'job_420', title: 'Queued just now', createdAt: iso(-20_000) }),
608
+ ]
609
+ await renderOnClock()
610
+ const row = rowFor('Rewrite meta descriptions')
611
+ expect(within(row).getByText('Running')).toBeTruthy()
612
+ expect(within(row).queryByText('Retrying')).toBeNull()
613
+ expect(within(rowFor('Queued just now')).getByText('now')).toBeTruthy()
614
+ expect(screen.getByText(/^Ran 2[45]s ago/)).toBeTruthy()
615
+ })
616
+
617
+ it('advances a running job’s progress in place', async () => {
618
+ useClock()
619
+ await renderOnClock()
620
+ const row = rowFor('Rewrite meta descriptions')
621
+ const cells = Array.from(row.children)
622
+ const cellClasses = cells.map((cell) => cell.className)
623
+ const track = within(row).getByTestId('job-progress-track')
624
+ const fill = track.firstElementChild as HTMLElement
625
+ const progress = within(row).getByText('18 / 42 documents')
626
+ expect(fill.style.width).toBe('43%')
627
+ // Fixed tracks for every column, so no cell's content can resize another.
628
+ expect(row.className).toContain(
629
+ 'grid-cols-[minmax(240px,1.6fr)_minmax(150px,.9fr)_minmax(170px,1fr)_minmax(120px,.7fr)_96px]',
630
+ )
631
+ // Digits keep one width, and the count never wraps onto a second line.
632
+ expect(progress.className).toContain('tabular-nums')
633
+ expect(progress.className).toContain('whitespace-nowrap')
634
+
635
+ server.jobs = server.jobs.map((job) =>
636
+ job.id === 'job_417'
637
+ ? { ...job, progress: { ...job.progress, done: 19, completed: 19 } }
638
+ : job,
639
+ )
640
+ await advance(5000)
641
+ expect(progress.textContent).toBe('19 / 42 documents')
642
+
643
+ // The same nodes update: nothing is remounted or restyled but the fill.
644
+ expect(rowFor('Rewrite meta descriptions')).toBe(row)
645
+ cells.forEach((cell, index) => expect(row.children[index]).toBe(cell))
646
+ expect(cells.map((cell) => cell.className)).toEqual(cellClasses)
647
+ expect(track.firstElementChild).toBe(fill)
648
+ expect(fill.style.width).toBe('45%')
649
+ expect(within(row).getByText('Slice 20 · working')).toBeTruthy()
650
+ })
651
+
652
+ it('shows a load error with Try again, then focuses the list it loads', async () => {
653
+ useClock()
654
+ server.listError = { status: 500, message: 'Database unavailable' }
655
+ render(<Jobs onNavigate={() => {}} {...ADMIN} />)
656
+ await advance(0)
657
+ const alert = screen.getByRole('alert')
658
+ expect(alert.textContent).toContain('Jobs couldn’t be loaded. Database unavailable')
659
+ expect(screen.queryByRole('list', { name: 'Jobs' })).toBeNull()
660
+
661
+ server.listError = null
662
+ const tryAgain = within(alert).getByRole('button', { name: 'Try again' })
663
+ tryAgain.focus()
664
+ fireEvent.click(tryAgain)
665
+ await advance(0)
666
+ expect(screen.getByRole('list', { name: 'Jobs' })).toBeTruthy()
667
+ // The button went with the error; focus moves to the first row, not the body.
668
+ expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Generate alt text' }))
669
+ })
670
+ })
671
+
672
+ describe('Jobs list actions', () => {
673
+ it('runs a scan now, and opens the existing job when one is active', async () => {
674
+ await renderLoaded()
675
+ let response = { job: scan('seo-scan', { id: 'job_new', createdAt: iso(0) }), reused: false }
676
+ server.post = () => {
677
+ server.jobs = [response.job, ...server.jobs.filter((job) => job.id !== response.job.id)]
678
+ return { data: response, status: response.reused ? 200 : 201 }
679
+ }
680
+
681
+ fireEvent.click(screen.getByRole('button', { name: 'Run now: SEO scan' }))
682
+ await waitFor(() =>
683
+ expect(toast).toHaveBeenCalledWith('SEO scan queued. It starts on the next worker run.'),
684
+ )
685
+ expect(server.calls).toContainEqual({ endpoint: '/jobs/run', method: 'POST' })
686
+ expect(
687
+ within(screen.getByRole('list', { name: 'Jobs' })).getAllByRole('listitem'),
688
+ ).toHaveLength(10)
689
+
690
+ response = { job: scan('seo-scan', { id: 'job_new', status: 'running' }), reused: true }
691
+ fireEvent.click(screen.getByRole('button', { name: 'Run now: SEO scan' }))
692
+ await waitFor(() =>
693
+ expect(toast).toHaveBeenCalledWith('SEO scan is already running. Reusing that job.'),
694
+ )
695
+ expect(await screen.findByRole('dialog', { name: 'SEO scan' })).toBeTruthy()
696
+ })
697
+
698
+ it('keeps focus on a busy button, with one icon, and ignores clicks until it answers', async () => {
699
+ await renderLoaded()
700
+ let answer: (reply: Reply) => void = () => {}
701
+ server.post = () => new Promise<Reply>((resolve) => (answer = resolve))
702
+ const run = screen.getByRole('button', { name: 'Run now: Content health' })
703
+ run.focus()
704
+ fireEvent.click(run)
705
+ await waitFor(() => expect(run.getAttribute('aria-busy')).toBe('true'))
706
+ // Not `disabled`: Chromium drops focus from a focused button that becomes disabled.
707
+ expect(run.hasAttribute('disabled')).toBe(false)
708
+ expect(run.getAttribute('aria-disabled')).toBe('true')
709
+ expect(document.activeElement).toBe(run)
710
+ expect(run.querySelectorAll('svg')).toHaveLength(1)
711
+ expect(within(run).getByTestId('busy-spinner')).toBeTruthy()
712
+ fireEvent.click(run)
713
+ expect(server.calls.filter((call) => call.method === 'POST')).toHaveLength(1)
714
+
715
+ answer({ error: 'Insufficient permissions', status: 403 })
716
+ await waitFor(() =>
717
+ expect(toast.error).toHaveBeenCalledWith(
718
+ 'Content health didn’t start: Insufficient permissions',
719
+ ),
720
+ )
721
+ expect(run.getAttribute('aria-disabled')).toBeNull()
722
+ expect(document.activeElement).toBe(run)
723
+
724
+ // An icon-only button swaps its icon for the spinner rather than adding one.
725
+ const retry = screen.getByRole('button', { name: 'Retry Translate excerpts to Spanish' })
726
+ fireEvent.click(retry)
727
+ await waitFor(() => expect(retry.getAttribute('aria-busy')).toBe('true'))
728
+ expect(retry.querySelectorAll('svg')).toHaveLength(1)
729
+ expect(within(retry).getByTestId('busy-spinner')).toBeTruthy()
730
+ answer({ error: 'nope', status: 409 })
731
+ await waitFor(() => expect(retry.getAttribute('aria-busy')).toBeNull())
732
+ expect(retry.querySelectorAll('svg')).toHaveLength(1)
733
+ })
734
+
735
+ it('retries a failed job and says where it picks up', async () => {
736
+ await renderLoaded()
737
+ server.post = (endpoint) => {
738
+ if (endpoint !== '/jobs/job_415/retry') return { error: 'nope', status: 500 }
739
+ server.jobs = server.jobs.map((job) =>
740
+ job.id === 'job_415' ? { ...job, status: 'queued', attempts: 0, lastError: null } : job,
741
+ )
742
+ return { data: server.jobs[3], status: 200 }
743
+ }
744
+ const retry = screen.getByRole('button', { name: 'Retry Translate excerpts to Spanish' })
745
+ retry.focus()
746
+ fireEvent.click(retry)
747
+ await waitFor(() => expect(toast).toHaveBeenCalledWith('Retrying from document 8'))
748
+ expect(within(rowFor('Translate excerpts to Spanish')).getByText('In progress')).toBeTruthy()
749
+ // The Retry button went with the failure; focus lands on the row.
750
+ await waitFor(() =>
751
+ expect(document.activeElement).toBe(
752
+ screen.getByRole('button', { name: 'Translate excerpts to Spanish' }),
753
+ ),
754
+ )
755
+ })
756
+
757
+ it('confirms a cancel with the kit’s copy and shows Cancelling', async () => {
758
+ await renderLoaded()
759
+ server.post = (endpoint) => {
760
+ if (endpoint !== '/jobs/job_417/cancel') return { error: 'nope', status: 500 }
761
+ server.jobs = server.jobs.map((job) =>
762
+ job.id === 'job_417' ? { ...job, cancelRequestedAt: iso(0) } : job,
763
+ )
764
+ return { data: server.jobs[1], status: 200 }
765
+ }
766
+ const cancel = screen.getByRole('button', { name: 'Cancel Rewrite meta descriptions' })
767
+ // Keep running leaves the job alone and returns focus to the Cancel button.
768
+ cancel.focus()
769
+ fireEvent.click(cancel)
770
+ fireEvent.click(
771
+ within(await screen.findByRole('dialog')).getByRole('button', { name: 'Keep running' }),
772
+ )
773
+ await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
774
+ await waitFor(() => expect(document.activeElement).toBe(cancel))
775
+ expect(server.calls.some((call) => call.method === 'POST')).toBe(false)
776
+
777
+ fireEvent.click(cancel)
778
+ const dialog = await screen.findByRole('dialog', {
779
+ name: 'Cancel “Rewrite meta descriptions”?',
780
+ })
781
+ expect(dialog.textContent).toContain(
782
+ 'It’s in the middle of a slice. It will stop when that slice finishes, usually within 20 seconds. The 18 changes already made stay in review. Nothing is applied unless you approve it.',
783
+ )
784
+ fireEvent.click(within(dialog).getByRole('button', { name: 'Cancel job' }))
785
+ await waitFor(() => expect(toast).toHaveBeenCalledWith('Stopping after this slice'))
786
+ await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
787
+ const row = rowFor('Rewrite meta descriptions')
788
+ expect(within(row).getByText('Cancelling')).toBeTruthy()
789
+ expect(within(row).getByText('Stops after this slice')).toBeTruthy()
790
+ expect(within(row).queryByRole('button', { name: /^Cancel/ })).toBeNull()
791
+ // The Cancel button went with the cancel, so focus lands on the row.
792
+ await waitFor(() =>
793
+ expect(document.activeElement).toBe(
794
+ screen.getByRole('button', { name: 'Rewrite meta descriptions' }),
795
+ ),
796
+ )
797
+ })
798
+
799
+ it('says a job whose worker stopped mid-slice stops on the next worker run', async () => {
800
+ server.jobs = [
801
+ batch({
802
+ id: 'job_stale',
803
+ title: 'Stuck batch',
804
+ status: 'running',
805
+ done: 4,
806
+ attempts: 1,
807
+ lockedAt: iso(-11 * MIN),
808
+ }),
809
+ ]
810
+ await renderLoaded()
811
+ const row = rowFor('Stuck batch')
812
+ expect(within(row).getByText('Retrying')).toBeTruthy()
813
+ server.post = () => {
814
+ server.jobs = server.jobs.map((job) => ({ ...job, cancelRequestedAt: iso(0) }))
815
+ return { data: server.jobs[0], status: 200 }
816
+ }
817
+ fireEvent.click(within(row).getByRole('button', { name: 'Cancel Stuck batch' }))
818
+ const dialog = await screen.findByRole('dialog')
819
+ expect(dialog.textContent).toContain(
820
+ 'Its last slice stopped without finishing. It stops on the next worker run, within a minute.',
821
+ )
822
+ expect(dialog.textContent).not.toContain('stops right away')
823
+ fireEvent.click(within(dialog).getByRole('button', { name: 'Cancel job' }))
824
+ await waitFor(() => expect(toast).toHaveBeenCalledWith('Stopping on the next worker run'))
825
+ })
826
+
827
+ it('focuses the selected filter when a cancel empties it', async () => {
828
+ server.jobs = [
829
+ batch({ id: 'job_q', title: 'Waiting batch' }),
830
+ scan('seo-scan', { id: 'job_done', status: 'succeeded' }),
831
+ ]
832
+ await renderLoaded()
833
+ fireEvent.click(screen.getByRole('tab', { name: /^Active/ }))
834
+ server.post = () => {
835
+ server.jobs = server.jobs.map((job) =>
836
+ job.id === 'job_q' ? { ...job, status: 'cancelled' } : job,
837
+ )
838
+ return { data: server.jobs[0], status: 200 }
839
+ }
840
+ const cancel = screen.getByRole('button', { name: 'Cancel Waiting batch' })
841
+ cancel.focus()
842
+ fireEvent.click(cancel)
843
+ fireEvent.click(
844
+ within(await screen.findByRole('dialog')).getByRole('button', { name: 'Cancel job' }),
845
+ )
846
+ await waitFor(() => expect(toast).toHaveBeenCalledWith('Job cancelled'))
847
+ await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
848
+ expect(screen.getByText('No jobs here.')).toBeTruthy()
849
+ await waitFor(() =>
850
+ expect(document.activeElement).toBe(screen.getByRole('tab', { name: /^Active/ })),
851
+ )
852
+ })
853
+ })