nuxt-ui-tools 1.22.2 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/i18n/locales/en.js +5 -0
- package/dist/runtime/i18n/locales/fr.js +5 -0
- package/dist/runtime/i18n/types.d.ts +5 -0
- package/dist/runtime/shared/styles/scrollbars.css +1 -1
- package/dist/runtime/table/components/data-list/data-list-root.vue +2 -0
- package/dist/runtime/table/components/data-list/data-list-selection-actions.d.vue.ts +1 -1
- package/dist/runtime/table/components/data-list/data-list-selection-actions.vue +32 -5
- package/dist/runtime/table/components/data-list/data-list-selection-actions.vue.d.ts +1 -1
- package/dist/runtime/table/components/data-list/data-list-selection-sheet.d.vue.ts +10 -0
- package/dist/runtime/table/components/data-list/data-list-selection-sheet.vue +238 -0
- package/dist/runtime/table/components/data-list/data-list-selection-sheet.vue.d.ts +10 -0
- package/dist/runtime/table/components/grid/grid-renderer.vue +2 -0
- package/dist/runtime/table/components/summary/summary-renderer.vue +2 -0
- package/dist/runtime/table/components/table/table-overlay-scrollbars.vue +4 -4
- package/dist/runtime/table/components/table/table-renderer.vue +2 -0
- package/dist/runtime/table/components/utils/overlay-inset-spacer.d.vue.ts +3 -0
- package/dist/runtime/table/components/utils/overlay-inset-spacer.vue +13 -0
- package/dist/runtime/table/components/utils/overlay-inset-spacer.vue.d.ts +3 -0
- package/dist/runtime/table/composables/use-data-list-overlay-inset.d.ts +10 -0
- package/dist/runtime/table/composables/use-data-list-overlay-inset.js +11 -0
- package/dist/runtime/table/composables/use-table-actions.js +11 -2
- package/dist/runtime/table/constants/mobile.d.ts +0 -7
- package/dist/runtime/table/constants/mobile.js +0 -1
- package/dist/runtime/table/types/actions.d.ts +7 -0
- package/dist/runtime/table/types/data-list.d.ts +8 -1
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -718,8 +718,13 @@ export default defineUiToolsLocale({
|
|
|
718
718
|
selectRowAt: "Select row {position}"
|
|
719
719
|
},
|
|
720
720
|
selectionBar: {
|
|
721
|
+
actions: "Actions",
|
|
722
|
+
actionsTitle: "Actions on the selection",
|
|
721
723
|
allResults: "All results",
|
|
722
724
|
clear: "Clear selection",
|
|
725
|
+
countAll: "All {count} results",
|
|
726
|
+
countSelectedOne: "{count} selected",
|
|
727
|
+
countSelectedOther: "{count} selected",
|
|
723
728
|
more: "More actions",
|
|
724
729
|
selection: "Selection"
|
|
725
730
|
},
|
|
@@ -718,8 +718,13 @@ export default defineUiToolsLocale({
|
|
|
718
718
|
selectRowAt: "S\xE9lectionner la ligne {position}"
|
|
719
719
|
},
|
|
720
720
|
selectionBar: {
|
|
721
|
+
actions: "Actions",
|
|
722
|
+
actionsTitle: "Actions sur la s\xE9lection",
|
|
721
723
|
allResults: "Tous les r\xE9sultats",
|
|
722
724
|
clear: "Effacer la s\xE9lection",
|
|
725
|
+
countAll: "Tous les {count} r\xE9sultats",
|
|
726
|
+
countSelectedOne: "{count} s\xE9lectionn\xE9",
|
|
727
|
+
countSelectedOther: "{count} s\xE9lectionn\xE9s",
|
|
723
728
|
more: "Plus d\u2019actions",
|
|
724
729
|
selection: "S\xE9lection"
|
|
725
730
|
},
|
|
@@ -60,6 +60,11 @@ export interface UiToolsTableMessages {
|
|
|
60
60
|
allResults: string;
|
|
61
61
|
more: string;
|
|
62
62
|
clear: string;
|
|
63
|
+
actions: string;
|
|
64
|
+
actionsTitle: string;
|
|
65
|
+
countSelectedOne: string;
|
|
66
|
+
countSelectedOther: string;
|
|
67
|
+
countAll: string;
|
|
63
68
|
};
|
|
64
69
|
summaries: {
|
|
65
70
|
total: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
@layer theme, base, components, utilities;@layer base{:root{--nut-scrollbar-size:11px;--nut-scrollbar-gap:3px;--nut-scrollbar-thumb:color-mix(in srgb,var(--ui-text,#1f1d1a) 18%,transparent);--nut-scrollbar-thumb-hover:color-mix(in srgb,var(--ui-text,#1f1d1a) 32%,transparent);--nut-scrollbar-thumb-active:color-mix(in srgb,var(--ui-text,#1f1d1a) 44%,transparent)}@supports (scrollbar-width:thin){*{scrollbar-color:transparent transparent;scrollbar-width:thin;transition:scrollbar-color .2s ease}:hover{scrollbar-color:var(--nut-scrollbar-thumb) transparent}}@supports not (scrollbar-width:thin){::-webkit-scrollbar{background:transparent;height:var(--nut-scrollbar-size);width:var(--nut-scrollbar-size)}::-webkit-scrollbar-corner,::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background-clip:padding-box;background-color:transparent;border:var(--nut-scrollbar-gap) solid transparent;border-radius:999px}:hover::-webkit-scrollbar-thumb{background-color:var(--nut-scrollbar-thumb)}::-webkit-scrollbar-thumb:hover{background-color:var(--nut-scrollbar-thumb-hover)}::-webkit-scrollbar-thumb:active{background-color:var(--nut-scrollbar-thumb-active)}}}
|
|
1
|
+
@layer theme, base, components, utilities;@layer base{:root{--nut-scrollbar-size:11px;--nut-scrollbar-gap:3px;--nut-table-scrollbar-size:5px;--nut-table-scrollbar-idle-opacity:0;--nut-table-scrollbar-idle-pointer-events:none;--nut-scrollbar-thumb:color-mix(in srgb,var(--ui-text,#1f1d1a) 18%,transparent);--nut-scrollbar-thumb-hover:color-mix(in srgb,var(--ui-text,#1f1d1a) 32%,transparent);--nut-scrollbar-thumb-active:color-mix(in srgb,var(--ui-text,#1f1d1a) 44%,transparent)}@supports (scrollbar-width:thin){*{scrollbar-color:transparent transparent;scrollbar-width:thin;transition:scrollbar-color .2s ease}:hover{scrollbar-color:var(--nut-scrollbar-thumb) transparent}}@supports not (scrollbar-width:thin){::-webkit-scrollbar{background:transparent;height:var(--nut-scrollbar-size);width:var(--nut-scrollbar-size)}::-webkit-scrollbar-corner,::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background-clip:padding-box;background-color:transparent;border:var(--nut-scrollbar-gap) solid transparent;border-radius:999px}:hover::-webkit-scrollbar-thumb{background-color:var(--nut-scrollbar-thumb)}::-webkit-scrollbar-thumb:hover{background-color:var(--nut-scrollbar-thumb-hover)}::-webkit-scrollbar-thumb:active{background-color:var(--nut-scrollbar-thumb-active)}}}
|
|
@@ -4,6 +4,7 @@ import { computed, onBeforeUnmount, onMounted } from "vue";
|
|
|
4
4
|
import { provideUiToolsLocale, useUiToolsLocaleRef } from "#ui-tools/i18n";
|
|
5
5
|
import { isObject } from "../../../shared/utils/predicate";
|
|
6
6
|
import { useDataListBreakpoint } from "../../composables/use-data-list-breakpoint";
|
|
7
|
+
import { provideDataListOverlayInset } from "../../composables/use-data-list-overlay-inset";
|
|
7
8
|
import { provideDataListUi } from "../../composables/use-data-list-ui";
|
|
8
9
|
import { provideTableInternals } from "../../composables/use-table-internals";
|
|
9
10
|
import { mergeDataListUiConfig } from "../../utils";
|
|
@@ -18,6 +19,7 @@ const props = defineProps({
|
|
|
18
19
|
const appConfig = useAppConfig();
|
|
19
20
|
provideTableInternals(props.table["__internals"]);
|
|
20
21
|
provideUiToolsLocale(useUiToolsLocaleRef(computed(() => props.locale)));
|
|
22
|
+
provideDataListOverlayInset();
|
|
21
23
|
const { isMobile } = useDataListBreakpoint();
|
|
22
24
|
provideDataListUi(
|
|
23
25
|
computed(() => {
|
|
@@ -4,7 +4,7 @@ type __VLS_Props = {
|
|
|
4
4
|
size?: DataListControlSize;
|
|
5
5
|
/** `absolute` docks the bar inside the nearest positioned ancestor; `fixed` pins it to the viewport. */
|
|
6
6
|
position?: 'absolute' | 'fixed';
|
|
7
|
-
/** Number of actions
|
|
7
|
+
/** Number of actions the desktop bar renders inline before the overflow menu. Below `md` every action opens from a sheet. */
|
|
8
8
|
maxVisible?: number;
|
|
9
9
|
/** Shows the selection / all-results scope switch when the schema allows an `all` scope. */
|
|
10
10
|
scope?: boolean;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import UButton from "@nuxt/ui/components/Button.vue";
|
|
3
3
|
import UDropdownMenu from "@nuxt/ui/components/DropdownMenu.vue";
|
|
4
|
-
import { useResizeObserver } from "@vueuse/core";
|
|
5
|
-
import { computed, nextTick, ref, useTemplateRef, watch } from "vue";
|
|
4
|
+
import { useElementSize, useResizeObserver } from "@vueuse/core";
|
|
5
|
+
import { computed, nextTick, onBeforeUnmount, ref, useTemplateRef, watch, watchEffect } from "vue";
|
|
6
6
|
import { useUiToolsLocale } from "#ui-tools/i18n";
|
|
7
7
|
import { isString, isNullish } from "../../../shared/utils/predicate";
|
|
8
8
|
import { useDataListBreakpoint } from "../../composables/use-data-list-breakpoint";
|
|
9
|
+
import { useDataListOverlayInset } from "../../composables/use-data-list-overlay-inset";
|
|
9
10
|
import { useDataListUi } from "../../composables/use-data-list-ui";
|
|
10
11
|
import { useTableInternals } from "../../composables/use-table-internals";
|
|
11
12
|
import { mergeDataListProps, mergeDataListUiClass } from "../../utils";
|
|
12
13
|
import { resolveTableActionLabel } from "../../utils/actions";
|
|
14
|
+
import DataListSelectionSheet from "./data-list-selection-sheet.vue";
|
|
13
15
|
const props = defineProps({
|
|
14
16
|
size: { type: String, required: false },
|
|
15
17
|
position: { type: String, required: false, default: "absolute" },
|
|
@@ -59,7 +61,12 @@ const scopeEnabled = computed(
|
|
|
59
61
|
() => props.scope && internals.schema.value.selection?.scope !== "page" && !isNullish(matchingCount.value) && matchingCount.value > 0
|
|
60
62
|
);
|
|
61
63
|
const bulkScope = computed(() => internals.selection.bulkScope.value);
|
|
62
|
-
const maxVisible = computed(() => props.maxVisible ??
|
|
64
|
+
const maxVisible = computed(() => props.maxVisible ?? 3);
|
|
65
|
+
const placement = computed(() => {
|
|
66
|
+
if (props.position === "fixed")
|
|
67
|
+
return isMobile.value ? "fixed inset-x-0 bottom-[calc(12px+env(safe-area-inset-bottom))]" : "fixed inset-x-0 bottom-4";
|
|
68
|
+
return isMobile.value ? "absolute inset-x-0 bottom-3" : "absolute inset-x-0 bottom-[22px]";
|
|
69
|
+
});
|
|
63
70
|
const fitted = ref(Number.POSITIVE_INFINITY);
|
|
64
71
|
const visibleCount = computed(() => Math.min(maxVisible.value, fitted.value));
|
|
65
72
|
const visibleActions = computed(() => actions.value.slice(0, visibleCount.value));
|
|
@@ -99,6 +106,7 @@ async function fit() {
|
|
|
99
106
|
watch(
|
|
100
107
|
() => [
|
|
101
108
|
selectedCount.value > 0,
|
|
109
|
+
isMobile.value,
|
|
102
110
|
maxVisible.value,
|
|
103
111
|
actions.value.map((action) => resolveTableActionLabel(action.definition.label)).join("\n")
|
|
104
112
|
],
|
|
@@ -106,6 +114,15 @@ watch(
|
|
|
106
114
|
{ flush: "post" }
|
|
107
115
|
);
|
|
108
116
|
useResizeObserver(rootElement, () => void fit());
|
|
117
|
+
const overlayInset = useDataListOverlayInset();
|
|
118
|
+
const { height: rootHeight } = useElementSize(rootElement);
|
|
119
|
+
watchEffect(() => {
|
|
120
|
+
if (overlayInset)
|
|
121
|
+
overlayInset.bottom.value = isMobile.value && rootHeight.value ? rootHeight.value + 24 : 0;
|
|
122
|
+
});
|
|
123
|
+
onBeforeUnmount(() => {
|
|
124
|
+
if (overlayInset) overlayInset.bottom.value = 0;
|
|
125
|
+
});
|
|
109
126
|
</script>
|
|
110
127
|
|
|
111
128
|
<template>
|
|
@@ -115,20 +132,30 @@ useResizeObserver(rootElement, () => void fit());
|
|
|
115
132
|
ref="rootElement"
|
|
116
133
|
:class="
|
|
117
134
|
mergeDataListUiClass(
|
|
118
|
-
`nut-dl-selbar pointer-events-none z-40 flex justify-center px-4 ${
|
|
135
|
+
`nut-dl-selbar pointer-events-none z-40 flex justify-center px-4 ${placement}`,
|
|
119
136
|
void 0,
|
|
120
137
|
ui.root
|
|
121
138
|
)
|
|
122
139
|
"
|
|
123
140
|
>
|
|
124
|
-
<div
|
|
141
|
+
<div
|
|
142
|
+
class="pointer-events-auto flex max-w-full justify-center"
|
|
143
|
+
:class="{ 'w-full': isMobile }"
|
|
144
|
+
>
|
|
125
145
|
<slot
|
|
126
146
|
:actions="actions"
|
|
127
147
|
:selected-count="selectedCount"
|
|
128
148
|
:selection="selection"
|
|
129
149
|
:clear-selection="clearSelection"
|
|
130
150
|
>
|
|
151
|
+
<DataListSelectionSheet
|
|
152
|
+
v-if="isMobile"
|
|
153
|
+
:scope="scopeEnabled"
|
|
154
|
+
:ui="ui"
|
|
155
|
+
:controls="controlProps"
|
|
156
|
+
/>
|
|
131
157
|
<div
|
|
158
|
+
v-else
|
|
132
159
|
:class="
|
|
133
160
|
mergeDataListUiClass(
|
|
134
161
|
'nut-dl-selbar__bar flex h-11 max-w-[min(840px,100%)] items-center gap-2.5 rounded-[10px] bg-inverted px-1.5 text-[13px] text-inverted shadow-[0_20px_50px_-18px_rgb(31_29_26/0.6)]',
|
|
@@ -4,7 +4,7 @@ type __VLS_Props = {
|
|
|
4
4
|
size?: DataListControlSize;
|
|
5
5
|
/** `absolute` docks the bar inside the nearest positioned ancestor; `fixed` pins it to the viewport. */
|
|
6
6
|
position?: 'absolute' | 'fixed';
|
|
7
|
-
/** Number of actions
|
|
7
|
+
/** Number of actions the desktop bar renders inline before the overflow menu. Below `md` every action opens from a sheet. */
|
|
8
8
|
maxVisible?: number;
|
|
9
9
|
/** Shows the selection / all-results scope switch when the schema allows an `all` scope. */
|
|
10
10
|
scope?: boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DataListSelectionActionsProps, DataListSelectionActionsUi } from '../../types/index.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** Shows the selection / all-results scope switch at the top of the sheet. */
|
|
4
|
+
scope: boolean;
|
|
5
|
+
ui: DataListSelectionActionsUi;
|
|
6
|
+
controls: DataListSelectionActionsProps;
|
|
7
|
+
};
|
|
8
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
|
+
declare const _default: typeof __VLS_export;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import UButton from "@nuxt/ui/components/Button.vue";
|
|
3
|
+
import UDrawer from "@nuxt/ui/components/Drawer.vue";
|
|
4
|
+
import UIcon from "@nuxt/ui/components/Icon.vue";
|
|
5
|
+
import { computed, nextTick, ref, useId, useTemplateRef, watch } from "vue";
|
|
6
|
+
import { useUiToolsLocale } from "#ui-tools/i18n";
|
|
7
|
+
import { isString } from "../../../shared/utils/predicate";
|
|
8
|
+
import { useTableInternals } from "../../composables/use-table-internals";
|
|
9
|
+
import { mergeDataListProps, mergeDataListUiClass } from "../../utils";
|
|
10
|
+
import { resolveTableActionLabel } from "../../utils/actions";
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
scope: { type: Boolean, required: true },
|
|
13
|
+
ui: { type: Object, required: true },
|
|
14
|
+
controls: { type: Object, required: true }
|
|
15
|
+
});
|
|
16
|
+
const internals = useTableInternals();
|
|
17
|
+
const { locale, t } = useUiToolsLocale();
|
|
18
|
+
const id = useId();
|
|
19
|
+
const open = ref(false);
|
|
20
|
+
const sheetBody = useTemplateRef("sheetBody");
|
|
21
|
+
const selectedCount = computed(() => internals.selection.selectedCount.value);
|
|
22
|
+
const matchingCount = computed(() => internals.pagination.rowCount.value ?? 0);
|
|
23
|
+
const bulkScope = computed(() => internals.selection.bulkScope.value);
|
|
24
|
+
const targetsAll = computed(() => props.scope && bulkScope.value === "all");
|
|
25
|
+
const count = computed(() => {
|
|
26
|
+
const template = targetsAll.value ? t("table.selectionBar.countAll") : selectedCount.value === 1 ? t("table.selectionBar.countSelectedOne") : t("table.selectionBar.countSelectedOther");
|
|
27
|
+
const [before = "", after = ""] = template.split("{count}");
|
|
28
|
+
const value = formatCount(targetsAll.value ? matchingCount.value : selectedCount.value);
|
|
29
|
+
return { after, before, text: `${before}${value}${after}`, value };
|
|
30
|
+
});
|
|
31
|
+
const scopeOptions = computed(() => [
|
|
32
|
+
{
|
|
33
|
+
count: selectedCount.value,
|
|
34
|
+
label: t("table.selectionBar.selection"),
|
|
35
|
+
value: "selection"
|
|
36
|
+
},
|
|
37
|
+
{ count: matchingCount.value, label: t("table.selectionBar.allResults"), value: "all" }
|
|
38
|
+
]);
|
|
39
|
+
const rows = computed(() => internals.actions.bulkActions.value.map(toRow));
|
|
40
|
+
const groups = computed(
|
|
41
|
+
() => [
|
|
42
|
+
rows.value.filter((row) => !row.destructive),
|
|
43
|
+
rows.value.filter((row) => row.destructive)
|
|
44
|
+
].filter((group) => group.length)
|
|
45
|
+
);
|
|
46
|
+
const busy = computed(() => rows.value.some((row) => row.pending));
|
|
47
|
+
const triggerProps = computed(
|
|
48
|
+
() => mergeDataListProps(
|
|
49
|
+
{
|
|
50
|
+
color: "primary",
|
|
51
|
+
label: t("table.selectionBar.actions"),
|
|
52
|
+
size: "lg",
|
|
53
|
+
trailingIcon: "i-lucide-chevron-up",
|
|
54
|
+
variant: "solid"
|
|
55
|
+
},
|
|
56
|
+
props.controls.trigger
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
const dismissProps = computed(
|
|
60
|
+
() => mergeDataListProps(
|
|
61
|
+
{ color: "neutral", icon: "i-lucide-x", size: "lg", square: true, variant: "ghost" },
|
|
62
|
+
props.controls.dismiss
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
function formatCount(value) {
|
|
66
|
+
return new Intl.NumberFormat(locale.value.code).format(value);
|
|
67
|
+
}
|
|
68
|
+
function toRow(action) {
|
|
69
|
+
const pending = action.running || action.state.loading;
|
|
70
|
+
const icon = isString(action.definition.icon) ? action.definition.icon : void 0;
|
|
71
|
+
return {
|
|
72
|
+
action,
|
|
73
|
+
destructive: action.definition.color === "error",
|
|
74
|
+
detail: action.state.disabled && action.state.disabledReason || action.definition.description,
|
|
75
|
+
detailId: `${id}-${action.definition.key}`,
|
|
76
|
+
icon: pending ? "i-lucide-loader-circle" : icon,
|
|
77
|
+
label: resolveTableActionLabel(action.definition.label) ?? action.definition.key,
|
|
78
|
+
pending,
|
|
79
|
+
unavailable: action.state.disabled || pending
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
watch(open, async (value) => {
|
|
83
|
+
if (!value) return;
|
|
84
|
+
await nextTick();
|
|
85
|
+
sheetBody.value?.querySelector(
|
|
86
|
+
'[role="radio"][aria-checked="true"], .nut-dl-selsheet__action:not([aria-disabled])'
|
|
87
|
+
)?.focus({ preventScroll: true });
|
|
88
|
+
});
|
|
89
|
+
function run(row) {
|
|
90
|
+
if (row.unavailable) return;
|
|
91
|
+
open.value = false;
|
|
92
|
+
void row.action.execute();
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<template>
|
|
97
|
+
<div
|
|
98
|
+
:class="
|
|
99
|
+
mergeDataListUiClass(
|
|
100
|
+
'nut-dl-selbar__bar nut-dl-selbar__bar--compact flex h-[60px] w-full items-center gap-1 rounded-[calc(var(--ui-radius)*2+0.5rem)] bg-inverted p-2 text-inverted shadow-[0_20px_50px_-18px_rgb(31_29_26/0.6)]',
|
|
101
|
+
void 0,
|
|
102
|
+
ui.bar
|
|
103
|
+
)
|
|
104
|
+
"
|
|
105
|
+
role="region"
|
|
106
|
+
:aria-label="t('table.selectionBar.selection')"
|
|
107
|
+
>
|
|
108
|
+
<UButton
|
|
109
|
+
v-bind="dismissProps"
|
|
110
|
+
:aria-label="t('table.selectionBar.clear')"
|
|
111
|
+
:title="t('table.selectionBar.clear')"
|
|
112
|
+
:ui="{
|
|
113
|
+
base: mergeDataListUiClass(
|
|
114
|
+
'nut-dl-selbar__dismiss size-11 shrink-0 justify-center text-inverted/80 hover:bg-white/12 hover:text-inverted focus-visible:outline-2 focus-visible:outline-[var(--ui-text-inverted)] active:bg-white/16 active:text-inverted',
|
|
115
|
+
void 0,
|
|
116
|
+
ui.dismiss
|
|
117
|
+
)
|
|
118
|
+
}"
|
|
119
|
+
@click="internals.selection.clearSelection()"
|
|
120
|
+
/>
|
|
121
|
+
|
|
122
|
+
<span
|
|
123
|
+
:class="
|
|
124
|
+
mergeDataListUiClass(
|
|
125
|
+
'nut-dl-selbar__count min-w-0 flex-1 text-[15px] leading-tight font-semibold',
|
|
126
|
+
void 0,
|
|
127
|
+
ui.count
|
|
128
|
+
)
|
|
129
|
+
"
|
|
130
|
+
role="status"
|
|
131
|
+
>
|
|
132
|
+
{{ count.before }}<span class="text-primary tabular-nums">{{ count.value }}</span
|
|
133
|
+
>{{ count.after }}
|
|
134
|
+
</span>
|
|
135
|
+
|
|
136
|
+
<UDrawer
|
|
137
|
+
v-model:open="open"
|
|
138
|
+
direction="bottom"
|
|
139
|
+
:handle="true"
|
|
140
|
+
:title="t('table.selectionBar.actionsTitle')"
|
|
141
|
+
:ui="{
|
|
142
|
+
header: 'sr-only',
|
|
143
|
+
content: mergeDataListUiClass(
|
|
144
|
+
'nut-dl-sheet nut-dl-selsheet max-h-[85vh] rounded-t-[16px]',
|
|
145
|
+
void 0,
|
|
146
|
+
ui.sheet
|
|
147
|
+
),
|
|
148
|
+
container: 'gap-0 p-0',
|
|
149
|
+
body: 'min-h-0 overflow-y-auto p-0 pb-[max(12px,env(safe-area-inset-bottom))]'
|
|
150
|
+
}"
|
|
151
|
+
>
|
|
152
|
+
<UButton
|
|
153
|
+
v-bind="triggerProps"
|
|
154
|
+
:aria-label="t('table.selectionBar.actionsTitle')"
|
|
155
|
+
:loading="busy"
|
|
156
|
+
:ui="{
|
|
157
|
+
base: mergeDataListUiClass(
|
|
158
|
+
'nut-dl-selbar__trigger h-11 shrink-0 rounded-lg px-4 text-[15px] font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-text-inverted)]',
|
|
159
|
+
void 0,
|
|
160
|
+
ui.trigger
|
|
161
|
+
),
|
|
162
|
+
trailingIcon: 'size-4'
|
|
163
|
+
}"
|
|
164
|
+
/>
|
|
165
|
+
|
|
166
|
+
<template #body>
|
|
167
|
+
<div ref="sheetBody">
|
|
168
|
+
<div
|
|
169
|
+
v-if="scope"
|
|
170
|
+
class="nut-dl-selsheet__scope mx-[10px] mt-2 mb-1 grid grid-cols-[repeat(2,minmax(0,1fr))] gap-0.5 rounded-md bg-elevated p-0.5"
|
|
171
|
+
role="radiogroup"
|
|
172
|
+
:aria-label="t('table.selectionBar.selection')"
|
|
173
|
+
>
|
|
174
|
+
<button
|
|
175
|
+
v-for="option in scopeOptions"
|
|
176
|
+
:key="option.value"
|
|
177
|
+
type="button"
|
|
178
|
+
role="radio"
|
|
179
|
+
class="nut-dl-selsheet__scope-btn flex min-h-11 min-w-0 flex-wrap items-center justify-center gap-x-1.5 rounded-[5px] px-1 py-1 text-center text-[13px] leading-tight font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-primary/40"
|
|
180
|
+
:class="
|
|
181
|
+
bulkScope === option.value ? 'bg-default text-highlighted shadow-[0_0_0_1px_var(--ui-border)]' : 'text-muted hover:text-default'
|
|
182
|
+
"
|
|
183
|
+
:aria-checked="bulkScope === option.value"
|
|
184
|
+
@click="internals.selection.setBulkScope(option.value)"
|
|
185
|
+
>
|
|
186
|
+
<span>{{ option.label }}</span>
|
|
187
|
+
<b class="font-semibold text-primary tabular-nums">{{ formatCount(option.count) }}</b>
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
<div
|
|
191
|
+
v-else
|
|
192
|
+
class="nut-dl-sheet__title px-5 pt-3 pb-1.5 text-[13px] font-semibold text-muted"
|
|
193
|
+
>
|
|
194
|
+
{{ count.text }}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div class="px-[10px] pt-1">
|
|
198
|
+
<template v-for="(group, groupIndex) in groups" :key="groupIndex">
|
|
199
|
+
<div v-if="groupIndex" class="my-1 border-t border-default" />
|
|
200
|
+
<button
|
|
201
|
+
v-for="row in group"
|
|
202
|
+
:key="row.action.definition.key"
|
|
203
|
+
type="button"
|
|
204
|
+
class="nut-dl-sheet__row nut-dl-selsheet__action flex min-h-[46px] w-full items-center gap-[10px] rounded-lg px-[10px] py-2 text-left text-[15px] outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset"
|
|
205
|
+
:class="
|
|
206
|
+
row.unavailable ? 'cursor-not-allowed text-dimmed' : row.destructive ? 'text-error active:bg-error/10' : 'text-highlighted active:bg-elevated'
|
|
207
|
+
"
|
|
208
|
+
:aria-disabled="row.unavailable || void 0"
|
|
209
|
+
:aria-describedby="row.detail ? row.detailId : void 0"
|
|
210
|
+
@click="run(row)"
|
|
211
|
+
>
|
|
212
|
+
<UIcon
|
|
213
|
+
v-if="row.icon"
|
|
214
|
+
:name="row.icon"
|
|
215
|
+
class="size-4 shrink-0"
|
|
216
|
+
:class="[
|
|
217
|
+
row.pending ? 'animate-spin motion-reduce:animate-none' : '',
|
|
218
|
+
row.destructive && !row.unavailable ? 'text-error' : 'text-muted'
|
|
219
|
+
]"
|
|
220
|
+
/>
|
|
221
|
+
<span class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
222
|
+
<span class="nut-dl-selsheet__label">{{ row.label }}</span>
|
|
223
|
+
<span
|
|
224
|
+
v-if="row.detail"
|
|
225
|
+
:id="row.detailId"
|
|
226
|
+
class="nut-dl-selsheet__detail text-[12.5px] leading-snug text-muted"
|
|
227
|
+
>
|
|
228
|
+
{{ row.detail }}
|
|
229
|
+
</span>
|
|
230
|
+
</span>
|
|
231
|
+
</button>
|
|
232
|
+
</template>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</template>
|
|
236
|
+
</UDrawer>
|
|
237
|
+
</div>
|
|
238
|
+
</template>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DataListSelectionActionsProps, DataListSelectionActionsUi } from '../../types/index.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** Shows the selection / all-results scope switch at the top of the sheet. */
|
|
4
|
+
scope: boolean;
|
|
5
|
+
ui: DataListSelectionActionsUi;
|
|
6
|
+
controls: DataListSelectionActionsProps;
|
|
7
|
+
};
|
|
8
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
|
+
declare const _default: typeof __VLS_export;
|
|
10
|
+
export default _default;
|
|
@@ -11,6 +11,7 @@ import { mergeDataListUiClass, resolveTableRowId } from "../../utils";
|
|
|
11
11
|
import DataListErrorState from "../data-list/data-list-error-state.vue";
|
|
12
12
|
import ProgressLine from "../layout/progress-line.vue";
|
|
13
13
|
import TableEmptyState from "../table/table-empty-state.vue";
|
|
14
|
+
import OverlayInsetSpacer from "../utils/overlay-inset-spacer.vue";
|
|
14
15
|
import GridCard from "./grid-card.vue";
|
|
15
16
|
import GridSkeleton from "./grid-skeleton.vue";
|
|
16
17
|
const props = defineProps({
|
|
@@ -283,6 +284,7 @@ function refreshData() {
|
|
|
283
284
|
<GridCard :row-index="index" />
|
|
284
285
|
</div>
|
|
285
286
|
</div>
|
|
287
|
+
<OverlayInsetSpacer />
|
|
286
288
|
</div>
|
|
287
289
|
|
|
288
290
|
<div
|
|
@@ -19,6 +19,7 @@ import DataListErrorState from "../data-list/data-list-error-state.vue";
|
|
|
19
19
|
import ProgressLine from "../layout/progress-line.vue";
|
|
20
20
|
import TableEmptyState from "../table/table-empty-state.vue";
|
|
21
21
|
import TableSelectionControl from "../table/table-selection-control.vue";
|
|
22
|
+
import OverlayInsetSpacer from "../utils/overlay-inset-spacer.vue";
|
|
22
23
|
import SummaryRow from "./summary-row.vue";
|
|
23
24
|
const props = defineProps({
|
|
24
25
|
height: { type: String, required: false },
|
|
@@ -265,6 +266,7 @@ watch(
|
|
|
265
266
|
<span>{{ t("table.controls.loadingMore") }}</span>
|
|
266
267
|
</li>
|
|
267
268
|
</ul>
|
|
269
|
+
<OverlayInsetSpacer />
|
|
268
270
|
</div>
|
|
269
271
|
|
|
270
272
|
<ProgressLine
|
|
@@ -149,8 +149,8 @@ defineExpose({ measure: schedule });
|
|
|
149
149
|
<template>
|
|
150
150
|
<div
|
|
151
151
|
v-if="hasVertical"
|
|
152
|
-
class="nut-dl-sb pointer-events-
|
|
153
|
-
:class="{ 'pointer-events-auto opacity-100': visible }"
|
|
152
|
+
class="nut-dl-sb [pointer-events:var(--nut-table-scrollbar-idle-pointer-events)] absolute top-1 right-[3px] bottom-2 z-[6] w-(--nut-table-scrollbar-size) opacity-(--nut-table-scrollbar-idle-opacity) transition-opacity duration-200 ease-out motion-reduce:transition-none"
|
|
153
|
+
:class="{ 'pointer-events-auto! opacity-100': visible }"
|
|
154
154
|
>
|
|
155
155
|
<span
|
|
156
156
|
class="absolute top-0 left-0 w-full rounded-full bg-(--nut-scrollbar-thumb) transition-colors duration-150 will-change-transform hover:bg-(--nut-scrollbar-thumb-hover)"
|
|
@@ -164,8 +164,8 @@ defineExpose({ measure: schedule });
|
|
|
164
164
|
</div>
|
|
165
165
|
<div
|
|
166
166
|
v-if="hasHorizontal"
|
|
167
|
-
class="nut-dl-sb pointer-events-
|
|
168
|
-
:class="{ 'pointer-events-auto opacity-100': visible }"
|
|
167
|
+
class="nut-dl-sb [pointer-events:var(--nut-table-scrollbar-idle-pointer-events)] absolute right-2 bottom-[3px] left-1 z-[6] h-(--nut-table-scrollbar-size) opacity-(--nut-table-scrollbar-idle-opacity) transition-opacity duration-200 ease-out motion-reduce:transition-none"
|
|
168
|
+
:class="{ 'pointer-events-auto! opacity-100': visible }"
|
|
169
169
|
>
|
|
170
170
|
<span
|
|
171
171
|
class="absolute top-0 left-0 h-full rounded-full bg-(--nut-scrollbar-thumb) transition-colors duration-150 will-change-transform hover:bg-(--nut-scrollbar-thumb-hover)"
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "../../utils/tree";
|
|
24
24
|
import DataListErrorState from "../data-list/data-list-error-state.vue";
|
|
25
25
|
import ProgressLine from "../layout/progress-line.vue";
|
|
26
|
+
import OverlayInsetSpacer from "../utils/overlay-inset-spacer.vue";
|
|
26
27
|
import TableCell from "./table-cell";
|
|
27
28
|
import TableColumnHeader from "./table-column-header.vue";
|
|
28
29
|
import TableEmptyState from "./table-empty-state.vue";
|
|
@@ -921,6 +922,7 @@ defineExpose({ resetColumnSizing });
|
|
|
921
922
|
/>
|
|
922
923
|
</slot>
|
|
923
924
|
</div>
|
|
925
|
+
<OverlayInsetSpacer />
|
|
924
926
|
</div>
|
|
925
927
|
<ProgressLine
|
|
926
928
|
:active="refreshing"
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useDataListOverlayInset } from "../../composables/use-data-list-overlay-inset";
|
|
3
|
+
const inset = useDataListOverlayInset();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div
|
|
8
|
+
v-if="inset?.bottom.value"
|
|
9
|
+
class="nut-dl-overlay-inset shrink-0"
|
|
10
|
+
aria-hidden="true"
|
|
11
|
+
:style="{ height: `${inset.bottom.value}px` }"
|
|
12
|
+
/>
|
|
13
|
+
</template>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shares the height a floating part, such as the mobile selection bar, covers at the bottom of the
|
|
3
|
+
* list, so the scroll areas can end with as much room and their last row scrolls above it.
|
|
4
|
+
*/
|
|
5
|
+
export declare function provideDataListOverlayInset(): {
|
|
6
|
+
bottom: import("vue").Ref<number, number>;
|
|
7
|
+
};
|
|
8
|
+
export declare function useDataListOverlayInset(): {
|
|
9
|
+
bottom: import("vue").Ref<number, number>;
|
|
10
|
+
} | undefined;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createInjectionState } from "@vueuse/core";
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
const [provideDataListOverlayInsetState, useInjectedDataListOverlayInset] = createInjectionState(
|
|
4
|
+
() => ({ bottom: ref(0) })
|
|
5
|
+
);
|
|
6
|
+
export function provideDataListOverlayInset() {
|
|
7
|
+
return provideDataListOverlayInsetState();
|
|
8
|
+
}
|
|
9
|
+
export function useDataListOverlayInset() {
|
|
10
|
+
return useInjectedDataListOverlayInset();
|
|
11
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { computed, ref } from "vue";
|
|
2
2
|
import { isRecord } from "../../shared/utils/path.js";
|
|
3
3
|
import { isFunction } from "../../shared/utils/predicate.js";
|
|
4
|
-
import { resolveTableActionDefinitions } from "../utils/actions.js";
|
|
4
|
+
import { resolveTableActionDefinitions, resolveTableActionLabel } from "../utils/actions.js";
|
|
5
5
|
export function useTableActions(options) {
|
|
6
6
|
const runningKeys = ref([]);
|
|
7
7
|
const tableApi = options.tableApi.value;
|
|
@@ -37,12 +37,15 @@ export function useTableActions(options) {
|
|
|
37
37
|
const disabled = resolveFlag(definition.disabled, actionContext);
|
|
38
38
|
const loading = resolveFlag(definition.loading, actionContext);
|
|
39
39
|
const requiresSelection = "requiresSelection" in definition && definition.requiresSelection;
|
|
40
|
-
|
|
40
|
+
const state = {
|
|
41
41
|
disabled: disabled || Boolean(requiresSelection && !actionContext.selectedRows.length),
|
|
42
42
|
loading,
|
|
43
43
|
running: isRunning(definition.key),
|
|
44
44
|
visible: condition
|
|
45
45
|
};
|
|
46
|
+
const reason = disabled ? resolveReason(definition.disabledReason, actionContext) : void 0;
|
|
47
|
+
if (reason) state.disabledReason = reason;
|
|
48
|
+
return state;
|
|
46
49
|
}
|
|
47
50
|
async function execute(definition) {
|
|
48
51
|
const state = resolveState(definition);
|
|
@@ -110,6 +113,12 @@ export function useTableActions(options) {
|
|
|
110
113
|
function resolveCondition(value, context) {
|
|
111
114
|
return isBooleanResolver(value) ? value(context) : value ?? true;
|
|
112
115
|
}
|
|
116
|
+
function resolveReason(value, context) {
|
|
117
|
+
return resolveTableActionLabel(isReasonResolver(value) ? value(context) : value);
|
|
118
|
+
}
|
|
119
|
+
function isReasonResolver(value) {
|
|
120
|
+
return isFunction(value);
|
|
121
|
+
}
|
|
113
122
|
function resolveFlag(value, context) {
|
|
114
123
|
return isBooleanResolver(value) ? value(context) : value ?? false;
|
|
115
124
|
}
|
|
@@ -52,13 +52,6 @@ export declare const DATA_LIST_MOBILE_DEFAULTS: {
|
|
|
52
52
|
readonly clear: "min-h-11 min-w-11";
|
|
53
53
|
};
|
|
54
54
|
};
|
|
55
|
-
readonly selectionActions: {
|
|
56
|
-
readonly ui: {
|
|
57
|
-
readonly action: "h-11 min-w-11";
|
|
58
|
-
readonly dismiss: "size-11";
|
|
59
|
-
readonly overflow: "size-11";
|
|
60
|
-
};
|
|
61
|
-
};
|
|
62
55
|
readonly sortMenu: {
|
|
63
56
|
readonly ui: {
|
|
64
57
|
readonly trigger: "min-h-11 min-w-11";
|
|
@@ -10,7 +10,6 @@ export const DATA_LIST_MOBILE_DEFAULTS = {
|
|
|
10
10
|
pagination: { ui: { button: TOUCH_TARGET } },
|
|
11
11
|
refresh: { ui: { base: TOUCH_TARGET } },
|
|
12
12
|
search: { ui: { base: "min-h-11", clear: TOUCH_TARGET } },
|
|
13
|
-
selectionActions: { ui: { action: "h-11 min-w-11", dismiss: "size-11", overflow: "size-11" } },
|
|
14
13
|
sortMenu: { ui: { trigger: TOUCH_TARGET } },
|
|
15
14
|
table: { ui: { emptyAction: TOUCH_TARGET, rowAction: TOUCH_TARGET, selectControl: TOUCH_AREA } },
|
|
16
15
|
viewToggle: { ui: { base: TOUCH_TARGET } }
|
|
@@ -17,6 +17,11 @@ export interface TableToolbarAction<TRow extends GenericObject = GenericObject,
|
|
|
17
17
|
label?: TableTextValue;
|
|
18
18
|
condition?: boolean | ((context: TContext) => boolean);
|
|
19
19
|
disabled?: boolean | ((context: TableActionContext<TRow, TContext, TPageContext>) => boolean);
|
|
20
|
+
/**
|
|
21
|
+
* Why the action is disabled. The mobile actions sheet shows it under the label while the action
|
|
22
|
+
* is disabled, in place of `description`.
|
|
23
|
+
*/
|
|
24
|
+
disabledReason?: TableTextValue | ((context: TableActionContext<TRow, TContext, TPageContext>) => TableTextValue | undefined);
|
|
20
25
|
loading?: boolean | ((context: TableActionContext<TRow, TContext, TPageContext>) => boolean);
|
|
21
26
|
/**
|
|
22
27
|
* Runs the action. Returning `false` or `{ success: false }` marks the run as failed without
|
|
@@ -34,6 +39,8 @@ export interface TableBulkAction<TRow extends GenericObject = GenericObject, TCo
|
|
|
34
39
|
export interface TableActionState {
|
|
35
40
|
visible: boolean;
|
|
36
41
|
disabled: boolean;
|
|
42
|
+
/** The resolved `disabledReason` while the action is disabled. */
|
|
43
|
+
disabledReason?: string;
|
|
37
44
|
loading: boolean;
|
|
38
45
|
running: boolean;
|
|
39
46
|
}
|
|
@@ -421,6 +421,8 @@ export interface DataListSelectionActionsProps {
|
|
|
421
421
|
overflow?: DataListButtonProps;
|
|
422
422
|
dismiss?: DataListButtonProps;
|
|
423
423
|
scope?: DataListButtonProps;
|
|
424
|
+
/** The « Actions » button that opens the actions sheet below `md`. */
|
|
425
|
+
trigger?: DataListButtonProps;
|
|
424
426
|
}
|
|
425
427
|
/** Component props exposed by the refresh control. */
|
|
426
428
|
export interface DataListRefreshProps {
|
|
@@ -499,7 +501,10 @@ export interface DataListTableConfig extends DataListPartConfig<DataListTableUi,
|
|
|
499
501
|
/** Horizontal padding of the first and last cells, in px; defaults to the cell padding. */
|
|
500
502
|
gutter?: number;
|
|
501
503
|
}
|
|
502
|
-
/**
|
|
504
|
+
/**
|
|
505
|
+
* UI slots rendered by the floating selection bar. Below `md` the bar keeps `root`, `bar`, `count`
|
|
506
|
+
* and `dismiss`, opens its actions in a sheet from `trigger`, and styles the sheet with `sheet`.
|
|
507
|
+
*/
|
|
503
508
|
export interface DataListSelectionActionsUi {
|
|
504
509
|
root?: DataListUiClass;
|
|
505
510
|
bar?: DataListUiClass;
|
|
@@ -509,4 +514,6 @@ export interface DataListSelectionActionsUi {
|
|
|
509
514
|
action?: DataListUiClass;
|
|
510
515
|
overflow?: DataListUiClass;
|
|
511
516
|
dismiss?: DataListUiClass;
|
|
517
|
+
trigger?: DataListUiClass;
|
|
518
|
+
sheet?: DataListUiClass;
|
|
512
519
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-ui-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "Nuxt module and runtime toolkit for UI-heavy Nuxt applications",
|
|
5
5
|
"homepage": "https://github.com/ChronicStone/nuxt-ui-tools#readme",
|
|
6
6
|
"bugs": "https://github.com/ChronicStone/nuxt-ui-tools/issues",
|