@antelopejs/dms 0.7.5 → 0.7.7
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.
- package/CHANGELOG.md +33 -0
- package/dist/implementations/dms-base/export-jobs.d.ts +4 -1
- package/dist/implementations/dms-base/export-jobs.js +10 -6
- package/dist/implementations/dms-base/export-jobs.js.map +1 -1
- package/dist/implementations/dms-base/search-route.js +20 -11
- package/dist/implementations/dms-base/search-route.js.map +1 -1
- package/dist/test/attachment-host/capabilities.d.ts +2 -0
- package/dist/test/attachment-host/capabilities.js +11 -0
- package/dist/test/attachment-host/capabilities.js.map +1 -1
- package/dist/test/integration/table-view-capabilities.test.js +23 -5
- package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +29 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js +14 -0
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/export-jobs-binding.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/export-jobs-binding.test.js +49 -0
- package/dist/test/unit/interfaces/dms-base/export-jobs-binding.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-hidden-columns.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-hidden-columns.test.js +105 -0
- package/dist/test/unit/interfaces/dms-base/table-view-hidden-columns.test.js.map +1 -0
- package/dist/test/unit/request-tenant-guards.test.js +38 -0
- package/dist/test/unit/request-tenant-guards.test.js.map +1 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/accounts.vue +9 -3
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +6 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +6 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/CommandPaletteAssistantAnswer.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +2 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageSkeleton.vue +13 -4
- package/frontend-vue/layers/dms-layout/app/build/components/layout/RolePreviewBar.vue +9 -3
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +5 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSummaryRow.vue +4 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/RoleEditor.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityEmail.vue +4 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityPassword.vue +4 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +19 -4
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/theme/AppearanceDeveloper.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/theme/CssVariablesList.vue +6 -3
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +20 -5
- package/frontend-vue/layers/dms-ui/app/build/components/chart/internal/ChartSkeleton.vue +4 -0
- package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +10 -2
- package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +7 -2
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +103 -41
- package/frontend-vue/layers/dms-ui/app/build/components/skeleton/RowSkeleton.vue +8 -1
- package/frontend-vue/layers/dms-ui/app/build/components/stat/StatCell.vue +54 -13
- package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +9 -2
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +8 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +22 -5
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/ExpandedRowDetail.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +3 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +3 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +25 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +20 -2
- package/frontend-vue/layers/dms-ui/app/build/utils/characterCount.ts +8 -0
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +3 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +13 -5
- package/frontend-vue/layers/dms-ui/app/components/card/NavCard.vue +7 -1
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +10 -3
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +8 -0
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +4 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +5 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +27 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +24 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/RichText.vue +13 -4
- package/frontend-vue/layers/dms-ui/app/components/form/components/Textarea.vue +27 -2
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +8 -4
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -4
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +15 -4
- package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +69 -42
- package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +15 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +13 -3
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +45 -9
- package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +5 -0
- package/frontend-vue/tests/confirm-dialog-fields.test.ts +12 -0
- package/frontend-vue/tests/drawer-target-direction.test.ts +1 -0
- package/frontend-vue/tests/field-suffix-count.test.ts +120 -0
- package/frontend-vue/tests/form-section-nav-groups.test.ts +177 -0
- package/frontend-vue/tests/nav-card-badge.test.ts +5 -1
- package/frontend-vue/tests/nuxt-ui-aria-labels.test.ts +58 -0
- package/frontend-vue/tests/page-fill-height.test.ts +2 -2
- package/frontend-vue/tests/save-bar.test.ts +49 -0
- package/frontend-vue/tests/stat-group-detail-lines.test.ts +63 -0
- package/frontend-vue/tests/tab-fit.test.ts +99 -0
- package/frontend-vue/tests/table-cell-sublines.test.ts +15 -0
- package/frontend-vue/tests/table-cell-wrap.test.ts +24 -2
- package/frontend-vue/tests/table-row-action-errors.test.ts +26 -0
- package/frontend-vue/tests/top-list-card-actions.test.ts +91 -0
- package/package.json +2 -2
- package/skills/dms/SKILL.md +12 -1
|
@@ -61,7 +61,10 @@ const attrs = useAttrs();
|
|
|
61
61
|
const theme = tv({
|
|
62
62
|
slots: {
|
|
63
63
|
root: "dms-card dms-card--interactive group flex flex-col gap-3 p-[18px] text-start",
|
|
64
|
-
|
|
64
|
+
// Two lines before the ellipsis: a title cut on its first line no longer
|
|
65
|
+
// says where the card leads, and the card already grows with its text.
|
|
66
|
+
title:
|
|
67
|
+
"text-highlighted line-clamp-2 min-w-0 text-sm font-[650] [overflow-wrap:anywhere]",
|
|
65
68
|
tag: `${MONO_TAG_CLASS} bg-elevated shrink-0 text-dimmed`,
|
|
66
69
|
badge: "shrink-0",
|
|
67
70
|
arrow:
|
|
@@ -78,6 +81,8 @@ const passthrough = computed(() => {
|
|
|
78
81
|
const { class: _class, ...rest } = attrs;
|
|
79
82
|
return rest;
|
|
80
83
|
});
|
|
84
|
+
|
|
85
|
+
const { t } = useI18n();
|
|
81
86
|
</script>
|
|
82
87
|
|
|
83
88
|
<template>
|
|
@@ -110,6 +115,7 @@ const passthrough = computed(() => {
|
|
|
110
115
|
</div>
|
|
111
116
|
|
|
112
117
|
<USkeleton
|
|
118
|
+
:aria-label="t('dms.a11y.loading')"
|
|
113
119
|
v-if="props.statePending && !props.state"
|
|
114
120
|
aria-hidden="true"
|
|
115
121
|
class="mt-auto h-[18px] w-28 self-start"
|
|
@@ -178,7 +178,11 @@ const hasMeta = computed(
|
|
|
178
178
|
<div
|
|
179
179
|
class="text-highlighted mt-2 text-[30px] leading-[1.1] font-[650] tracking-[-0.035em] tabular-nums"
|
|
180
180
|
>
|
|
181
|
-
<USkeleton
|
|
181
|
+
<USkeleton
|
|
182
|
+
:aria-label="t('dms.a11y.loading')"
|
|
183
|
+
v-if="isFirstLoad"
|
|
184
|
+
class="h-[33px] w-36"
|
|
185
|
+
/>
|
|
182
186
|
<span
|
|
183
187
|
v-else
|
|
184
188
|
class="transition-opacity"
|
|
@@ -193,8 +197,11 @@ const hasMeta = computed(
|
|
|
193
197
|
class="mt-1.5 flex h-5 items-center gap-2"
|
|
194
198
|
aria-hidden="true"
|
|
195
199
|
>
|
|
196
|
-
<USkeleton
|
|
197
|
-
|
|
200
|
+
<USkeleton
|
|
201
|
+
:aria-label="t('dms.a11y.loading')"
|
|
202
|
+
class="h-4 w-14 rounded-full"
|
|
203
|
+
/>
|
|
204
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-20" />
|
|
198
205
|
</div>
|
|
199
206
|
<div
|
|
200
207
|
v-else-if="hasMeta"
|
|
@@ -3,6 +3,7 @@ import { nextTick, useTemplateRef, watch, type FunctionalComponent } from "vue";
|
|
|
3
3
|
import { DialogDescription, DialogTitle } from "reka-ui";
|
|
4
4
|
import DmsIconWell from "../icon-well/IconWell.vue";
|
|
5
5
|
import DmsModalEscHint from "../../build/components/modal/ModalEscHint.vue";
|
|
6
|
+
import DmsFormRequiredLegend from "../../build/components/form/FormRequiredLegend.vue";
|
|
6
7
|
import {
|
|
7
8
|
ConfirmActionError,
|
|
8
9
|
type ConfirmBodyRender,
|
|
@@ -174,6 +175,12 @@ const headerIcon = computed(() =>
|
|
|
174
175
|
: (props.icon ?? DEFAULT_ICONS[props.color] ?? FALLBACK_ICON),
|
|
175
176
|
);
|
|
176
177
|
|
|
178
|
+
// A field marked required says what its mark means, as a form does under
|
|
179
|
+
// its fields.
|
|
180
|
+
const hasRequiredFields = computed(() =>
|
|
181
|
+
props.fields.some((field) => field.required),
|
|
182
|
+
);
|
|
183
|
+
|
|
177
184
|
const hasBody = computed(
|
|
178
185
|
() =>
|
|
179
186
|
props.impact.length > 0 ||
|
|
@@ -398,6 +405,7 @@ function handleCancel() {
|
|
|
398
405
|
</template>
|
|
399
406
|
</template>
|
|
400
407
|
</UFormField>
|
|
408
|
+
<DmsFormRequiredLegend v-if="hasRequiredFields" />
|
|
401
409
|
|
|
402
410
|
<component
|
|
403
411
|
:is="bodyComponent"
|
|
@@ -954,8 +954,9 @@ onUnmounted(async () => {
|
|
|
954
954
|
</div>
|
|
955
955
|
|
|
956
956
|
<!-- One bar for every form (see `saveBarState`): the v2 floating bar
|
|
957
|
-
on a page, the footer band of a card, a drawer or a modal.
|
|
958
|
-
|
|
957
|
+
on a page, the footer band of a card, a drawer or a modal. The
|
|
958
|
+
floating bar overlays the form and takes no space while hidden; the
|
|
959
|
+
band holds its place. Neither moves anything when it shows. -->
|
|
959
960
|
<DmsSaveBar
|
|
960
961
|
v-if="canSave && !isInstant && !loadFailure"
|
|
961
962
|
:variant="usesSaveBar ? 'floating' : 'band'"
|
|
@@ -967,7 +968,7 @@ onUnmounted(async () => {
|
|
|
967
968
|
:save-label="usesSaveBar ? props.submitLabel : submitButtonLabel"
|
|
968
969
|
:cancellable="canCancel"
|
|
969
970
|
:resettable="hasChangeableFields"
|
|
970
|
-
:class="usesSaveBar ? 'mx-3
|
|
971
|
+
:class="usesSaveBar ? 'mx-3' : surfaceClasses.foot"
|
|
971
972
|
@discard="discardChanges"
|
|
972
973
|
@cancel="cancelForm"
|
|
973
974
|
/>
|
|
@@ -402,7 +402,12 @@ function nodeClasses(node: CascaderNode, isHighlighted: boolean): string[] {
|
|
|
402
402
|
class="w-52 space-y-2 p-2"
|
|
403
403
|
aria-busy="true"
|
|
404
404
|
>
|
|
405
|
-
<USkeleton
|
|
405
|
+
<USkeleton
|
|
406
|
+
:aria-label="t('dms.a11y.loading')"
|
|
407
|
+
v-for="i in 5"
|
|
408
|
+
:key="i"
|
|
409
|
+
class="h-6 w-full"
|
|
410
|
+
/>
|
|
406
411
|
</div>
|
|
407
412
|
|
|
408
413
|
<div
|
|
@@ -40,7 +40,11 @@ const isVNodeValue = computed(() => isVNode(formattedValue.value));
|
|
|
40
40
|
</script>
|
|
41
41
|
|
|
42
42
|
<template>
|
|
43
|
-
<USkeleton
|
|
43
|
+
<USkeleton
|
|
44
|
+
:aria-label="t('dms.a11y.loading')"
|
|
45
|
+
v-if="loading"
|
|
46
|
+
class="h-9 w-full"
|
|
47
|
+
/>
|
|
44
48
|
|
|
45
49
|
<Component
|
|
46
50
|
:is="dedicatedComponent"
|
|
@@ -4,16 +4,27 @@ import type {
|
|
|
4
4
|
InputNumberEmits,
|
|
5
5
|
InputNumberSlots,
|
|
6
6
|
} from "@nuxt/ui/components/InputNumber.vue";
|
|
7
|
+
import { computed } from "vue";
|
|
7
8
|
import { useForwardPropsEmits } from "reka-ui";
|
|
8
9
|
import { useControlError } from "../../../build/composables/form/useControlError";
|
|
9
10
|
|
|
10
11
|
defineOptions({ inheritAttrs: false });
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
interface InputNumberFieldProps extends InputNumberProps {
|
|
14
|
+
/** Unit after the field ("days"): an i18n key with `$`, or a literal. */
|
|
15
|
+
suffix?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = defineProps<InputNumberFieldProps>();
|
|
13
19
|
const emits = defineEmits<InputNumberEmits>();
|
|
14
20
|
defineSlots<InputNumberSlots>();
|
|
15
21
|
|
|
16
22
|
const forwarded = useForwardPropsEmits(props, emits);
|
|
23
|
+
const inputProps = computed(() => {
|
|
24
|
+
const { suffix: _suffix, ...rest } = forwarded.value;
|
|
25
|
+
return rest;
|
|
26
|
+
});
|
|
27
|
+
const { processI18n } = useTranslation();
|
|
17
28
|
|
|
18
29
|
// v2 number fields keep the value left-aligned with a compact caret stepper.
|
|
19
30
|
const DEFAULT_ORIENTATION = "vertical";
|
|
@@ -33,12 +44,25 @@ function onIncomingText(event: InputEvent): void {
|
|
|
33
44
|
</script>
|
|
34
45
|
|
|
35
46
|
<template>
|
|
36
|
-
|
|
47
|
+
<!-- The unit sits after the field rather than inside it, where the
|
|
48
|
+
stepper already takes the end of the box. -->
|
|
49
|
+
<div
|
|
50
|
+
:class="props.suffix ? 'flex items-center gap-2' : 'contents'"
|
|
51
|
+
@beforeinput.capture="onIncomingText"
|
|
52
|
+
>
|
|
37
53
|
<UInputNumber
|
|
38
|
-
v-bind="{ ...
|
|
54
|
+
v-bind="{ ...inputProps, ...$attrs }"
|
|
55
|
+
:class="props.suffix && 'min-w-0 flex-1'"
|
|
39
56
|
:orientation="props.orientation ?? DEFAULT_ORIENTATION"
|
|
40
57
|
:increment="{ color: 'neutral' }"
|
|
41
58
|
:decrement="{ color: 'neutral' }"
|
|
42
59
|
/>
|
|
60
|
+
<span
|
|
61
|
+
v-if="props.suffix"
|
|
62
|
+
class="text-muted shrink-0 text-[13px]"
|
|
63
|
+
data-field-suffix
|
|
64
|
+
>
|
|
65
|
+
{{ processI18n(props.suffix) }}
|
|
66
|
+
</span>
|
|
43
67
|
</div>
|
|
44
68
|
</template>
|
|
@@ -4,15 +4,37 @@ import type {
|
|
|
4
4
|
InputEmits,
|
|
5
5
|
InputSlots,
|
|
6
6
|
} from "@nuxt/ui/components/Input.vue";
|
|
7
|
+
import { computed } from "vue";
|
|
7
8
|
import { useForwardPropsEmits } from "reka-ui";
|
|
9
|
+
import { characterCountText } from "../../../build/utils/characterCount";
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
interface InputTextProps extends InputProps {
|
|
12
|
+
/** Longest value accepted; the counter reads against it. */
|
|
13
|
+
maxLength?: number;
|
|
14
|
+
/** Shows the characters typed at the end of the field ("4 / 60"). */
|
|
15
|
+
showCount?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = defineProps<InputTextProps>();
|
|
10
19
|
const emits = defineEmits<InputEmits>();
|
|
11
20
|
defineSlots<InputSlots>();
|
|
12
21
|
|
|
13
22
|
const forwarded = useForwardPropsEmits(props, emits);
|
|
23
|
+
const inputProps = computed(() => {
|
|
24
|
+
const { showCount: _showCount, ...rest } = forwarded.value;
|
|
25
|
+
return rest;
|
|
26
|
+
});
|
|
27
|
+
const countText = computed(() =>
|
|
28
|
+
characterCountText(props.modelValue, props.maxLength),
|
|
29
|
+
);
|
|
14
30
|
</script>
|
|
15
31
|
|
|
16
32
|
<template>
|
|
17
|
-
<UInput v-bind="
|
|
33
|
+
<UInput v-bind="inputProps">
|
|
34
|
+
<template v-if="props.showCount" #trailing>
|
|
35
|
+
<span class="text-dimmed text-xs tabular-nums" data-character-count>
|
|
36
|
+
{{ countText }}
|
|
37
|
+
</span>
|
|
38
|
+
</template>
|
|
39
|
+
</UInput>
|
|
18
40
|
</template>
|
|
@@ -314,10 +314,19 @@ const toolbarButtons = computed(() => [
|
|
|
314
314
|
padding, so the swap to the editor moves nothing below it. -->
|
|
315
315
|
<template #fallback>
|
|
316
316
|
<div class="min-h-[140px] px-3 py-2.5" aria-busy="true">
|
|
317
|
-
<USkeleton
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
317
|
+
<USkeleton
|
|
318
|
+
:aria-label="t('dms.a11y.loading')"
|
|
319
|
+
class="mb-4 h-8 w-3/4"
|
|
320
|
+
/>
|
|
321
|
+
<USkeleton
|
|
322
|
+
:aria-label="t('dms.a11y.loading')"
|
|
323
|
+
class="mb-3 h-4 w-full"
|
|
324
|
+
/>
|
|
325
|
+
<USkeleton
|
|
326
|
+
:aria-label="t('dms.a11y.loading')"
|
|
327
|
+
class="mb-3 h-4 w-5/6"
|
|
328
|
+
/>
|
|
329
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-4 w-4/5" />
|
|
321
330
|
</div>
|
|
322
331
|
</template>
|
|
323
332
|
</DmsClientOnly>
|
|
@@ -4,15 +4,40 @@ import type {
|
|
|
4
4
|
TextareaEmits,
|
|
5
5
|
TextareaSlots,
|
|
6
6
|
} from "@nuxt/ui/components/Textarea.vue";
|
|
7
|
+
import { computed } from "vue";
|
|
7
8
|
import { useForwardPropsEmits } from "reka-ui";
|
|
9
|
+
import { characterCountText } from "../../../build/utils/characterCount";
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
interface TextareaFieldProps extends TextareaProps {
|
|
12
|
+
/** Longest value accepted; the counter reads against it. */
|
|
13
|
+
maxLength?: number;
|
|
14
|
+
/** Shows the characters typed under the field ("4 / 60"). */
|
|
15
|
+
showCount?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = defineProps<TextareaFieldProps>();
|
|
10
19
|
const emits = defineEmits<TextareaEmits>();
|
|
11
20
|
defineSlots<TextareaSlots>();
|
|
12
21
|
|
|
13
22
|
const forwarded = useForwardPropsEmits(props, emits);
|
|
23
|
+
const textareaProps = computed(() => {
|
|
24
|
+
const { showCount: _showCount, ...rest } = forwarded.value;
|
|
25
|
+
return rest;
|
|
26
|
+
});
|
|
27
|
+
const countText = computed(() =>
|
|
28
|
+
characterCountText(props.modelValue, props.maxLength),
|
|
29
|
+
);
|
|
14
30
|
</script>
|
|
15
31
|
|
|
16
32
|
<template>
|
|
17
|
-
<
|
|
33
|
+
<div v-if="props.showCount" class="grid gap-1">
|
|
34
|
+
<UTextarea v-bind="textareaProps" class="w-full" />
|
|
35
|
+
<span
|
|
36
|
+
class="text-dimmed justify-self-end text-xs tabular-nums"
|
|
37
|
+
data-character-count
|
|
38
|
+
>
|
|
39
|
+
{{ countText }}
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
<UTextarea v-else v-bind="textareaProps" />
|
|
18
43
|
</template>
|
|
@@ -82,7 +82,7 @@ const COLUMN_CLASSES: Record<number, string> = {
|
|
|
82
82
|
3: "gap-x-8 sm:grid-cols-2 lg:grid-cols-3 [&>div:first-child]:border-t-0 sm:[&>div:nth-child(2)]:border-t-0 lg:[&>div:nth-child(3)]:border-t-0",
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
const { locale } = useI18n();
|
|
85
|
+
const { locale, t } = useI18n();
|
|
86
86
|
|
|
87
87
|
const columnClass = computed(
|
|
88
88
|
() =>
|
|
@@ -185,10 +185,10 @@ function keyOf(item: KeyValueItem, index: number): string {
|
|
|
185
185
|
<template v-if="showPlaceholders">
|
|
186
186
|
<div v-for="index in props.skeletonCount" :key="index" :class="rowClass">
|
|
187
187
|
<dt class="min-w-[120px] flex-none">
|
|
188
|
-
<USkeleton class="h-3 w-20" />
|
|
188
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-20" />
|
|
189
189
|
</dt>
|
|
190
190
|
<dd class="ms-auto">
|
|
191
|
-
<USkeleton class="h-3 w-24" />
|
|
191
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-24" />
|
|
192
192
|
</dd>
|
|
193
193
|
</div>
|
|
194
194
|
</template>
|
|
@@ -202,7 +202,11 @@ function keyOf(item: KeyValueItem, index: number): string {
|
|
|
202
202
|
<dd
|
|
203
203
|
class="ms-auto flex min-w-0 flex-wrap items-center justify-end gap-x-1.5 gap-y-0.5 text-end"
|
|
204
204
|
>
|
|
205
|
-
<USkeleton
|
|
205
|
+
<USkeleton
|
|
206
|
+
:aria-label="t('dms.a11y.loading')"
|
|
207
|
+
v-if="props.loading || item.loading"
|
|
208
|
+
class="h-3 w-24"
|
|
209
|
+
/>
|
|
206
210
|
<slot
|
|
207
211
|
v-else-if="slots.value"
|
|
208
212
|
name="value"
|
|
@@ -196,7 +196,11 @@ const isStat = computed(() => props.variant === "stat");
|
|
|
196
196
|
/>
|
|
197
197
|
</div>
|
|
198
198
|
<!-- The value's placeholder takes its 33px line. -->
|
|
199
|
-
<USkeleton
|
|
199
|
+
<USkeleton
|
|
200
|
+
:aria-label="t('dms.a11y.loading')"
|
|
201
|
+
v-if="isFirstLoad"
|
|
202
|
+
class="mt-1 h-[33px] w-32"
|
|
203
|
+
/>
|
|
200
204
|
<div
|
|
201
205
|
v-else-if="hasError"
|
|
202
206
|
class="mt-auto flex items-center justify-between gap-2"
|
|
@@ -242,13 +246,17 @@ const isStat = computed(() => props.variant === "stat");
|
|
|
242
246
|
>
|
|
243
247
|
<div class="grid">
|
|
244
248
|
<div v-if="showDelta" class="flex h-6 items-center">
|
|
245
|
-
<USkeleton class="h-3 w-[58px]" />
|
|
249
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-[58px]" />
|
|
246
250
|
</div>
|
|
247
251
|
<div v-if="footnote" class="flex h-4 items-center">
|
|
248
|
-
<USkeleton class="h-2.5 w-28" />
|
|
252
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-2.5 w-28" />
|
|
249
253
|
</div>
|
|
250
254
|
</div>
|
|
251
|
-
<USkeleton
|
|
255
|
+
<USkeleton
|
|
256
|
+
:aria-label="t('dms.a11y.loading')"
|
|
257
|
+
v-if="showSparkline"
|
|
258
|
+
class="ml-auto h-[34px] w-24"
|
|
259
|
+
/>
|
|
252
260
|
</div>
|
|
253
261
|
<!-- The trend's line holds its place, as in the skeleton, while there is
|
|
254
262
|
no change to show (no comparison period): no "0%" stand-in, and
|
|
@@ -143,12 +143,23 @@ const Wrapper = props.card ? resolveComponent("DmsCard") : "div";
|
|
|
143
143
|
<span v-if="props.label" class="text-toned">
|
|
144
144
|
{{ translate(props.label) }}
|
|
145
145
|
</span>
|
|
146
|
-
<USkeleton
|
|
147
|
-
|
|
146
|
+
<USkeleton
|
|
147
|
+
:aria-label="t('dms.a11y.loading')"
|
|
148
|
+
v-else
|
|
149
|
+
class="h-3 w-1/3"
|
|
150
|
+
/>
|
|
151
|
+
<USkeleton
|
|
152
|
+
:aria-label="t('dms.a11y.loading')"
|
|
153
|
+
class="ms-auto h-3 w-12"
|
|
154
|
+
/>
|
|
148
155
|
</span>
|
|
149
|
-
<USkeleton
|
|
156
|
+
<USkeleton
|
|
157
|
+
:aria-label="t('dms.a11y.loading')"
|
|
158
|
+
class="w-full rounded-full"
|
|
159
|
+
:class="skeletonTrackClass"
|
|
160
|
+
/>
|
|
150
161
|
<span v-if="skeletonHasLegend" class="flex h-4 items-center">
|
|
151
|
-
<USkeleton class="h-2.5 w-40" />
|
|
162
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-2.5 w-40" />
|
|
152
163
|
</span>
|
|
153
164
|
</div>
|
|
154
165
|
<div v-else-if="hasError" class="grid min-w-0 gap-1.5" role="alert">
|
|
@@ -9,9 +9,10 @@ import { trackFloatingSaveBar } from "../../build/composables/form/floatingSaveB
|
|
|
9
9
|
import DmsUnsavedStatus from "../../build/components/form/UnsavedStatus.vue";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* `floating`: the v2 save bar, a card
|
|
13
|
-
* while the form runs past it. `band`: the
|
|
14
|
-
* drawer or a modal (its surface classes come
|
|
12
|
+
* `floating`: the v2 save bar, a card floating over the bottom of the panel
|
|
13
|
+
* while the form runs past it, taking no space while hidden. `band`: the
|
|
14
|
+
* footer band of a form card, a drawer or a modal (its surface classes come
|
|
15
|
+
* from the form).
|
|
15
16
|
*/
|
|
16
17
|
export type SaveBarVariant = "floating" | "band";
|
|
17
18
|
|
|
@@ -66,26 +67,50 @@ const SECONDARY_LABELS: Record<FormKind, string> = {
|
|
|
66
67
|
action: "dms.button.reset",
|
|
67
68
|
};
|
|
68
69
|
|
|
69
|
-
// Its states have the same height,
|
|
70
|
-
//
|
|
70
|
+
// Its states have the same height, so switching between them never moves the
|
|
71
|
+
// content around it. The floating bar overlays the form: its root is a sticky
|
|
72
|
+
// anchor at the form's end that takes no space while the bar hides, and the
|
|
73
|
+
// bar's height plus its gaps while it shows, so the last field scrolls clear
|
|
74
|
+
// of it and nothing above it moves. The band, a footer, keeps its place.
|
|
71
75
|
const theme = tv({
|
|
72
76
|
slots: {
|
|
73
|
-
root: "
|
|
77
|
+
root: "",
|
|
78
|
+
bar: "flex items-center",
|
|
74
79
|
actions: "ms-auto flex shrink-0 items-center gap-2",
|
|
75
80
|
},
|
|
76
81
|
variants: {
|
|
77
82
|
variant: {
|
|
78
83
|
floating: {
|
|
79
|
-
root: "
|
|
84
|
+
root: "pointer-events-none sticky bottom-1 z-5 h-0 [--dms-save-bar-h:50px]",
|
|
85
|
+
bar: "border-accented pointer-events-auto absolute inset-x-0 bottom-3 h-(--dms-save-bar-h) gap-3 rounded-[10px] border bg-(--ui-bg) ps-4 pe-3 text-[13px] shadow-lg transition-[opacity,translate,visibility] duration-200 ease-out",
|
|
80
86
|
},
|
|
81
87
|
band: {
|
|
82
|
-
root: "
|
|
88
|
+
root: "transition-[opacity,translate,visibility] duration-200 ease-out",
|
|
89
|
+
bar: "gap-2",
|
|
83
90
|
},
|
|
84
91
|
},
|
|
85
92
|
hidden: {
|
|
86
|
-
true:
|
|
93
|
+
true: "",
|
|
94
|
+
false: "",
|
|
87
95
|
},
|
|
88
96
|
},
|
|
97
|
+
compoundVariants: [
|
|
98
|
+
{
|
|
99
|
+
variant: "floating",
|
|
100
|
+
hidden: false,
|
|
101
|
+
class: { root: "h-[calc(var(--dms-save-bar-h)+2rem)]" },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
variant: "floating",
|
|
105
|
+
hidden: true,
|
|
106
|
+
class: { bar: "invisible translate-y-2 opacity-0" },
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
variant: "band",
|
|
110
|
+
hidden: true,
|
|
111
|
+
class: { root: "invisible translate-y-2 opacity-0" },
|
|
112
|
+
},
|
|
113
|
+
],
|
|
89
114
|
});
|
|
90
115
|
|
|
91
116
|
const BUTTONS = {
|
|
@@ -112,7 +137,7 @@ trackFloatingSaveBar(
|
|
|
112
137
|
() => props.variant === "floating" && !state.value.isHidden,
|
|
113
138
|
);
|
|
114
139
|
|
|
115
|
-
// Empty while there is nothing to save: the hidden bar keeps its line.
|
|
140
|
+
// Empty while there is nothing to save: the hidden bar keeps its one line.
|
|
116
141
|
const statusText = computed(() => {
|
|
117
142
|
if (!props.dirty) return "";
|
|
118
143
|
const changed = props.changes.map((label) => processI18n(label)).join(", ");
|
|
@@ -133,39 +158,41 @@ const saveText = computed(() => {
|
|
|
133
158
|
:inert="state.isHidden || undefined"
|
|
134
159
|
:aria-hidden="state.isHidden || undefined"
|
|
135
160
|
>
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<div :class="ui.actions()">
|
|
142
|
-
<UButton
|
|
143
|
-
v-if="state.secondary === 'discard'"
|
|
144
|
-
:label="t(SECONDARY_LABELS[props.kind])"
|
|
145
|
-
color="neutral"
|
|
146
|
-
:variant="buttons.discardVariant"
|
|
147
|
-
:size="buttons.size"
|
|
148
|
-
:disabled="props.saving"
|
|
149
|
-
@click="emit('discard')"
|
|
150
|
-
/>
|
|
151
|
-
<UButton
|
|
152
|
-
v-else-if="state.secondary === 'cancel'"
|
|
153
|
-
:label="t('dms.button.cancel')"
|
|
154
|
-
color="neutral"
|
|
155
|
-
variant="outline"
|
|
156
|
-
:size="buttons.size"
|
|
157
|
-
@click="emit('cancel')"
|
|
158
|
-
/>
|
|
159
|
-
<UButton
|
|
160
|
-
v-if="state.showsSubmit"
|
|
161
|
-
:label="saveText"
|
|
162
|
-
:size="buttons.size"
|
|
163
|
-
:loading="props.saving"
|
|
164
|
-
:disabled="state.isSubmitHeld"
|
|
165
|
-
:type="props.form ? 'submit' : 'button'"
|
|
166
|
-
:form="props.form"
|
|
167
|
-
@click="props.form ? undefined : emit('save')"
|
|
161
|
+
<div :class="ui.bar()">
|
|
162
|
+
<DmsUnsavedStatus
|
|
163
|
+
v-if="state.showsStatus"
|
|
164
|
+
:text="statusText"
|
|
165
|
+
:is-band="props.variant === 'band'"
|
|
168
166
|
/>
|
|
167
|
+
<div :class="ui.actions()">
|
|
168
|
+
<UButton
|
|
169
|
+
v-if="state.secondary === 'discard'"
|
|
170
|
+
:label="t(SECONDARY_LABELS[props.kind])"
|
|
171
|
+
color="neutral"
|
|
172
|
+
:variant="buttons.discardVariant"
|
|
173
|
+
:size="buttons.size"
|
|
174
|
+
:disabled="props.saving"
|
|
175
|
+
@click="emit('discard')"
|
|
176
|
+
/>
|
|
177
|
+
<UButton
|
|
178
|
+
v-else-if="state.secondary === 'cancel'"
|
|
179
|
+
:label="t('dms.button.cancel')"
|
|
180
|
+
color="neutral"
|
|
181
|
+
variant="outline"
|
|
182
|
+
:size="buttons.size"
|
|
183
|
+
@click="emit('cancel')"
|
|
184
|
+
/>
|
|
185
|
+
<UButton
|
|
186
|
+
v-if="state.showsSubmit"
|
|
187
|
+
:label="saveText"
|
|
188
|
+
:size="buttons.size"
|
|
189
|
+
:loading="props.saving"
|
|
190
|
+
:disabled="state.isSubmitHeld"
|
|
191
|
+
:type="props.form ? 'submit' : 'button'"
|
|
192
|
+
:form="props.form"
|
|
193
|
+
@click="props.form ? undefined : emit('save')"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
169
196
|
</div>
|
|
170
197
|
</div>
|
|
171
198
|
</template>
|
|
@@ -21,6 +21,7 @@ const props = withDefaults(defineProps<TabProps>(), {
|
|
|
21
21
|
stateKey: "tab",
|
|
22
22
|
badgesUrl: undefined,
|
|
23
23
|
realtimeTopic: undefined,
|
|
24
|
+
fit: false,
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
// The badges a module counts for a record's tabs, in one request, again when
|
|
@@ -72,10 +73,23 @@ const ROOT_CLASSES: Record<AxeOrientation, string> = {
|
|
|
72
73
|
[AxeOrientation.vertical]: "max-sm:flex-col max-sm:items-stretch",
|
|
73
74
|
};
|
|
74
75
|
|
|
76
|
+
// A fitted pill list hugs its tabs: the triggers stop growing to share the
|
|
77
|
+
// full width. Link tabs never grow, and a vertical list has its own width.
|
|
78
|
+
const FIT_CLASSES: Record<TabVariant, { list: string; trigger: string }> = {
|
|
79
|
+
[TabVariant.pill]: { list: "w-fit max-w-full", trigger: "grow-0" },
|
|
80
|
+
[TabVariant.link]: { list: "", trigger: "" },
|
|
81
|
+
};
|
|
82
|
+
|
|
75
83
|
const panelClass = computed(() => PANEL_CLASSES[props.orientation]);
|
|
84
|
+
const fitClasses = computed(() =>
|
|
85
|
+
props.fit && props.orientation === AxeOrientation.horizontal
|
|
86
|
+
? FIT_CLASSES[props.variant]
|
|
87
|
+
: undefined,
|
|
88
|
+
);
|
|
76
89
|
const tabsUi = computed(() => ({
|
|
77
90
|
root: ROOT_CLASSES[props.orientation],
|
|
78
|
-
list: LIST_CLASSES[props.orientation],
|
|
91
|
+
list: [LIST_CLASSES[props.orientation], fitClasses.value?.list],
|
|
92
|
+
trigger: fitClasses.value?.trigger,
|
|
79
93
|
// The panel may shrink below its content's width (a wide table scrolls
|
|
80
94
|
// inside it instead of pushing the page sideways).
|
|
81
95
|
content: "min-w-0",
|
|
@@ -451,6 +451,7 @@ const FieldValue = (fieldProps: { column: TableViewColumn; item: T }) => {
|
|
|
451
451
|
{{ col.label }}
|
|
452
452
|
</span>
|
|
453
453
|
<USkeleton
|
|
454
|
+
:aria-label="t('dms.a11y.loading')"
|
|
454
455
|
v-if="isBoardLoading && !cells[col.value]"
|
|
455
456
|
aria-hidden="true"
|
|
456
457
|
class="ms-auto h-[16.5px] w-5 shrink-0 rounded-[5px]"
|
|
@@ -478,17 +479,26 @@ const FieldValue = (fieldProps: { column: TableViewColumn; item: T }) => {
|
|
|
478
479
|
class="border-default grid gap-2 rounded-lg border bg-(--dms-surface-card) px-3 pt-2.5 pb-[11px] shadow-xs"
|
|
479
480
|
>
|
|
480
481
|
<div class="-mb-0.5 flex min-h-5 items-center">
|
|
481
|
-
<USkeleton
|
|
482
|
+
<USkeleton
|
|
483
|
+
:aria-label="t('dms.a11y.loading')"
|
|
484
|
+
class="h-2.5 w-10"
|
|
485
|
+
/>
|
|
482
486
|
</div>
|
|
483
487
|
<div class="flex h-[17px] items-center">
|
|
484
|
-
<USkeleton
|
|
488
|
+
<USkeleton
|
|
489
|
+
:aria-label="t('dms.a11y.loading')"
|
|
490
|
+
class="h-3 w-3/4"
|
|
491
|
+
/>
|
|
485
492
|
</div>
|
|
486
493
|
<div
|
|
487
494
|
v-for="fieldColumn in cardColumns"
|
|
488
495
|
:key="fieldColumn.id"
|
|
489
496
|
class="-mt-0.5 flex h-[18px] items-center"
|
|
490
497
|
>
|
|
491
|
-
<USkeleton
|
|
498
|
+
<USkeleton
|
|
499
|
+
:aria-label="t('dms.a11y.loading')"
|
|
500
|
+
class="h-2.5 w-1/2"
|
|
501
|
+
/>
|
|
492
502
|
</div>
|
|
493
503
|
</div>
|
|
494
504
|
</div>
|