@antelopejs/dms 0.8.3 → 0.9.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 (351) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
  3. package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
  4. package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
  5. package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
  6. package/dist/implementations/dms/frontend-bootstrap.js +13 -0
  7. package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
  8. package/dist/implementations/dms/frontend-modules.d.ts +4 -2
  9. package/dist/implementations/dms/frontend-modules.js +8 -7
  10. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  11. package/dist/implementations/dms/manifest.d.ts +6 -0
  12. package/dist/implementations/dms/manifest.js +13 -0
  13. package/dist/implementations/dms/manifest.js.map +1 -1
  14. package/dist/implementations/dms/menu-categories.d.ts +16 -0
  15. package/dist/implementations/dms/menu-categories.js +30 -0
  16. package/dist/implementations/dms/menu-categories.js.map +1 -0
  17. package/dist/implementations/dms/nav-badges.d.ts +0 -8
  18. package/dist/implementations/dms/nav-badges.js +0 -16
  19. package/dist/implementations/dms/nav-badges.js.map +1 -1
  20. package/dist/implementations/dms/page.d.ts +11 -5
  21. package/dist/implementations/dms/page.js +56 -41
  22. package/dist/implementations/dms/page.js.map +1 -1
  23. package/dist/implementations/dms-html-render/index.js +5 -2
  24. package/dist/implementations/dms-html-render/index.js.map +1 -1
  25. package/dist/implementations/dms-notifications/index.d.ts +4 -3
  26. package/dist/implementations/dms-notifications/index.js +14 -6
  27. package/dist/implementations/dms-notifications/index.js.map +1 -1
  28. package/dist/pages/settings/settings-index.js +3 -2
  29. package/dist/pages/settings/settings-index.js.map +1 -1
  30. package/dist/pages/settings/users/invites.js +2 -2
  31. package/dist/pages/settings/users/invites.js.map +1 -1
  32. package/dist/pages/settings/users/member-change-notifications.js +17 -6
  33. package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
  34. package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
  35. package/dist/pages/settings/users/member-invite-form.js +7 -4
  36. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  37. package/dist/pages/settings/users/member-roles-field.js +2 -1
  38. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  39. package/dist/pages/settings/users/members.js +4 -1
  40. package/dist/pages/settings/users/members.js.map +1 -1
  41. package/dist/pages/settings/users/notifications.d.ts +6 -0
  42. package/dist/pages/settings/users/notifications.js +23 -15
  43. package/dist/pages/settings/users/notifications.js.map +1 -1
  44. package/dist/pages/settings/users/profile.js +1 -1
  45. package/dist/pages/settings/users/profile.js.map +1 -1
  46. package/dist/pages/settings/users/region-form.d.ts +1 -1
  47. package/dist/pages/settings/users/region-form.js +2 -1
  48. package/dist/pages/settings/users/region-form.js.map +1 -1
  49. package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
  50. package/dist/pages/settings/users/role-editor-store.js +17 -4
  51. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  52. package/dist/pages/settings/users/roles.js +1 -0
  53. package/dist/pages/settings/users/roles.js.map +1 -1
  54. package/dist/realtime/registry.d.ts +1 -0
  55. package/dist/realtime/registry.js +5 -0
  56. package/dist/realtime/registry.js.map +1 -1
  57. package/dist/routes/realtime.d.ts +5 -1
  58. package/dist/routes/realtime.js +16 -9
  59. package/dist/routes/realtime.js.map +1 -1
  60. package/dist/test/integration/account-deletion.test.js +2 -2
  61. package/dist/test/integration/account-deletion.test.js.map +1 -1
  62. package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
  63. package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
  64. package/dist/test/integration/auth/current-password.test.js +2 -1
  65. package/dist/test/integration/auth/current-password.test.js.map +1 -1
  66. package/dist/test/integration/auth/session-revocation.test.js +2 -1
  67. package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
  68. package/dist/test/integration/auth/sign-in-country.test.js +2 -1
  69. package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
  70. package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
  71. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  72. package/dist/test/integration/auth/two-factor-password.test.js +3 -2
  73. package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
  74. package/dist/test/integration/email-change.test.js +2 -1
  75. package/dist/test/integration/email-change.test.js.map +1 -1
  76. package/dist/test/integration/files/component-files.test.js +2 -2
  77. package/dist/test/integration/files/component-files.test.js.map +1 -1
  78. package/dist/test/integration/member-settings-access.test.js +8 -7
  79. package/dist/test/integration/member-settings-access.test.js.map +1 -1
  80. package/dist/test/integration/menu-access-events.test.d.ts +1 -0
  81. package/dist/test/integration/menu-access-events.test.js +148 -0
  82. package/dist/test/integration/menu-access-events.test.js.map +1 -0
  83. package/dist/test/integration/notification-inbox.test.js +4 -1
  84. package/dist/test/integration/notification-inbox.test.js.map +1 -1
  85. package/dist/test/integration/notification-locked-subjects.test.js +3 -2
  86. package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
  87. package/dist/test/integration/notification-unread-badge.test.js +51 -5
  88. package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
  89. package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
  90. package/dist/test/integration/page-layout-route-guard.test.js +56 -0
  91. package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
  92. package/dist/test/integration/permission-warnings.test.js +3 -2
  93. package/dist/test/integration/permission-warnings.test.js.map +1 -1
  94. package/dist/test/integration/removed-member-notifications.test.js +3 -2
  95. package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
  96. package/dist/test/integration/role-delete-reassign.test.js +2 -1
  97. package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
  98. package/dist/test/integration/role-permission-ancestors.test.js +2 -1
  99. package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
  100. package/dist/test/unit/frontend-bootstrap.test.js +13 -0
  101. package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
  102. package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
  103. package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
  104. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
  105. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
  106. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
  107. package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
  108. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  109. package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
  110. package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
  111. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
  112. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  113. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
  114. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
  115. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
  116. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  117. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  118. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  119. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  120. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  121. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  122. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  123. package/frontend-vue/deferred-plugins.ts +3 -4
  124. package/frontend-vue/dms.email.ts +5 -1
  125. package/frontend-vue/dms.frontend.ts +15 -5
  126. package/frontend-vue/dms.server.ts +74 -0
  127. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  128. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  129. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  130. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  131. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  132. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  133. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  134. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  135. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  136. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  137. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  138. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  139. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  140. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  141. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  142. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  143. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  144. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  145. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  146. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  147. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  148. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  149. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  150. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  151. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  152. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  153. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  154. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  155. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  156. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  157. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  158. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  159. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  160. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  161. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  162. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  163. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  164. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  165. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  166. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  167. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  168. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  169. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  170. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  171. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  172. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  173. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  174. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  175. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  176. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  179. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  180. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  181. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  182. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  183. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  184. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  185. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  186. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  187. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  188. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  189. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  190. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  191. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  192. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  193. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  194. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  195. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  196. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  197. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  198. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  199. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  200. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  201. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +6 -28
  202. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  203. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  204. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  205. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  206. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  207. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  208. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  209. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  210. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  211. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  212. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  213. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  214. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  215. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  216. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  217. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  218. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  219. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  220. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  221. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  222. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  223. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  224. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  225. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  226. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  227. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  228. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  229. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  230. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  231. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  232. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  233. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  234. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  235. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  236. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  237. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  238. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  239. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  240. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  241. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  242. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  243. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  244. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  245. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  246. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  247. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  248. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  249. package/frontend-vue/package.json +2 -2
  250. package/frontend-vue/pnpm-lock.yaml +5 -5
  251. package/frontend-vue/server/private-options.ts +27 -0
  252. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  253. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  254. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  255. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  256. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  257. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  258. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  259. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  260. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  261. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  262. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  263. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  264. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  265. package/frontend-vue/tests/email-layout.test.ts +66 -4
  266. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  267. package/frontend-vue/tests/favicon.test.ts +131 -0
  268. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  269. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  270. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  271. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  272. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  273. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  274. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  275. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  276. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  277. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  278. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  279. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  280. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  281. package/frontend-vue/tests/module-routing.test.ts +2 -5
  282. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  283. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  284. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  285. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  286. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  287. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  288. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  289. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  290. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  291. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  292. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  293. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  294. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  295. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  296. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  297. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  298. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  299. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  300. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  301. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  302. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  303. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  304. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  305. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  306. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  307. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  308. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  309. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  310. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  311. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  312. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  313. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  314. package/frontend-vue/tests/server-routes.test.ts +582 -0
  315. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  316. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  317. package/frontend-vue/tests/side-panels.test.ts +8 -1
  318. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  319. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  320. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  321. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  322. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  323. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  324. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  325. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  326. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  327. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  328. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  329. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  330. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  331. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  332. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  333. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  334. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  335. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  336. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  337. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  338. package/frontend-vue/tests/user-language.test.ts +8 -5
  339. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  340. package/frontend-vue/vitest.config.ts +6 -0
  341. package/package.json +2 -2
  342. package/skills/dms/SKILL.md +2 -1
  343. package/skills/dms-module/SKILL.md +1 -1
  344. package/skills/dms-pages/SKILL.md +4 -1
  345. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  346. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  347. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  348. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  349. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  350. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  351. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Global reactive access to the signed-in user's identity (name, email, ...).
3
3
  *
4
- * `user` is the shared session state from `useUserSession`, so every consumer
4
+ * `user` is the shared session state from `useDmsUserSession`, so every consumer
5
5
  * (header menu, switch-account page, ...) re-renders as soon as it changes.
6
6
  * `refresh` re-fetches the session through the frontend server session hook
7
7
  * pulls a fresh `/api/auth/me`) and syncs the stored multi-account entry, so a
8
8
  * profile edit propagates immediately without re-logging in.
9
9
  */
10
10
  export function useCurrentUser() {
11
- const { user, fetch: refreshSession } = useUserSession<User>();
11
+ const { user, fetch: refreshSession } = useDmsUserSession<User>();
12
12
  const { syncActiveAccount } = useMultiAccount();
13
13
 
14
14
  async function refresh(): Promise<void> {
@@ -116,13 +116,23 @@ async function requestAccountSwitch(account: StoredAccount) {
116
116
  account.isExpired = false;
117
117
  }
118
118
 
119
- async function refreshSessionSurfaces(refreshUser: () => Promise<unknown>) {
119
+ /**
120
+ * Moves what the browser shows to the session it switched to. The session
121
+ * read again moves the data scope (the previous account's data is dropped)
122
+ * and drops the prefetched pages; the site layout and the page come back
123
+ * through Inertia, whose once prop is keyed on the account and workspace.
124
+ *
125
+ * @internal
126
+ */
127
+ export async function refreshSessionSurfaces(
128
+ refreshUser: () => Promise<unknown>,
129
+ ): Promise<void> {
120
130
  await refreshUser();
121
131
 
122
- const { refresh: refreshSiteLayout } = useSiteLayout();
132
+ const { resync: reloadSiteLayout } = useSiteLayout();
123
133
  const { clear: clearPermissions } = usePermissions();
124
134
  clearPermissions();
125
- await refreshSiteLayout();
135
+ await reloadSiteLayout();
126
136
  }
127
137
 
128
138
  export async function validateAccount(account: StoredAccount) {
@@ -187,8 +197,11 @@ function createValidateAllAccounts(
187
197
 
188
198
  export function useMultiAccount() {
189
199
  const accounts = useAccountsStorage();
190
- const { user, session, fetch: refreshUser } =
191
- useUserSession<User, BrowserSession>();
200
+ const {
201
+ user,
202
+ session,
203
+ fetch: refreshUser,
204
+ } = useDmsUserSession<User, BrowserSession>();
192
205
 
193
206
  const getActiveAccount = (): StoredAccount | undefined =>
194
207
  buildActiveAccount(
@@ -9,11 +9,29 @@ interface PermissionsState {
9
9
  fetchGeneration: Ref<number>;
10
10
  }
11
11
 
12
+ // The set a server render loads (the `auth` middleware preloads it before the
13
+ // page renders) is the one the page renders with: the browser starts from it
14
+ // rather than asking again, and its age keeps counting from the server's load.
15
+ // The generation only guards the runs in flight of one side.
16
+ const TRANSFERRED = { transfer: true };
17
+
12
18
  function usePermissionsState(): PermissionsState {
13
19
  return {
14
- permissions: useDmsState<Set<string>>("dms-user-permissions", () => new Set()),
15
- isLoaded: useDmsState<boolean>("dms-permissions-loaded", () => false),
16
- lastFetchTime: useDmsState<number>("dms-permissions-lastFetch", () => 0),
20
+ permissions: useDmsState<Set<string>>(
21
+ "dms-user-permissions",
22
+ () => new Set(),
23
+ TRANSFERRED,
24
+ ),
25
+ isLoaded: useDmsState<boolean>(
26
+ "dms-permissions-loaded",
27
+ () => false,
28
+ TRANSFERRED,
29
+ ),
30
+ lastFetchTime: useDmsState<number>(
31
+ "dms-permissions-lastFetch",
32
+ () => 0,
33
+ TRANSFERRED,
34
+ ),
17
35
  fetchGeneration: useDmsState<number>("dms-permissions-gen", () => 0),
18
36
  };
19
37
  }
@@ -30,15 +48,25 @@ function createFetchPermissions(state: PermissionsState) {
30
48
  };
31
49
 
32
50
  // A refetch asked while one is in flight gets its own pass instead of the
33
- // older response, so an invalidation is never swallowed. Scoped to the DMS app
51
+ // older response, so an invalidation is never swallowed. Scoped to the DMS
34
52
  // app, so concurrent server-rendered requests never share a pass.
35
53
  const deduper = useTrailingDeduper("permissions");
36
- return (): Promise<void> => deduper.run(runFetch);
54
+ const fetchPermissions = (): Promise<void> => deduper.run(runFetch);
55
+ // A caller that only needs a set has nothing to invalidate: it joins the
56
+ // load in flight instead of queueing a second one.
57
+ const loadPermissions = async (): Promise<void> => {
58
+ if (state.isLoaded.value) return;
59
+ await deduper.join(runFetch);
60
+ // The run it joined was started before a `clear()` (another account):
61
+ // its answer was dropped, and this caller still needs one.
62
+ if (!state.isLoaded.value) await deduper.join(runFetch);
63
+ };
64
+ return { fetchPermissions, loadPermissions };
37
65
  }
38
66
 
39
67
  export function usePermissions() {
40
68
  const state = usePermissionsState();
41
- const fetchPermissions = createFetchPermissions(state);
69
+ const { fetchPermissions, loadPermissions } = createFetchPermissions(state);
42
70
 
43
71
  async function refreshIfStale() {
44
72
  const elapsed = Date.now() - state.lastFetchTime.value;
@@ -66,6 +94,7 @@ export function usePermissions() {
66
94
  permissions: state.permissions,
67
95
  isLoaded: state.isLoaded,
68
96
  fetchPermissions,
97
+ loadPermissions,
69
98
  refreshIfStale,
70
99
  hasPermission,
71
100
  clear,
@@ -5,7 +5,7 @@ const IN_FLIGHT_REFRESH_KEY = "__dmsAuthInFlightRefresh";
5
5
 
6
6
  export const useSessionRecovery = () => {
7
7
  const dmsApp = useDmsApp();
8
- const { loggedIn, fetch: fetchSession } = useUserSession();
8
+ const { loggedIn, fetch: fetchSession } = useDmsUserSession();
9
9
  // useDmsRoute() must not be called here: this composable runs inside route
10
10
  // middleware (via useAuthFetch), where the DMS runtime warns that useDmsRoute is
11
11
  // misleading. Resolve the route lazily from the router instead.
@@ -1,16 +1,25 @@
1
+ import { readerClock } from "./periodClock";
1
2
  import type { PeriodComparison, PeriodRange } from "./types";
2
3
 
3
4
  const NON_OVERLAP_GAP_MS = 1;
4
5
 
5
6
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
6
7
 
8
+ // In the reader's calendar (`readerClock`), as the range was resolved.
7
9
  function isLocalMidnight(date: Date): boolean {
8
- return (
9
- date.getHours() === 0 &&
10
- date.getMinutes() === 0 &&
11
- date.getSeconds() === 0 &&
12
- date.getMilliseconds() === 0
13
- );
10
+ const { hours, minutes, seconds, milliseconds } = readerClock().wall(date);
11
+ return hours === 0 && minutes === 0 && seconds === 0 && milliseconds === 0;
12
+ }
13
+
14
+ /** `date` moved by whole calendar days and years, its time of day kept. */
15
+ function shiftCalendar(date: Date, days: number, years: number): Date {
16
+ const clock = readerClock();
17
+ const wall = clock.wall(date);
18
+ return clock.instant({
19
+ ...wall,
20
+ year: wall.year + years,
21
+ day: wall.day + days,
22
+ });
14
23
  }
15
24
 
16
25
  /** A range of whole local days: midnight to the last millisecond of a day. */
@@ -27,10 +36,8 @@ function shiftPreviousPeriod(range: PeriodRange): PeriodRange {
27
36
  if (isWholeDayRange(range)) {
28
37
  const end = range.to.getTime() + NON_OVERLAP_GAP_MS;
29
38
  const days = Math.round((end - range.from.getTime()) / MS_PER_DAY);
30
- const from = new Date(range.from);
31
- from.setDate(from.getDate() - days);
32
39
  return {
33
- from,
40
+ from: shiftCalendar(range.from, -days, 0),
34
41
  to: new Date(range.from.getTime() - NON_OVERLAP_GAP_MS),
35
42
  };
36
43
  }
@@ -44,11 +51,10 @@ function shiftPreviousPeriod(range: PeriodRange): PeriodRange {
44
51
  }
45
52
 
46
53
  function shiftPreviousYear(range: PeriodRange): PeriodRange {
47
- const previousFrom = new Date(range.from);
48
- previousFrom.setFullYear(previousFrom.getFullYear() - 1);
49
- const previousTo = new Date(range.to);
50
- previousTo.setFullYear(previousTo.getFullYear() - 1);
51
- return { from: previousFrom, to: previousTo };
54
+ return {
55
+ from: shiftCalendar(range.from, 0, -1),
56
+ to: shiftCalendar(range.to, 0, -1),
57
+ };
52
58
  }
53
59
 
54
60
  const COMPARISON_RESOLVERS: Record<
@@ -0,0 +1,121 @@
1
+ import { readerTimeZone } from "../../utils/regional";
2
+
3
+ /** The fields of a wall-clock time, months counted from 0 as `Date` does. */
4
+ export interface WallTime {
5
+ year: number;
6
+ month: number;
7
+ day: number;
8
+ hours: number;
9
+ minutes: number;
10
+ seconds: number;
11
+ milliseconds: number;
12
+ }
13
+
14
+ /**
15
+ * A calendar in one time zone: the wall-clock time of an instant, and the
16
+ * instant of a wall-clock time. Fields out of range roll over as `Date`'s do
17
+ * (day 0 is the last day of the month before).
18
+ */
19
+ export interface PeriodClock {
20
+ wall(date: Date): WallTime;
21
+ instant(wall: WallTime): Date;
22
+ }
23
+
24
+ /** The calendar of the zone the code runs in, through `Date`'s own methods. */
25
+ export const localClock: PeriodClock = {
26
+ wall: (date) => ({
27
+ year: date.getFullYear(),
28
+ month: date.getMonth(),
29
+ day: date.getDate(),
30
+ hours: date.getHours(),
31
+ minutes: date.getMinutes(),
32
+ seconds: date.getSeconds(),
33
+ milliseconds: date.getMilliseconds(),
34
+ }),
35
+ instant: (wall) =>
36
+ new Date(
37
+ wall.year,
38
+ wall.month,
39
+ wall.day,
40
+ wall.hours,
41
+ wall.minutes,
42
+ wall.seconds,
43
+ wall.milliseconds,
44
+ ),
45
+ };
46
+
47
+ const WALL_PARTS = ["year", "month", "day", "hour", "minute", "second"];
48
+ const zonedFormatters = new Map<string, Intl.DateTimeFormat>();
49
+
50
+ function zonedFormatter(timeZone: string): Intl.DateTimeFormat {
51
+ let formatter = zonedFormatters.get(timeZone);
52
+ if (!formatter) {
53
+ formatter = new Intl.DateTimeFormat("en-US", {
54
+ timeZone,
55
+ hourCycle: "h23",
56
+ year: "numeric",
57
+ month: "numeric",
58
+ day: "numeric",
59
+ hour: "numeric",
60
+ minute: "numeric",
61
+ second: "numeric",
62
+ });
63
+ zonedFormatters.set(timeZone, formatter);
64
+ }
65
+ return formatter;
66
+ }
67
+
68
+ function utcOf(wall: WallTime): number {
69
+ return Date.UTC(
70
+ wall.year,
71
+ wall.month,
72
+ wall.day,
73
+ wall.hours,
74
+ wall.minutes,
75
+ wall.seconds,
76
+ wall.milliseconds,
77
+ );
78
+ }
79
+
80
+ /** The calendar of `timeZone`, through `Intl` whatever zone the code runs in. */
81
+ export function zonedClock(timeZone: string): PeriodClock {
82
+ const wall = (date: Date): WallTime => {
83
+ const parts = Object.fromEntries(
84
+ zonedFormatter(timeZone)
85
+ .formatToParts(date)
86
+ .filter((part) => WALL_PARTS.includes(part.type))
87
+ .map((part) => [part.type, Number(part.value)]),
88
+ );
89
+ return {
90
+ year: parts.year!,
91
+ month: parts.month! - 1,
92
+ day: parts.day!,
93
+ hours: parts.hour!,
94
+ minutes: parts.minute!,
95
+ seconds: parts.second!,
96
+ milliseconds: date.getUTCMilliseconds(),
97
+ };
98
+ };
99
+ // The zone's offset at an instant: how far its wall clock is from UTC.
100
+ const offsetAt = (time: number) => utcOf(wall(new Date(time))) - time;
101
+ return {
102
+ wall,
103
+ instant: (target) => {
104
+ const asUtc = utcOf(target);
105
+ // Twice: the offset at the first guess is the right one unless the
106
+ // guess and the answer fall on either side of a daylight saving change.
107
+ const guess = asUtc - offsetAt(asUtc);
108
+ return new Date(asUtc - offsetAt(guess));
109
+ },
110
+ };
111
+ }
112
+
113
+ /**
114
+ * The calendar a period is resolved in: the reader's browser's zone. That is
115
+ * the code's own in the browser; on a server, the zone the browser's cookie
116
+ * names (see `readerTimeZone`), or the server's own without one.
117
+ */
118
+ export function readerClock(): PeriodClock {
119
+ const zone = readerTimeZone();
120
+ return zone ? zonedClock(zone) : localClock;
121
+ }
@@ -0,0 +1,42 @@
1
+ import { getCurrentInstance } from "vue";
2
+
3
+ /**
4
+ * The instant a server render resolves its periods from, handed to the
5
+ * browser with the page.
6
+ */
7
+ const RENDER_NOW_STATE_KEY = "dms-period-render-now";
8
+
9
+ /**
10
+ * Whether the component setting up is hydrating the server's HTML: Vue binds
11
+ * a hydrating component's vnode to its server-rendered node before its setup
12
+ * runs, where a fresh mount has no node yet.
13
+ */
14
+ function isHydratingSetup(): boolean {
15
+ return !!getCurrentInstance()?.vnode.el;
16
+ }
17
+
18
+ /**
19
+ * The instant a period set up now resolves from, or `null` for the clock's.
20
+ *
21
+ * A server render resolves every period of the request from one instant,
22
+ * which it transfers; the browser hydrating that render resolves from the
23
+ * same one, so a relative preset (`last-24h`) or a calendar one read across
24
+ * midnight keeps the bounds, key and labels the server rendered, and nothing
25
+ * reads again on hydration. Anywhere else (a client navigation, a call
26
+ * outside a component) it is `null`: the period follows the clock.
27
+ */
28
+ export function periodRenderNow(): Date | null {
29
+ if (!getCurrentInstance()) return null;
30
+ if (typeof window === "undefined") {
31
+ const now = useDmsState<number>(RENDER_NOW_STATE_KEY, () => Date.now(), {
32
+ transfer: true,
33
+ });
34
+ return new Date(now.value);
35
+ }
36
+ if (!isHydratingSetup()) return null;
37
+ const transferred = useDmsState<number | null>(
38
+ RENDER_NOW_STATE_KEY,
39
+ () => null,
40
+ ).value;
41
+ return typeof transferred === "number" ? new Date(transferred) : null;
42
+ }
@@ -0,0 +1,31 @@
1
+ import type { Ref } from "vue";
2
+ import { isKnownTimeZone } from "../../utils/regional";
3
+
4
+ /**
5
+ * The browser's time zone, kept in a cookie for a server render: a period is
6
+ * made of calendar days in the browser's zone, which the server has no other
7
+ * way to know.
8
+ */
9
+ export const TIME_ZONE_COOKIE = "dms-time-zone";
10
+ const TIME_ZONE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
11
+
12
+ /** The browser's time zone as its cookie holds it; `null` before it is written. */
13
+ export function useTimeZoneCookie(): Ref<string | null> {
14
+ return useDmsCookie<string | null>(TIME_ZONE_COOKIE, {
15
+ default: () => null,
16
+ maxAge: TIME_ZONE_COOKIE_MAX_AGE,
17
+ path: "/",
18
+ sameSite: "lax",
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Whether this render resolves a period as the browser does. The browser
24
+ * always does; a server once the browser's cookie names its time zone, in
25
+ * which the presets then resolve (`readerClock`).
26
+ */
27
+ export function resolvesPeriodsAsBrowser(): boolean {
28
+ if (typeof window !== "undefined") return true;
29
+ const zone = useTimeZoneCookie().value;
30
+ return !!zone && isKnownTimeZone(zone);
31
+ }
@@ -8,22 +8,39 @@ import {
8
8
  type PeriodPreset,
9
9
  type PeriodRange,
10
10
  } from "./types";
11
+ import { readerClock } from "./periodClock";
12
+
13
+ // Resolved in the reader's calendar (`readerClock`): the browser's zone, on
14
+ // the server as in the browser.
15
+ function atTime(
16
+ date: Date,
17
+ hours: number,
18
+ minutes: number,
19
+ seconds: number,
20
+ milliseconds: number,
21
+ ): Date {
22
+ const clock = readerClock();
23
+ return clock.instant({
24
+ ...clock.wall(date),
25
+ hours,
26
+ minutes,
27
+ seconds,
28
+ milliseconds,
29
+ });
30
+ }
11
31
 
12
32
  function startOfDay(date: Date): Date {
13
- const next = new Date(date);
14
- next.setHours(0, 0, 0, 0);
15
- return next;
33
+ return atTime(date, 0, 0, 0, 0);
16
34
  }
17
35
 
18
36
  function endOfDay(date: Date): Date {
19
- const next = new Date(date);
20
- next.setHours(
37
+ return atTime(
38
+ date,
21
39
  END_OF_DAY_HOURS,
22
40
  END_OF_DAY_MINUTES,
23
41
  END_OF_DAY_SECONDS,
24
42
  END_OF_DAY_MS,
25
43
  );
26
- return next;
27
44
  }
28
45
 
29
46
  function addDays(date: Date, amount: number): Date {
@@ -34,35 +51,47 @@ function addHours(date: Date, amount: number): Date {
34
51
  return new Date(date.getTime() + amount * MS_PER_HOUR);
35
52
  }
36
53
 
54
+ /** Midnight of a day given as year, month and day; they roll over as `Date`'s. */
55
+ function dayStart(year: number, month: number, day: number): Date {
56
+ return readerClock().instant({
57
+ year,
58
+ month,
59
+ day,
60
+ hours: 0,
61
+ minutes: 0,
62
+ seconds: 0,
63
+ milliseconds: 0,
64
+ });
65
+ }
66
+
37
67
  function startOfMonth(date: Date, monthOffset = 0): Date {
38
- return new Date(date.getFullYear(), date.getMonth() + monthOffset, 1);
68
+ const { year, month } = readerClock().wall(date);
69
+ return dayStart(year, month + monthOffset, 1);
39
70
  }
40
71
 
41
72
  function endOfMonth(date: Date, monthOffset = 0): Date {
42
- const next = new Date(
43
- date.getFullYear(),
44
- date.getMonth() + monthOffset + 1,
45
- 0,
46
- );
47
- return endOfDay(next);
73
+ const { year, month } = readerClock().wall(date);
74
+ return endOfDay(dayStart(year, month + monthOffset + 1, 0));
48
75
  }
49
76
 
50
77
  function startOfQuarter(date: Date, quarterOffset = 0): Date {
51
- const quarter = Math.floor(date.getMonth() / 3) + quarterOffset;
52
- return new Date(date.getFullYear(), quarter * 3, 1);
78
+ const { year, month } = readerClock().wall(date);
79
+ const quarter = Math.floor(month / 3) + quarterOffset;
80
+ return dayStart(year, quarter * 3, 1);
53
81
  }
54
82
 
55
83
  function endOfQuarter(date: Date, quarterOffset = 0): Date {
56
- const quarter = Math.floor(date.getMonth() / 3) + quarterOffset;
57
- return endOfDay(new Date(date.getFullYear(), quarter * 3 + 3, 0));
84
+ const { year, month } = readerClock().wall(date);
85
+ const quarter = Math.floor(month / 3) + quarterOffset;
86
+ return endOfDay(dayStart(year, quarter * 3 + 3, 0));
58
87
  }
59
88
 
60
89
  function startOfYear(date: Date, yearOffset = 0): Date {
61
- return new Date(date.getFullYear() + yearOffset, 0, 1);
90
+ return dayStart(readerClock().wall(date).year + yearOffset, 0, 1);
62
91
  }
63
92
 
64
93
  function endOfYear(date: Date, yearOffset = 0): Date {
65
- return endOfDay(new Date(date.getFullYear() + yearOffset, 11, 31));
94
+ return endOfDay(dayStart(readerClock().wall(date).year + yearOffset, 11, 31));
66
95
  }
67
96
 
68
97
  const PRESET_RANGE_RESOLVERS: Record<
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  computed,
3
3
  getCurrentScope,
4
+ onMounted,
4
5
  onScopeDispose,
5
6
  ref,
7
+ shallowRef,
6
8
  watch,
7
9
  type ComputedRef,
8
10
  type Ref,
@@ -23,6 +25,7 @@ import {
23
25
  resolvePresetRange,
24
26
  } from "./presetResolvers";
25
27
  import { resolveComparisonRange } from "./compareResolvers";
28
+ import { periodRenderNow } from "./periodRenderNow";
26
29
 
27
30
  export interface UsePeriodOptions {
28
31
  defaultPreset?: PeriodPreset;
@@ -47,6 +50,11 @@ export interface UsePeriodReturn {
47
50
  presets: ComputedRef<PeriodPreset[]>;
48
51
  comparisons: ComputedRef<PeriodComparison[]>;
49
52
  state: ComputedRef<PeriodState>;
53
+ /**
54
+ * The instant `state` was resolved from: what a label relative to now
55
+ * reads, so it holds still with the period (see `periodRenderNow`).
56
+ */
57
+ resolvedAt: ComputedRef<Date>;
50
58
  setPreset: (preset: PeriodPreset) => void;
51
59
  setComparison: (comparison: PeriodComparison) => void;
52
60
  setCustomRange: (range: PeriodRange | null) => void;
@@ -135,8 +143,7 @@ interface PeriodRefs {
135
143
  customCompareRange: Ref<PeriodRange | null>;
136
144
  }
137
145
 
138
- function buildState(refs: PeriodRefs): PeriodState {
139
- const now = new Date();
146
+ function buildState(refs: PeriodRefs, now: Date): PeriodState {
140
147
  const fallback = refs.customRange.value ?? defaultFallbackRange(now);
141
148
  const range = resolvePresetRange(refs.preset.value, now, fallback);
142
149
  const compareRange = resolveComparisonRange(
@@ -176,6 +183,9 @@ function makeSetters(refs: PeriodRefs) {
176
183
  * Reactive period state machine behind `PeriodSelector` — presets,
177
184
  * comparisons and custom ranges resolved into a publishable `state`.
178
185
  *
186
+ * A server render and the browser hydrating it resolve from the same instant
187
+ * (`periodRenderNow`), so both render the same bounds.
188
+ *
179
189
  * Relative presets (`last-hour`, `last-24h`) re-resolve every minute, and
180
190
  * calendar presets (`today`, `last-7-days`, `this-month`...) at each local
181
191
  * midnight, so a page left open overnight moves on to the new day.
@@ -201,10 +211,28 @@ export function usePeriod(options: UsePeriodOptions = {}): UsePeriodReturn {
201
211
  () => options.comparisons ?? DEFAULT_COMPARISONS,
202
212
  );
203
213
  const presetClock = usePresetClock(refs.preset, options.holdClock);
204
- const state = computed<PeriodState>(() => {
214
+ // The server render's instant, until the clock ticks or the period changes
215
+ // once on screen: what the setup applies (the URL's period) the server
216
+ // applied as well.
217
+ const renderNow = shallowRef(periodRenderNow());
218
+ if (renderNow.value) {
219
+ onMounted(() => {
220
+ watch(
221
+ [presetClock, ...Object.values(refs)],
222
+ () => {
223
+ renderNow.value = null;
224
+ },
225
+ { flush: "sync" },
226
+ );
227
+ });
228
+ }
229
+ const resolution = computed(() => {
205
230
  void presetClock.value;
206
- return buildState(refs);
231
+ const now = renderNow.value ?? new Date();
232
+ return { now, state: buildState(refs, now) };
207
233
  });
234
+ const state = computed<PeriodState>(() => resolution.value.state);
235
+ const resolvedAt = computed<Date>(() => resolution.value.now);
208
236
  const setters = makeSetters(refs);
209
237
 
210
238
  return {
@@ -212,6 +240,7 @@ export function usePeriod(options: UsePeriodOptions = {}): UsePeriodReturn {
212
240
  presets,
213
241
  comparisons,
214
242
  state,
243
+ resolvedAt,
215
244
  ...setters,
216
245
  };
217
246
  }