@antelopejs/dms 0.7.9 → 0.7.11

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 (173) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/config.d.ts +2 -0
  3. package/dist/config.js +9 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/implementations/dms/api-base-url.d.ts +1 -0
  6. package/dist/implementations/dms/api-base-url.js +6 -0
  7. package/dist/implementations/dms/api-base-url.js.map +1 -0
  8. package/dist/implementations/dms/frontend-modules.js +7 -6
  9. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  10. package/dist/implementations/dms/page.js +8 -2
  11. package/dist/implementations/dms/page.js.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/test/unit/api-base-url.test.d.ts +1 -0
  15. package/dist/test/unit/api-base-url.test.js +31 -0
  16. package/dist/test/unit/api-base-url.test.js.map +1 -0
  17. package/dist/test/unit/interfaces/dms/page-layout-query.test.d.ts +1 -0
  18. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +106 -0
  19. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -0
  20. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -0
  21. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  22. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.d.ts +1 -0
  23. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +46 -0
  24. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -0
  25. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.d.ts +1 -0
  26. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +26 -0
  27. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -0
  28. package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +10 -0
  29. package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
  30. package/dist/test/unit/interfaces/dms-base/guard.test.d.ts +1 -0
  31. package/dist/test/unit/interfaces/dms-base/guard.test.js +53 -0
  32. package/dist/test/unit/interfaces/dms-base/guard.test.js.map +1 -0
  33. package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js +23 -0
  34. package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js.map +1 -1
  35. package/dist/test/unit/interfaces/dms-base/page-live.test.js +55 -0
  36. package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -1
  37. package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +17 -0
  38. package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -1
  39. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +2 -0
  40. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
  41. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +28 -0
  42. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  43. package/frontend-vue/layers/dms-core/app/build/composables/user/preferencesCookie.ts +149 -0
  44. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +321 -0
  45. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +4 -3
  46. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +4 -3
  47. package/frontend-vue/layers/dms-core/app/composables/period/types.ts +3 -1
  48. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +2 -2
  49. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +9 -2
  50. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +48 -16
  51. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +21 -4
  52. package/frontend-vue/layers/dms-core/app/composables/user/usePreferences.ts +81 -49
  53. package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +35 -0
  54. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
  55. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
  56. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +10 -1
  57. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +32 -1
  58. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +13 -3
  59. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +27 -3
  60. package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +16 -0
  61. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +75 -0
  62. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +8 -1
  63. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +13 -2
  64. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +7 -0
  65. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +7 -0
  66. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -4
  67. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +59 -0
  68. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
  69. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
  70. package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +2 -0
  71. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockInlineStatus.vue +67 -0
  72. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +60 -20
  73. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +15 -8
  74. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +14 -0
  75. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +29 -4
  76. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +76 -28
  77. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +26 -2
  78. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useGuard.ts +115 -0
  79. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +1 -1
  80. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +58 -3
  81. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/secondaryTarget.ts +86 -0
  82. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +5 -0
  83. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +6 -0
  84. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveCadence.ts +42 -0
  85. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveHolds.ts +66 -0
  86. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +42 -0
  87. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveStorage.ts +42 -0
  88. package/frontend-vue/layers/dms-ui/app/build/composables/live/missedTally.ts +65 -0
  89. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +247 -88
  90. package/frontend-vue/layers/dms-ui/app/build/composables/live/useLivePeriodSpan.ts +33 -0
  91. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +79 -5
  92. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparisonControl.ts +70 -0
  93. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +55 -0
  94. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +41 -5
  95. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +22 -9
  96. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +13 -6
  97. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +1 -0
  98. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +27 -2
  99. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +18 -3
  100. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +19 -26
  101. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +66 -0
  102. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +7 -1
  103. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +65 -11
  104. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTablePreferences.ts +149 -0
  105. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +39 -8
  106. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
  107. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +16 -11
  108. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +39 -16
  109. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +29 -0
  110. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +18 -5
  111. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts +17 -0
  112. package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +79 -0
  113. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +46 -1
  114. package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +4 -0
  115. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +40 -2
  116. package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +11 -1
  117. package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +12 -2
  118. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +129 -4
  119. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +6 -0
  120. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +6 -1
  121. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +33 -0
  122. package/frontend-vue/layers/dms-ui/app/components/form/components/ChoiceCards.vue +22 -4
  123. package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +75 -0
  124. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -17
  125. package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +15 -0
  126. package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +12 -15
  127. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +104 -16
  128. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +104 -98
  129. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +11 -1
  130. package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +2 -0
  131. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +11 -1
  132. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +15 -1
  133. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +86 -10
  134. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewShiftF.ts +20 -0
  135. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +12 -0
  136. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -0
  137. package/frontend-vue/layers/dms-ui/app/composables/table-view/useTableViewEmptyState.ts +55 -0
  138. package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
  139. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +13 -4
  140. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +12 -3
  141. package/frontend-vue/tests/activity-feed-fill-height.test.ts +1 -0
  142. package/frontend-vue/tests/activity-feed-head.test.ts +140 -0
  143. package/frontend-vue/tests/block-not-found.test.ts +220 -0
  144. package/frontend-vue/tests/choice-cards-columns.test.ts +60 -0
  145. package/frontend-vue/tests/confirm-secondary-action.test.ts +269 -0
  146. package/frontend-vue/tests/guard.test.ts +242 -0
  147. package/frontend-vue/tests/header-action-page-query.test.ts +286 -0
  148. package/frontend-vue/tests/live-toggle.test.ts +87 -1
  149. package/frontend-vue/tests/page-layout-registration.test.ts +1 -0
  150. package/frontend-vue/tests/page-live-parity.test.ts +367 -0
  151. package/frontend-vue/tests/page-realtime-remount.test.ts +185 -0
  152. package/frontend-vue/tests/period-selector-parity.test.ts +138 -0
  153. package/frontend-vue/tests/period-selector-url.test.ts +84 -6
  154. package/frontend-vue/tests/period-url-query.test.ts +52 -0
  155. package/frontend-vue/tests/preferences-cookie-size.test.ts +311 -0
  156. package/frontend-vue/tests/row-side-panel.test.ts +109 -10
  157. package/frontend-vue/tests/tab-slot-spacing.test.ts +8 -0
  158. package/frontend-vue/tests/table-actions-search-label.test.ts +57 -1
  159. package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
  160. package/frontend-vue/tests/table-density-menu.test.ts +4 -3
  161. package/frontend-vue/tests/table-live-hold.test.ts +95 -5
  162. package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
  163. package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
  164. package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
  165. package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
  166. package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
  167. package/frontend-vue/tests/table-view-views.test.ts +56 -15
  168. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
  169. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  170. package/package.json +2 -2
  171. package/skills/dms/SKILL.md +9 -3
  172. package/skills/dms-module/SKILL.md +5 -0
  173. package/skills/dms-pages/REFERENCE.md +23 -2
@@ -26,6 +26,11 @@ interface ListRowProps {
26
26
  description?: string;
27
27
  /** Meta entries, joined by "·" (the `meta` slot overrides them). */
28
28
  meta?: string[];
29
+ /**
30
+ * A last line under the meta, dimmed and mono whatever `mono` says: the
31
+ * evidence behind a feed entry (`400 "Cart is empty" × 16`).
32
+ */
33
+ detail?: string;
29
34
  /** Mono meta (times, paths, ids); the title stays sans. */
30
35
  mono?: boolean;
31
36
  /**
@@ -81,6 +86,7 @@ const props = withDefaults(defineProps<ListRowProps>(), {
81
86
  title: undefined,
82
87
  description: undefined,
83
88
  meta: undefined,
89
+ detail: undefined,
84
90
  mono: false,
85
91
  size: "md",
86
92
  // Undefined, not false: an absent read state is not "read".
@@ -123,6 +129,7 @@ const theme = tv({
123
129
  // The "·" closes each entry followed by another (rather than opening it),
124
130
  // so a wrapped line never starts with a separator.
125
131
  meta: "flex flex-wrap items-center gap-x-2 gap-y-1 [&>span:has(+span)]:after:text-dimmed [&>span:has(+span)]:after:ms-2 [&>span:has(+span)]:after:content-['·']",
132
+ detail: "text-dimmed mt-0.5 font-mono text-[11px] font-medium",
126
133
  trailing: "flex shrink-0 items-center gap-2",
127
134
  trailingText:
128
135
  "text-dimmed text-right font-mono text-[11.5px] font-medium whitespace-nowrap",
@@ -183,6 +190,7 @@ const theme = tv({
183
190
  true: {
184
191
  title: "truncate max-sm:whitespace-normal",
185
192
  meta: "flex-nowrap overflow-hidden whitespace-nowrap max-sm:flex-wrap max-sm:whitespace-normal",
193
+ detail: "truncate max-sm:whitespace-normal",
186
194
  },
187
195
  },
188
196
  bare: {
@@ -285,6 +293,13 @@ const unreadText = computed(
285
293
  </span>
286
294
  </slot>
287
295
  </div>
296
+ <p
297
+ v-if="props.detail"
298
+ :class="ui.detail()"
299
+ :title="props.truncate ? props.detail : undefined"
300
+ >
301
+ {{ props.detail }}
302
+ </p>
288
303
  </div>
289
304
  </div>
290
305
  <div v-if="hasTrailing && !props.bare" :class="ui.trailing()">
@@ -10,6 +10,8 @@ import DmsBlockActions, {
10
10
  type BlockAction,
11
11
  } from "../../build/components/blocks/BlockActions.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 { useWatch } from "../../../../dms-core/app/composables/watch/useWatch";
14
16
  import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
15
17
  import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
@@ -119,6 +121,10 @@ const showSkeleton = computed(() => isLoading.value && !data.value);
119
121
  const hasError = computed(
120
122
  () => !isLoading.value && !data.value && Boolean(error.value),
121
123
  );
124
+ // A 404 is a status too, but nothing to retry: the figures do not exist.
125
+ const notFound = computed(() =>
126
+ hasError.value ? readFetchNotFound(error.value) : null,
127
+ );
122
128
  // The placeholder is the meter's own shape: the label line (the label is
123
129
  // configuration, shown as is), the bar at its size, the legend line.
124
130
  const SKELETON_TRACK_HEIGHTS: Record<string, string> = {
@@ -162,24 +168,15 @@ const Wrapper = props.card ? resolveComponent("DmsCard") : "div";
162
168
  <USkeleton :aria-label="t('dms.a11y.loading')" class="h-2.5 w-40" />
163
169
  </span>
164
170
  </div>
165
- <div v-else-if="hasError" class="grid min-w-0 gap-1.5" role="alert">
171
+ <div v-else-if="hasError" class="grid min-w-0 gap-1.5">
166
172
  <span v-if="props.label" class="text-toned text-[13px]">
167
173
  {{ translate(props.label) }}
168
174
  </span>
169
- <div class="flex items-center justify-between gap-2">
170
- <p class="text-error flex min-w-0 items-center gap-1.5 text-[12.5px]">
171
- <UIcon name="i-ph-warning-circle" class="size-4 shrink-0" />
172
- <span class="truncate">{{ t("dms.table.load_error_title") }}</span>
173
- </p>
174
- <UButton
175
- :label="t('dms.table.load_error_retry')"
176
- icon="i-ph-arrows-clockwise"
177
- color="neutral"
178
- variant="outline"
179
- size="xs"
180
- @click="refresh()"
181
- />
182
- </div>
175
+ <DmsBlockInlineStatus
176
+ :state="notFound ? 'not-found' : 'error'"
177
+ :not-found="notFound"
178
+ @retry="refresh()"
179
+ />
183
180
  </div>
184
181
  <DmsMeter
185
182
  v-else
@@ -11,18 +11,28 @@ import type {
11
11
  PeriodRange,
12
12
  } from "#dms-core/app/composables/period/types";
13
13
  import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
14
- import { usePeriodDraft } from "../../build/composables/period/usePeriodDraft";
14
+ import {
15
+ startingRange,
16
+ usePeriodDraft,
17
+ } from "../../build/composables/period/usePeriodDraft";
18
+ import {
19
+ type ComparisonControl,
20
+ resolveComparisonControl,
21
+ } from "../../build/composables/period/periodComparisonControl";
22
+ import { isRelativePreset } from "#dms-core/app/composables/period/presetResolvers";
15
23
  import { usePeriodLocation } from "../../build/composables/period/usePeriodLocation";
16
24
  import {
17
25
  type CalendarRange,
18
26
  calendarToRange,
19
27
  formatPeriodRange,
28
+ formatRelativeInstant,
20
29
  periodDuration,
21
30
  rangeToCalendar,
22
31
  } from "../../build/composables/period/periodDisplay";
23
32
  import PeriodRangeCalendar from "../../build/components/period/PeriodRangeCalendar.vue";
24
33
  import { useUserRegionalPreferences } from "#dms-core/app/composables/user/useUserRegionalPreferences";
25
34
  import { usePageLive } from "../../composables/live/usePageLive";
35
+ import { useLivePeriodSpan } from "../../build/composables/live/useLivePeriodSpan";
26
36
 
27
37
  interface Props extends DefaultComponentProps {
28
38
  id: string;
@@ -35,6 +45,9 @@ interface Props extends DefaultComponentProps {
35
45
  align?: "left" | "center" | "right";
36
46
  size?: "xs" | "sm" | "md" | "lg" | "xl";
37
47
  showRangeLabel?: boolean;
48
+ rangeLabel?: "absolute" | "relative";
49
+ comparisonControl?: ComparisonControl;
50
+ rememberKey?: string;
38
51
  variant?: "default" | "segmented";
39
52
  }
40
53
 
@@ -44,6 +57,7 @@ const CARET_UP_ICON = "i-ph-caret-up-light";
44
57
  const CUSTOM_PRESET: PeriodPreset = "custom";
45
58
  const NO_COMPARISON: PeriodComparison = "none";
46
59
  const SEGMENTED_VARIANT: NonNullable<Props["variant"]> = "segmented";
60
+ const RELATIVE_RANGE_LABEL: NonNullable<Props["rangeLabel"]> = "relative";
47
61
  // Two months side by side from this width; one below (design @container 760px).
48
62
  const TWO_MONTHS_QUERY = "(min-width: 768px)";
49
63
 
@@ -51,6 +65,7 @@ const props = withDefaults(defineProps<Props>(), {
51
65
  align: "right",
52
66
  size: "sm",
53
67
  showRangeLabel: true,
68
+ rangeLabel: "absolute",
54
69
  variant: "default",
55
70
  });
56
71
 
@@ -98,19 +113,23 @@ const { t, locale } = useI18n();
98
113
  // A paused live page keeps its period where it was: a relative preset
99
114
  // sliding every minute would refetch every block reading it.
100
115
  const { isPaused: isPagePaused } = usePageLive();
116
+ const comparisonSetup = resolveComparisonControl(props);
101
117
  const period = usePeriod({
102
118
  defaultPreset: props.defaultPreset,
103
- defaultComparison: props.defaultComparison,
119
+ defaultComparison: comparisonSetup.defaultComparison,
104
120
  presets: props.presets,
105
- comparisons: props.comparisons,
121
+ comparisons: comparisonSetup.comparisons,
106
122
  holdClock: () => isPagePaused.value,
107
123
  });
124
+ // A page declaring `live.slowRateAfter` slows down over a long period.
125
+ useLivePeriodSpan(() => period.state.value.range);
108
126
 
109
127
  // Before the scope registers, so the cards' first request already carries
110
128
  // the period of the URL (or of the user's last choice), never the default.
111
129
  usePeriodLocation(period, {
112
130
  id: props.id,
113
131
  pageId: props.pageId,
132
+ rememberKey: props.rememberKey,
114
133
  fallback: {
115
134
  preset: period.preset.value,
116
135
  comparison: period.comparison.value,
@@ -226,7 +245,7 @@ function applyDraft() {
226
245
  }
227
246
 
228
247
  const draftCalendar = computed<CalendarRange>(() =>
229
- rangeToCalendar(draft.range.value),
248
+ rangeToCalendar(draft.range.value ?? startingRange(new Date())),
230
249
  );
231
250
 
232
251
  const draftCompareCalendar = computed<CalendarRange | null>(() => {
@@ -272,7 +291,9 @@ const draftComparison = computed<PeriodComparison>({
272
291
  });
273
292
 
274
293
  const draftDurationLabel = computed(() => {
275
- const { unit, count } = periodDuration(draft.range.value);
294
+ const range = draft.range.value;
295
+ if (!range) return presetLabel(draft.preset.value);
296
+ const { unit, count } = periodDuration(range);
276
297
  return t(`dms.period.popover.${unit}`, { count }, count);
277
298
  });
278
299
 
@@ -282,12 +303,36 @@ const draftCompareLabel = computed(() => {
282
303
  });
283
304
 
284
305
  // ── Trigger, comparison chip, range label ──────────────────────────────
285
- const committedRangeLabel = computed(() =>
286
- formatPeriodRange(period.state.value.range, locale.value),
306
+ const committedRangeLabel = computed(() => {
307
+ const { range } = period.state.value;
308
+ return range ? formatPeriodRange(range, locale.value) : null;
309
+ });
310
+
311
+ /**
312
+ * The instant a relative label is read from: a rolling window's own end, so
313
+ * it holds still with the window while the page is paused.
314
+ */
315
+ function labelNow(preset: PeriodPreset, range: PeriodRange): Date {
316
+ return isRelativePreset(preset) ? range.to : new Date();
317
+ }
318
+
319
+ // Only a window reaching now reads "since …"; one that has ended keeps its
320
+ // bounds.
321
+ const relativeRangeLabel = computed<string | null>(() => {
322
+ const { preset, range } = period.state.value;
323
+ if (props.rangeLabel !== RELATIVE_RANGE_LABEL || !range) return null;
324
+ const now = labelNow(preset, range);
325
+ if (range.to.getTime() < now.getTime()) return null;
326
+ const from = formatRelativeInstant(range.from, now, locale.value);
327
+ return t("dms.period.since", { from }, `since ${from}`);
328
+ });
329
+
330
+ const rangeLabelText = computed(
331
+ () => relativeRangeLabel.value ?? committedRangeLabel.value,
287
332
  );
288
333
 
289
334
  const triggerLabel = computed(() =>
290
- isCustomActive.value
335
+ isCustomActive.value && committedRangeLabel.value
291
336
  ? committedRangeLabel.value
292
337
  : presetLabel(period.preset.value),
293
338
  );
@@ -318,6 +363,21 @@ const canCompare = computed(() =>
318
363
  period.comparisons.value.some((comparison) => comparison !== NO_COMPARISON),
319
364
  );
320
365
 
366
+ const isComparisonMenu = computed(
367
+ () => canCompare.value && comparisonSetup.control === "menu",
368
+ );
369
+ const toggledComparison = comparisonSetup.toggled;
370
+ const canChooseComparison = computed(
371
+ () => canCompare.value && comparisonSetup.control !== "hidden",
372
+ );
373
+ // Every period has no bounds, so nothing to compare it with.
374
+ const isComparisonDisabled = computed(() => !period.state.value.range);
375
+
376
+ function toggleComparison() {
377
+ if (!toggledComparison) return;
378
+ period.setComparison(hasComparison.value ? NO_COMPARISON : toggledComparison);
379
+ }
380
+
321
381
  const comparisonItems = computed(() => [
322
382
  period.comparisons.value.map((comparison) => ({
323
383
  label: comparisonLabel(comparison),
@@ -343,7 +403,7 @@ function compareRangeText(compare: PeriodRange, main: PeriodRange): string {
343
403
 
344
404
  const compareRangeLabel = computed(() => {
345
405
  const { compareRange, range } = period.state.value;
346
- return compareRange ? compareRangeText(compareRange, range) : null;
406
+ return compareRange && range ? compareRangeText(compareRange, range) : null;
347
407
  });
348
408
 
349
409
  // ── Segmented variant ──────────────────────────────────────────────────
@@ -361,6 +421,13 @@ interface SegmentItem {
361
421
  icon?: string;
362
422
  }
363
423
 
424
+ function customSegmentLabel(): string {
425
+ const { range } = period.state.value;
426
+ return isCustomActive.value && range
427
+ ? formatPeriodRange(range, locale.value, false)
428
+ : presetLabel(CUSTOM_PRESET);
429
+ }
430
+
364
431
  const segmentedItems = computed<SegmentItem[]>(() => {
365
432
  const items: SegmentItem[] = listedPresets.value.map((preset) => ({
366
433
  label: segmentLabel(preset),
@@ -368,9 +435,7 @@ const segmentedItems = computed<SegmentItem[]>(() => {
368
435
  }));
369
436
  if (isCustomEnabled.value) {
370
437
  items.push({
371
- label: isCustomActive.value
372
- ? formatPeriodRange(period.state.value.range, locale.value, false)
373
- : presetLabel(CUSTOM_PRESET),
438
+ label: customSegmentLabel(),
374
439
  value: CUSTOM_PRESET,
375
440
  icon: CALENDAR_ICON,
376
441
  });
@@ -564,7 +629,7 @@ function onSegmentedClick(event: MouseEvent) {
564
629
  />
565
630
  </label>
566
631
  <label
567
- v-if="canCompare"
632
+ v-if="canChooseComparison"
568
633
  class="grid gap-1 min-[22rem]:col-span-2 md:col-span-1"
569
634
  >
570
635
  <span :class="FIELD_LABEL_CLASS">
@@ -634,15 +699,38 @@ function onSegmentedClick(event: MouseEvent) {
634
699
  </template>
635
700
  </UPopover>
636
701
 
702
+ <UButton
703
+ v-if="toggledComparison"
704
+ variant="ghost"
705
+ color="neutral"
706
+ :size="size"
707
+ :aria-pressed="hasComparison"
708
+ :disabled="isComparisonDisabled"
709
+ class="order-2"
710
+ :class="hasComparison ? 'text-toned' : 'text-dimmed'"
711
+ @click="toggleComparison"
712
+ >
713
+ <template #leading>
714
+ <span
715
+ class="h-[2.5px] w-3 shrink-0 rounded-[2px]"
716
+ :class="comparisonKeyClass(hasComparison)"
717
+ aria-hidden="true"
718
+ />
719
+ </template>
720
+ {{ comparisonLabel(toggledComparison) }}
721
+ </UButton>
722
+
637
723
  <UDropdownMenu
638
- v-if="canCompare"
724
+ v-else-if="isComparisonMenu"
639
725
  :items="comparisonItems"
640
726
  :content="{ align: 'end' }"
727
+ :disabled="isComparisonDisabled"
641
728
  >
642
729
  <UButton
643
730
  variant="ghost"
644
731
  color="neutral"
645
732
  :size="size"
733
+ :disabled="isComparisonDisabled"
646
734
  :trailing-icon="CARET_DOWN_ICON"
647
735
  :aria-label="$t('dms.period.comparison_aria', 'Comparison')"
648
736
  class="text-muted order-2"
@@ -663,12 +751,12 @@ function onSegmentedClick(event: MouseEvent) {
663
751
  a render that straddles a minute, or a server in another zone, may
664
752
  print other bounds than the browser, which then corrects them. -->
665
753
  <span
666
- v-if="showRangeLabel"
754
+ v-if="showRangeLabel && rangeLabelText"
667
755
  data-allow-mismatch="text"
668
756
  class="text-dimmed hidden font-mono text-[11.5px] font-medium whitespace-nowrap tabular-nums sm:inline"
669
757
  :class="rangeLabelOrder"
670
758
  >
671
- <b class="text-toned font-[550]">{{ committedRangeLabel }}</b>
759
+ <b class="text-toned font-[550]">{{ rangeLabelText }}</b>
672
760
  <template v-if="compareRangeLabel">
673
761
  ·
674
762
  <span class="text-(--dms-chart-2)">