nuxt-ui-tools 1.14.0 → 1.16.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 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.14.0",
8
+ "version": "1.16.0",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "1.0.3",
11
11
  "unbuild": "unknown"
@@ -91,13 +91,16 @@ function controlClass(field) {
91
91
  mergeFormUiClass(
92
92
  [
93
93
  rowPadding,
94
+ 'text-left font-medium',
94
95
  bordered ? 'border-b border-r border-default' : 'border-b border-default'
95
96
  ].join(' '),
96
97
  formUi.ui.value.matrix?.ui?.corner
97
98
  )
98
99
  "
99
100
  :style="{ width: rowHeaderWidth, minWidth: rowHeaderWidth }"
100
- />
101
+ >
102
+ {{ resolveFormText(fieldProps.rowHeaderLabel) }}
103
+ </th>
101
104
  <th
102
105
  v-for="column in visibleFields"
103
106
  :id="columnLabelId(column)"
@@ -17,6 +17,8 @@ export type FormMatrixEntry = FormMatrixRow | FormMatrixSection;
17
17
  export interface FormMatrixProps {
18
18
  minWidth?: number | string;
19
19
  rowHeaderWidth?: number | string;
20
+ /** Heading of the row label column, shown in the header corner. */
21
+ rowHeaderLabel?: FormText;
20
22
  bordered?: boolean;
21
23
  striped?: boolean;
22
24
  hoverable?: boolean;
@@ -1,4 +1,4 @@
1
- import type { DropdownMenuProps } from '@nuxt/ui/components/DropdownMenu.vue';
1
+ import type { DropdownMenuItem, DropdownMenuProps } from '@nuxt/ui/components/DropdownMenu.vue';
2
2
  import type { DataListControlSize } from '../../types/index.js';
3
3
  type __VLS_Props = {
4
4
  content?: DropdownMenuProps['content'];
@@ -10,10 +10,10 @@ type __VLS_Props = {
10
10
  ui?: DropdownMenuProps['ui'];
11
11
  };
12
12
  declare var __VLS_8: {
13
- items: import("@nuxt/ui/runtime/components/DropdownMenu.d.vue.js").DropdownMenuItem[];
13
+ items: DropdownMenuItem[][];
14
14
  open: boolean;
15
15
  }, __VLS_24: {
16
- items: import("@nuxt/ui/runtime/components/DropdownMenu.d.vue.js").DropdownMenuItem[];
16
+ items: DropdownMenuItem[][];
17
17
  open: boolean;
18
18
  };
19
19
  type __VLS_Slots = {} & {
@@ -6,7 +6,7 @@ import { computed, ref, useAttrs } from "vue";
6
6
  import { useDataListBreakpoint } from "../../composables/use-data-list-breakpoint";
7
7
  import { useTableInternals } from "../../composables/use-table-internals";
8
8
  import { useTableRowActionScope } from "../../composables/use-table-row-actions";
9
- import { createRowActionDropdownItems, resolveVisibleTableRowActions } from "../../utils";
9
+ import { createRowActionDropdownGroups, resolveVisibleTableRowActions } from "../../utils";
10
10
  defineOptions({
11
11
  inheritAttrs: false
12
12
  });
@@ -23,12 +23,18 @@ const attrs = useAttrs();
23
23
  const internals = useTableInternals();
24
24
  const { isMobile } = useDataListBreakpoint();
25
25
  const sheetOpen = ref(false);
26
- const sheetItems = computed(() => {
27
- const groups = Array.isArray(items.value[0]) ? items.value : [items.value];
28
- return groups.flatMap(
29
- (group, index) => group.map((item, itemIndex) => ({ ...item, dividerBefore: index > 0 && itemIndex === 0 }))
30
- );
31
- });
26
+ const sheetItems = computed(
27
+ () => items.value.flatMap(
28
+ (group, index) => group.map((item, itemIndex) => ({
29
+ color: item.color,
30
+ disabled: item.disabled,
31
+ dividerBefore: index > 0 && itemIndex === 0,
32
+ icon: item.icon,
33
+ label: item.label,
34
+ onSelect: item.onSelect
35
+ }))
36
+ )
37
+ );
32
38
  function runSheetItem(item) {
33
39
  sheetOpen.value = false;
34
40
  item.onSelect?.(new Event("select"));
@@ -48,7 +54,7 @@ const items = computed(() => {
48
54
  if (!scope?.value) {
49
55
  return [];
50
56
  }
51
- return createRowActionDropdownItems({
57
+ return createRowActionDropdownGroups({
52
58
  actions: visibleActions.value,
53
59
  scope: scope.value
54
60
  });
@@ -1,4 +1,4 @@
1
- import type { DropdownMenuProps } from '@nuxt/ui/components/DropdownMenu.vue';
1
+ import type { DropdownMenuItem, DropdownMenuProps } from '@nuxt/ui/components/DropdownMenu.vue';
2
2
  import type { DataListControlSize } from '../../types/index.js';
3
3
  type __VLS_Props = {
4
4
  content?: DropdownMenuProps['content'];
@@ -10,10 +10,10 @@ type __VLS_Props = {
10
10
  ui?: DropdownMenuProps['ui'];
11
11
  };
12
12
  declare var __VLS_8: {
13
- items: import("@nuxt/ui/runtime/components/DropdownMenu.d.vue.js").DropdownMenuItem[];
13
+ items: DropdownMenuItem[][];
14
14
  open: boolean;
15
15
  }, __VLS_24: {
16
- items: import("@nuxt/ui/runtime/components/DropdownMenu.d.vue.js").DropdownMenuItem[];
16
+ items: DropdownMenuItem[][];
17
17
  open: boolean;
18
18
  };
19
19
  type __VLS_Slots = {} & {
@@ -18,17 +18,18 @@ const emptyProps = computed(() => dataListUi.ui.value.table?.props?.empty);
18
18
  const filtered = computed(
19
19
  () => internals.filters.hasActiveUiFilters.value || Boolean(internals.filters.searchQuery.value)
20
20
  );
21
+ const custom = computed(() => filtered.value ? void 0 : emptyProps.value);
21
22
  const icon = computed(
22
- () => emptyProps.value?.icon ?? (filtered.value ? "i-lucide-search-x" : "i-lucide-inbox")
23
+ () => custom.value?.icon ?? (filtered.value ? "i-lucide-search-x" : "i-lucide-inbox")
23
24
  );
24
25
  const title = computed(
25
- () => emptyProps.value?.title ?? (filtered.value ? t("table.states.empty.filteredTitle") : t("table.states.empty.title"))
26
+ () => custom.value?.title ?? (filtered.value ? t("table.states.empty.filteredTitle") : t("table.states.empty.title"))
26
27
  );
27
28
  const description = computed(() => {
28
- if (emptyProps.value?.description === false) {
29
+ if (custom.value?.description === false) {
29
30
  return "";
30
31
  }
31
- return emptyProps.value?.description || (filtered.value ? t("table.states.empty.filteredDescription") : t("table.states.empty.description"));
32
+ return custom.value?.description || (filtered.value ? t("table.states.empty.filteredDescription") : t("table.states.empty.description"));
32
33
  });
33
34
  function reset() {
34
35
  internals.filters.clearAllFilters();
@@ -2,6 +2,9 @@ import { useCookie } from "nuxt/app";
2
2
  import { computed, ref, watch } from "vue";
3
3
  import {
4
4
  ROW_ACTIONS_COLUMN_ID,
5
+ applyColumnPreferences,
6
+ decodeColumnPreferences,
7
+ encodeColumnPreferences,
5
8
  hasVisibleTableRowActions,
6
9
  createColumnMenuItems,
7
10
  createDefaultColumnState,
@@ -79,12 +82,18 @@ export function useTableColumns(params) {
79
82
  { immediate: true }
80
83
  );
81
84
  const persistPreferences = computed(() => params.schema.value.persistence?.preferences !== false);
82
- const preferencesCookie = useCookie(
83
- `${params.schema.value.tableKey}::columns`,
84
- { default: () => null, maxAge: 60 * 60 * 24 * 365 }
85
- );
86
- if (persistPreferences.value && preferencesCookie.value) {
87
- tableState.value = applyPersistedPreferences(tableState.value, preferencesCookie.value);
85
+ const preferencesCookie = useCookie(`${params.schema.value.tableKey}::columns`, {
86
+ decode: (value) => value ?? null,
87
+ default: () => null,
88
+ encode: (value) => value ?? "",
89
+ maxAge: 60 * 60 * 24 * 365
90
+ });
91
+ const storedPreferences = persistPreferences.value ? decodeColumnPreferences(preferencesCookie.value) : null;
92
+ if (storedPreferences) {
93
+ tableState.value = applyColumnPreferences({
94
+ preferences: storedPreferences,
95
+ state: tableState.value
96
+ });
88
97
  }
89
98
  watch(
90
99
  tableState,
@@ -92,12 +101,19 @@ export function useTableColumns(params) {
92
101
  if (!persistPreferences.value) {
93
102
  return;
94
103
  }
95
- const next = toPersistedPreferences(state);
96
- if (JSON.stringify(next) !== JSON.stringify(preferencesCookie.value)) {
104
+ const next = encodeColumnPreferences({
105
+ defaults: createResetColumnState({
106
+ currentState: state,
107
+ runtimeColumns: runtimeColumns.value,
108
+ schema: params.schema.value
109
+ }),
110
+ state
111
+ });
112
+ if (next !== preferencesCookie.value) {
97
113
  preferencesCookie.value = next;
98
114
  }
99
115
  },
100
- { deep: true }
116
+ { deep: true, immediate: true }
101
117
  );
102
118
  watch(
103
119
  () => params.state.queryState.sorting.value,
@@ -251,36 +267,3 @@ export function useTableColumns(params) {
251
267
  visibleOrderedColumns
252
268
  };
253
269
  }
254
- function toPersistedPreferences(state) {
255
- return {
256
- order: state.columnOrder,
257
- pinning: state.columnPinning,
258
- sizing: state.columnSizing,
259
- visibility: state.columnVisibility
260
- };
261
- }
262
- function applyPersistedPreferences(state, persisted) {
263
- const known = new Set(state.columnOrder);
264
- return {
265
- ...state,
266
- columnOrder: [
267
- ...persisted.order.filter((id) => known.has(id)),
268
- ...state.columnOrder.filter((id) => !persisted.order.includes(id))
269
- ],
270
- columnPinning: {
271
- left: (persisted.pinning.left ?? state.columnPinning.left ?? []).filter(
272
- (id) => known.has(id)
273
- ),
274
- right: (persisted.pinning.right ?? state.columnPinning.right ?? []).filter(
275
- (id) => known.has(id)
276
- )
277
- },
278
- columnSizing: Object.fromEntries(
279
- Object.entries(persisted.sizing).filter(([id]) => known.has(id))
280
- ),
281
- columnVisibility: {
282
- ...state.columnVisibility,
283
- ...Object.fromEntries(Object.entries(persisted.visibility).filter(([id]) => known.has(id)))
284
- }
285
- };
286
- }
@@ -57,6 +57,11 @@ export interface TableRowActionContext<TRow extends GenericObject = GenericObjec
57
57
  export interface TableRowAction<TRow extends GenericObject = GenericObject, TContext extends GenericObject = GenericObject, TPageContext extends GenericObject = GenericObject> extends Omit<DropdownMenuItem, 'children' | 'disabled' | 'loading' | 'label' | 'onSelect'> {
58
58
  key: string;
59
59
  label?: TableTextValue;
60
+ /**
61
+ * Actions sharing a group render together, separated from the other groups by a divider.
62
+ * Groups follow the order of their first visible action; actions without a group share one.
63
+ */
64
+ group?: string;
60
65
  condition?: boolean | ((context: TableRowActionContext<TRow, TContext, TPageContext>) => boolean);
61
66
  disabled?: boolean | ((context: TableRowActionContext<TRow, TContext, TPageContext>) => boolean);
62
67
  loading?: boolean | ((context: TableRowActionContext<TRow, TContext, TPageContext>) => boolean);
@@ -38,5 +38,9 @@ export declare function createRowActionDropdownItems(options: {
38
38
  actions: ResolvedRowAction[];
39
39
  scope: TableInjectedRowActionScope;
40
40
  }): DropdownMenuItem[];
41
+ export declare function createRowActionDropdownGroups(options: {
42
+ actions: ResolvedRowAction[];
43
+ scope: TableInjectedRowActionScope;
44
+ }): DropdownMenuItem[][];
41
45
  export declare function resolveTableActionLabel(label: TableTextValue | undefined): string | undefined;
42
46
  export {};
@@ -44,6 +44,16 @@ export function createRowActionDropdownItems(options) {
44
44
  (action) => mapRowActionToDropdownItem({ action, scope: options.scope })
45
45
  );
46
46
  }
47
+ export function createRowActionDropdownGroups(options) {
48
+ const groups = /* @__PURE__ */ new Map();
49
+ for (const action of options.actions) {
50
+ const key = action.group ?? "";
51
+ const items = groups.get(key) ?? [];
52
+ items.push(mapRowActionToDropdownItem({ action, scope: options.scope }));
53
+ groups.set(key, items);
54
+ }
55
+ return [...groups.values()];
56
+ }
47
57
  function mapRowActionToDropdownItem(options) {
48
58
  const children = options.action.children ? createRowActionDropdownItems({
49
59
  actions: options.action.children,
@@ -67,6 +77,7 @@ function mapRowActionToDropdownItem(options) {
67
77
  };
68
78
  delete item.condition;
69
79
  delete item.action;
80
+ delete item.group;
70
81
  return item;
71
82
  }
72
83
  function resolveActionLabel(action) {
@@ -1,4 +1,5 @@
1
1
  export * from './menu.js';
2
+ export * from './preferences.js';
2
3
  export * from './render.js';
3
4
  export * from './schema.js';
4
5
  export * from './state.js';
@@ -1,4 +1,5 @@
1
1
  export * from "./menu.js";
2
+ export * from "./preferences.js";
2
3
  export * from "./render.js";
3
4
  export * from "./schema.js";
4
5
  export * from "./state.js";
@@ -0,0 +1,40 @@
1
+ import type { TableColumnState } from './types.js';
2
+ export interface ColumnPreferences {
3
+ order?: string[];
4
+ hidden?: string[];
5
+ shown?: string[];
6
+ left?: string[];
7
+ right?: string[];
8
+ widths?: Record<string, number>;
9
+ }
10
+ export declare function encodeColumnPreferences(options: {
11
+ state: TableColumnState;
12
+ defaults: TableColumnState;
13
+ }): string | null;
14
+ export declare function decodeColumnPreferences(value: string | null | undefined): ColumnPreferences | null;
15
+ export declare function applyColumnPreferences(options: {
16
+ state: TableColumnState;
17
+ preferences: ColumnPreferences;
18
+ }): {
19
+ columnOrder: string[];
20
+ columnPinning: {
21
+ left: string[];
22
+ right: string[];
23
+ };
24
+ columnSizing: Record<string, number>;
25
+ columnVisibility: {
26
+ [x: string]: boolean;
27
+ };
28
+ columnSizingInfo: {
29
+ startOffset: number | null;
30
+ startSize: number | null;
31
+ deltaOffset: number | null;
32
+ deltaPercentage: number | null;
33
+ isResizingColumn: false | string;
34
+ columnSizingStart: [string, number][];
35
+ };
36
+ sorting: {
37
+ id: string;
38
+ desc: boolean;
39
+ }[];
40
+ };
@@ -0,0 +1,145 @@
1
+ import { isArray, isFiniteNumber, isPlainObject, isString } from "../../../shared/utils/predicate.js";
2
+ import { uniqueColumnIds } from "./schema.js";
3
+ import { ROW_ACTIONS_COLUMN_ID, SELECT_COLUMN_ID } from "./types.js";
4
+ const FORMAT_VERSION = "1";
5
+ const SECTION_SEPARATOR = "!";
6
+ const ITEM_SEPARATOR = "~";
7
+ const WIDTH_SEPARATOR = "*";
8
+ const INTERNAL_COLUMN_IDS = /* @__PURE__ */ new Set([SELECT_COLUMN_ID, ROW_ACTIONS_COLUMN_ID]);
9
+ export function encodeColumnPreferences(options) {
10
+ const sections = [];
11
+ const order = userColumnIds(options.state.columnOrder);
12
+ const defaultOrder = userColumnIds(options.defaults.columnOrder);
13
+ const visibilityChanges = defaultOrder.filter(
14
+ (id) => (options.state.columnVisibility[id] ?? options.defaults.columnVisibility[id]) !== options.defaults.columnVisibility[id]
15
+ );
16
+ const left = userColumnIds(options.state.columnPinning.left ?? []);
17
+ const right = userColumnIds(options.state.columnPinning.right ?? []);
18
+ const widths = Object.entries(options.state.columnSizing).filter(
19
+ ([, width]) => Number.isFinite(width)
20
+ );
21
+ if (!sameIds(order, defaultOrder)) {
22
+ sections.push(`o${joinIds(order)}`);
23
+ }
24
+ const hidden = visibilityChanges.filter((id) => options.state.columnVisibility[id] === false);
25
+ const shown = visibilityChanges.filter((id) => options.state.columnVisibility[id] === true);
26
+ if (hidden.length) {
27
+ sections.push(`h${joinIds(hidden)}`);
28
+ }
29
+ if (shown.length) {
30
+ sections.push(`v${joinIds(shown)}`);
31
+ }
32
+ if (!sameIds(left, userColumnIds(options.defaults.columnPinning.left ?? []))) {
33
+ sections.push(`l${joinIds(left)}`);
34
+ }
35
+ if (!sameIds(right, userColumnIds(options.defaults.columnPinning.right ?? []))) {
36
+ sections.push(`r${joinIds(right)}`);
37
+ }
38
+ if (widths.length) {
39
+ sections.push(
40
+ `w${widths.map(([id, width]) => `${escapeId(id)}${WIDTH_SEPARATOR}${Math.round(width)}`).join(ITEM_SEPARATOR)}`
41
+ );
42
+ }
43
+ return sections.length ? [FORMAT_VERSION, ...sections].join(SECTION_SEPARATOR) : null;
44
+ }
45
+ export function decodeColumnPreferences(value) {
46
+ if (!value) {
47
+ return null;
48
+ }
49
+ if (value.startsWith("{") || value.startsWith("%7B")) {
50
+ return decodeLegacyPreferences(value);
51
+ }
52
+ const [version, ...sections] = value.split(SECTION_SEPARATOR);
53
+ if (version !== FORMAT_VERSION) {
54
+ return null;
55
+ }
56
+ const preferences = {};
57
+ for (const section of sections) {
58
+ const key = section.charAt(0);
59
+ const ids = splitIds(section.slice(1));
60
+ if (key === "o") preferences.order = ids;
61
+ else if (key === "h") preferences.hidden = ids;
62
+ else if (key === "v") preferences.shown = ids;
63
+ else if (key === "l") preferences.left = ids;
64
+ else if (key === "r") preferences.right = ids;
65
+ else if (key === "w") preferences.widths = decodeWidths(section.slice(1));
66
+ }
67
+ return preferences;
68
+ }
69
+ export function applyColumnPreferences(options) {
70
+ const known = new Set(options.state.columnOrder);
71
+ const keep = (ids) => (ids ?? []).filter((id) => known.has(id));
72
+ const { preferences, state } = options;
73
+ const order = keep(preferences.order);
74
+ return {
75
+ ...state,
76
+ columnOrder: preferences.order ? uniqueColumnIds({ columnIds: [...order, ...state.columnOrder] }) : state.columnOrder,
77
+ columnPinning: {
78
+ left: preferences.left ? uniqueColumnIds({ columnIds: [SELECT_COLUMN_ID, ...keep(preferences.left)] }) : state.columnPinning.left ?? [],
79
+ right: preferences.right ? withRowActionsLast({
80
+ ids: keep(preferences.right),
81
+ hadRowActions: (state.columnPinning.right ?? []).includes(ROW_ACTIONS_COLUMN_ID)
82
+ }) : state.columnPinning.right ?? []
83
+ },
84
+ columnSizing: preferences.widths ? Object.fromEntries(Object.entries(preferences.widths).filter(([id]) => known.has(id))) : state.columnSizing,
85
+ columnVisibility: {
86
+ ...state.columnVisibility,
87
+ ...Object.fromEntries(keep(preferences.hidden).map((id) => [id, false])),
88
+ ...Object.fromEntries(keep(preferences.shown).map((id) => [id, true]))
89
+ }
90
+ };
91
+ }
92
+ function decodeLegacyPreferences(value) {
93
+ let legacy;
94
+ try {
95
+ legacy = JSON.parse(value.startsWith("{") ? value : decodeURIComponent(value));
96
+ } catch {
97
+ return null;
98
+ }
99
+ if (!isPlainObject(legacy)) {
100
+ return null;
101
+ }
102
+ const visibility = isPlainObject(legacy.visibility) ? Object.entries(legacy.visibility) : [];
103
+ const pinning = isPlainObject(legacy.pinning) ? legacy.pinning : {};
104
+ return {
105
+ hidden: visibility.filter(([, visible]) => visible === false).map(([id]) => id),
106
+ left: isArray(pinning.left) ? pinning.left.filter((id) => isString(id)) : void 0,
107
+ order: isArray(legacy.order) ? legacy.order.filter((id) => isString(id)) : void 0,
108
+ right: isArray(pinning.right) ? pinning.right.filter((id) => isString(id)) : void 0,
109
+ widths: isPlainObject(legacy.sizing) ? Object.fromEntries(
110
+ Object.entries(legacy.sizing).filter(
111
+ (entry) => isFiniteNumber(entry[1])
112
+ )
113
+ ) : void 0
114
+ };
115
+ }
116
+ function decodeWidths(value) {
117
+ return Object.fromEntries(
118
+ value.split(ITEM_SEPARATOR).flatMap((entry) => {
119
+ const separator = entry.lastIndexOf(WIDTH_SEPARATOR);
120
+ const width = Number(entry.slice(separator + 1));
121
+ return separator > 0 && Number.isFinite(width) ? [[decodeURIComponent(entry.slice(0, separator)), width]] : [];
122
+ })
123
+ );
124
+ }
125
+ function userColumnIds(ids) {
126
+ return ids.filter((id) => !INTERNAL_COLUMN_IDS.has(id));
127
+ }
128
+ function sameIds(first, second) {
129
+ return first.length === second.length && first.every((id, index) => id === second[index]);
130
+ }
131
+ function joinIds(ids) {
132
+ return ids.map(escapeId).join(ITEM_SEPARATOR);
133
+ }
134
+ function splitIds(value) {
135
+ return value ? value.split(ITEM_SEPARATOR).map((id) => decodeURIComponent(id)) : [];
136
+ }
137
+ function escapeId(id) {
138
+ return encodeURIComponent(id).replace(
139
+ /[!'()*~]/gu,
140
+ (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`
141
+ );
142
+ }
143
+ function withRowActionsLast(options) {
144
+ return options.hadRowActions ? [...options.ids, ROW_ACTIONS_COLUMN_ID] : options.ids;
145
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-ui-tools",
3
- "version": "1.14.0",
3
+ "version": "1.16.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",