@antelopejs/dms 0.9.8 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +1 -1
  3. package/data/module-store/catalog.json +2 -2
  4. package/dist/attachments/table-view.js +2 -2
  5. package/dist/attachments/table-view.js.map +1 -1
  6. package/dist/automation/actions.d.ts +2 -0
  7. package/dist/automation/actions.js +116 -71
  8. package/dist/automation/actions.js.map +1 -1
  9. package/dist/automation/i18n.d.ts +4 -0
  10. package/dist/automation/i18n.js +19 -0
  11. package/dist/automation/i18n.js.map +1 -0
  12. package/dist/automation/send-notification.d.ts +21 -0
  13. package/dist/automation/send-notification.js +163 -0
  14. package/dist/automation/send-notification.js.map +1 -0
  15. package/dist/automation/triggers.js +19 -25
  16. package/dist/automation/triggers.js.map +1 -1
  17. package/dist/client-origin-cors.d.ts +1 -0
  18. package/dist/client-origin-cors.js +19 -0
  19. package/dist/client-origin-cors.js.map +1 -0
  20. package/dist/config.d.ts +3 -2
  21. package/dist/config.js +5 -7
  22. package/dist/config.js.map +1 -1
  23. package/dist/frontend-module.d.ts +4 -0
  24. package/dist/frontend-module.js +37 -0
  25. package/dist/frontend-module.js.map +1 -0
  26. package/dist/implementations/dms/client-ip.js +1 -1
  27. package/dist/implementations/dms/client-ip.js.map +1 -1
  28. package/dist/implementations/dms/dev-reload.d.ts +5 -0
  29. package/dist/implementations/dms/dev-reload.js +23 -3
  30. package/dist/implementations/dms/dev-reload.js.map +1 -1
  31. package/dist/implementations/dms/frontend-modules.d.ts +2 -4
  32. package/dist/implementations/dms/frontend-modules.js +38 -94
  33. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  34. package/dist/implementations/dms/frontend-ready.d.ts +12 -2
  35. package/dist/implementations/dms/frontend-ready.js +34 -4
  36. package/dist/implementations/dms/frontend-ready.js.map +1 -1
  37. package/dist/implementations/dms/layout-banners.d.ts +2 -1
  38. package/dist/implementations/dms/layout-banners.js +9 -0
  39. package/dist/implementations/dms/layout-banners.js.map +1 -1
  40. package/dist/implementations/dms/manifest.d.ts +7 -2
  41. package/dist/implementations/dms/manifest.js +12 -7
  42. package/dist/implementations/dms/manifest.js.map +1 -1
  43. package/dist/implementations/dms/page.d.ts +4 -3
  44. package/dist/implementations/dms/page.js +49 -18
  45. package/dist/implementations/dms/page.js.map +1 -1
  46. package/dist/implementations/dms/permissions-resolver.d.ts +2 -2
  47. package/dist/implementations/dms/permissions-resolver.js +2 -2
  48. package/dist/implementations/dms/permissions-resolver.js.map +1 -1
  49. package/dist/implementations/dms/realtime.js +1 -0
  50. package/dist/implementations/dms/realtime.js.map +1 -1
  51. package/dist/implementations/dms-base/bulk-selection.d.ts +1 -1
  52. package/dist/implementations/dms-base/bulk-selection.js +23 -1
  53. package/dist/implementations/dms-base/bulk-selection.js.map +1 -1
  54. package/dist/implementations/dms-base/filter-tokens.js +5 -3
  55. package/dist/implementations/dms-base/filter-tokens.js.map +1 -1
  56. package/dist/implementations/dms-base/search-route.d.ts +1 -0
  57. package/dist/implementations/dms-base/search-route.js +35 -13
  58. package/dist/implementations/dms-base/search-route.js.map +1 -1
  59. package/dist/implementations/dms-base/table-view.js +11 -6
  60. package/dist/implementations/dms-base/table-view.js.map +1 -1
  61. package/dist/implementations/dms-notifications/index.d.ts +2 -2
  62. package/dist/implementations/dms-notifications/index.js +5 -5
  63. package/dist/implementations/dms-notifications/index.js.map +1 -1
  64. package/dist/index.js +32 -55
  65. package/dist/index.js.map +1 -1
  66. package/dist/pages/modules/index.d.ts +1 -0
  67. package/dist/pages/modules/index.js +1 -0
  68. package/dist/pages/modules/index.js.map +1 -1
  69. package/dist/pages/modules/modules-store.d.ts +9 -0
  70. package/dist/pages/modules/modules-store.js +30 -0
  71. package/dist/pages/modules/modules-store.js.map +1 -0
  72. package/dist/pages/settings/users/account-credentials.js +2 -2
  73. package/dist/pages/settings/users/account-credentials.js.map +1 -1
  74. package/dist/pages/settings/users/account-data-store.js +25 -8
  75. package/dist/pages/settings/users/account-data-store.js.map +1 -1
  76. package/dist/pages/settings/users/invite-extension-routes.js +9 -3
  77. package/dist/pages/settings/users/invite-extension-routes.js.map +1 -1
  78. package/dist/pages/settings/users/member-management.js +13 -14
  79. package/dist/pages/settings/users/member-management.js.map +1 -1
  80. package/dist/pages/settings/users/profile.d.ts +1 -4
  81. package/dist/pages/settings/users/profile.js +0 -35
  82. package/dist/pages/settings/users/profile.js.map +1 -1
  83. package/dist/pages/settings/users/role-editor-store.js +26 -18
  84. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  85. package/dist/pages/settings/users/role-editor.d.ts +0 -1
  86. package/dist/pages/settings/users/role-editor.js +1 -2
  87. package/dist/pages/settings/users/role-editor.js.map +1 -1
  88. package/dist/pages/settings/users/role-preview.js +1 -2
  89. package/dist/pages/settings/users/role-preview.js.map +1 -1
  90. package/dist/pages/settings/users/roles.js +2 -2
  91. package/dist/pages/settings/users/roles.js.map +1 -1
  92. package/dist/pages/settings/users/two-factor-operations.js.map +1 -1
  93. package/dist/realtime/broker.d.ts +6 -0
  94. package/dist/realtime/broker.js +8 -1
  95. package/dist/realtime/broker.js.map +1 -1
  96. package/dist/realtime/presence.d.ts +8 -3
  97. package/dist/realtime/presence.js +8 -4
  98. package/dist/realtime/presence.js.map +1 -1
  99. package/dist/realtime/sessions.d.ts +11 -1
  100. package/dist/realtime/sessions.js +21 -3
  101. package/dist/realtime/sessions.js.map +1 -1
  102. package/dist/realtime/table-view.d.ts +2 -2
  103. package/dist/realtime/table-view.js +4 -3
  104. package/dist/realtime/table-view.js.map +1 -1
  105. package/dist/routes/auth/client-ip.d.ts +1 -0
  106. package/dist/routes/auth/client-ip.js +18 -0
  107. package/dist/routes/auth/client-ip.js.map +1 -1
  108. package/dist/routes/auth/oauth/config.d.ts +2 -1
  109. package/dist/routes/auth/oauth/config.js +5 -1
  110. package/dist/routes/auth/oauth/config.js.map +1 -1
  111. package/dist/routes/auth/session-handoff.d.ts +0 -1
  112. package/dist/routes/auth/session-handoff.js +0 -2
  113. package/dist/routes/auth/session-handoff.js.map +1 -1
  114. package/dist/routes/auth.js +3 -2
  115. package/dist/routes/auth.js.map +1 -1
  116. package/dist/routes/index.d.ts +0 -1
  117. package/dist/routes/index.js +0 -1
  118. package/dist/routes/index.js.map +1 -1
  119. package/dist/routes/realtime.d.ts +13 -1
  120. package/dist/routes/realtime.js +114 -29
  121. package/dist/routes/realtime.js.map +1 -1
  122. package/dist/startup-config.d.ts +2 -0
  123. package/dist/startup-config.js +62 -0
  124. package/dist/startup-config.js.map +1 -0
  125. package/dist/store/interfaces.d.ts +3 -2
  126. package/dist/store/interfaces.js +16 -6
  127. package/dist/store/interfaces.js.map +1 -1
  128. package/dist/store/listing.js +25 -15
  129. package/dist/store/listing.js.map +1 -1
  130. package/dist/store/operations.js +5 -2
  131. package/dist/store/operations.js.map +1 -1
  132. package/dist/store/requirements.js +3 -2
  133. package/dist/store/requirements.js.map +1 -1
  134. package/dist/store/runtime.d.ts +10 -1
  135. package/dist/store/runtime.js +33 -6
  136. package/dist/store/runtime.js.map +1 -1
  137. package/dist/store/types.d.ts +10 -0
  138. package/dist/utils/ensure-default-tenant.d.ts +2 -1
  139. package/dist/utils/ensure-default-tenant.js +13 -1
  140. package/dist/utils/ensure-default-tenant.js.map +1 -1
  141. package/dist/utils/export-jobs.js +2 -1
  142. package/dist/utils/export-jobs.js.map +1 -1
  143. package/dist/validation/auth.schema.d.ts +2 -2
  144. package/dist/validation/member-invite.schema.d.ts +2 -10
  145. package/dist/validation/member-invite.schema.js +2 -15
  146. package/dist/validation/member-invite.schema.js.map +1 -1
  147. package/frontend-vue/dms.frontend.ts +24 -5
  148. package/frontend-vue/dms.server.ts +17 -0
  149. package/frontend-vue/layers/dms-auth/server/accounts.ts +3 -4
  150. package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +33 -0
  151. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +5 -1
  152. package/frontend-vue/layers/dms-core/app/build/utils/crossTab.ts +199 -0
  153. package/frontend-vue/layers/dms-core/app/build/utils/sharedRealtimeConnection.ts +410 -0
  154. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +77 -12
  155. package/frontend-vue/layers/dms-core/app/composables/data-types/useDataType.ts +26 -0
  156. package/frontend-vue/layers/dms-core/app/composables/live/livePauseSource.ts +29 -0
  157. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +150 -9
  158. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +6 -4
  159. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +27 -1
  160. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +256 -119
  161. package/frontend-vue/layers/dms-core/app/composables/translation/useComposedText.ts +21 -8
  162. package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +25 -5
  163. package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -5
  164. package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +61 -26
  165. package/frontend-vue/layers/dms-core/app/plugins/dev-error-log.ts +16 -0
  166. package/frontend-vue/layers/dms-core/app/types/user.ts +6 -0
  167. package/frontend-vue/layers/dms-core/app/utils/composedText.ts +7 -2
  168. package/frontend-vue/layers/dms-core/app/utils/formatter.ts +134 -35
  169. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +18 -1
  170. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +136 -2
  171. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +136 -2
  172. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +0 -2
  173. package/frontend-vue/layers/dms-layout/app/app.config.ts +0 -1
  174. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +11 -1
  175. package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Italic-Variable.woff2 +0 -0
  176. package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Variable.woff2 +0 -0
  177. package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Italic-Variable.woff2 +0 -0
  178. package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Variable.woff2 +0 -0
  179. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +3 -2
  180. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +41 -21
  181. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +6 -2
  182. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +4 -19
  183. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +11 -107
  184. package/frontend-vue/layers/dms-layout/app/build/components/layout/GithubStarPrompt.vue +18 -7
  185. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +4 -0
  186. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +43 -82
  187. package/frontend-vue/layers/dms-layout/app/build/components/layout/SidebarUserMenu.vue +44 -3
  188. package/frontend-vue/layers/dms-layout/app/build/components/layout/UserIdentity.vue +11 -3
  189. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleCatalogTile.vue +132 -35
  190. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleManageMenu.vue +59 -0
  191. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleSourceLabel.vue +25 -0
  192. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreBanners.vue +73 -0
  193. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreOperation.vue +116 -0
  194. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +8 -3
  195. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +85 -207
  196. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +10 -3
  197. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTileSkeleton.vue +68 -0
  198. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +24 -8
  199. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxItem.vue +8 -5
  200. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/region/RegionTimeZoneInput.vue +1 -0
  201. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-permissions.ts +19 -0
  202. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/useRoleEditor.ts +2 -1
  203. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useSidePanelWidth.ts +31 -70
  204. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationGroupsOpen.ts +11 -1
  205. package/frontend-vue/layers/dms-layout/app/build/utils/dev-reload-stream.ts +81 -0
  206. package/frontend-vue/layers/dms-layout/app/build/utils/github-repository.ts +2 -2
  207. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +232 -36
  208. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +122 -14
  209. package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +32 -3
  210. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +49 -11
  211. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleHistory.ts +3 -2
  212. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +59 -4
  213. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +194 -0
  214. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +23 -3
  215. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +4 -2
  216. package/frontend-vue/layers/dms-layout/app/composables/page/usePageRecordDeletion.ts +162 -0
  217. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +6 -1
  218. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +6 -2
  219. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +6 -2
  220. package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +291 -0
  221. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +87 -474
  222. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +3 -0
  223. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +13 -22
  224. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +11 -10
  225. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +22 -52
  226. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-quick-actions.client.ts +7 -7
  227. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +11 -10
  228. package/frontend-vue/layers/dms-layout/app/plugins/dms-dev-reload.client.ts +14 -9
  229. package/frontend-vue/layers/dms-layout/app/theme/actions.ts +5 -4
  230. package/frontend-vue/layers/dms-layout/app/theme/form-controls.ts +9 -4
  231. package/frontend-vue/layers/dms-layout/app/theme/surfaces.ts +22 -6
  232. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +9 -0
  233. package/frontend-vue/layers/dms-layout/app/types/page.ts +15 -1
  234. package/frontend-vue/layers/dms-layout/app/utils/account-menu.ts +11 -0
  235. package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +57 -0
  236. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +26 -0
  237. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +45 -18
  238. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +44 -17
  239. package/frontend-vue/layers/dms-ui/app/build/components/containers/TargetComponentTree.vue +31 -0
  240. package/frontend-vue/layers/dms-ui/app/build/components/containers/constants.ts +25 -0
  241. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +244 -15
  242. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +42 -25
  243. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowHeader.vue +153 -0
  244. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowNotFound.vue +44 -0
  245. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/drawerRowHeader.ts +35 -0
  246. package/frontend-vue/layers/dms-ui/app/build/components/form/FormEntries.vue +44 -9
  247. package/frontend-vue/layers/dms-ui/app/build/components/form/FormErrorSummary.vue +11 -4
  248. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +38 -5
  249. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldNotes.vue +29 -5
  250. package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +5 -1
  251. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +3 -1
  252. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSections.vue +106 -27
  253. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +12 -4
  254. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +4 -2
  255. package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +48 -1
  256. package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +55 -54
  257. package/frontend-vue/layers/dms-ui/app/build/components/table/RowSelection.vue +2 -1
  258. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +396 -288
  259. package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +2 -1
  260. package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +2 -1
  261. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardAvatar.vue +59 -0
  262. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +19 -2
  263. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardRowActions.vue +38 -0
  264. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +148 -104
  265. package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +17 -2
  266. package/frontend-vue/layers/dms-ui/app/build/composables/actions/bulkSelection.ts +43 -9
  267. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowComponentOptions.ts +76 -0
  268. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +172 -27
  269. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +270 -46
  270. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +6 -1
  271. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +2 -0
  272. package/frontend-vue/layers/dms-ui/app/build/composables/containers/closingLayers.ts +186 -0
  273. package/frontend-vue/layers/dms-ui/app/build/composables/containers/context.ts +14 -0
  274. package/frontend-vue/layers/dms-ui/app/build/composables/containers/drawerSize.ts +229 -0
  275. package/frontend-vue/layers/dms-ui/app/build/composables/containers/useResizableSize.ts +175 -0
  276. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +18 -35
  277. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +44 -0
  278. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +123 -27
  279. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerFieldTypes.ts +2 -1
  280. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationFilter.ts +75 -0
  281. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +92 -0
  282. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationValue.ts +39 -0
  283. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/twoLineCell.ts +3 -3
  284. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts +6 -2
  285. package/frontend-vue/layers/dms-ui/app/build/composables/form/createFormFields.ts +15 -0
  286. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldSaved.ts +40 -0
  287. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +62 -0
  288. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryContext.ts +5 -0
  289. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +42 -7
  290. package/frontend-vue/layers/dms-ui/app/build/composables/form/formLayout.ts +106 -4
  291. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +11 -5
  292. package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +32 -0
  293. package/frontend-vue/layers/dms-ui/app/build/composables/form/formValidation.ts +36 -4
  294. package/frontend-vue/layers/dms-ui/app/build/composables/form/readonlyValue.ts +19 -0
  295. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +27 -0
  296. package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +78 -0
  297. package/frontend-vue/layers/dms-ui/app/build/composables/form/serverFieldErrors.ts +49 -0
  298. package/frontend-vue/layers/dms-ui/app/build/composables/form/toasterClearance.ts +118 -0
  299. package/frontend-vue/layers/dms-ui/app/build/composables/form/useInstantForm.ts +12 -1
  300. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +41 -3
  301. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +10 -2
  302. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageReadClock.ts +28 -0
  303. package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +1 -1
  304. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +17 -3
  305. package/frontend-vue/layers/dms-ui/app/build/composables/live/useUpdatedLabel.ts +38 -0
  306. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/pageToolbarContext.ts +75 -0
  307. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/toolbarControlHeight.ts +29 -0
  308. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparison.ts +52 -0
  309. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +21 -0
  310. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodShortLabels.ts +28 -0
  311. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +1 -0
  312. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +97 -0
  313. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRowScrollAnchor.ts +106 -0
  314. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +9 -0
  315. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +1 -2
  316. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/archivedCount.ts +31 -0
  317. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/filterEditorValue.ts +38 -0
  318. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/formatFilterValue.ts +33 -2
  319. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/scrollEdges.ts +31 -0
  320. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useCardFields.ts +33 -0
  321. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useMobileDisplay.ts +48 -0
  322. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +41 -18
  323. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +3 -2
  324. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLinkedVisit.ts +78 -0
  325. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +135 -22
  326. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +56 -25
  327. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +90 -35
  328. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +86 -10
  329. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/bulkActions.ts +14 -4
  330. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/card.ts +127 -2
  331. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/compareValues.ts +13 -0
  332. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/expandedRows.ts +28 -0
  333. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/groupedRows.ts +114 -1
  334. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/linkedArrival.ts +308 -0
  335. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/locateRow.ts +106 -0
  336. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pageArrival.ts +34 -0
  337. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +17 -0
  338. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/quickFilters.ts +41 -33
  339. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +24 -8
  340. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableCounts.ts +113 -0
  341. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +27 -4
  342. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +22 -6
  343. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +21 -0
  344. package/frontend-vue/layers/dms-ui/app/build/utils/addressSuggestions.ts +49 -0
  345. package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +67 -29
  346. package/frontend-vue/layers/dms-ui/app/build/utils/eyebrow.ts +12 -0
  347. package/frontend-vue/layers/dms-ui/app/build/utils/fieldOptionsI18n.ts +51 -11
  348. package/frontend-vue/layers/dms-ui/app/build/utils/htmlGrammar.ts +79 -0
  349. package/frontend-vue/layers/dms-ui/app/build/utils/inputWidth.ts +53 -0
  350. package/frontend-vue/layers/dms-ui/app/build/utils/numberAdjustment.ts +6 -3
  351. package/frontend-vue/layers/dms-ui/app/build/utils/relativeDate.ts +19 -5
  352. package/frontend-vue/layers/dms-ui/app/build/utils/scrollWithin.ts +99 -0
  353. package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +1 -1
  354. package/frontend-vue/layers/dms-ui/app/build/utils/streamHighlight.ts +90 -0
  355. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +36 -14
  356. package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
  357. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +9 -1
  358. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +80 -32
  359. package/frontend-vue/layers/dms-ui/app/components/blocks/PageToolbarBlock.vue +137 -0
  360. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +5 -0
  361. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +16 -2
  362. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +24 -5
  363. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +41 -23
  364. package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +23 -12
  365. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +1 -1
  366. package/frontend-vue/layers/dms-ui/app/components/container-footer/ContainerFooter.vue +34 -0
  367. package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +25 -2
  368. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +2 -2
  369. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +119 -36
  370. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +53 -22
  371. package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +20 -1
  372. package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +13 -1
  373. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +8 -3
  374. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +53 -5
  375. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +29 -5
  376. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +45 -8
  377. package/frontend-vue/layers/dms-ui/app/components/form/components/InputPercentage.vue +28 -5
  378. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +5 -0
  379. package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +19 -2
  380. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +2 -1
  381. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +226 -65
  382. package/frontend-vue/layers/dms-ui/app/components/form/components/Repeater.vue +80 -30
  383. package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +210 -0
  384. package/frontend-vue/layers/dms-ui/app/components/form/components/Select.vue +4 -2
  385. package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +11 -2
  386. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +79 -70
  387. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +117 -21
  388. package/frontend-vue/layers/dms-ui/app/components/meter/Meter.vue +2 -1
  389. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +113 -34
  390. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +14 -2
  391. package/frontend-vue/layers/dms-ui/app/components/section-header/Eyebrow.vue +6 -1
  392. package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +13 -0
  393. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +52 -3
  394. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +2 -1
  395. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +74 -25
  396. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +48 -44
  397. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanDisplay.vue +4 -1
  398. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +426 -97
  399. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +33 -5
  400. package/frontend-vue/layers/dms-ui/app/composables/chart/apexCategoryLabels.ts +68 -0
  401. package/frontend-vue/layers/dms-ui/app/composables/chart/apexDatetimeBuckets.ts +306 -0
  402. package/frontend-vue/layers/dms-ui/app/composables/chart/formatValue.ts +4 -3
  403. package/frontend-vue/layers/dms-ui/app/composables/chart/heatmapColors.ts +93 -0
  404. package/frontend-vue/layers/dms-ui/app/composables/chart/localizeSeries.ts +62 -0
  405. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +116 -8
  406. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.types.ts +7 -0
  407. package/frontend-vue/layers/dms-ui/app/composables/containers/types.ts +84 -1
  408. package/frontend-vue/layers/dms-ui/app/composables/containers/useDrawer.ts +69 -8
  409. package/frontend-vue/layers/dms-ui/app/composables/form/types/field-saved.ts +11 -0
  410. package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +16 -0
  411. package/frontend-vue/layers/dms-ui/app/composables/form/types/functions.ts +1 -0
  412. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +189 -28
  413. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +29 -12
  414. package/frontend-vue/layers/dms-ui/app/composables/page-toolbar/usePageToolbarSize.ts +30 -0
  415. package/frontend-vue/layers/dms-ui/app/composables/table-view/kanban.ts +4 -2
  416. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +22 -3
  417. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  418. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +24 -1
  419. package/frontend-vue/layers/dms-ui/app/plugins/closing-layers.client.ts +10 -0
  420. package/frontend-vue/layers/dms-ui/app/plugins/live-pause-source.ts +15 -0
  421. package/frontend-vue/layers/dms-ui/app/plugins/register.ts +2 -0
  422. package/frontend-vue/layers/dms-ui/app/types/input-number.ts +3 -2
  423. package/frontend-vue/layers/dms-ui/app/types/page-toolbar.ts +7 -0
  424. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +0 -2
  425. package/frontend-vue/layers/dms-ui/app/types/row-navigation.ts +17 -0
  426. package/frontend-vue/layers/dms-ui/app/types/table/query.ts +2 -1
  427. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +3 -1
  428. package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +94 -3
  429. package/frontend-vue/layers/dms-ui/app/utils/componentTree.ts +24 -0
  430. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +20 -5
  431. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +20 -5
  432. package/frontend-vue/package.json +6 -2
  433. package/package.json +11 -10
  434. package/skills/dms/SKILL.md +1 -1
  435. package/skills/dms-auth/SKILL.md +5 -3
  436. package/skills/dms-best-practices/REFERENCE.md +2 -0
  437. package/skills/dms-best-practices/SKILL.md +4 -0
  438. package/skills/dms-module/SKILL.md +3 -2
  439. package/skills/dms-pages/REFERENCE.md +129 -16
  440. package/skills/dms-pages/SKILL.md +1 -1
  441. package/skills/dms-project/DEV-SERVERS.md +23 -1
  442. package/skills/dms-project/REFERENCE.md +1 -1
  443. package/skills/dms-project/SKILL.md +4 -1
  444. package/skills/dms-project/scripts/dms-dev.sh +205 -17
  445. package/dist/hooks/tenant-instances.d.ts +0 -1
  446. package/dist/hooks/tenant-instances.js +0 -13
  447. package/dist/hooks/tenant-instances.js.map +0 -1
  448. package/dist/routes/startup-gate.d.ts +0 -7
  449. package/dist/routes/startup-gate.js +0 -39
  450. package/dist/routes/startup-gate.js.map +0 -1
  451. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +0 -42
@@ -10,9 +10,12 @@ import DmsBlockActions, {
10
10
  // would draw an empty card for the moment that chunk takes.
11
11
  import DmsTopListRow from "./internal/TopListRow.vue";
12
12
  import { useChartFetch } from "../../composables/chart/useChartFetch";
13
+ import { readFetchNotFound } from "../../build/utils/fetchNotFound";
14
+ import DmsBlockInlineStatus from "../../build/components/blocks/BlockInlineStatus.vue";
13
15
  import { formatValue } from "../../composables/chart/formatValue";
14
16
  import { resolveSparklineAccent } from "../../composables/chart/resolveSparklineAccent";
15
17
  import { usePeriodScopePreset } from "#dms-core/app/composables/period/usePeriodScope";
18
+ import { usePeriodComparisonDisplay } from "../../build/composables/period/periodComparison";
16
19
  import {
17
20
  hasUrlVariables,
18
21
  resolveLinkVariables,
@@ -119,7 +122,7 @@ const staticData = computed<TopListCardResponse | null>(() =>
119
122
  props.staticItems ? { items: props.staticItems } : null,
120
123
  );
121
124
 
122
- const { data, isLoading } = useChartFetch<TopListCardResponse>({
125
+ const { data, isLoading, error, refresh } = useChartFetch<TopListCardResponse>({
123
126
  fetchUrl: props.fetchUrl,
124
127
  fetchUrlMethod: props.fetchUrlMethod,
125
128
  routeParams: () => props.routeParams,
@@ -226,11 +229,26 @@ const badgeLabel = computed<string | null>(() => {
226
229
  return t(`${PRESET_LABEL_KEY_PREFIX}.${preset}`, preset);
227
230
  });
228
231
 
232
+ // With no comparison selected on the scope, the rows show no change: their
233
+ // trend column goes rather than staying blank.
234
+ const comparison = usePeriodComparisonDisplay(props.periodScope);
235
+ const showRowDelta = computed(() => props.showDelta && !comparison.isOff.value);
236
+
229
237
  const isFirstLoad = computed(() => isLoading.value && data.value === null);
230
238
  // New inputs (a period applied) are on their way: the rows on screen still
231
239
  // belong to the previous ones, so they dim like the KPI values do.
232
240
  const isRefreshing = computed(() => isLoading.value && data.value !== null);
233
241
 
242
+ // A failed first load shows a message and a retry, never the empty state (a
243
+ // list that loaded with no rows); a failed refetch keeps the last rows.
244
+ const hasError = computed(
245
+ () => !isLoading.value && !data.value && Boolean(error.value),
246
+ );
247
+ // A 404 is a status too, but nothing to retry: the list does not exist.
248
+ const notFound = computed(() =>
249
+ hasError.value ? readFetchNotFound(error.value) : null,
250
+ );
251
+
234
252
  const emptyLabelDisplay = computed(() =>
235
253
  props.emptyLabel
236
254
  ? processI18n(props.emptyLabel)
@@ -262,7 +280,7 @@ function columnsTemplate(sparkline: boolean, icon: boolean): string {
262
280
  columns.push(TITLE_COLUMN_WIDTH);
263
281
  columns.push(VALUE_COLUMN_WIDTH);
264
282
  if (sparkline && props.showSparkline) columns.push(SPARKLINE_COLUMN_WIDTH);
265
- if (props.showDelta) columns.push(TREND_COLUMN_WIDTH);
283
+ if (showRowDelta.value) columns.push(TREND_COLUMN_WIDTH);
266
284
  columns.push(EDGE_COLUMN_WIDTH);
267
285
  return columns.join(" ");
268
286
  }
@@ -338,7 +356,7 @@ const gridStyle = computed(() => ({
338
356
  :has-any-icon="hasAnyIcon"
339
357
  :show-sparkline="showSparkline"
340
358
  :sparkline-accent="sparklineAccentFor(item)"
341
- :show-delta="showDelta"
359
+ :show-delta="showRowDelta"
342
360
  :invert="invert"
343
361
  :formatted-value="formattedValue(item)"
344
362
  :bar-percent="barPercent(item)"
@@ -346,6 +364,14 @@ const gridStyle = computed(() => ({
346
364
  />
347
365
  </ul>
348
366
 
367
+ <DmsBlockInlineStatus
368
+ v-else-if="hasError"
369
+ :state="notFound ? 'not-found' : 'error'"
370
+ :not-found="notFound"
371
+ class="min-h-48 px-[18px]"
372
+ @retry="refresh()"
373
+ />
374
+
349
375
  <DmsEmptyState
350
376
  v-else-if="!isFirstLoad"
351
377
  :icon="EMPTY_ICON"
@@ -359,10 +385,12 @@ const gridStyle = computed(() => ({
359
385
  class="divide-y divide-(--ui-border-muted) py-1"
360
386
  aria-busy="true"
361
387
  >
388
+ <!-- A loaded row's height: 60px with its bar, 56px without. -->
362
389
  <div
363
390
  v-for="row in props.skeletonCount"
364
391
  :key="row"
365
- class="flex h-14 items-center gap-3 px-[18px]"
392
+ class="flex items-center gap-3 px-[18px]"
393
+ :class="props.showBar ? 'h-[60px]' : 'h-14'"
366
394
  >
367
395
  <USkeleton
368
396
  :aria-label="t('dms.a11y.loading')"
@@ -379,7 +407,7 @@ const gridStyle = computed(() => ({
379
407
  <USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-14" />
380
408
  <USkeleton
381
409
  :aria-label="t('dms.a11y.loading')"
382
- v-if="showDelta"
410
+ v-if="showRowDelta"
383
411
  class="h-3 w-[46px]"
384
412
  />
385
413
  </div>
@@ -0,0 +1,68 @@
1
+ import type { ChartXAxisFormatter } from "./types";
2
+
3
+ /**
4
+ * Width of one character of the axis font (11px, monospace) in pixels, with a
5
+ * little room for a proportional fallback font.
6
+ */
7
+ const AXIS_CHAR_WIDTH = 7;
8
+ /** Blank space kept between two neighbouring labels. */
9
+ const LABEL_GAP = 8;
10
+
11
+ /** What Apex hands an x-axis label formatter (`xaxis.labels.formatter`). */
12
+ interface ApexLabelFormatterOpts {
13
+ /** The label's column; only set while Apex draws the axis. */
14
+ i?: number;
15
+ w?: { globals?: { gridWidth?: number; labels?: unknown[] } };
16
+ }
17
+
18
+ type ApexLabelFormatter = (
19
+ value: string,
20
+ timestamp?: number,
21
+ opts?: ApexLabelFormatterOpts,
22
+ ) => string;
23
+
24
+ /**
25
+ * How many columns each drawn label stands for: 1 while every label fits its
26
+ * column, more once the longest one is wider than a column.
27
+ */
28
+ export function categoryLabelStep(
29
+ gridWidth: number,
30
+ labelCount: number,
31
+ longestLabel: number,
32
+ ): number {
33
+ if (labelCount <= 1 || gridWidth <= 0) return 1;
34
+ const columnWidth = gridWidth / labelCount;
35
+ const labelWidth = longestLabel * AXIS_CHAR_WIDTH + LABEL_GAP;
36
+ return Math.max(1, Math.ceil(labelWidth / columnWidth));
37
+ }
38
+
39
+ /**
40
+ * A heatmap gives every column a label and ApexCharts does not thin them
41
+ * (`hideOverlappingLabels` only acts on other charts), so on a phone they run
42
+ * into each other ("07:0009:00"). This formatter keeps one label every few
43
+ * columns, from the plot's width, and leaves the tooltip's (no column index)
44
+ * whole.
45
+ */
46
+ export function thinnedCategoryFormatter(
47
+ format: ChartXAxisFormatter | undefined,
48
+ ): ApexLabelFormatter {
49
+ const write = (value: string, timestamp?: number): string =>
50
+ format ? format(value, timestamp) : String(value ?? "");
51
+ return (value, timestamp, opts) => {
52
+ const label = write(value, timestamp);
53
+ const index = opts?.i;
54
+ const globals = opts?.w?.globals;
55
+ if (typeof index !== "number" || !globals) return label;
56
+ const labels = globals.labels ?? [];
57
+ const longest = labels.reduce<number>(
58
+ (max, entry) => Math.max(max, write(String(entry ?? "")).length),
59
+ 0,
60
+ );
61
+ const step = categoryLabelStep(
62
+ globals.gridWidth ?? 0,
63
+ labels.length,
64
+ longest,
65
+ );
66
+ return index % step === 0 ? label : "";
67
+ };
68
+ }
@@ -0,0 +1,306 @@
1
+ import type { ChartSeries } from "./types";
2
+ import type { UseApexChartInput } from "./useApexChart.types";
3
+
4
+ /** The calendar step a series of dates moves by. */
5
+ export type CalendarBucket = "month" | "day";
6
+
7
+ /** How a bucket's dates are written: on the axis, then in the tooltip. */
8
+ interface BucketFormats {
9
+ /** ApexCharts tokens, as its own datetime axis writes them. */
10
+ axis: string;
11
+ /** Intl options, for the tooltip and wherever Apex lends no formatter. */
12
+ axisIntl: Intl.DateTimeFormatOptions;
13
+ tooltipIntl: Intl.DateTimeFormatOptions;
14
+ }
15
+
16
+ const BUCKET_FORMATS: Record<CalendarBucket, BucketFormats> = {
17
+ month: {
18
+ axis: "MMM 'yy",
19
+ axisIntl: { month: "short", year: "2-digit" },
20
+ tooltipIntl: { month: "long", year: "numeric" },
21
+ },
22
+ day: {
23
+ axis: "dd MMM",
24
+ axisIntl: { day: "2-digit", month: "short" },
25
+ tooltipIntl: { day: "numeric", month: "short", year: "numeric" },
26
+ },
27
+ };
28
+
29
+ /** Beyond this many buckets the datetime axis thins its own labels better. */
30
+ const MAX_BUCKETS = 120;
31
+ const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
32
+ const BUCKETED_TYPES = new Set(["column"]);
33
+
34
+ interface CalendarDay {
35
+ year: number;
36
+ month: number;
37
+ day: number;
38
+ }
39
+
40
+ type ApexDateFormatter = (date: Date, format: string) => string;
41
+
42
+ /** What Apex hands an axis label formatter, past the value. */
43
+ interface ApexLabelOptions {
44
+ dateFormatter?: ApexDateFormatter;
45
+ w?: unknown;
46
+ }
47
+
48
+ /** A column chart's dates turned into one category per calendar bucket. */
49
+ export interface DatetimeBuckets {
50
+ bucket: CalendarBucket;
51
+ /** Every bucket from the first date to the last, as `YYYY-MM-DD`. */
52
+ keys: string[];
53
+ /**
54
+ * Per series, the index in the route's `data` of the point in each bucket,
55
+ * `-1` for an empty bucket: a click names the point the route sent.
56
+ */
57
+ sourceIndexes: number[][];
58
+ }
59
+
60
+ function pad(value: number): string {
61
+ return String(value).padStart(2, "0");
62
+ }
63
+
64
+ function keyOf(day: CalendarDay): string {
65
+ return `${day.year}-${pad(day.month)}-${pad(day.day)}`;
66
+ }
67
+
68
+ function dateOfKey(key: string): Date {
69
+ const [, year, month, day] = DATE_ONLY.exec(key)!;
70
+ return new Date(Number(year), Number(month) - 1, Number(day));
71
+ }
72
+
73
+ function localDay(date: Date): CalendarDay | null {
74
+ const isMidnight =
75
+ date.getHours() === 0 && date.getMinutes() === 0 && date.getSeconds() === 0;
76
+ if (!isMidnight) return null;
77
+ return {
78
+ year: date.getFullYear(),
79
+ month: date.getMonth() + 1,
80
+ day: date.getDate(),
81
+ };
82
+ }
83
+
84
+ function utcDay(date: Date): CalendarDay | null {
85
+ const isMidnight =
86
+ date.getUTCHours() === 0 &&
87
+ date.getUTCMinutes() === 0 &&
88
+ date.getUTCSeconds() === 0;
89
+ if (!isMidnight) return null;
90
+ return {
91
+ year: date.getUTCFullYear(),
92
+ month: date.getUTCMonth() + 1,
93
+ day: date.getUTCDate(),
94
+ };
95
+ }
96
+
97
+ /**
98
+ * The calendar day a point stands for. A date-only string is that day; a
99
+ * timestamp counts when it falls on a midnight, the browser's or UTC's: a
100
+ * route bucketing by month often sends `2026-10-01T00:00:00Z`.
101
+ */
102
+ function calendarDayOf(
103
+ x: unknown,
104
+ readDay: typeof localDay,
105
+ ): CalendarDay | null {
106
+ if (typeof x === "string") {
107
+ const match = DATE_ONLY.exec(x);
108
+ if (match) {
109
+ return {
110
+ year: Number(match[1]),
111
+ month: Number(match[2]),
112
+ day: Number(match[3]),
113
+ };
114
+ }
115
+ }
116
+ if (typeof x !== "string" && typeof x !== "number" && !(x instanceof Date)) {
117
+ return null;
118
+ }
119
+ const date = new Date(x);
120
+ return Number.isNaN(date.getTime()) ? null : readDay(date);
121
+ }
122
+
123
+ function pointX(point: ChartSeries["data"][number]): unknown {
124
+ return typeof point === "object" && point !== null ? point.x : undefined;
125
+ }
126
+
127
+ /** Each series' days, in order, or `null` when a point is not a day. */
128
+ function readDays(
129
+ series: ChartSeries[],
130
+ readDay: typeof localDay,
131
+ ): CalendarDay[][] | null {
132
+ const days: CalendarDay[][] = [];
133
+ for (const entry of series) {
134
+ const seriesDays: CalendarDay[] = [];
135
+ for (const point of entry.data) {
136
+ const day = calendarDayOf(pointX(point), readDay);
137
+ if (!day) return null;
138
+ seriesDays.push(day);
139
+ }
140
+ days.push(seriesDays);
141
+ }
142
+ return days.some((entry) => entry.length > 0) ? days : null;
143
+ }
144
+
145
+ function nextBucket(day: CalendarDay, bucket: CalendarBucket): CalendarDay {
146
+ const date =
147
+ bucket === "month"
148
+ ? new Date(day.year, day.month, 1)
149
+ : new Date(day.year, day.month - 1, day.day + 1);
150
+ return {
151
+ year: date.getFullYear(),
152
+ month: date.getMonth() + 1,
153
+ day: date.getDate(),
154
+ };
155
+ }
156
+
157
+ /** Every bucket from `first` to `last`, or `null` past the bucket cap. */
158
+ function bucketKeys(
159
+ first: string,
160
+ last: string,
161
+ bucket: CalendarBucket,
162
+ ): string[] | null {
163
+ const keys: string[] = [];
164
+ let cursor = calendarDayOf(first, localDay)!;
165
+ while (keyOf(cursor) <= last) {
166
+ keys.push(keyOf(cursor));
167
+ if (keys.length > MAX_BUCKETS) return null;
168
+ cursor = nextBucket(cursor, bucket);
169
+ }
170
+ return keys;
171
+ }
172
+
173
+ /**
174
+ * Where each series' points fall among `keys`, or `null` when a series holds
175
+ * two points in one bucket or a date between buckets (the 15th on a monthly
176
+ * axis): those keep the datetime axis.
177
+ */
178
+ function sourceIndexesOf(
179
+ days: CalendarDay[][],
180
+ keys: string[],
181
+ ): number[][] | null {
182
+ const positions = new Map(keys.map((key, position) => [key, position]));
183
+ const indexes: number[][] = [];
184
+ for (const seriesDays of days) {
185
+ const slots = keys.map(() => -1);
186
+ for (const [index, day] of seriesDays.entries()) {
187
+ const position = positions.get(keyOf(day));
188
+ if (position === undefined || slots[position] !== -1) return null;
189
+ slots[position] = index;
190
+ }
191
+ indexes.push(slots);
192
+ }
193
+ return indexes;
194
+ }
195
+
196
+ /**
197
+ * Whether two dates sit one bucket apart somewhere: weekly or quarterly
198
+ * points have none, and the datetime axis spaces them better than buckets
199
+ * mostly left empty.
200
+ */
201
+ function hasAdjacentBuckets(sourceIndexes: number[][]): boolean {
202
+ return sourceIndexes.some((slots) =>
203
+ slots.some(
204
+ (index, position) =>
205
+ index !== -1 &&
206
+ slots[position + 1] !== undefined &&
207
+ slots[position + 1] !== -1,
208
+ ),
209
+ );
210
+ }
211
+
212
+ function layoutBuckets(
213
+ days: CalendarDay[][],
214
+ bucket: CalendarBucket,
215
+ ): DatetimeBuckets | null {
216
+ const sorted = days.flat().map(keyOf).sort();
217
+ const keys = bucketKeys(sorted[0]!, sorted.at(-1)!, bucket);
218
+ if (!keys) return null;
219
+ const sourceIndexes = sourceIndexesOf(days, keys);
220
+ if (!sourceIndexes) return null;
221
+ const isSingleBucket = keys.length === 1;
222
+ if (!isSingleBucket && !hasAdjacentBuckets(sourceIndexes)) return null;
223
+ return { bucket, keys, sourceIndexes };
224
+ }
225
+
226
+ /**
227
+ * Whether a column chart on a datetime axis plots calendar buckets — every
228
+ * point on a day, or on the first of a month — and which ones. Apex places
229
+ * such columns by time and ticks the axis on its own: months of uneven length
230
+ * leave uneven gaps, and a tick every half month repeats each month's name.
231
+ */
232
+ export function detectDatetimeBuckets(
233
+ input: UseApexChartInput,
234
+ ): DatetimeBuckets | null {
235
+ if (input.xaxisType !== "datetime") return null;
236
+ if (!BUCKETED_TYPES.has(input.type)) return null;
237
+ const days =
238
+ readDays(input.series, localDay) ?? readDays(input.series, utcDay);
239
+ if (!days) return null;
240
+ const bucket: CalendarBucket = days.flat().every((day) => day.day === 1)
241
+ ? "month"
242
+ : "day";
243
+ return layoutBuckets(days, bucket);
244
+ }
245
+
246
+ /**
247
+ * The series on the bucket axis: one point per bucket, named by it, an empty
248
+ * bucket holding `null` so it keeps its slot.
249
+ */
250
+ export function toBucketSeries(
251
+ series: ChartSeries[],
252
+ buckets: DatetimeBuckets,
253
+ ): ChartSeries[] {
254
+ return series.map((entry, seriesIndex) => ({
255
+ ...entry,
256
+ data: buckets.keys.map((key, position) => {
257
+ const point = entry.data[buckets.sourceIndexes[seriesIndex]![position]!];
258
+ return typeof point === "object"
259
+ ? { ...point, x: key }
260
+ : { x: key, y: null };
261
+ }),
262
+ }));
263
+ }
264
+
265
+ function intlFormat(
266
+ key: string,
267
+ locale: string | undefined,
268
+ options: Intl.DateTimeFormatOptions,
269
+ ): string {
270
+ return new Intl.DateTimeFormat(locale, options).format(dateOfKey(key));
271
+ }
272
+
273
+ /**
274
+ * The axis label of a bucket: in Apex's own tokens (`xAxis.datetimeFormat`,
275
+ * else what its datetime axis writes for that step) when Apex lends its
276
+ * formatter, which speaks the chart's locale, else through Intl.
277
+ */
278
+ export function bucketAxisFormatter(
279
+ buckets: DatetimeBuckets,
280
+ input: UseApexChartInput,
281
+ ): (value: string, timestamp?: unknown, opts?: ApexLabelOptions) => string {
282
+ const formats = BUCKET_FORMATS[buckets.bucket];
283
+ return (value, _timestamp, opts) => {
284
+ if (!DATE_ONLY.test(String(value))) return String(value ?? "");
285
+ if (input.xAxisFormatter) {
286
+ return input.xAxisFormatter(value, dateOfKey(value).getTime());
287
+ }
288
+ if (opts?.dateFormatter && opts.w) {
289
+ const format = input.xAxis?.datetimeFormat ?? formats.axis;
290
+ return opts.dateFormatter.call({ w: opts.w }, dateOfKey(value), format);
291
+ }
292
+ return intlFormat(value, input.locale, formats.axisIntl);
293
+ };
294
+ }
295
+
296
+ /** The tooltip title of a bucket: the whole month, or the whole day. */
297
+ export function bucketTooltipFormatter(
298
+ buckets: DatetimeBuckets,
299
+ input: UseApexChartInput,
300
+ ): (value: string) => string {
301
+ const formats = BUCKET_FORMATS[buckets.bucket];
302
+ return (value) =>
303
+ DATE_ONLY.test(String(value))
304
+ ? intlFormat(value, input.locale, formats.tooltipIntl)
305
+ : String(value ?? "");
306
+ }
@@ -1,3 +1,4 @@
1
+ import { readableNumberFormat } from "#dms-core/app/utils/formatter";
1
2
  import type { ValueFormat, ValuePrecision } from "./types";
2
3
 
3
4
  const PERCENT_DIVISOR = 100;
@@ -81,7 +82,7 @@ export function formatValue(
81
82
  precision?: ValuePrecision,
82
83
  ): string {
83
84
  const config = resolveConfig(format);
84
- return new Intl.NumberFormat(
85
+ return readableNumberFormat(
85
86
  locale,
86
87
  precisionOptions(config.options(currency), precision),
87
88
  ).format(config.transform(value));
@@ -98,7 +99,7 @@ export function formatDeltaPercent(
98
99
  delta: number,
99
100
  locale = DEFAULT_LOCALE,
100
101
  ): string {
101
- return new Intl.NumberFormat(locale, {
102
+ return readableNumberFormat(locale, {
102
103
  style: "percent",
103
104
  minimumFractionDigits: PERCENT_FRACTION_DIGITS,
104
105
  maximumFractionDigits: PERCENT_FRACTION_DIGITS,
@@ -144,7 +145,7 @@ export function formatValueParts(
144
145
  };
145
146
  }
146
147
 
147
- const parts = new Intl.NumberFormat(
148
+ const parts = readableNumberFormat(
148
149
  locale,
149
150
  precisionOptions(config.options(currency), precision),
150
151
  ).formatToParts(config.transform(value));
@@ -0,0 +1,93 @@
1
+ const HEX_SHORT_LENGTH = 3;
2
+ const HEX_LONG_LENGTH = 6;
3
+ const HEX_RADIX = 16;
4
+ const COLOR_CHANNEL_MAX = 255;
5
+ /** Below this relative luminance a card surface reads as a dark theme. */
6
+ const DARK_SURFACE_LUMINANCE = 0.5;
7
+ const LUMINANCE_WEIGHTS = { red: 0.2126, green: 0.7152, blue: 0.0722 };
8
+ const RGB_FUNCTION = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i;
9
+
10
+ interface RgbColor {
11
+ red: number;
12
+ green: number;
13
+ blue: number;
14
+ }
15
+
16
+ function parseHex(color: string): RgbColor | null {
17
+ const digits = color.slice(1);
18
+ const long =
19
+ digits.length === HEX_SHORT_LENGTH
20
+ ? [...digits].map((digit) => digit + digit).join("")
21
+ : digits;
22
+ if (long.length !== HEX_LONG_LENGTH) return null;
23
+ const value = Number.parseInt(long, HEX_RADIX);
24
+ if (Number.isNaN(value)) return null;
25
+ return {
26
+ red: (value >> 16) & COLOR_CHANNEL_MAX,
27
+ green: (value >> 8) & COLOR_CHANNEL_MAX,
28
+ blue: value & COLOR_CHANNEL_MAX,
29
+ };
30
+ }
31
+
32
+ /** The channels of a resolved chart colour (`#hex` or comma `rgb()`). */
33
+ function parseColor(color: string): RgbColor | null {
34
+ if (color.startsWith("#")) return parseHex(color);
35
+ const match = RGB_FUNCTION.exec(color);
36
+ if (!match) return null;
37
+ return {
38
+ red: Number(match[1]),
39
+ green: Number(match[2]),
40
+ blue: Number(match[3]),
41
+ };
42
+ }
43
+
44
+ /** Whether the card surface is a dark theme's. */
45
+ export function isDarkSurface(surface: string): boolean {
46
+ const rgb = parseColor(surface);
47
+ if (!rgb) return false;
48
+ const luminance =
49
+ (LUMINANCE_WEIGHTS.red * rgb.red +
50
+ LUMINANCE_WEIGHTS.green * rgb.green +
51
+ LUMINANCE_WEIGHTS.blue * rgb.blue) /
52
+ COLOR_CHANNEL_MAX;
53
+ return luminance < DARK_SURFACE_LUMINANCE;
54
+ }
55
+
56
+ /**
57
+ * `color` laid over the card surface at `weight` (0 = the surface, 1 = the
58
+ * colour): a faint cell fades into the card in both themes, rather than
59
+ * towards white. A colour that cannot be read is kept as it is.
60
+ */
61
+ function mixOverSurface(
62
+ color: string,
63
+ surface: string,
64
+ weight: number,
65
+ ): string {
66
+ const front = parseColor(color);
67
+ const back = parseColor(surface);
68
+ if (!front || !back) return color;
69
+ const channel = (key: keyof RgbColor) =>
70
+ Math.round(back[key] + (front[key] - back[key]) * weight);
71
+ return `rgb(${channel("red")}, ${channel("green")}, ${channel("blue")})`;
72
+ }
73
+
74
+ /**
75
+ * The colours of a heatmap's value ranges, lowest first: each palette colour
76
+ * over the surface, from `1 - intensity` of it for the lowest range to the
77
+ * full colour for the highest.
78
+ */
79
+ export function heatmapRangeColors(
80
+ palette: string[],
81
+ surface: string,
82
+ intensity: number,
83
+ ): string[] {
84
+ const faintest = 1 - Math.min(Math.max(intensity, 0), 1);
85
+ const lastIndex = Math.max(palette.length - 1, 1);
86
+ return palette.map((color, index) =>
87
+ mixOverSurface(
88
+ color,
89
+ surface,
90
+ faintest + ((1 - faintest) * index) / lastIndex,
91
+ ),
92
+ );
93
+ }
@@ -0,0 +1,62 @@
1
+ import type { ChartSeries, DonutRecord } from "./types";
2
+
3
+ /** Writes one display string in the reader's language (`processI18n`). */
4
+ export type ChartTextTranslate = (text: string) => string;
5
+
6
+ type ChartSeriesEntry = ChartSeries["data"][number];
7
+
8
+ interface TextPoint {
9
+ x?: unknown;
10
+ label?: unknown;
11
+ }
12
+
13
+ function translateText(value: unknown, translate: ChartTextTranslate): unknown {
14
+ return typeof value === "string" ? translate(value) : value;
15
+ }
16
+
17
+ /**
18
+ * A point's category (`x`) and `label` are what Apex prints on the axis, in
19
+ * the tooltip and on a slice: a `$` key there is translated like a name. A
20
+ * date or a number is left alone, and so is any string without the `$`.
21
+ */
22
+ function localizePoint(
23
+ point: ChartSeriesEntry,
24
+ translate: ChartTextTranslate,
25
+ ): ChartSeriesEntry {
26
+ if (typeof point !== "object" || point === null) return point;
27
+ const text = point as TextPoint;
28
+ const localized: Record<string, unknown> = { ...point };
29
+ if ("x" in text) localized.x = translateText(text.x, translate);
30
+ if ("label" in text) localized.label = translateText(text.label, translate);
31
+ return localized as unknown as ChartSeriesEntry;
32
+ }
33
+
34
+ /**
35
+ * A series as the reader sees it: its `name`, and the categories and labels
36
+ * of its points, translated when they follow the `$`-prefix convention.
37
+ * Apex prints all of them verbatim, so a route's `$` keys are resolved here,
38
+ * in the browser, in the language on screen.
39
+ */
40
+ export function localizeSeries(
41
+ series: ChartSeries,
42
+ translate: ChartTextTranslate,
43
+ ): ChartSeries {
44
+ return {
45
+ ...series,
46
+ name: translateText(series.name, translate) as string,
47
+ data: Array.isArray(series.data)
48
+ ? series.data.map((point) => localizePoint(point, translate))
49
+ : series.data,
50
+ };
51
+ }
52
+
53
+ /** The slices of a circular chart, their labels translated. */
54
+ export function localizeDonutRecords(
55
+ records: DonutRecord[],
56
+ translate: ChartTextTranslate,
57
+ ): DonutRecord[] {
58
+ return records.map((record) => ({
59
+ ...record,
60
+ label: translateText(record.label, translate) as string,
61
+ }));
62
+ }