@nova-design-system/nova-vue 3.19.0 → 3.21.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/README.md CHANGED
@@ -2,26 +2,6 @@
2
2
 
3
3
  **Nova Components Vue** provides an easy way to use [Nova’s native Web Components](https://www.npmjs.com/package/@nova-design-system/nova-webcomponents) within your Vue applications.
4
4
 
5
- - [Nova Components Vue](#nova-components-vue)
6
- - [Key Features](#key-features)
7
- - [Installation](#installation)
8
- - [Setting up Tailwind](#setting-up-tailwind)
9
- - [About Tailwind and the Nova Plugin](#about-tailwind-and-the-nova-plugin)
10
- - [1. Install Tailwind CSS and the Vite Plugin](#1-install-tailwind-css-and-the-vite-plugin)
11
- - [2. Configure the Vite Plugin](#2-configure-the-vite-plugin)
12
- - [3. Create `tailwind.config.ts`](#3-create-tailwindconfigts)
13
- - [4. Configure Tailwind and Nova Plugin in `main.css`](#4-configure-tailwind-and-nova-plugin-in-maincss)
14
- - [5. Register NovaComponents and include the Nova Tokens](#5-register-novacomponents-and-include-the-nova-tokens)
15
- - [6. Use Nova Components with Tailwind Utilities](#6-use-nova-components-with-tailwind-utilities)
16
- - [7. Setup the Nova Font](#7-setup-the-nova-font)
17
- - [Creating Your Own Style Components with Tailwind](#creating-your-own-style-components-with-tailwind)
18
- - [Nova Font Pro Integration](#nova-font-pro-integration)
19
- - [Option 1: Import in Global CSS (Recommended)](#option-1-import-in-global-css-recommended)
20
- - [Option 2: HTML Integration](#option-2-html-integration)
21
-
22
-
23
- ---
24
-
25
5
  ## Key Features
26
6
 
27
7
  - **Lightweight Integration**: Leverage Nova Web Components with minimal configuration in Vue.
@@ -1,4 +1,5 @@
1
1
  import { type VNode, type PropType } from 'vue';
2
+ import { type SortingState } from '@tanstack/vue-table';
2
3
  /**
3
4
  * Creates a typed NvDatatable component for a specific row type. This is the
4
5
  * standard approach for generic components in Vue.
@@ -6,9 +7,9 @@ import { type VNode, type PropType } from 'vue';
6
7
  * @returns {component} A Vue component definition typed for the specified row
7
8
  * type.
8
9
  */
9
- export declare function createNvDatatable<T extends NvDatatableRow>(): import("vue").DefineComponent<import("vue").ExtractPropTypes<{
10
+ export declare function createNvDatatable<T>(): import("vue").DefineComponent<import("vue").ExtractPropTypes<{
10
11
  columns: {
11
- type: PropType<NvDatatableColumn<T>[]>;
12
+ type: PropType<NvDatatableColumn<T, keyof T, T[keyof T]>[]>;
12
13
  required: true;
13
14
  default: () => any[];
14
15
  };
@@ -21,6 +22,10 @@ export declare function createNvDatatable<T extends NvDatatableRow>(): import("v
21
22
  type: PropType<NvDatatablePaginationConfig>;
22
23
  default: any;
23
24
  };
25
+ sorting: {
26
+ type: PropType<NvDatatableSortingConfig>;
27
+ default: any;
28
+ };
24
29
  renderPagination: {
25
30
  type: PropType<(api: NvDatatableRenderPaginationAPI) => VNode>;
26
31
  default: any;
@@ -33,7 +38,7 @@ export declare function createNvDatatable<T extends NvDatatableRow>(): import("v
33
38
  [key: string]: any;
34
39
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
35
40
  columns: {
36
- type: PropType<NvDatatableColumn<T>[]>;
41
+ type: PropType<NvDatatableColumn<T, keyof T, T[keyof T]>[]>;
37
42
  required: true;
38
43
  default: () => any[];
39
44
  };
@@ -46,6 +51,10 @@ export declare function createNvDatatable<T extends NvDatatableRow>(): import("v
46
51
  type: PropType<NvDatatablePaginationConfig>;
47
52
  default: any;
48
53
  };
54
+ sorting: {
55
+ type: PropType<NvDatatableSortingConfig>;
56
+ default: any;
57
+ };
49
58
  renderPagination: {
50
59
  type: PropType<(api: NvDatatableRenderPaginationAPI) => VNode>;
51
60
  default: any;
@@ -55,95 +64,92 @@ export declare function createNvDatatable<T extends NvDatatableRow>(): import("v
55
64
  default: boolean;
56
65
  };
57
66
  }>> & Readonly<{}>, {
58
- columns: NvDatatableColumn<T>[];
67
+ columns: NvDatatableColumn<T, keyof T, T[keyof T]>[];
59
68
  rows: T[];
60
69
  pagination: NvDatatablePaginationConfig;
70
+ sorting: NvDatatableSortingConfig;
61
71
  renderPagination: (api: NvDatatableRenderPaginationAPI) => VNode;
62
72
  stickyHeader: boolean;
63
73
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
74
+ /********************************* UTILS **************************************/
64
75
  /**
65
- * Default NvDatatable component with basic row type.
66
- * For typed usage, use createNvDatatable<YourRowType>().
76
+ * Creates a strongly-typed column factory for a given row type.
77
+ *
78
+ * @template Row The row data type (e.g., `Product`)
79
+ *
80
+ * @returns {function} A function that accepts a column definition and infers:
81
+ * - `K` as the field key (`keyof Row`)
82
+ * - `F` as the return type of `valueFormatter` (defaults to `Row[K]`)
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * // Define your row type
87
+ * interface Product {
88
+ * name: string;
89
+ * price: number;
90
+ * }
91
+ *
92
+ * const col = makeColumn<Product>();
67
93
  */
68
- export declare const NvDatatable: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
69
- columns: {
70
- type: PropType<NvDatatableColumn<NvDatatableRow>[]>;
71
- required: true;
72
- default: () => any[];
73
- };
74
- rows: {
75
- type: PropType<NvDatatableRow[]>;
76
- required: true;
77
- default: () => any[];
78
- };
79
- pagination: {
80
- type: PropType<NvDatatablePaginationConfig>;
81
- default: any;
82
- };
83
- renderPagination: {
84
- type: PropType<(api: NvDatatableRenderPaginationAPI) => VNode>;
85
- default: any;
86
- };
87
- stickyHeader: {
88
- type: BooleanConstructor;
89
- default: boolean;
90
- };
91
- }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
92
- [key: string]: any;
93
- }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
94
- columns: {
95
- type: PropType<NvDatatableColumn<NvDatatableRow>[]>;
96
- required: true;
97
- default: () => any[];
98
- };
99
- rows: {
100
- type: PropType<NvDatatableRow[]>;
101
- required: true;
102
- default: () => any[];
103
- };
104
- pagination: {
105
- type: PropType<NvDatatablePaginationConfig>;
106
- default: any;
107
- };
108
- renderPagination: {
109
- type: PropType<(api: NvDatatableRenderPaginationAPI) => VNode>;
110
- default: any;
111
- };
112
- stickyHeader: {
113
- type: BooleanConstructor;
114
- default: boolean;
115
- };
116
- }>> & Readonly<{}>, {
117
- columns: NvDatatableColumn<NvDatatableRow>[];
118
- rows: NvDatatableRow[];
119
- pagination: NvDatatablePaginationConfig;
120
- renderPagination: (api: NvDatatableRenderPaginationAPI) => VNode;
121
- stickyHeader: boolean;
122
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
94
+ export declare function makeColumn<Row>(): <K extends keyof Row, F = Row[K]>(col: NvDatatableColumn<Row, K, F>) => NvDatatableColumn<Row, keyof Row, Row[keyof Row]>;
123
95
  /********************************* TYPES **************************************/
124
96
  /**
125
- * Type definition for a datatable row.
97
+ * Slot props for NvDatatable cell slots.
98
+ * Use this type to provide type safety for custom cell slot implementations.
99
+ *
100
+ * @template Row The row data type
101
+ * @template K The field key (keyof Row)
102
+ * @template F The formatted value type (defaults to Row[K])
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * type PersonRow = {
107
+ * id: number;
108
+ * isActive: boolean;
109
+ * };
110
+ *
111
+ * // For a column with valueFormatter that returns string
112
+ * type StatusSlotProps = NvDatatableSlotProps<PersonRow, 'isActive', string>;
113
+ *
114
+ * // For a column without valueFormatter (uses raw value)
115
+ * type IdSlotProps = NvDatatableSlotProps<PersonRow, 'id', number>;
116
+ * ```
126
117
  */
127
- export type NvDatatableRow = Record<string, string | number | boolean | null | undefined | typeof Date>;
118
+ export interface NvDatatableSlotProps<Row, K extends keyof Row = keyof Row, F = Row[K]> {
119
+ /** The cell value (formatted if valueFormatter was used) */
120
+ value: F;
121
+ /** The full row data */
122
+ row: Row;
123
+ /** The field name */
124
+ field: K;
125
+ /** The row index (zero-based) */
126
+ rowIndex: number;
127
+ }
128
128
  /**
129
- * Parameters for custom cell rendering function.
129
+ * Props for NvDatatable component.
130
130
  */
131
- export interface NvTableRenderCellParams<T extends NvDatatableRow, V> {
132
- /** Cell value */
133
- value: V;
131
+ export interface NvDatatableProps<T> {
132
+ /** Column definitions */
133
+ columns: Array<NvDatatableColumn<T>>;
134
134
  /** Row data */
135
- row: T;
136
- /** Field name */
137
- field: keyof T;
138
- /** Row index */
139
- rowIndex: number;
135
+ rows: Array<T>;
136
+ /** Optional pagination configuration */
137
+ pagination?: NvDatatablePaginationConfig;
138
+ /** Optional render function for custom pagination UI */
139
+ renderPagination?: (api: NvDatatableRenderPaginationAPI) => VNode;
140
+ /** Should the header stick to the top of the table when scrolling? */
141
+ stickyHeader?: boolean;
142
+ /** CSS class */
143
+ class?: string;
144
+ /** Inline styles */
145
+ style?: string | Record<string, string>;
140
146
  }
141
147
  /**
142
148
  * Column definition for NvDatatable.
143
149
  */
144
- export interface NvDatatableColumn<T extends NvDatatableRow> {
150
+ export interface NvDatatableColumn<Row, K extends keyof Row = keyof Row, F = Row[K]> {
145
151
  /** Field name from row data */
146
- field: keyof T;
152
+ field: K;
147
153
  /** Display name for column header */
148
154
  headerName?: string;
149
155
  /** Column width in pixels */
@@ -152,27 +158,44 @@ export interface NvDatatableColumn<T extends NvDatatableRow> {
152
158
  resizable?: boolean;
153
159
  /** Whether column is hidden */
154
160
  hidden?: boolean;
161
+ /** Transform the raw value before rendering. Receives the cell value and full row object. */
162
+ valueFormatter?: (params: NvTableValueFormatterParams<Row, Row[K], K>) => F;
155
163
  /** Custom cell renderer */
156
- renderCell?: (params: NvTableRenderCellParams<T, T[keyof T]>) => VNode | string | number;
164
+ renderCell?: (params: NvTableRenderCellParams<Row, F, K>) => VNode | string | number;
165
+ /** Enable/disable sorting for this column */
166
+ sortable?: boolean;
167
+ /** Custom sorting function or built-in function name */
168
+ sortingFn?: ((rowA: any, rowB: any, columnId: string) => number) | string;
169
+ /** Start with descending sort for this column */
170
+ sortDescFirst?: boolean;
171
+ /** Invert the sort order (useful for rankings) */
172
+ invertSorting?: boolean;
173
+ /** Where to place undefined values in sort */
174
+ sortUndefined?: 'first' | 'last' | false | -1 | 1;
157
175
  }
158
176
  /**
159
- * Props for NvDatatable component.
177
+ * Parameters for custom cell rendering function.
160
178
  */
161
- export interface NvDatatableProps<T extends NvDatatableRow> {
162
- /** Column definitions */
163
- columns: Array<NvDatatableColumn<T>>;
179
+ export interface NvTableRenderCellParams<Row, Value, Field> {
180
+ /** Cell value */
181
+ value: Value;
164
182
  /** Row data */
165
- rows: Array<T>;
166
- /** Optional pagination configuration */
167
- pagination?: NvDatatablePaginationConfig;
168
- /** Optional render function for custom pagination UI */
169
- renderPagination?: (api: NvDatatableRenderPaginationAPI) => VNode;
170
- /** Should the header stick to the top of the table when scrolling? */
171
- stickyHeader?: boolean;
172
- /** CSS class */
173
- class?: string;
174
- /** Inline styles */
175
- style?: string | Record<string, string>;
183
+ row: Row;
184
+ /** Field name */
185
+ field: Field;
186
+ /** Row index */
187
+ rowIndex: number;
188
+ }
189
+ /**
190
+ * Parameters for valueFormatter function.
191
+ */
192
+ export interface NvTableValueFormatterParams<Row, Value, Field> {
193
+ /** Cell original value */
194
+ value: Value;
195
+ /** Row data */
196
+ row: Row;
197
+ /** Field name */
198
+ field: Field;
176
199
  }
177
200
  /**
178
201
  * Pagination configuration for NvDatatable.
@@ -238,3 +261,27 @@ export interface NvDatatableRenderPaginationAPI {
238
261
  /** Whether more items are available (only for infinite scroll) */
239
262
  hasMore?: boolean;
240
263
  }
264
+ /**
265
+ * Sorting configuration for NvDatatable.
266
+ * Supports both client-side and server-side sorting.
267
+ */
268
+ export interface NvDatatableSortingConfig {
269
+ /** Sorting mode */
270
+ mode: 'client' | 'server';
271
+ /** Enable multi-column sorting with Shift+Click */
272
+ enableMultiSort?: boolean;
273
+ /** Allow cycling through to "no sort" state */
274
+ enableSortingRemoval?: boolean;
275
+ /** Maximum number of columns for multi-sort */
276
+ maxMultiSortColCount?: number;
277
+ /** Start with descending sort as first toggle state */
278
+ sortDescFirst?: boolean;
279
+ /** Controlled sort state (for server-side sorting) */
280
+ sortState?: SortingState;
281
+ /** Callback when sorting changes (for server-side sorting) */
282
+ onSortingChange?: (sorting: SortingState) => void;
283
+ }
284
+ /**
285
+ * Sorting state type - array of sort descriptors
286
+ */
287
+ export type NvDataTableSortingState = SortingState;
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable jsdoc/require-jsdoc */
2
2
  import { defineComponent, computed, h, ref, watch, watchEffect, onUnmounted, } from 'vue';
3
- import { useVueTable, getCoreRowModel, getPaginationRowModel, } from '@tanstack/vue-table';
4
- import { NvTable } from '../generated/components';
3
+ import { useVueTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, } from '@tanstack/vue-table';
4
+ import { NvTable, NvTableheader } from '../generated/components';
5
5
  /**
6
6
  * Creates a typed NvDatatable component for a specific row type. This is the
7
7
  * standard approach for generic components in Vue.
@@ -27,6 +27,10 @@ export function createNvDatatable() {
27
27
  type: Object,
28
28
  default: undefined,
29
29
  },
30
+ sorting: {
31
+ type: Object,
32
+ default: undefined,
33
+ },
30
34
  renderPagination: {
31
35
  type: Function,
32
36
  default: undefined,
@@ -42,59 +46,75 @@ export function createNvDatatable() {
42
46
  pageIndex: 0,
43
47
  pageSize: props.pagination?.initialPageSize || 10,
44
48
  });
49
+ // Sorting state (controlled or uncontrolled)
50
+ const sortingState = ref(props.sorting?.sortState || []);
45
51
  // Ref for observing last row (infinite scroll)
46
52
  const lastRowRef = ref(null);
47
53
  const tableColumns = computed(() => props.columns
48
54
  .filter((col) => !col.hidden)
49
- .map((col) => ({
50
- accessorKey: col.field,
51
- header: col.headerName || String(col.field),
52
- size: col.width,
53
- enableResizing: col.resizable ?? true,
54
- cell: (context) => {
55
- const value = context.getValue();
56
- const row = context.row.original;
57
- const rowIndex = context.row.index;
58
- const field = col.field;
59
- // Priority: slot > renderCell > default
60
- // Sanitize field name to ensure valid HTML attribute name
61
- // Replace invalid characters (anything not a-z, A-Z, 0-9, -, _) with underscore
62
- const sanitizedField = String(field).replace(/[^a-zA-Z0-9_-]/g, '_');
63
- const slotName = `cell-${sanitizedField}`;
64
- if (slots[slotName]) {
65
- return slots[slotName]({ value, row, field, rowIndex });
66
- }
67
- // Use custom renderCell if provided
68
- if (col.renderCell) {
69
- return col.renderCell({
70
- value,
71
- row,
72
- field,
73
- rowIndex,
74
- });
75
- }
76
- // Default rendering
77
- return value;
78
- },
79
- })));
80
- // Create table instance based on pagination mode
81
- // Client-side pagination needs direct config, server-side needs reactive config
82
- let table;
83
- if (!props.pagination || props.pagination.mode === 'infinite') {
84
- // No pagination or infinite scroll - simple config
85
- table = useVueTable({
86
- get data() {
87
- return computed(() => props.rows);
88
- },
89
- get columns() {
90
- return tableColumns.value;
55
+ .map((col) => {
56
+ const columnDef = {
57
+ accessorKey: col.field,
58
+ accessorFn: col.valueFormatter
59
+ ? (row) => {
60
+ const rawValue = row[col.field];
61
+ return col.valueFormatter({
62
+ value: rawValue,
63
+ row,
64
+ field: col.field,
65
+ });
66
+ }
67
+ : undefined,
68
+ header: col.headerName || String(col.field),
69
+ size: col.width,
70
+ enableResizing: col.resizable ?? true,
71
+ // Sorting configuration
72
+ enableSorting: props.sorting ? col.sortable ?? true : false,
73
+ cell: (context) => {
74
+ const value = context.getValue();
75
+ const row = context.row.original;
76
+ const rowIndex = context.row.index;
77
+ const field = col.field;
78
+ // Priority: slot > renderCell > default
79
+ // Sanitize field name to ensure valid HTML attribute name
80
+ // Replace invalid characters (anything not a-z, A-Z, 0-9, -, _) with underscore
81
+ const sanitizedField = String(field).replace(/[^a-zA-Z0-9_-]/g, '_');
82
+ const slotName = `cell-${sanitizedField}`;
83
+ if (slots[slotName]) {
84
+ return slots[slotName]({ value, row, field, rowIndex });
85
+ }
86
+ // Use custom renderCell if provided
87
+ if (col.renderCell) {
88
+ return col.renderCell({
89
+ value,
90
+ row,
91
+ field,
92
+ rowIndex,
93
+ });
94
+ }
95
+ // Default rendering
96
+ return value;
91
97
  },
92
- getCoreRowModel: getCoreRowModel(),
93
- });
94
- }
95
- else if (props.pagination.mode === 'client') {
96
- // Client-side pagination - table manages its own state
97
- table = useVueTable({
98
+ };
99
+ // Add optional sorting properties only if defined
100
+ if (col.sortingFn !== undefined) {
101
+ // @ts-expect-error - TanStack typing is strict but this works at runtime
102
+ columnDef.sortingFn = col.sortingFn;
103
+ }
104
+ if (col.sortDescFirst !== undefined) {
105
+ columnDef.sortDescFirst = col.sortDescFirst;
106
+ }
107
+ if (col.invertSorting !== undefined) {
108
+ columnDef.invertSorting = col.invertSorting;
109
+ }
110
+ if (col.sortUndefined !== undefined) {
111
+ columnDef.sortUndefined = col.sortUndefined;
112
+ }
113
+ return columnDef;
114
+ }));
115
+ // Determine base table configuration with sorting
116
+ const getBaseTableConfig = () => {
117
+ const baseConfig = {
98
118
  get data() {
99
119
  return computed(() => props.rows);
100
120
  },
@@ -102,54 +122,103 @@ export function createNvDatatable() {
102
122
  return tableColumns.value;
103
123
  },
104
124
  getCoreRowModel: getCoreRowModel(),
105
- getPaginationRowModel: getPaginationRowModel(),
106
- initialState: {
107
- pagination: {
108
- pageIndex: 0,
109
- pageSize: props.pagination.initialPageSize || 10,
125
+ // Sorting configuration
126
+ ...(props.sorting && {
127
+ get state() {
128
+ return {
129
+ sorting: props.sorting.mode === 'server' && props.sorting.sortState
130
+ ? props.sorting.sortState
131
+ : sortingState.value,
132
+ };
110
133
  },
111
- },
112
- });
113
- }
114
- else {
115
- // Server-side pagination - manual pagination with reactive state
116
- table = useVueTable({
117
- get data() {
118
- return computed(() => props.rows);
119
- },
120
- get columns() {
121
- return tableColumns.value;
122
- },
123
- getCoreRowModel: getCoreRowModel(),
124
- manualPagination: true,
125
- get pageCount() {
126
- if (!props.pagination || props.pagination.mode !== 'server') {
134
+ onSortingChange: (updaterOrValue) => {
135
+ const currentSort = props.sorting.mode === 'server' && props.sorting.sortState
136
+ ? props.sorting.sortState
137
+ : sortingState.value;
138
+ const newSort = typeof updaterOrValue === 'function'
139
+ ? updaterOrValue(currentSort)
140
+ : updaterOrValue;
141
+ // Always update internal state for reactivity
142
+ sortingState.value = newSort;
143
+ // For server-side sorting, also call the callback
144
+ if (props.sorting?.mode === 'server' &&
145
+ props.sorting.onSortingChange) {
146
+ props.sorting.onSortingChange(newSort);
147
+ }
148
+ },
149
+ manualSorting: props.sorting.mode === 'server',
150
+ enableSorting: true,
151
+ enableMultiSort: props.sorting.enableMultiSort ?? false,
152
+ enableSortingRemoval: props.sorting.enableSortingRemoval ?? true,
153
+ maxMultiSortColCount: props.sorting.maxMultiSortColCount,
154
+ sortDescFirst: props.sorting.sortDescFirst ?? false,
155
+ // When multi-sort is enabled, treat all clicks as multi-sort events
156
+ isMultiSortEvent: props.sorting.enableMultiSort
157
+ ? () => true
158
+ : undefined,
159
+ getSortedRowModel: props.sorting.mode === 'client' ? getSortedRowModel() : undefined,
160
+ }),
161
+ };
162
+ return baseConfig;
163
+ };
164
+ // Create reactive table instance based on pagination mode
165
+ const table = computed(() => {
166
+ const baseConfig = getBaseTableConfig();
167
+ if (!props.pagination || props.pagination.mode === 'infinite') {
168
+ // No pagination or infinite scroll - simple config
169
+ return useVueTable(baseConfig);
170
+ }
171
+ else if (props.pagination.mode === 'client') {
172
+ // Client-side pagination - table manages its own state
173
+ return useVueTable({
174
+ ...baseConfig,
175
+ getPaginationRowModel: getPaginationRowModel(),
176
+ initialState: {
177
+ pagination: {
178
+ pageIndex: 0,
179
+ pageSize: props.pagination.initialPageSize || 10,
180
+ },
181
+ },
182
+ });
183
+ }
184
+ else {
185
+ // Server-side pagination - manual pagination with reactive state
186
+ return useVueTable({
187
+ ...baseConfig,
188
+ manualPagination: true,
189
+ get pageCount() {
190
+ if (!props.pagination || props.pagination.mode !== 'server') {
191
+ return -1;
192
+ }
193
+ const pageSize = paginationState.value.pageSize;
194
+ if (props.pagination.totalPageCount !== undefined) {
195
+ return props.pagination.totalPageCount;
196
+ }
197
+ else if (props.pagination.totalRowCount !== undefined) {
198
+ return Math.ceil(props.pagination.totalRowCount / pageSize);
199
+ }
127
200
  return -1;
128
- }
129
- const pageSize = paginationState.value.pageSize;
130
- if (props.pagination.totalPageCount !== undefined) {
131
- return props.pagination.totalPageCount;
132
- }
133
- else if (props.pagination.totalRowCount !== undefined) {
134
- return Math.ceil(props.pagination.totalRowCount / pageSize);
135
- }
136
- return -1;
137
- },
138
- get state() {
139
- return {
140
- pagination: paginationState.value,
141
- };
142
- },
143
- onPaginationChange: (updaterOrValue) => {
144
- if (typeof updaterOrValue === 'function') {
145
- paginationState.value = updaterOrValue(paginationState.value);
146
- }
147
- else {
148
- paginationState.value = updaterOrValue;
149
- }
150
- },
151
- });
152
- }
201
+ },
202
+ get state() {
203
+ const sortState = baseConfig.state
204
+ ? baseConfig.state.sorting
205
+ : undefined;
206
+ return {
207
+ pagination: paginationState.value,
208
+ ...(sortState !== undefined && { sorting: sortState }),
209
+ };
210
+ },
211
+ onPaginationChange: (updaterOrValue) => {
212
+ if (typeof updaterOrValue === 'function') {
213
+ paginationState.value = updaterOrValue(paginationState.value);
214
+ }
215
+ else {
216
+ paginationState.value = updaterOrValue;
217
+ }
218
+ },
219
+ });
220
+ }
221
+ });
153
222
  // Handle pagination changes for server mode
154
223
  watch(paginationState, (newState) => {
155
224
  if (props.pagination?.mode === 'server' &&
@@ -205,8 +274,8 @@ export function createNvDatatable() {
205
274
  if (!props.pagination) {
206
275
  return null;
207
276
  }
208
- const tablePaginationState = table.getState().pagination;
209
- const pageCount = table.getPageCount();
277
+ const tablePaginationState = table.value.getState().pagination;
278
+ const pageCount = table.value.getPageCount();
210
279
  const rowCount = props.pagination.mode === 'server'
211
280
  ? props.pagination.totalRowCount || props.rows.length
212
281
  : props.rows.length;
@@ -215,14 +284,14 @@ export function createNvDatatable() {
215
284
  pageSize: tablePaginationState.pageSize,
216
285
  pageCount,
217
286
  rowCount,
218
- firstPage: () => table.setPageIndex(0),
219
- previousPage: () => table.previousPage(),
220
- nextPage: () => table.nextPage(),
221
- lastPage: () => table.setPageIndex(pageCount - 1),
222
- setPageIndex: (index) => table.setPageIndex(index),
223
- setPageSize: (size) => table.setPageSize(size),
224
- canPreviousPage: table.getCanPreviousPage(),
225
- canNextPage: table.getCanNextPage(),
287
+ firstPage: () => table.value.setPageIndex(0),
288
+ previousPage: () => table.value.previousPage(),
289
+ nextPage: () => table.value.nextPage(),
290
+ lastPage: () => table.value.setPageIndex(pageCount - 1),
291
+ setPageIndex: (index) => table.value.setPageIndex(index),
292
+ setPageSize: (size) => table.value.setPageSize(size),
293
+ canPreviousPage: table.value.getCanPreviousPage(),
294
+ canNextPage: table.value.getCanNextPage(),
226
295
  isLoading: props.pagination.mode === 'infinite'
227
296
  ? props.pagination.isLoading
228
297
  : undefined,
@@ -232,7 +301,7 @@ export function createNvDatatable() {
232
301
  };
233
302
  });
234
303
  return () => {
235
- const tableRows = table.getRowModel().rows;
304
+ const tableRows = table.value.getRowModel().rows;
236
305
  const isInfiniteScroll = props.pagination?.mode === 'infinite';
237
306
  const tableElement = h(NvTable, attrs, {
238
307
  default: () => [
@@ -240,25 +309,44 @@ export function createNvDatatable() {
240
309
  h('thead', {
241
310
  'data-sticky-top': props.stickyHeader ? 'true' : undefined,
242
311
  }, [
243
- ...table.getHeaderGroups().map((headerGroup) => h('tr', { key: headerGroup.id }, [
244
- ...headerGroup.headers.map((header) => h('th', {
245
- key: header.id,
246
- 'data-testid': `datatable-header-${header.id}`,
247
- style: {
248
- width: header.column.columnDef.size
249
- ? `${header.column.columnDef.size}px`
250
- : undefined,
251
- },
252
- 'data-no-resize': header.column.columnDef
253
- .enableResizing
312
+ ...table.value.getHeaderGroups().map((headerGroup) => h('tr', { key: headerGroup.id }, [
313
+ ...headerGroup.headers.map((header) => {
314
+ const canSort = header.column.getCanSort();
315
+ const sortDirection = header.column.getIsSorted();
316
+ return h('th', {
317
+ key: header.id,
318
+ 'data-testid': `datatable-header-${header.id}`,
319
+ style: {
320
+ width: header.column.columnDef.size
321
+ ? `${header.column.columnDef.size}px`
322
+ : undefined,
323
+ },
324
+ 'data-no-resize': header.column.columnDef
325
+ .enableResizing
326
+ ? null
327
+ : true,
328
+ }, header.isPlaceholder
254
329
  ? null
255
- : true,
256
- }, header.isPlaceholder
257
- ? null
258
- : typeof header.column.columnDef.header ===
259
- 'function'
260
- ? header.column.columnDef.header(header.getContext())
261
- : header.column.columnDef.header)),
330
+ : canSort
331
+ ? h(NvTableheader, {
332
+ sortable: true,
333
+ sortDirection: sortDirection || 'none',
334
+ onSortDirectionChanged: (event) => {
335
+ // Call the TanStack handler
336
+ const handler = header.column.getToggleSortingHandler();
337
+ handler?.(event);
338
+ },
339
+ }, {
340
+ default: () => typeof header.column.columnDef.header ===
341
+ 'function'
342
+ ? header.column.columnDef.header(header.getContext())
343
+ : header.column.columnDef.header,
344
+ })
345
+ : typeof header.column.columnDef.header ===
346
+ 'function'
347
+ ? header.column.columnDef.header(header.getContext())
348
+ : header.column.columnDef.header);
349
+ }),
262
350
  ])),
263
351
  ]),
264
352
  h('tbody', {}, [
@@ -266,12 +354,12 @@ export function createNvDatatable() {
266
354
  const isLastRow = isInfiniteScroll && index === tableRows.length - 1;
267
355
  return h('tr', {
268
356
  key: row.id,
269
- 'data-testid': `datatable-row-${row.id}`,
357
+ 'data-testid': `datatable-row-${index}`,
270
358
  ref: isLastRow ? lastRowRef : undefined,
271
359
  }, [
272
360
  ...row.getVisibleCells().map((cell) => h('td', {
273
361
  key: cell.id,
274
- 'data-testid': `datatable-cell-${cell.id}`,
362
+ 'data-testid': `datatable-cell-${cell.column.id}`,
275
363
  }, typeof cell.column.columnDef.cell === 'function'
276
364
  ? cell.column.columnDef.cell(cell.getContext())
277
365
  : cell.getValue())),
@@ -304,8 +392,28 @@ export function createNvDatatable() {
304
392
  },
305
393
  });
306
394
  }
395
+ /********************************* UTILS **************************************/
307
396
  /**
308
- * Default NvDatatable component with basic row type.
309
- * For typed usage, use createNvDatatable<YourRowType>().
397
+ * Creates a strongly-typed column factory for a given row type.
398
+ *
399
+ * @template Row The row data type (e.g., `Product`)
400
+ *
401
+ * @returns {function} A function that accepts a column definition and infers:
402
+ * - `K` as the field key (`keyof Row`)
403
+ * - `F` as the return type of `valueFormatter` (defaults to `Row[K]`)
404
+ *
405
+ * @example
406
+ * ```ts
407
+ * // Define your row type
408
+ * interface Product {
409
+ * name: string;
410
+ * price: number;
411
+ * }
412
+ *
413
+ * const col = makeColumn<Product>();
310
414
  */
311
- export const NvDatatable = createNvDatatable();
415
+ export function makeColumn() {
416
+ return function define(col) {
417
+ return col;
418
+ };
419
+ }
@@ -62,6 +62,7 @@ export declare const NvRow: import("vue").DefineSetupFnComponent<JSX.NvRow & imp
62
62
  export declare const NvSplit: import("vue").DefineSetupFnComponent<JSX.NvSplit & import("./vue-component-lib/utils").InputProps<number[]>, {}, {}, JSX.NvSplit & import("./vue-component-lib/utils").InputProps<number[]> & {}, import("vue").PublicProps>;
63
63
  export declare const NvStack: import("vue").DefineSetupFnComponent<JSX.NvStack & import("./vue-component-lib/utils").InputProps<string | number | boolean>, {}, {}, JSX.NvStack & import("./vue-component-lib/utils").InputProps<string | number | boolean> & {}, import("vue").PublicProps>;
64
64
  export declare const NvTable: import("vue").DefineSetupFnComponent<JSX.NvTable & import("./vue-component-lib/utils").InputProps<string | number | boolean>, {}, {}, JSX.NvTable & import("./vue-component-lib/utils").InputProps<string | number | boolean> & {}, import("vue").PublicProps>;
65
+ export declare const NvTableheader: import("vue").DefineSetupFnComponent<JSX.NvTableheader & import("./vue-component-lib/utils").InputProps<string | number | boolean>, {}, {}, JSX.NvTableheader & import("./vue-component-lib/utils").InputProps<string | number | boolean> & {}, import("vue").PublicProps>;
65
66
  export declare const NvToggle: import("vue").DefineSetupFnComponent<JSX.NvToggle & import("./vue-component-lib/utils").InputProps<boolean>, {}, {}, JSX.NvToggle & import("./vue-component-lib/utils").InputProps<boolean> & {}, import("vue").PublicProps>;
66
67
  export declare const NvTogglebutton: import("vue").DefineSetupFnComponent<JSX.NvTogglebutton & import("./vue-component-lib/utils").InputProps<string | number | boolean>, {}, {}, JSX.NvTogglebutton & import("./vue-component-lib/utils").InputProps<string | number | boolean> & {}, import("vue").PublicProps>;
67
68
  export declare const NvTogglebuttongroup: import("vue").DefineSetupFnComponent<JSX.NvTogglebuttongroup & import("./vue-component-lib/utils").InputProps<string[]>, {}, {}, JSX.NvTogglebuttongroup & import("./vue-component-lib/utils").InputProps<string[]> & {}, import("vue").PublicProps>;
@@ -75,7 +75,8 @@ export const NvButton = /*@__PURE__*/ defineContainer('nv-button', undefined, [
75
75
  'disabled',
76
76
  'fluid',
77
77
  'type',
78
- 'form'
78
+ 'form',
79
+ 'disableTabindex'
79
80
  ]);
80
81
  export const NvButtongroup = /*@__PURE__*/ defineContainer('nv-buttongroup', undefined, [
81
82
  'size',
@@ -515,7 +516,8 @@ export const NvIconbutton = /*@__PURE__*/ defineContainer('nv-iconbutton', undef
515
516
  'active',
516
517
  'name',
517
518
  'type',
518
- 'shape'
519
+ 'shape',
520
+ 'disableTabindex'
519
521
  ]);
520
522
  export const NvLoader = /*@__PURE__*/ defineContainer('nv-loader', undefined, [
521
523
  'size',
@@ -584,6 +586,11 @@ export const NvStack = /*@__PURE__*/ defineContainer('nv-stack', undefined, [
584
586
  'vertical'
585
587
  ]);
586
588
  export const NvTable = /*@__PURE__*/ defineContainer('nv-table', undefined);
589
+ export const NvTableheader = /*@__PURE__*/ defineContainer('nv-tableheader', undefined, [
590
+ 'sortable',
591
+ 'sortDirection',
592
+ 'sortDirectionChanged'
593
+ ]);
587
594
  export const NvToggle = /*@__PURE__*/ defineContainer('nv-toggle', undefined, [
588
595
  'inputId',
589
596
  'name',
@@ -33,6 +33,8 @@ export interface NotificationOptions {
33
33
  actions?: NotificationAction[];
34
34
  /** Custom components for the notification actions. */
35
35
  actionSlot?: Component;
36
+ /** Duration in milliseconds before auto-dismissing. 0 = sticky (no auto-dismiss). Default: 0 */
37
+ duration?: number;
36
38
  }
37
39
  /**
38
40
  * A notification with all required fields populated.
@@ -25,6 +25,7 @@ class NotificationManager {
25
25
  notifications = ref([]);
26
26
  options;
27
27
  containerApp = null;
28
+ timers = new Map();
28
29
  constructor(options = {}) {
29
30
  this.options = {
30
31
  position: options.position || 'top-right',
@@ -116,6 +117,7 @@ class NotificationManager {
116
117
  actions: options.actions ?? [],
117
118
  actionSlot: options.actionSlot,
118
119
  icon: options.icon,
120
+ duration: options.duration ?? 0,
119
121
  createdAt: Date.now(),
120
122
  };
121
123
  // Remove oldest notifications if we exceed max
@@ -130,6 +132,13 @@ class NotificationManager {
130
132
  const ref = this.elRefs.get(id);
131
133
  const el = unwrapNotificationEl(ref);
132
134
  el?.show();
135
+ // Set up auto-dismiss timer if duration > 0
136
+ if (notification.duration && notification.duration > 0) {
137
+ const timer = setTimeout(() => {
138
+ this.dismiss(id);
139
+ }, notification.duration);
140
+ this.timers.set(id, timer);
141
+ }
133
142
  }, 0);
134
143
  return id;
135
144
  };
@@ -140,6 +149,12 @@ class NotificationManager {
140
149
  * @param {string} id - The notification ID to dismiss
141
150
  */
142
151
  dismiss = (id) => {
152
+ // Clear timer if exists
153
+ const timer = this.timers.get(id);
154
+ if (timer) {
155
+ clearTimeout(timer);
156
+ this.timers.delete(id);
157
+ }
143
158
  const ref = this.elRefs.get(id);
144
159
  const el = unwrapNotificationEl(ref);
145
160
  el?.dismiss();
@@ -150,6 +165,12 @@ class NotificationManager {
150
165
  * @param {string} id - The notification ID to dismiss
151
166
  */
152
167
  remove = (id) => {
168
+ // Clear timer if exists
169
+ const timer = this.timers.get(id);
170
+ if (timer) {
171
+ clearTimeout(timer);
172
+ this.timers.delete(id);
173
+ }
153
174
  this.notifications.value = this.notifications.value.filter((notification) => notification.id !== id);
154
175
  this.elRefs.delete(id);
155
176
  };
@@ -157,6 +178,9 @@ class NotificationManager {
157
178
  * Clear all active notifications.
158
179
  */
159
180
  removeAll = () => {
181
+ // Clear all timers
182
+ this.timers.forEach((timer) => clearTimeout(timer));
183
+ this.timers.clear();
160
184
  this.notifications.value = [];
161
185
  };
162
186
  /**
@@ -175,6 +199,9 @@ class NotificationManager {
175
199
  * Destroy the notification manager and clean up resources.
176
200
  */
177
201
  destroy() {
202
+ // Clear all timers
203
+ this.timers.forEach((timer) => clearTimeout(timer));
204
+ this.timers.clear();
178
205
  if (this.containerApp) {
179
206
  this.containerApp.unmount();
180
207
  this.containerApp = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nova-design-system/nova-vue",
3
- "version": "3.19.0",
3
+ "version": "3.21.0",
4
4
  "description": "Nova is a design system created by Elia Group to empower creators to efficiently build solutions that people love to use.",
5
5
  "author": "Elia Group",
6
6
  "homepage": "https://nova.eliagroup.io",