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.
Files changed (27) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/i18n/locales/en.js +5 -0
  3. package/dist/runtime/i18n/locales/fr.js +5 -0
  4. package/dist/runtime/i18n/types.d.ts +5 -0
  5. package/dist/runtime/shared/styles/scrollbars.css +1 -1
  6. package/dist/runtime/table/components/data-list/data-list-root.vue +2 -0
  7. package/dist/runtime/table/components/data-list/data-list-selection-actions.d.vue.ts +1 -1
  8. package/dist/runtime/table/components/data-list/data-list-selection-actions.vue +32 -5
  9. package/dist/runtime/table/components/data-list/data-list-selection-actions.vue.d.ts +1 -1
  10. package/dist/runtime/table/components/data-list/data-list-selection-sheet.d.vue.ts +10 -0
  11. package/dist/runtime/table/components/data-list/data-list-selection-sheet.vue +238 -0
  12. package/dist/runtime/table/components/data-list/data-list-selection-sheet.vue.d.ts +10 -0
  13. package/dist/runtime/table/components/grid/grid-renderer.vue +2 -0
  14. package/dist/runtime/table/components/summary/summary-renderer.vue +2 -0
  15. package/dist/runtime/table/components/table/table-overlay-scrollbars.vue +4 -4
  16. package/dist/runtime/table/components/table/table-renderer.vue +2 -0
  17. package/dist/runtime/table/components/utils/overlay-inset-spacer.d.vue.ts +3 -0
  18. package/dist/runtime/table/components/utils/overlay-inset-spacer.vue +13 -0
  19. package/dist/runtime/table/components/utils/overlay-inset-spacer.vue.d.ts +3 -0
  20. package/dist/runtime/table/composables/use-data-list-overlay-inset.d.ts +10 -0
  21. package/dist/runtime/table/composables/use-data-list-overlay-inset.js +11 -0
  22. package/dist/runtime/table/composables/use-table-actions.js +11 -2
  23. package/dist/runtime/table/constants/mobile.d.ts +0 -7
  24. package/dist/runtime/table/constants/mobile.js +0 -1
  25. package/dist/runtime/table/types/actions.d.ts +7 -0
  26. package/dist/runtime/table/types/data-list.d.ts +8 -1
  27. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "configKey": "nuxtUiTools",
6
6
  "docs": "https://ui.nuxt.com/docs/getting-started/installation/nuxt",
7
7
  "name": "nuxt-ui-tools",
8
- "version": "1.22.2",
8
+ "version": "1.24.0",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "1.0.3",
11
11
  "unbuild": "unknown"
@@ -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 rendered inline before the overflow menu. */
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 ?? (isMobile.value ? 1 : 3));
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 ${position === 'fixed' ? 'fixed inset-x-0 bottom-4' : 'absolute inset-x-0 bottom-[22px]'}`,
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 class="pointer-events-auto flex max-w-full justify-center">
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 rendered inline before the overflow menu. */
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-none absolute top-1 right-[3px] bottom-2 z-[6] w-[5px] opacity-0 transition-opacity duration-200 ease-out motion-reduce:transition-none"
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-none absolute right-2 bottom-[3px] left-1 z-[6] h-[5px] opacity-0 transition-opacity duration-200 ease-out motion-reduce:transition-none"
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
- return {
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
- /** UI slots rendered by the floating selection bar. */
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.22.2",
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",