@antelopejs/dms 0.7.21 → 0.8.1

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 (208) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/implementations/dms/resolver-timeout.d.ts +1 -0
  3. package/dist/implementations/dms/resolver-timeout.js +13 -3
  4. package/dist/implementations/dms/resolver-timeout.js.map +1 -1
  5. package/dist/implementations/dms-auth/account-emails.d.ts +4 -0
  6. package/dist/implementations/dms-auth/account-emails.js +62 -0
  7. package/dist/implementations/dms-auth/account-emails.js.map +1 -0
  8. package/dist/implementations/dms-auth/index.d.ts +1 -3
  9. package/dist/implementations/dms-auth/index.js +15 -61
  10. package/dist/implementations/dms-auth/index.js.map +1 -1
  11. package/dist/pages/settings/users/email-change.js +3 -4
  12. package/dist/pages/settings/users/email-change.js.map +1 -1
  13. package/dist/pages/settings/users/invites.js +1 -1
  14. package/dist/pages/settings/users/invites.js.map +1 -1
  15. package/dist/pages/settings/users/members.js +1 -1
  16. package/dist/pages/settings/users/members.js.map +1 -1
  17. package/dist/pages/settings/users/notifications.js +1 -0
  18. package/dist/pages/settings/users/notifications.js.map +1 -1
  19. package/dist/test/antelope.test.js +2 -1
  20. package/dist/test/antelope.test.js.map +1 -1
  21. package/dist/test/helpers/db.js +19 -11
  22. package/dist/test/helpers/db.js.map +1 -1
  23. package/dist/test/integration/auth/two-factor-codes.test.js +4 -2
  24. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  25. package/dist/test/unit/implementations/dms/dynamic-menu-hardening.test.js +8 -5
  26. package/dist/test/unit/implementations/dms/dynamic-menu-hardening.test.js.map +1 -1
  27. package/dist/test/unit/implementations/dms/layout-banners.test.js +20 -7
  28. package/dist/test/unit/implementations/dms/layout-banners.test.js.map +1 -1
  29. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js +34 -0
  30. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js.map +1 -0
  31. package/dist/test/unit/interfaces/dms/hook-handler-results.test.js +49 -0
  32. package/dist/test/unit/interfaces/dms/hook-handler-results.test.js.map +1 -0
  33. package/dist/test/unit/interfaces/dms/page-topics.test.js +17 -0
  34. package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
  35. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +2 -2
  36. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -1
  37. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js +29 -0
  38. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js.map +1 -0
  39. package/dist/test/unit/interfaces/dms-base/resource-form.test.js +5 -0
  40. package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -1
  41. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js +12 -2
  42. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js.map +1 -1
  43. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +29 -4
  44. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
  45. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +14 -11
  46. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  47. package/dist/test/unit/interfaces/dms-base/table-view-views.test.js +8 -5
  48. package/dist/test/unit/interfaces/dms-base/table-view-views.test.js.map +1 -1
  49. package/dist/test/unit/utils/email-subjects.test.js +27 -0
  50. package/dist/test/unit/utils/email-subjects.test.js.map +1 -0
  51. package/dist/utils/admin-invite-email.js +2 -4
  52. package/dist/utils/admin-invite-email.js.map +1 -1
  53. package/dist/utils/email-change-mail.d.ts +1 -1
  54. package/dist/utils/email-change-mail.js +11 -6
  55. package/dist/utils/email-change-mail.js.map +1 -1
  56. package/dist/utils/email-subjects.d.ts +3 -0
  57. package/dist/utils/email-subjects.js +32 -0
  58. package/dist/utils/email-subjects.js.map +1 -0
  59. package/dist/utils/export-jobs.js +15 -10
  60. package/dist/utils/export-jobs.js.map +1 -1
  61. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +29 -11
  62. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +17 -0
  63. package/frontend-vue/layers/dms-layout/app/app.config.ts +5 -0
  64. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +18 -2
  65. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +16 -1
  66. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useHeaderActionControls.ts +14 -0
  67. package/frontend-vue/layers/dms-layout/app/components/layout/HStack.vue +16 -0
  68. package/frontend-vue/layers/dms-layout/app/components/layout/VStack.vue +2 -0
  69. package/frontend-vue/layers/dms-layout/app/composables/useFooterLinks.ts +11 -1
  70. package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +1 -1
  71. package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +1 -1
  72. package/frontend-vue/layers/dms-layout/app/emails/EmailChangeNotice.vue +13 -7
  73. package/frontend-vue/layers/dms-layout/app/emails/EmailChangeVerification.vue +18 -7
  74. package/frontend-vue/layers/dms-layout/app/emails/EmailExportReady.vue +23 -12
  75. package/frontend-vue/layers/dms-layout/app/emails/EmailResetPassword.vue +18 -7
  76. package/frontend-vue/layers/dms-layout/app/emails/EmailTwoFactor.vue +23 -4
  77. package/frontend-vue/layers/dms-layout/app/emails/EmailUserValidation.vue +18 -6
  78. package/frontend-vue/layers/dms-layout/app/emails/components/EmailOTP.vue +8 -2
  79. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +40 -0
  80. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +40 -0
  81. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +2 -1
  82. package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +11 -3
  83. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +27 -36
  84. package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexTheme.ts +59 -2
  85. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +27 -4
  86. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +10 -2
  87. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +0 -5
  88. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryOrder.ts +23 -0
  89. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +19 -15
  90. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +26 -2
  91. package/frontend-vue/layers/dms-ui/app/build/composables/page/pageUrlValues.ts +41 -0
  92. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +2 -2
  93. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +3 -5
  94. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +7 -12
  95. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +3 -3
  96. package/frontend-vue/layers/dms-ui/app/build/utils/link.ts +7 -3
  97. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +32 -9
  98. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +8 -1
  99. package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +9 -4
  100. package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +5 -11
  101. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +2 -1
  102. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +32 -1
  103. package/frontend-vue/layers/dms-ui/app/components/check-list/CheckList.vue +1 -1
  104. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +12 -2
  105. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +7 -3
  106. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +7 -0
  107. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +4 -1
  108. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +4 -2
  109. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +3 -2
  110. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -1
  111. package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +14 -6
  112. package/frontend-vue/layers/dms-ui/app/components/grid/columns.ts +52 -3
  113. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +2 -1
  114. package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +9 -2
  115. package/frontend-vue/layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue +13 -5
  116. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +1 -1
  117. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +3 -2
  118. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +53 -31
  119. package/frontend-vue/layers/dms-ui/app/composables/chart/apexPlotOptions.ts +15 -4
  120. package/frontend-vue/layers/dms-ui/app/composables/chart/apexYRange.ts +123 -0
  121. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +49 -49
  122. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartTheme.ts +20 -1
  123. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +1 -1
  124. package/frontend-vue/layers/dms-ui/app/composables/form/types/props.ts +2 -0
  125. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +8 -6
  126. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +1 -1
  127. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +3 -5
  128. package/frontend-vue/layers/dms-ui/app/utils/bannerToneIcons.ts +13 -0
  129. package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +26 -0
  130. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -0
  131. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -0
  132. package/frontend-vue/package.json +1 -1
  133. package/frontend-vue/pnpm-lock.yaml +5 -5
  134. package/frontend-vue/tests/activity-feed-fill-height.test.ts +7 -0
  135. package/frontend-vue/tests/banner-tone-icon.test.ts +55 -0
  136. package/frontend-vue/tests/builtin-emails.test.ts +141 -0
  137. package/frontend-vue/tests/cascader.test.ts +27 -0
  138. package/frontend-vue/tests/chart-auto-y-range.test.ts +25 -9
  139. package/frontend-vue/tests/chart-colors.test.ts +8 -0
  140. package/frontend-vue/tests/chart-resize-after-animation.test.ts +100 -0
  141. package/frontend-vue/tests/chart-type-defaults.test.ts +128 -0
  142. package/frontend-vue/tests/check-list-marker.test.ts +58 -0
  143. package/frontend-vue/tests/confirm-from.test.ts +1 -1
  144. package/frontend-vue/tests/confirm-secondary-action.test.ts +2 -2
  145. package/frontend-vue/tests/context-links.test.ts +2 -2
  146. package/frontend-vue/tests/custom-action-confirm-from.test.ts +2 -2
  147. package/frontend-vue/tests/download-action-target.test.ts +2 -2
  148. package/frontend-vue/tests/file-drop-prompt.test.ts +72 -0
  149. package/frontend-vue/tests/footer-config.test.ts +135 -0
  150. package/frontend-vue/tests/form-actions.test.ts +7 -0
  151. package/frontend-vue/tests/form-enclosing-form.test.ts +0 -21
  152. package/frontend-vue/tests/form-entry-order.test.ts +45 -0
  153. package/frontend-vue/tests/form-footer-kind.test.ts +11 -6
  154. package/frontend-vue/tests/form-load-error.test.ts +38 -23
  155. package/frontend-vue/tests/grid-responsive-columns.test.ts +39 -31
  156. package/frontend-vue/tests/header-actions.test.ts +55 -9
  157. package/frontend-vue/tests/instant-save-badge.test.ts +11 -4
  158. package/frontend-vue/tests/list-row-truncate.test.ts +46 -0
  159. package/frontend-vue/tests/nuxt-ui-aria-labels.test.ts +1 -28
  160. package/frontend-vue/tests/period-live-pause.test.ts +0 -13
  161. package/frontend-vue/tests/period-max-days.test.ts +0 -14
  162. package/frontend-vue/tests/roles-name-errors.test.ts +2 -3
  163. package/frontend-vue/tests/row-side-panel.test.ts +0 -12
  164. package/frontend-vue/tests/save-bar.test.ts +15 -3
  165. package/frontend-vue/tests/save-status-error.test.ts +2 -2
  166. package/frontend-vue/tests/table-cell-wrap.test.ts +0 -12
  167. package/frontend-vue/tests/table-density-menu.test.ts +0 -21
  168. package/frontend-vue/tests/table-live-hold.test.ts +0 -44
  169. package/frontend-vue/tests/table-row-action-errors.test.ts +5 -5
  170. package/frontend-vue/tests/table-row-hover.test.ts +2 -12
  171. package/frontend-vue/tests/table-scroll-area.test.ts +0 -8
  172. package/frontend-vue/tests/table-view-card-props.test.ts +0 -17
  173. package/frontend-vue/tests/table-view-cards-reorder.test.ts +0 -16
  174. package/frontend-vue/tests/table-view-grouped.test.ts +0 -55
  175. package/frontend-vue/tests/table-view-pinned-rows.test.ts +0 -15
  176. package/frontend-vue/tests/table-view-record-link.test.ts +0 -17
  177. package/frontend-vue/tests/table-view-refresh.test.ts +0 -17
  178. package/frontend-vue/tests/table-view-sources.test.ts +1 -1
  179. package/frontend-vue/tests/table-view-url-tab.test.ts +0 -14
  180. package/frontend-vue/tests/template-tokens.test.ts +235 -0
  181. package/frontend-vue/tests/tone.test.ts +0 -19
  182. package/frontend-vue/tests/url-interpolation.test.ts +6 -6
  183. package/package.json +6 -6
  184. package/skills/dms/SKILL.md +2 -1
  185. package/skills/dms-pages/REFERENCE.md +2 -2
  186. package/skills/dms-project/SKILL.md +0 -1
  187. package/dist/test/integration/auth/login.test.js +0 -57
  188. package/dist/test/integration/auth/login.test.js.map +0 -1
  189. package/dist/test/integration/auth/refresh.test.js +0 -59
  190. package/dist/test/integration/auth/refresh.test.js.map +0 -1
  191. package/dist/test/integration/auth/sessions.test.js +0 -58
  192. package/dist/test/integration/auth/sessions.test.js.map +0 -1
  193. package/dist/test/integration/auth/signup.test.js +0 -99
  194. package/dist/test/integration/auth/signup.test.js.map +0 -1
  195. package/dist/test/unit/skills/dms-dev-script.test.d.ts +0 -1
  196. package/dist/test/unit/skills/dms-dev-script.test.js +0 -137
  197. package/dist/test/unit/skills/dms-dev-script.test.js.map +0 -1
  198. package/frontend-vue/tests/form-reload-on-reopen.test.ts +0 -20
  199. package/frontend-vue/tests/icon-aria-hidden.test.ts +0 -29
  200. package/frontend-vue/tests/skeleton-paragraph.test.ts +0 -30
  201. package/frontend-vue/tests/tab-slot-spacing.test.ts +0 -29
  202. package/frontend-vue/tests/table-empty-body-viewport.test.ts +0 -25
  203. package/frontend-vue/tests/table-header-placement.test.ts +0 -21
  204. package/frontend-vue/tests/vue-inventory.test.ts +0 -107
  205. /package/dist/test/{integration/auth/login.test.d.ts → unit/implementations/dms-html-render/built-in-emails.test.d.ts} +0 -0
  206. /package/dist/test/{integration/auth/sessions.test.d.ts → unit/interfaces/dms/hook-handler-results.test.d.ts} +0 -0
  207. /package/dist/test/{integration/auth/refresh.test.d.ts → unit/interfaces/dms-base/query-response-delta.test.d.ts} +0 -0
  208. /package/dist/test/{integration/auth/signup.test.d.ts → unit/utils/email-subjects.test.d.ts} +0 -0
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
2
3
  import { getCurrentInstance, h, render } from "vue";
3
4
  import { useFormField } from "@nuxt/ui/composables/useFormField";
4
5
  import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
@@ -84,10 +85,11 @@ function checkSyntax(text: string): void {
84
85
 
85
86
  async function loadCompletions(): Promise<CodeEditorCompletion[]> {
86
87
  const fixed = props.completions ?? [];
87
- if (!props.completionsUrl) return fixed;
88
+ const url = pageRequestUrl(props.completionsUrl);
89
+ if (!url) return fixed;
88
90
  const response = await $authFetch<
89
91
  CodeEditorCompletion[] | CompletionsResponse
90
- >(props.completionsUrl).catch(() => []);
92
+ >(url).catch(() => []);
91
93
  const fetched = Array.isArray(response) ? response : (response.items ?? []);
92
94
  return [...fixed, ...fetched];
93
95
  }
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
2
3
  import { useConfirm } from "../../../composables/confirm/useConfirm";
3
4
  import DmsCopyButton from "../../copy/CopyButton.vue";
4
5
 
@@ -43,7 +44,7 @@ const masked = computed(() => {
43
44
  });
44
45
 
45
46
  async function rotate(): Promise<void> {
46
- const url = props.rotateUrl;
47
+ const url = pageRequestUrl(props.rotateUrl);
47
48
  if (!url) return;
48
49
  const confirmed = await confirm({
49
50
  title: t("dms.form.secret.rotate_title"),
@@ -95,7 +96,7 @@ async function rotate(): Promise<void> {
95
96
  </UInput>
96
97
  <DmsCopyButton v-if="props.copyable && value" :value="value" />
97
98
  <UButton
98
- v-if="props.rotateUrl"
99
+ v-if="pageRequestUrl(props.rotateUrl)"
99
100
  icon="i-ph-arrows-clockwise"
100
101
  :label="t('dms.form.secret.rotate')"
101
102
  color="neutral"
@@ -3,6 +3,7 @@
3
3
  lang="ts"
4
4
  generic="T extends ArrayOrNested<SelectMenuItem>, M extends boolean = false"
5
5
  >
6
+ import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
6
7
  import { useForwardPropsEmits } from "reka-ui";
7
8
  import type {
8
9
  SelectMenuProps,
@@ -193,7 +194,9 @@ interface RelationItems {
193
194
  }
194
195
 
195
196
  async function fetchRelationItems(): Promise<RelationItems> {
196
- const data = await $authFetch<RelationResponse>(props.searchUrl, {
197
+ const url = pageRequestUrl(props.searchUrl);
198
+ if (!url) return { results: [], total: 0 };
199
+ const data = await $authFetch<RelationResponse>(url, {
197
200
  params: {
198
201
  search: searchTermDebounced.value || undefined,
199
202
  },
@@ -4,6 +4,7 @@ import type { DefaultComponentProps } from "../../../../dms-core/app/types/compo
4
4
  import { GRID_CONTEXT, type GridContext } from "./constants";
5
5
  import {
6
6
  GRID_DEFAULT_MIN_COLUMN_WIDTH,
7
+ gridCollapsedCellStyles,
7
8
  gridColumnsTemplate,
8
9
  gridResponsiveStyles,
9
10
  gridScopeId,
@@ -39,13 +40,20 @@ const columnsTemplate = computed(() =>
39
40
  // its spans reach are container queries on its own width, identical in the
40
41
  // server render and the browser (see gridResponsiveStyles).
41
42
  const scope = gridScopeId(props.componentId || useId());
43
+ // Its cells close over the ones their blocks leave empty
44
+ // (see gridCollapsedCellStyles).
42
45
  const responsiveStyles = computed(() =>
43
- gridResponsiveStyles(
44
- scope,
45
- maxColumns.value,
46
- gap.value,
47
- minColumnWidth.value,
48
- ),
46
+ [
47
+ gridCollapsedCellStyles(scope, maxColumns.value),
48
+ gridResponsiveStyles(
49
+ scope,
50
+ maxColumns.value,
51
+ gap.value,
52
+ minColumnWidth.value,
53
+ ),
54
+ ]
55
+ .filter(Boolean)
56
+ .join("\n"),
49
57
  );
50
58
 
51
59
  const rowStyle = computed(() => ({
@@ -5,6 +5,18 @@ const SINGLE_COLUMN = 1;
5
5
  /** Track floor below which a column stops being usable, as a CSS length. */
6
6
  export const GRID_DEFAULT_MIN_COLUMN_WIDTH = "240px";
7
7
 
8
+ const UNITLESS_NUMBER = /^[+-]?(\d+\.?\d*|\.\d+)$/;
9
+
10
+ /**
11
+ * A length usable inside `calc()`. A bare `0` is a valid `gap` but not a
12
+ * length in a calculation, where it would void the whole template and leave
13
+ * the grid on a single column; a unitless number reads as pixels.
14
+ */
15
+ function calcLength(value: string): string {
16
+ const trimmed = value.trim();
17
+ return UNITLESS_NUMBER.test(trimmed) ? `${trimmed}px` : trimmed;
18
+ }
19
+
8
20
  /**
9
21
  * The column track template a `Grid` and every `GridRow` inside it share.
10
22
  *
@@ -26,8 +38,8 @@ export function gridColumnsTemplate(
26
38
  minColumnWidth: string,
27
39
  ): string {
28
40
  const columns = Math.max(maxColumns, SINGLE_COLUMN);
29
- const columnShare = `(100% - ${columns - SINGLE_COLUMN} * ${gap}) / ${columns}`;
30
- return `repeat(auto-fill, minmax(min(100%, max(${minColumnWidth}, ${columnShare})), 1fr))`;
41
+ const columnShare = `(100% - ${columns - SINGLE_COLUMN} * ${calcLength(gap)}) / ${columns}`;
42
+ return `repeat(auto-fill, minmax(min(100%, max(${calcLength(minColumnWidth)}, ${columnShare})), 1fr))`;
31
43
  }
32
44
 
33
45
  /** Custom property a `Grid` / `GridRow` hands its cells: the tracks left. */
@@ -43,7 +55,7 @@ function trackBreakpoint(
43
55
  gap: string,
44
56
  minColumnWidth: string,
45
57
  ): string {
46
- return `calc(${columns} * ${minColumnWidth} + ${columns - SINGLE_COLUMN} * ${gap})`;
58
+ return `calc(${columns} * ${calcLength(minColumnWidth)} + ${columns - SINGLE_COLUMN} * ${calcLength(gap)})`;
47
59
  }
48
60
 
49
61
  /** A container name / attribute value built from any id. */
@@ -100,6 +112,43 @@ export function gridResponsiveStyles(
100
112
  return rules.join("\n");
101
113
  }
102
114
 
115
+ /**
116
+ * A cell whose block renders nothing (an element holding only comments):
117
+ * a block hidden by its own condition, not a spacer, which is empty on
118
+ * purpose to line the next cells up.
119
+ */
120
+ const COLLAPSED_CELL = ":empty:not(.dms-spacer)";
121
+
122
+ /**
123
+ * The rules that let a row close over the cells its blocks leave empty, as a
124
+ * stylesheet: an empty cell takes no track, and the last filled cell before a
125
+ * run of trailing empty ones spans their tracks too, so a block whose
126
+ * neighbour turned itself off fills the row instead of leaving a hole beside
127
+ * it. The span is clamped to the tracks left like any other (see
128
+ * `gridResponsiveStyles`). The row's own stylesheet is its last child, which
129
+ * is what closes the trailing run.
130
+ */
131
+ export function gridCollapsedCellStyles(
132
+ scope: string,
133
+ maxColumns: number,
134
+ ): string {
135
+ const columns = Math.max(maxColumns, SINGLE_COLUMN);
136
+ if (columns <= SINGLE_COLUMN) return "";
137
+ const cells = `[data-dms-grid="${scope}"] > `;
138
+ const rules = [`${cells}${COLLAPSED_CELL} { display: none; }`];
139
+ for (let trailing = SINGLE_COLUMN; trailing < columns; trailing++) {
140
+ const run = Array.from(
141
+ { length: trailing },
142
+ () => `+ ${COLLAPSED_CELL} `,
143
+ ).join("");
144
+ const span = trailing + SINGLE_COLUMN;
145
+ rules.push(
146
+ `${cells}:not(${COLLAPSED_CELL}):has(${run}+ style:last-child) { grid-column: span min(${span}, var(${GRID_TRACKS_VAR}, ${span})); }`,
147
+ );
148
+ }
149
+ return rules.join("\n");
150
+ }
151
+
103
152
  /**
104
153
  * Prints a grid's responsive stylesheet inside the grid element. A render
105
154
  * function rather than template text: the server escapes interpolated text,
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageLink } from "../../build/composables/page/pageUrlValues";
2
3
  import { computed } from "vue";
3
4
  import { regionalDateTimeFormat } from "#dms-core/app/utils/regional";
4
5
  import DmsStatusPill from "../status-pill/StatusPill.vue";
@@ -216,7 +217,7 @@ function keyOf(item: KeyValueItem, index: number): string {
216
217
  <template v-else>
217
218
  <component
218
219
  :is="item.to ? DmsAutoLink : 'span'"
219
- :to="item.to"
220
+ :to="pageLink(item.to)"
220
221
  class="min-w-0"
221
222
  :class="item.to && item.type !== 'status' && 'hover:underline'"
222
223
  >
@@ -125,6 +125,7 @@ const theme = tv({
125
125
  dot: "size-2 rounded-full bg-(--dms-accent-fill) shadow-[0_0_0_3px_var(--dms-accent-tint)]",
126
126
  body: "min-w-0 flex-1",
127
127
  title: "flex items-center gap-2",
128
+ titleText: "",
128
129
  description: "text-muted mt-0.5 text-[12.5px]",
129
130
  // The "·" closes each entry followed by another (rather than opening it),
130
131
  // so a wrapped line never starts with a separator.
@@ -188,7 +189,11 @@ const theme = tv({
188
189
  // end, so the title and meta wrap there instead.
189
190
  truncate: {
190
191
  true: {
191
- title: "truncate max-sm:whitespace-normal",
192
+ title:
193
+ "truncate flex-nowrap max-sm:flex-wrap max-sm:whitespace-normal",
194
+ // The text box, not the flex title, carries the ellipsis: a flex
195
+ // container only clips its overflowing children.
196
+ titleText: "truncate max-sm:whitespace-normal",
192
197
  meta: "flex-nowrap overflow-hidden whitespace-nowrap max-sm:flex-wrap max-sm:whitespace-normal",
193
198
  detail: "truncate max-sm:whitespace-normal",
194
199
  },
@@ -281,7 +286,9 @@ const unreadText = computed(
281
286
  :class="ui.title()"
282
287
  :title="props.truncate ? props.title : undefined"
283
288
  >
284
- <slot>{{ props.title }}</slot>
289
+ <slot>
290
+ <span :class="ui.titleText()">{{ props.title }}</span>
291
+ </slot>
285
292
  </div>
286
293
  <p v-if="hasDescription" :class="ui.description()">
287
294
  <slot name="description">{{ props.description }}</slot>
@@ -3,10 +3,11 @@ import type { SaveStatusState } from "./SaveStatus.vue";
3
3
 
4
4
  interface InstantSaveBadgeProps {
5
5
  /**
6
- * Save activity of the page, for a subtle "just saved" flash: the icon
7
- * spins while a change is being written and turns into a tick once it is,
8
- * and the pill turns red while a save failed (the control says which).
9
- * The wording never changes. Defaults to `idle` (a lightning bolt).
6
+ * Save activity of the page, for a subtle "just saved" flash: the pill
7
+ * reads "Saving" with a spinning icon while a change is being written,
8
+ * then "Saved instantly" with a tick once it is, and turns red with "Not
9
+ * saved" while a save failed (the control says which). Defaults to `idle`
10
+ * (a lightning bolt and "Saved instantly").
10
11
  */
11
12
  state?: SaveStatusState;
12
13
  }
@@ -23,6 +24,13 @@ const ICONS: Record<SaveStatusState, string> = {
23
24
  error: "i-ph-warning-circle",
24
25
  };
25
26
 
27
+ const LABELS: Record<SaveStatusState, string> = {
28
+ idle: "dms.save_bar.instant",
29
+ saving: "dms.save_bar.saving",
30
+ saved: "dms.save_bar.instant",
31
+ error: "dms.save_bar.not_saved",
32
+ };
33
+
26
34
  const TONE_CLASSES = {
27
35
  ok: "border-success/40 bg-success/10 text-success",
28
36
  error: "border-error/40 bg-error/10 text-error",
@@ -49,6 +57,6 @@ const TONE_CLASSES = {
49
57
  class="size-3.5 shrink-0"
50
58
  :class="{ 'animate-spin': props.state === 'saving' }"
51
59
  />
52
- {{ t("dms.save_bar.instant") }}
60
+ {{ t(LABELS[props.state]) }}
53
61
  </span>
54
62
  </template>
@@ -40,7 +40,7 @@ interface SaveBarProps {
40
40
  kind?: FormKind;
41
41
  /**
42
42
  * Whether anything in the form can be changed: an `action` form nothing in
43
- * which can be changed shows its submit alone, all the time.
43
+ * which can be changed never offers Reset.
44
44
  */
45
45
  resettable?: boolean;
46
46
  }
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageLink } from "../../build/composables/page/pageUrlValues";
2
3
  import { computed } from "vue";
3
4
  import type { IconWellTone } from "../icon-well/IconWell.vue";
4
5
  import DmsStatCell from "../../build/components/stat/StatCell.vue";
@@ -146,7 +147,7 @@ function wellTone(item: StatGroupItem): IconWellTone {
146
147
  :icon="item.icon"
147
148
  :detail="item.detail"
148
149
  :detail-tone="item.detailTone"
149
- :to="item.to"
150
+ :to="pageLink(item.to)"
150
151
  size="sm"
151
152
  :tone="wellTone(item)"
152
153
  :loading="props.loading"
@@ -175,7 +176,7 @@ function wellTone(item: StatGroupItem): IconWellTone {
175
176
  :icon="item.icon"
176
177
  :detail="item.detail"
177
178
  :detail-tone="item.detailTone"
178
- :to="item.to"
179
+ :to="pageLink(item.to)"
179
180
  :tone="wellTone(item)"
180
181
  :loading="props.loading"
181
182
  stack
@@ -1,20 +1,28 @@
1
1
  <script setup lang="ts">
2
2
  import {
3
- buildCascaderPathLabels,
4
3
  CASCADER_FETCH_LIMIT,
5
4
  CASCADER_PATH_SEPARATOR,
5
+ cascaderValuePath,
6
6
  resolveCascaderKeyMapping,
7
7
  type CascaderKeyMapping,
8
8
  } from "../../utils/cascader";
9
+ import { MAX_RELATION_BADGES } from "../../build/composables/data-types/relationBadges";
9
10
 
10
11
  const DEFAULT_VALUE_KEY = "_id";
11
12
 
12
13
  interface CascaderPathProps {
14
+ /** One picked value, or the list a multiple cascader holds. */
13
15
  value: unknown;
14
16
  searchUrl?: string;
15
17
  keyMapping?: Partial<CascaderKeyMapping>;
16
18
  }
17
19
 
20
+ /** A picked value of a multiple cascader: its name, its path on hover. */
21
+ interface CascaderBadge {
22
+ label: string;
23
+ title: string;
24
+ }
25
+
18
26
  const props = defineProps<CascaderPathProps>();
19
27
 
20
28
  const { $authFetch } = useAuthFetch();
@@ -31,22 +39,6 @@ const keyMapping = computed(() =>
31
39
  }),
32
40
  );
33
41
 
34
- const valueObject = computed(() =>
35
- isObject(props.value) ? (props.value as Record<string, unknown>) : undefined,
36
- );
37
-
38
- const valueId = computed(() => {
39
- const raw = valueObject.value
40
- ? valueObject.value[keyMapping.value.value]
41
- : props.value;
42
- return raw === undefined || raw === null ? undefined : String(raw);
43
- });
44
-
45
- const ownLabel = computed(() => {
46
- const label = valueObject.value?.[keyMapping.value.label];
47
- return label === undefined || label === null ? undefined : String(label);
48
- });
49
-
50
42
  const { data } = await useDmsAsyncData(
51
43
  `cascader-path-${props.searchUrl}`,
52
44
  () =>
@@ -58,21 +50,51 @@ const { data } = await useDmsAsyncData(
58
50
  { lazy: true },
59
51
  );
60
52
 
61
- const pathLabel = computed(() => {
62
- if (!valueId.value) return ownLabel.value ?? "";
63
- const rows = data.value?.results;
64
- if (rows) {
65
- const labels = buildCascaderPathLabels(
66
- rows,
67
- keyMapping.value,
68
- valueId.value,
69
- );
70
- if (labels.length > 0) return labels.join(CASCADER_PATH_SEPARATOR);
71
- }
72
- return ownLabel.value ?? valueId.value;
73
- });
53
+ const pathOf = (item: unknown): string[] =>
54
+ cascaderValuePath(item, keyMapping.value, data.value?.results);
55
+
56
+ const pathLabel = computed(() =>
57
+ pathOf(props.value).join(CASCADER_PATH_SEPARATOR),
58
+ );
59
+
60
+ const badges = computed<CascaderBadge[]>(() =>
61
+ (Array.isArray(props.value) ? props.value : [])
62
+ .map(pathOf)
63
+ .filter((path) => path.length > 0)
64
+ .map((path) => ({
65
+ label: path.at(-1) ?? "",
66
+ title: path.join(CASCADER_PATH_SEPARATOR),
67
+ })),
68
+ );
69
+ const visibleBadges = computed(() =>
70
+ badges.value.slice(0, MAX_RELATION_BADGES),
71
+ );
72
+ const hiddenBadges = computed(() => badges.value.slice(MAX_RELATION_BADGES));
74
73
  </script>
75
74
 
76
75
  <template>
77
- <span class="truncate" :title="pathLabel">{{ pathLabel }}</span>
76
+ <span
77
+ v-if="Array.isArray(props.value)"
78
+ class="inline-flex flex-wrap items-center gap-1"
79
+ >
80
+ <UBadge
81
+ v-for="badge in visibleBadges"
82
+ :key="badge.title"
83
+ :label="badge.label"
84
+ :title="badge.title"
85
+ color="neutral"
86
+ variant="subtle"
87
+ size="sm"
88
+ class="max-w-40"
89
+ />
90
+ <UBadge
91
+ v-if="hiddenBadges.length"
92
+ :label="`+${hiddenBadges.length}`"
93
+ :title="hiddenBadges.map((badge) => badge.title).join(', ')"
94
+ color="neutral"
95
+ variant="subtle"
96
+ size="sm"
97
+ />
98
+ </span>
99
+ <span v-else class="truncate" :title="pathLabel">{{ pathLabel }}</span>
78
100
  </template>
@@ -9,7 +9,14 @@ import {
9
9
 
10
10
  const DEFAULT_BAR_RADIUS = 4;
11
11
  const DEFAULT_BAR_WIDTH_PCT = "60%";
12
- const DEFAULT_DONUT_HOLE = "76%";
12
+ const HUNDRED_PERCENT = 100;
13
+ /** Ring thickness, in percent of the radius, of a donut without `arcWidth`. */
14
+ const DEFAULT_ARC_WIDTH = 24;
15
+ /**
16
+ * The thinnest ring, in percent of the radius, a slice label ("33.0%") fits
17
+ * in: on a thinner one its white text runs past the ring onto the card.
18
+ */
19
+ const MIN_LABELLED_ARC_WIDTH = 35;
13
20
  const DEFAULT_RADIAL_HOLE = "60%";
14
21
  const CENTER_NAME_FONT_SIZE = "10.5px";
15
22
  const CENTER_NAME_FONT_WEIGHT = 600;
@@ -17,7 +24,6 @@ const CENTER_VALUE_FONT_SIZE = "20px";
17
24
  const CENTER_VALUE_FONT_WEIGHT = 650;
18
25
  const HEATMAP_RADIUS = 4;
19
26
  const HEATMAP_DEFAULT_INTENSITY = 0.6;
20
- const HUNDRED_PERCENT = 100;
21
27
 
22
28
  type PlotBuilder = (props: UseApexChartInput) => Record<string, unknown>;
23
29
 
@@ -47,8 +53,13 @@ function buildBarPlotOptions(
47
53
  }
48
54
 
49
55
  function donutSizeFor(arcWidth: number | undefined): string {
50
- if (arcWidth === undefined) return DEFAULT_DONUT_HOLE;
51
- return `${HUNDRED_PERCENT - arcWidth}%`;
56
+ return `${HUNDRED_PERCENT - (arcWidth ?? DEFAULT_ARC_WIDTH)}%`;
57
+ }
58
+
59
+ /** Whether a slice label fits inside the chart's ring; always true off donuts. */
60
+ export function ringFitsSliceLabels(input: UseApexChartInput): boolean {
61
+ if (input.type !== "donut") return true;
62
+ return (input.arcWidth ?? DEFAULT_ARC_WIDTH) >= MIN_LABELLED_ARC_WIDTH;
52
63
  }
53
64
 
54
65
  /** v2 donut centre: mono eyebrow over a 20px value. */
@@ -0,0 +1,123 @@
1
+ import { APEX_TYPE_CONFIGS } from "./apexTypeConfigs";
2
+ import type { ChartSeries } from "./types";
3
+ import type { UseApexChartInput } from "./useApexChart.types";
4
+
5
+ const DEFAULT_Y_RANGE_PADDING = 0.08;
6
+ const ZERO_BOUND_MAGNITUDE = 1;
7
+ const STEP_DIVISIONS = 10;
8
+ const NICE_STEP_FACTORS = [1, 2, 5, 10];
9
+ const DECIMAL_BASE = 10;
10
+ /** Enough digits to keep every real value while dropping binary float noise. */
11
+ const SIGNIFICANT_DIGITS = 12;
12
+
13
+ const DOWNWARD = -1;
14
+ const UPWARD = 1;
15
+
16
+ type Rounding = (value: number) => number;
17
+
18
+ /** Bounds Apex resolves from the extent it measured on the drawn series. */
19
+ interface AutoYBounds {
20
+ min?: (nativeMin: number) => number;
21
+ max?: (nativeMax: number) => number;
22
+ }
23
+
24
+ function isFiniteNumber(value: unknown): value is number {
25
+ return typeof value === "number" && Number.isFinite(value);
26
+ }
27
+
28
+ function withoutFloatNoise(value: number): number {
29
+ return Number(value.toPrecision(SIGNIFICANT_DIGITS));
30
+ }
31
+
32
+ function pointValues(point: ChartSeries["data"][number]): unknown[] {
33
+ if (typeof point === "number") return [point];
34
+ if (!point || typeof point !== "object") return [];
35
+ return Array.isArray(point.y) ? point.y : [point.y];
36
+ }
37
+
38
+ function seriesValues(series: ChartSeries[]): number[] {
39
+ return series.flatMap((entry) =>
40
+ entry.data.flatMap(pointValues).filter(isFiniteNumber),
41
+ );
42
+ }
43
+
44
+ function annotationValues(input: UseApexChartInput): number[] {
45
+ return (input.annotations ?? [])
46
+ .flatMap((annotation) => [annotation.y, annotation.y2])
47
+ .filter(isFiniteNumber);
48
+ }
49
+
50
+ function resolvePadding(input: UseApexChartInput): number {
51
+ const requested = input.autoYRange?.padding ?? DEFAULT_Y_RANGE_PADDING;
52
+ return Number.isFinite(requested) && requested >= 0
53
+ ? requested
54
+ : DEFAULT_Y_RANGE_PADDING;
55
+ }
56
+
57
+ function padOutward(bound: number, padding: number, direction: number): number {
58
+ return bound + direction * (Math.abs(bound) || ZERO_BOUND_MAGNITUDE) * padding;
59
+ }
60
+
61
+ /** The smallest 1, 2 or 5 times a power of ten that covers `span`. */
62
+ export function niceStep(span: number): number {
63
+ const magnitude = DECIMAL_BASE ** Math.floor(Math.log10(span));
64
+ const factor =
65
+ NICE_STEP_FACTORS.find((candidate) => candidate * magnitude >= span) ??
66
+ DECIMAL_BASE;
67
+ return withoutFloatNoise(factor * magnitude);
68
+ }
69
+
70
+ function snapToStep(value: number, step: number, round: Rounding): number {
71
+ return withoutFloatNoise(round(withoutFloatNoise(value / step)) * step);
72
+ }
73
+
74
+ /**
75
+ * The tick step for the padded extent of what is known up front: the series
76
+ * and the annotations. Apex only lays round ticks between bounds that are
77
+ * multiples of a round step; padded raw bounds give 603.6, 437.2…
78
+ */
79
+ function estimateStep(values: number[], padding: number): number | undefined {
80
+ if (values.length === 0) return undefined;
81
+ const lower = padOutward(Math.min(...values), padding, DOWNWARD);
82
+ const upper = padOutward(Math.max(...values), padding, UPWARD);
83
+ const span = upper - lower;
84
+ return span > 0 ? niceStep(span / STEP_DIVISIONS) : undefined;
85
+ }
86
+
87
+ function stepForBound(bound: number, knownStep: number | undefined): number {
88
+ if (knownStep !== undefined) return knownStep;
89
+ return niceStep((Math.abs(bound) || ZERO_BOUND_MAGNITUDE) / STEP_DIVISIONS);
90
+ }
91
+
92
+ /**
93
+ * Y bounds that take in the annotations, padded outward and rounded to a round
94
+ * step so the axis ticks read 100, 200… rather than the raw padded extent.
95
+ */
96
+ export function buildAutoYRange(input: UseApexChartInput): AutoYBounds {
97
+ if (!input.autoYRange || APEX_TYPE_CONFIGS[input.type].isCircular) return {};
98
+ const padding = resolvePadding(input);
99
+ const annotations = annotationValues(input);
100
+ const knownStep = estimateStep(
101
+ [...seriesValues(input.series), ...annotations],
102
+ padding,
103
+ );
104
+ const bound = (native: number, direction: number, round: Rounding) => {
105
+ const extent = [native, ...annotations];
106
+ const raw = direction === DOWNWARD ? Math.min(...extent) : Math.max(...extent);
107
+ const padded = padOutward(raw, padding, direction);
108
+ return snapToStep(padded, stepForBound(padded, knownStep), round);
109
+ };
110
+ return {
111
+ min: (nativeMin) => bound(nativeMin, DOWNWARD, Math.floor),
112
+ max: (nativeMax) => bound(nativeMax, UPWARD, Math.ceil),
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Axis labels without a format of their own. Apex writes them with as many
118
+ * decimals as the most precise data point, so 160 reads "160.00000000000000"
119
+ * once one price carries float noise.
120
+ */
121
+ export function formatPlainAxisValue(value: number): string {
122
+ return isFiniteNumber(value) ? String(withoutFloatNoise(value)) : String(value);
123
+ }