@antelopejs/dms 0.7.4 → 0.7.6

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 (109) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/implementations/dms-base/export-jobs.d.ts +4 -1
  3. package/dist/implementations/dms-base/export-jobs.js +10 -6
  4. package/dist/implementations/dms-base/export-jobs.js.map +1 -1
  5. package/dist/implementations/dms-base/search-route.js +20 -11
  6. package/dist/implementations/dms-base/search-route.js.map +1 -1
  7. package/dist/test/attachment-host/capabilities.d.ts +2 -0
  8. package/dist/test/attachment-host/capabilities.js +11 -0
  9. package/dist/test/attachment-host/capabilities.js.map +1 -1
  10. package/dist/test/integration/table-view-capabilities.test.js +23 -5
  11. package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
  12. package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +29 -0
  13. package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
  14. package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js +14 -0
  15. package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js.map +1 -1
  16. package/dist/test/unit/interfaces/dms-base/export-jobs-binding.test.d.ts +1 -0
  17. package/dist/test/unit/interfaces/dms-base/export-jobs-binding.test.js +49 -0
  18. package/dist/test/unit/interfaces/dms-base/export-jobs-binding.test.js.map +1 -0
  19. package/dist/test/unit/interfaces/dms-base/table-view-hidden-columns.test.d.ts +1 -0
  20. package/dist/test/unit/interfaces/dms-base/table-view-hidden-columns.test.js +105 -0
  21. package/dist/test/unit/interfaces/dms-base/table-view-hidden-columns.test.js.map +1 -0
  22. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +16 -0
  23. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  24. package/dist/test/unit/request-tenant-guards.test.js +38 -0
  25. package/dist/test/unit/request-tenant-guards.test.js.map +1 -1
  26. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/accounts.vue +9 -3
  27. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +6 -0
  28. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +6 -0
  29. package/frontend-vue/layers/dms-layout/app/build/components/layout/CommandPaletteAssistantAnswer.vue +1 -0
  30. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +2 -0
  31. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +1 -0
  32. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageSkeleton.vue +13 -4
  33. package/frontend-vue/layers/dms-layout/app/build/components/layout/RolePreviewBar.vue +9 -3
  34. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +5 -1
  35. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +1 -0
  36. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSummaryRow.vue +4 -0
  37. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/RoleEditor.vue +1 -1
  38. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityEmail.vue +4 -1
  39. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityPassword.vue +4 -1
  40. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +1 -0
  41. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +19 -4
  42. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/theme/AppearanceDeveloper.vue +1 -0
  43. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/theme/CssVariablesList.vue +6 -3
  44. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +20 -5
  45. package/frontend-vue/layers/dms-ui/app/build/components/chart/internal/ChartSkeleton.vue +4 -0
  46. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +10 -2
  47. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +7 -2
  48. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +87 -40
  49. package/frontend-vue/layers/dms-ui/app/build/components/skeleton/RowSkeleton.vue +8 -1
  50. package/frontend-vue/layers/dms-ui/app/build/components/stat/StatCell.vue +54 -13
  51. package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +9 -2
  52. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +22 -40
  53. package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +1 -0
  54. package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +1 -0
  55. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +22 -5
  56. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +49 -9
  57. package/frontend-vue/layers/dms-ui/app/build/components/table-view/ExpandedRowDetail.vue +1 -0
  58. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +3 -1
  59. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +3 -2
  60. package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +25 -0
  61. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useReorderHandles.ts +93 -0
  62. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableReorder.ts +8 -7
  63. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +20 -2
  64. package/frontend-vue/layers/dms-ui/app/build/utils/characterCount.ts +8 -0
  65. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +4 -1
  66. package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +3 -0
  67. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +13 -5
  68. package/frontend-vue/layers/dms-ui/app/components/card/NavCard.vue +7 -1
  69. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +10 -3
  70. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +8 -0
  71. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +4 -3
  72. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +6 -1
  73. package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +5 -1
  74. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +27 -3
  75. package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +24 -2
  76. package/frontend-vue/layers/dms-ui/app/components/form/components/RichText.vue +13 -4
  77. package/frontend-vue/layers/dms-ui/app/components/form/components/Textarea.vue +27 -2
  78. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +8 -4
  79. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -4
  80. package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +15 -4
  81. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +69 -42
  82. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +15 -1
  83. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +13 -3
  84. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +8 -2
  85. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +45 -9
  86. package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +2 -0
  87. package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +5 -0
  88. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +51 -0
  89. package/frontend-vue/tests/confirm-dialog-fields.test.ts +12 -0
  90. package/frontend-vue/tests/drawer-target-direction.test.ts +1 -0
  91. package/frontend-vue/tests/field-suffix-count.test.ts +120 -0
  92. package/frontend-vue/tests/form-section-nav-groups.test.ts +116 -0
  93. package/frontend-vue/tests/nav-card-badge.test.ts +5 -1
  94. package/frontend-vue/tests/nuxt-ui-aria-labels.test.ts +58 -0
  95. package/frontend-vue/tests/page-fill-height.test.ts +2 -2
  96. package/frontend-vue/tests/save-bar.test.ts +49 -0
  97. package/frontend-vue/tests/stat-group-detail-lines.test.ts +63 -0
  98. package/frontend-vue/tests/tab-fit.test.ts +99 -0
  99. package/frontend-vue/tests/table-cell-sublines.test.ts +15 -0
  100. package/frontend-vue/tests/table-cell-wrap.test.ts +27 -3
  101. package/frontend-vue/tests/table-row-action-errors.test.ts +26 -0
  102. package/frontend-vue/tests/table-row-hover.test.ts +3 -1
  103. package/frontend-vue/tests/table-scroll-area.test.ts +3 -1
  104. package/frontend-vue/tests/table-view-card-props.test.ts +8 -2
  105. package/frontend-vue/tests/table-view-cards-reorder.test.ts +250 -0
  106. package/frontend-vue/tests/table-view-reorder.test.ts +2 -2
  107. package/frontend-vue/tests/top-list-card-actions.test.ts +91 -0
  108. package/package.json +2 -2
  109. package/skills/dms/SKILL.md +14 -1
@@ -419,12 +419,27 @@ const summaryCards = computed<StatGroupItem[]>(() =>
419
419
  class="flex min-h-[228px] flex-col gap-3 p-4"
420
420
  >
421
421
  <div class="flex items-start justify-between">
422
- <USkeleton class="size-9 rounded-[10px]" />
423
- <USkeleton class="h-5 w-12 rounded-full" />
422
+ <USkeleton
423
+ :aria-label="t('dms.a11y.loading')"
424
+ class="size-9 rounded-[10px]"
425
+ />
426
+ <USkeleton
427
+ :aria-label="t('dms.a11y.loading')"
428
+ class="h-5 w-12 rounded-full"
429
+ />
424
430
  </div>
425
- <USkeleton class="h-2.5 w-4/5" />
426
- <USkeleton class="h-2.5 w-3/5" />
427
- <USkeleton class="mt-auto h-4 w-2/5" />
431
+ <USkeleton
432
+ :aria-label="t('dms.a11y.loading')"
433
+ class="h-2.5 w-4/5"
434
+ />
435
+ <USkeleton
436
+ :aria-label="t('dms.a11y.loading')"
437
+ class="h-2.5 w-3/5"
438
+ />
439
+ <USkeleton
440
+ :aria-label="t('dms.a11y.loading')"
441
+ class="mt-auto h-4 w-2/5"
442
+ />
428
443
  </DmsCard>
429
444
  </div>
430
445
 
@@ -22,6 +22,8 @@ const PERCENT_UNIT = "%";
22
22
  const RING_SIZE = "min(78%, 16rem)";
23
23
  const RING_HOLE_MASK =
24
24
  "radial-gradient(circle, transparent 54%, #000 calc(54% + 1px))";
25
+
26
+ const { t } = useI18n();
25
27
  </script>
26
28
 
27
29
  <template>
@@ -32,6 +34,7 @@ const RING_HOLE_MASK =
32
34
  aria-busy="true"
33
35
  >
34
36
  <USkeleton
37
+ :aria-label="t('dms.a11y.loading')"
35
38
  class="aspect-square rounded-full"
36
39
  :style="{
37
40
  height: RING_SIZE,
@@ -47,6 +50,7 @@ const RING_HOLE_MASK =
47
50
  aria-busy="true"
48
51
  >
49
52
  <USkeleton
53
+ :aria-label="t('dms.a11y.loading')"
50
54
  v-for="(barHeight, index) in GHOST_BAR_HEIGHTS"
51
55
  :key="index"
52
56
  class="flex-1 rounded-t-[4px] rounded-b-none"
@@ -115,6 +115,8 @@ function onContentPending() {
115
115
  function onContentResolve() {
116
116
  isContentLoading.value = false;
117
117
  }
118
+
119
+ const { t } = useI18n();
118
120
  </script>
119
121
 
120
122
  <template>
@@ -191,8 +193,14 @@ function onContentResolve() {
191
193
  :key="field"
192
194
  class="flex flex-col gap-2"
193
195
  >
194
- <USkeleton class="h-3 w-24" />
195
- <USkeleton class="h-8 w-full" />
196
+ <USkeleton
197
+ :aria-label="t('dms.a11y.loading')"
198
+ class="h-3 w-24"
199
+ />
200
+ <USkeleton
201
+ :aria-label="t('dms.a11y.loading')"
202
+ class="h-8 w-full"
203
+ />
196
204
  </div>
197
205
  </div>
198
206
  </template>
@@ -137,6 +137,8 @@ function onContentPending() {
137
137
  function onContentResolve() {
138
138
  isContentLoading.value = false;
139
139
  }
140
+
141
+ const { t } = useI18n();
140
142
  </script>
141
143
 
142
144
  <template>
@@ -205,8 +207,11 @@ function onContentResolve() {
205
207
  :key="field"
206
208
  class="flex flex-col gap-2"
207
209
  >
208
- <USkeleton class="h-3 w-24" />
209
- <USkeleton class="h-8 w-full" />
210
+ <USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-24" />
211
+ <USkeleton
212
+ :aria-label="t('dms.a11y.loading')"
213
+ class="h-8 w-full"
214
+ />
210
215
  </div>
211
216
  </div>
212
217
  </template>
@@ -1,8 +1,12 @@
1
1
  <script setup lang="ts">
2
- import type {
3
- FormSectionLayout,
4
- FormSectionState,
2
+ import { computed, useId } from "vue";
3
+ import {
4
+ type FormSectionLayout,
5
+ type FormSectionNavGroup,
6
+ type FormSectionState,
7
+ groupSectionsForNav,
5
8
  } from "../../composables/form/formSections";
9
+ import DmsEyebrow from "../../../components/section-header/Eyebrow.vue";
6
10
 
7
11
  type FormSectionNavVariant = "list" | "chips";
8
12
 
@@ -21,6 +25,18 @@ const emit = defineEmits<{ select: [sectionId: string] }>();
21
25
  const { t } = useI18n();
22
26
  const { processI18n } = useTranslation();
23
27
 
28
+ // The side list reads its sections in titled runs (`navGroup`); the row of
29
+ // chips has no room for headings and lists them all in one run.
30
+ const GROUPS_CLASSES: Record<FormSectionNavVariant, string> = {
31
+ list: "flex flex-col gap-4",
32
+ chips: "",
33
+ };
34
+
35
+ const GROUP_CLASSES: Record<FormSectionNavVariant, string> = {
36
+ list: "grid gap-1",
37
+ chips: "contents",
38
+ };
39
+
24
40
  const VARIANT_CLASSES: Record<FormSectionNavVariant, string> = {
25
41
  list: "flex flex-col gap-0.5",
26
42
  chips: "flex gap-1.5 overflow-x-auto pb-1",
@@ -37,6 +53,17 @@ const ACTIVE_CLASSES: Record<FormSectionNavVariant, string> = {
37
53
  chips: "border-accented bg-elevated text-highlighted",
38
54
  };
39
55
 
56
+ const groupIdPrefix = useId();
57
+ const groups = computed<FormSectionNavGroup[]>(() =>
58
+ props.variant === "list"
59
+ ? groupSectionsForNav(props.sections)
60
+ : [{ sections: props.sections }],
61
+ );
62
+
63
+ function groupHeadingId(index: number): string {
64
+ return `${groupIdPrefix}-group-${index}`;
65
+ }
66
+
40
67
  function stateLabel(state: FormSectionState | undefined): string | undefined {
41
68
  if (state?.invalidCount) {
42
69
  const count = state.invalidCount;
@@ -52,43 +79,63 @@ function onSelect(event: MouseEvent, sectionId: string): void {
52
79
  </script>
53
80
 
54
81
  <template>
55
- <nav :aria-label="t('dms.form.sections.nav')">
56
- <ul :class="VARIANT_CLASSES[props.variant]">
57
- <li v-for="section in props.sections" :key="section.id">
58
- <a
59
- :href="`#${props.anchorId(section.id)}`"
60
- :class="[
61
- ITEM_CLASSES[props.variant],
62
- section.id === props.activeId && ACTIVE_CLASSES[props.variant],
63
- ]"
64
- :aria-current="section.id === props.activeId ? 'location' : undefined"
65
- @click="onSelect($event, section.id)"
66
- >
67
- <UIcon
68
- v-if="section.icon && props.variant === 'list'"
69
- :name="section.icon"
70
- class="size-4 shrink-0"
71
- />
72
- <span class="truncate">{{ processI18n(section.label) }}</span>
73
- <!-- Read by its sr-only label below, not twice. -->
74
- <span
75
- v-if="props.states[section.id]?.invalidCount"
76
- class="text-error ms-auto inline-flex items-center gap-1 font-mono text-[11px]"
77
- aria-hidden="true"
82
+ <nav
83
+ :aria-label="t('dms.form.sections.nav')"
84
+ :class="GROUPS_CLASSES[props.variant]"
85
+ >
86
+ <div
87
+ v-for="(group, groupIndex) in groups"
88
+ :key="group.sections[0]?.id ?? groupIndex"
89
+ :class="GROUP_CLASSES[props.variant]"
90
+ >
91
+ <DmsEyebrow
92
+ v-if="group.label"
93
+ :id="groupHeadingId(groupIndex)"
94
+ :label="processI18n(group.label)"
95
+ class="px-2.5"
96
+ />
97
+ <ul
98
+ :class="VARIANT_CLASSES[props.variant]"
99
+ :aria-labelledby="group.label ? groupHeadingId(groupIndex) : undefined"
100
+ >
101
+ <li v-for="section in group.sections" :key="section.id">
102
+ <a
103
+ :href="`#${props.anchorId(section.id)}`"
104
+ :class="[
105
+ ITEM_CLASSES[props.variant],
106
+ section.id === props.activeId && ACTIVE_CLASSES[props.variant],
107
+ ]"
108
+ :aria-current="
109
+ section.id === props.activeId ? 'location' : undefined
110
+ "
111
+ @click="onSelect($event, section.id)"
78
112
  >
79
- <span class="bg-error size-1.5 rounded-full" aria-hidden="true" />
80
- {{ props.states[section.id]?.invalidCount }}
81
- </span>
82
- <span
83
- v-else-if="props.states[section.id]?.dirty"
84
- class="bg-warning ms-auto size-1.5 shrink-0 rounded-full"
85
- aria-hidden="true"
86
- />
87
- <span v-if="stateLabel(props.states[section.id])" class="sr-only">
88
- {{ stateLabel(props.states[section.id]) }}
89
- </span>
90
- </a>
91
- </li>
92
- </ul>
113
+ <UIcon
114
+ v-if="section.icon && props.variant === 'list'"
115
+ :name="section.icon"
116
+ class="size-4 shrink-0"
117
+ />
118
+ <span class="truncate">{{ processI18n(section.label) }}</span>
119
+ <!-- Read by its sr-only label below, not twice. -->
120
+ <span
121
+ v-if="props.states[section.id]?.invalidCount"
122
+ class="text-error ms-auto inline-flex items-center gap-1 font-mono text-[11px]"
123
+ aria-hidden="true"
124
+ >
125
+ <span class="bg-error size-1.5 rounded-full" aria-hidden="true" />
126
+ {{ props.states[section.id]?.invalidCount }}
127
+ </span>
128
+ <span
129
+ v-else-if="props.states[section.id]?.dirty"
130
+ class="bg-warning ms-auto size-1.5 shrink-0 rounded-full"
131
+ aria-hidden="true"
132
+ />
133
+ <span v-if="stateLabel(props.states[section.id])" class="sr-only">
134
+ {{ stateLabel(props.states[section.id]) }}
135
+ </span>
136
+ </a>
137
+ </li>
138
+ </ul>
139
+ </div>
93
140
  </nav>
94
141
  </template>
@@ -18,6 +18,8 @@ const props = withDefaults(defineProps<RowSkeletonProps>(), {
18
18
  linesClass: "space-y-1.5",
19
19
  isCentered: false,
20
20
  });
21
+
22
+ const { t } = useI18n();
21
23
  </script>
22
24
 
23
25
  <template>
@@ -26,9 +28,14 @@ const props = withDefaults(defineProps<RowSkeletonProps>(), {
26
28
  :class="props.isCentered ? 'items-center' : 'items-start'"
27
29
  aria-hidden="true"
28
30
  >
29
- <USkeleton class="shrink-0" :class="props.well" />
31
+ <USkeleton
32
+ :aria-label="t('dms.a11y.loading')"
33
+ class="shrink-0"
34
+ :class="props.well"
35
+ />
30
36
  <div class="min-w-0 flex-1" :class="props.linesClass">
31
37
  <USkeleton
38
+ :aria-label="t('dms.a11y.loading')"
32
39
  v-for="(line, index) in props.lines"
33
40
  :key="index"
34
41
  :class="line"
@@ -26,7 +26,10 @@ interface StatCellProps {
26
26
  /** Icon of the well; the cell has no well without one. */
27
27
  icon?: string;
28
28
  tone?: IconWellTone;
29
- /** One line under the value. */
29
+ /**
30
+ * The line under the value; a longer one wraps to a second line before its
31
+ * ellipsis, the row taking the height of its tallest cell.
32
+ */
30
33
  detail?: string;
31
34
  /** Tone of the detail line (`neutral` = muted text). */
32
35
  detailTone?: Tone;
@@ -91,6 +94,8 @@ const cellClass = computed(() => {
91
94
  ];
92
95
  });
93
96
  const detailClass = computed(() => DMS_TONE_TEXT[props.detailTone]);
97
+
98
+ const { t } = useI18n();
94
99
  </script>
95
100
 
96
101
  <template>
@@ -101,6 +106,7 @@ const detailClass = computed(() => DMS_TONE_TEXT[props.detailTone]);
101
106
  >
102
107
  <template v-if="isMd">
103
108
  <USkeleton
109
+ :aria-label="t('dms.a11y.loading')"
104
110
  v-if="props.placeholder || (props.loading && props.icon)"
105
111
  class="size-10 shrink-0 rounded-[10px]"
106
112
  />
@@ -113,9 +119,18 @@ const detailClass = computed(() => DMS_TONE_TEXT[props.detailTone]);
113
119
  <!-- A placeholder keeps the loaded lines' boxes (eyebrow 13px, value
114
120
  24px, detail 16px), so the card keeps its height when it lands. -->
115
121
  <div v-if="props.placeholder" class="grid flex-1 gap-[3px]">
116
- <USkeleton class="my-[1.5px] h-2.5 w-20" />
117
- <USkeleton class="my-px h-[22px] w-10" />
118
- <USkeleton class="my-0.5 h-3 w-24" />
122
+ <USkeleton
123
+ :aria-label="t('dms.a11y.loading')"
124
+ class="my-[1.5px] h-2.5 w-20"
125
+ />
126
+ <USkeleton
127
+ :aria-label="t('dms.a11y.loading')"
128
+ class="my-px h-[22px] w-10"
129
+ />
130
+ <USkeleton
131
+ :aria-label="t('dms.a11y.loading')"
132
+ class="my-0.5 h-3 w-24"
133
+ />
119
134
  </div>
120
135
  <div v-else class="grid min-w-0 flex-1 gap-[3px]">
121
136
  <DmsEyebrow
@@ -124,7 +139,11 @@ const detailClass = computed(() => DMS_TONE_TEXT[props.detailTone]);
124
139
  :class="wraps"
125
140
  :label="props.eyebrow"
126
141
  />
127
- <USkeleton v-if="props.loading" class="h-[22px] w-16" />
142
+ <USkeleton
143
+ :aria-label="t('dms.a11y.loading')"
144
+ v-if="props.loading"
145
+ class="h-[22px] w-16"
146
+ />
128
147
  <p
129
148
  v-else
130
149
  class="text-highlighted text-[22px] leading-[1.1] font-[650] tracking-[-0.035em] tabular-nums transition-opacity"
@@ -134,8 +153,8 @@ const detailClass = computed(() => DMS_TONE_TEXT[props.detailTone]);
134
153
  </p>
135
154
  <p
136
155
  v-if="props.detail && !props.loading"
137
- class="truncate text-xs"
138
- :class="[wraps, detailClass]"
156
+ class="line-clamp-2 text-xs [overflow-wrap:anywhere]"
157
+ :class="detailClass"
139
158
  >
140
159
  {{ props.detail }}
141
160
  </p>
@@ -145,6 +164,7 @@ const detailClass = computed(() => DMS_TONE_TEXT[props.detailTone]);
145
164
 
146
165
  <template v-else>
147
166
  <USkeleton
167
+ :aria-label="t('dms.a11y.loading')"
148
168
  v-if="props.placeholder || (props.loading && props.icon)"
149
169
  class="size-8 shrink-0 rounded-[9px]"
150
170
  />
@@ -157,13 +177,26 @@ const detailClass = computed(() => DMS_TONE_TEXT[props.detailTone]);
157
177
  <!-- Each placeholder line keeps the loaded line's box (eyebrow 11px,
158
178
  value 19px, detail 16px), so the group keeps its height. -->
159
179
  <div v-if="props.placeholder" class="grid flex-1 gap-0.5">
160
- <USkeleton class="my-px h-[9px] w-16" />
161
- <USkeleton class="my-[2.5px] h-3.5 w-24" />
162
- <USkeleton class="my-0.5 h-3 w-20" />
180
+ <USkeleton
181
+ :aria-label="t('dms.a11y.loading')"
182
+ class="my-px h-[9px] w-16"
183
+ />
184
+ <USkeleton
185
+ :aria-label="t('dms.a11y.loading')"
186
+ class="my-[2.5px] h-3.5 w-24"
187
+ />
188
+ <USkeleton
189
+ :aria-label="t('dms.a11y.loading')"
190
+ class="my-0.5 h-3 w-20"
191
+ />
163
192
  </div>
164
193
  <span v-else class="grid min-w-0 gap-0.5">
165
194
  <DmsEyebrow as="span" size="xs" :label="props.eyebrow" />
166
- <USkeleton v-if="props.loading" class="my-0.5 h-3.5 w-24" />
195
+ <USkeleton
196
+ :aria-label="t('dms.a11y.loading')"
197
+ v-if="props.loading"
198
+ class="my-0.5 h-3.5 w-24"
199
+ />
167
200
  <b
168
201
  v-else
169
202
  class="text-highlighted text-sm leading-[1.35] font-semibold whitespace-nowrap tabular-nums transition-opacity"
@@ -172,8 +205,16 @@ const detailClass = computed(() => DMS_TONE_TEXT[props.detailTone]);
172
205
  <slot name="value">{{ props.value }}</slot>
173
206
  </b>
174
207
  <template v-if="props.detail">
175
- <USkeleton v-if="props.loading" class="h-3 w-20" />
176
- <small v-else class="truncate text-xs" :class="detailClass">
208
+ <USkeleton
209
+ :aria-label="t('dms.a11y.loading')"
210
+ v-if="props.loading"
211
+ class="h-3 w-20"
212
+ />
213
+ <small
214
+ v-else
215
+ class="line-clamp-2 text-xs [overflow-wrap:anywhere]"
216
+ :class="detailClass"
217
+ >
177
218
  {{ props.detail }}
178
219
  </small>
179
220
  </template>
@@ -130,8 +130,14 @@ const uiTablePagination = computed(() => uiTablePaginationVariant());
130
130
  aria-hidden="true"
131
131
  :class="[uiTablePagination.root(), uiTablePagination.placeholderRoot()]"
132
132
  >
133
- <USkeleton :class="uiTablePagination.placeholderCount()" />
134
- <USkeleton :class="uiTablePagination.placeholderPager()" />
133
+ <USkeleton
134
+ :aria-label="t('dms.a11y.loading')"
135
+ :class="uiTablePagination.placeholderCount()"
136
+ />
137
+ <USkeleton
138
+ :aria-label="t('dms.a11y.loading')"
139
+ :class="uiTablePagination.placeholderPager()"
140
+ />
135
141
  </div>
136
142
  <nav
137
143
  v-else-if="rowCount"
@@ -183,6 +189,7 @@ const uiTablePagination = computed(() => uiTablePaginationVariant());
183
189
  </span>
184
190
  {{ summary.label }}
185
191
  <USkeleton
192
+ :aria-label="t('dms.a11y.loading')"
186
193
  v-if="summary.value === undefined"
187
194
  aria-hidden="true"
188
195
  :class="uiTablePagination.summaryPlaceholder()"
@@ -29,6 +29,7 @@ import {
29
29
  import type {
30
30
  CustomButton,
31
31
  TableViewDisplayCapabilities,
32
+ TableViewDisplayReorder,
32
33
  TableViewEmptyStatesConfig,
33
34
  } from "../../../composables/table-view/types";
34
35
  import type {
@@ -44,6 +45,10 @@ import type { ResolvedQuickFilter } from "../../composables/table-view/utils/qui
44
45
  import type { ClearableTableFilters } from "../../composables/table/utils/clearTableFilters";
45
46
  import type { EventHookOn } from "@vueuse/core";
46
47
  import type { VNodeChild } from "vue";
48
+ import {
49
+ REORDER_HANDLE_CLASS,
50
+ useReorderHandles,
51
+ } from "../../composables/table-view/useReorderHandles";
47
52
 
48
53
  export interface Data {
49
54
  [key: string]: unknown;
@@ -82,12 +87,7 @@ export interface TableAccumulation {
82
87
  }
83
88
 
84
89
  /** Rows ordered by hand (backend `reorder`): a handle moves a row. */
85
- export interface TableReorder {
86
- /** Off while a search, a filter or a tab narrows the rows. */
87
- enabled: boolean;
88
- /** Moves the row at `from` to `to`, within the page. */
89
- move: (from: number, to: number) => void;
90
- }
90
+ export type TableReorder = TableViewDisplayReorder;
91
91
 
92
92
  /** Where a table draws its views (see `TableProps.viewsPlacement`). */
93
93
  export type TableViewsPlacement = "band" | "header";
@@ -531,12 +531,15 @@ const theme = tv({
531
531
  handleHeadCell: `${HEADER_MATCH_BG} border-b-default w-8 border-b p-0`,
532
532
  handleCell:
533
533
  "border-b-muted w-8 border-b ps-2 pe-0 in-[tr:last-child]:border-b-0",
534
- handle:
535
- "inline-flex size-6 cursor-grab items-center justify-center rounded text-dimmed hover:text-highlighted focus-visible:outline-2 focus-visible:outline-(--dms-accent-line) disabled:cursor-not-allowed disabled:opacity-40 [&>svg]:size-4",
534
+ handle: REORDER_HANDLE_CLASS,
536
535
  },
537
536
  variants: {
537
+ // A wrapped cell grows its row past the 44px band: it keeps the band's
538
+ // breathing room above and below its lines instead of touching the
539
+ // hairlines (one line still fits the band, so its row keeps its height).
538
540
  cellWrap: {
539
541
  true: {
542
+ rowCell: "py-2.5",
540
543
  rowSpan: "line-clamp-none whitespace-normal [overflow-wrap:anywhere]",
541
544
  },
542
545
  },
@@ -809,23 +812,10 @@ const leadingCellCount = computed(
809
812
  () => (hasPresenceRail.value ? 1 : 0) + (props.reorder ? 1 : 0),
810
813
  );
811
814
 
812
- // Moving rows: the row dragged by its handle, dropped on another row; the
813
- // arrow keys on a handle move its row by one.
814
- const draggedRowIndex = ref<number | undefined>();
815
- const dropRow = (index: number) => {
816
- const from = draggedRowIndex.value;
817
- draggedRowIndex.value = undefined;
818
- if (from !== undefined && props.reorder?.enabled) {
819
- props.reorder.move(from, index);
820
- }
821
- };
822
- const stepRow = (index: number, step: number) => {
823
- const target = index + step;
824
- const count = table.getRowModel().rows.length;
825
- if (props.reorder?.enabled && target >= 0 && target < count) {
826
- props.reorder.move(index, target);
827
- }
828
- };
815
+ const reorderHandles = useReorderHandles({
816
+ reorder: () => props.reorder,
817
+ count: () => table.getRowModel().rows.length,
818
+ });
829
819
 
830
820
  // Pinned columns stick only while they cover at most 60% of the visible scroll
831
821
  // area: on a phone, labelled row actions or a wide pinned set would otherwise
@@ -1263,6 +1253,7 @@ defineShortcuts({
1263
1253
  {{ caption }}
1264
1254
  </span>
1265
1255
  <USkeleton
1256
+ :aria-label="t('dms.a11y.loading')"
1266
1257
  v-if="isFirstPageLoading"
1267
1258
  aria-hidden="true"
1268
1259
  :class="uiTable.captionCountPlaceholder()"
@@ -1497,8 +1488,8 @@ defineShortcuts({
1497
1488
  @dblclick="handleRowDoubleClick(row.original)"
1498
1489
  @mouseenter="handleRowHover(row)"
1499
1490
  @mouseleave="handleRowLeave"
1500
- @dragover="reorder?.enabled && $event.preventDefault()"
1501
- @drop="dropRow(row.index)"
1491
+ @dragover="reorderHandles.allowDrop"
1492
+ @drop="reorderHandles.drop(row.index)"
1502
1493
  >
1503
1494
  <td
1504
1495
  v-if="hasPresenceRail"
@@ -1515,20 +1506,8 @@ defineShortcuts({
1515
1506
  />
1516
1507
  <td v-if="reorder" :class="uiTable.handleCell()">
1517
1508
  <button
1518
- type="button"
1519
- :draggable="reorder.enabled"
1520
- :disabled="!reorder.enabled"
1521
- :aria-label="t('dms.table.reorder.move')"
1522
- :title="
1523
- reorder.enabled
1524
- ? t('dms.table.reorder.move')
1525
- : t('dms.table.reorder.disabled')
1526
- "
1509
+ v-bind="reorderHandles.handleFor(row.index)"
1527
1510
  :class="uiTable.handle()"
1528
- @dragstart="draggedRowIndex = row.index"
1529
- @dragend="draggedRowIndex = undefined"
1530
- @keydown.up.prevent="stepRow(row.index, -1)"
1531
- @keydown.down.prevent="stepRow(row.index, 1)"
1532
1511
  >
1533
1512
  <UIcon name="i-ph-dots-six-vertical" />
1534
1513
  </button>
@@ -1542,6 +1521,8 @@ defineShortcuts({
1542
1521
  pinned: getPinnedVariant(cell.column),
1543
1522
  loading,
1544
1523
  rowClickable: isRowClickable(row.original),
1524
+ cellWrap: (cell.column.columnDef as TableColumn<T>)
1525
+ .cellWrap,
1545
1526
  })
1546
1527
  "
1547
1528
  :style="{
@@ -1616,6 +1597,7 @@ defineShortcuts({
1616
1597
  :class="uiTable.rowCell()"
1617
1598
  >
1618
1599
  <USkeleton
1600
+ :aria-label="t('dms.a11y.loading')"
1619
1601
  v-if="column.columnDef.meta"
1620
1602
  :class="uiTable.skeletonCell()"
1621
1603
  :style="{ width: skeletonWidth(rowIndex, columnIndex) }"
@@ -196,6 +196,7 @@ const countFormat = computed(() => new Intl.NumberFormat(locale.value));
196
196
  {{ tab.label }}
197
197
  </span>
198
198
  <USkeleton
199
+ :aria-label="t('dms.a11y.loading')"
199
200
  v-if="tab.countPending"
200
201
  aria-hidden="true"
201
202
  :class="ui.countPlaceholder()"
@@ -243,6 +243,7 @@ const openTab = (id: string) => emit("open", id);
243
243
  />
244
244
  {{ view.label }}
245
245
  <USkeleton
246
+ :aria-label="t('dms.a11y.loading')"
246
247
  v-if="view.countPending"
247
248
  aria-hidden="true"
248
249
  class="h-3 w-4 rounded-[3px]"
@@ -21,6 +21,8 @@ const MAX_CARD_COUNT = 12;
21
21
  const cardCount = computed(() =>
22
22
  Math.max(1, Math.min(props.count, MAX_CARD_COUNT)),
23
23
  );
24
+
25
+ const { t } = useI18n();
24
26
  </script>
25
27
 
26
28
  <template>
@@ -34,13 +36,22 @@ const cardCount = computed(() =>
34
36
  class="border-default @container rounded-[10px] border bg-(--ui-bg) p-3.5 text-[12.5px]"
35
37
  >
36
38
  <div class="flex items-center gap-2.5">
37
- <USkeleton class="size-7 shrink-0 rounded-[7px]" />
39
+ <USkeleton
40
+ :aria-label="t('dms.a11y.loading')"
41
+ class="size-7 shrink-0 rounded-[7px]"
42
+ />
38
43
  <div class="min-w-0 flex-1">
39
44
  <div class="text-[13px]">
40
- <USkeleton class="inline-block h-3 w-3/5 align-middle" />
45
+ <USkeleton
46
+ :aria-label="t('dms.a11y.loading')"
47
+ class="inline-block h-3 w-3/5 align-middle"
48
+ />
41
49
  </div>
42
50
  <div class="font-mono text-[11px]">
43
- <USkeleton class="inline-block h-2 w-2/5 align-middle" />
51
+ <USkeleton
52
+ :aria-label="t('dms.a11y.loading')"
53
+ class="inline-block h-2 w-2/5 align-middle"
54
+ />
44
55
  </div>
45
56
  </div>
46
57
  </div>
@@ -49,10 +60,16 @@ const cardCount = computed(() =>
49
60
  >
50
61
  <div v-for="field in props.fields" :key="field" class="min-w-0">
51
62
  <div :class="['font-mono', props.labelClass]">
52
- <USkeleton class="inline-block h-2 w-1/2 align-middle" />
63
+ <USkeleton
64
+ :aria-label="t('dms.a11y.loading')"
65
+ class="inline-block h-2 w-1/2 align-middle"
66
+ />
53
67
  </div>
54
68
  <div class="mt-0.5">
55
- <USkeleton class="inline-block h-2.5 w-4/5 align-middle" />
69
+ <USkeleton
70
+ :aria-label="t('dms.a11y.loading')"
71
+ class="inline-block h-2.5 w-4/5 align-middle"
72
+ />
56
73
  </div>
57
74
  </div>
58
75
  </div>