@nova-design-system/nova-vue 3.16.0 → 3.18.0-beta.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.
@@ -17,6 +17,18 @@ export declare function createNvDatatable<T extends NvDatatableRow>(): import("v
17
17
  required: true;
18
18
  default: () => any[];
19
19
  };
20
+ pagination: {
21
+ type: PropType<NvDatatablePaginationConfig>;
22
+ default: any;
23
+ };
24
+ renderPagination: {
25
+ type: PropType<(api: NvDatatableRenderPaginationAPI) => VNode>;
26
+ default: any;
27
+ };
28
+ stickyHeader: {
29
+ type: BooleanConstructor;
30
+ default: boolean;
31
+ };
20
32
  }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
21
33
  [key: string]: any;
22
34
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -30,9 +42,24 @@ export declare function createNvDatatable<T extends NvDatatableRow>(): import("v
30
42
  required: true;
31
43
  default: () => any[];
32
44
  };
45
+ pagination: {
46
+ type: PropType<NvDatatablePaginationConfig>;
47
+ default: any;
48
+ };
49
+ renderPagination: {
50
+ type: PropType<(api: NvDatatableRenderPaginationAPI) => VNode>;
51
+ default: any;
52
+ };
53
+ stickyHeader: {
54
+ type: BooleanConstructor;
55
+ default: boolean;
56
+ };
33
57
  }>> & Readonly<{}>, {
34
58
  columns: NvDatatableColumn<T>[];
35
59
  rows: T[];
60
+ pagination: NvDatatablePaginationConfig;
61
+ renderPagination: (api: NvDatatableRenderPaginationAPI) => VNode;
62
+ stickyHeader: boolean;
36
63
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
37
64
  /**
38
65
  * Default NvDatatable component with basic row type.
@@ -49,6 +76,18 @@ export declare const NvDatatable: import("vue").DefineComponent<import("vue").Ex
49
76
  required: true;
50
77
  default: () => any[];
51
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
+ };
52
91
  }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
53
92
  [key: string]: any;
54
93
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -62,9 +101,24 @@ export declare const NvDatatable: import("vue").DefineComponent<import("vue").Ex
62
101
  required: true;
63
102
  default: () => any[];
64
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
+ };
65
116
  }>> & Readonly<{}>, {
66
117
  columns: NvDatatableColumn<NvDatatableRow>[];
67
118
  rows: NvDatatableRow[];
119
+ pagination: NvDatatablePaginationConfig;
120
+ renderPagination: (api: NvDatatableRenderPaginationAPI) => VNode;
121
+ stickyHeader: boolean;
68
122
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
69
123
  /********************************* TYPES **************************************/
70
124
  /**
@@ -109,8 +163,78 @@ export interface NvDatatableProps<T extends NvDatatableRow> {
109
163
  columns: Array<NvDatatableColumn<T>>;
110
164
  /** Row data */
111
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;
112
172
  /** CSS class */
113
173
  class?: string;
114
174
  /** Inline styles */
115
175
  style?: string | Record<string, string>;
116
176
  }
177
+ /**
178
+ * Pagination configuration for NvDatatable.
179
+ * Supports three modes: client-side, server-side with buttons, and infinite scroll.
180
+ */
181
+ export interface NvDatatablePaginationConfig {
182
+ /** Pagination mode */
183
+ mode: 'client' | 'server' | 'infinite';
184
+ /** Initial page size (default: 10) */
185
+ initialPageSize?: number;
186
+ /** Available page size options (default: [10, 20, 50, 100]) */
187
+ pageSizeOptions?: number[];
188
+ /** Total number of rows (for server-side pagination) */
189
+ totalRowCount?: number;
190
+ /** Total number of pages (for server-side pagination) */
191
+ totalPageCount?: number;
192
+ /** Callback when pagination state changes (for server-side pagination) */
193
+ onPaginationChange?: (state: {
194
+ /** Current page index */
195
+ pageIndex: number;
196
+ /** Current page size */
197
+ pageSize: number;
198
+ }) => void;
199
+ /** Whether there are more items to load (for infinite scroll) */
200
+ hasMore?: boolean;
201
+ /** Whether data is currently loading (for infinite scroll) */
202
+ isLoading?: boolean;
203
+ /** Distance from bottom in pixels to trigger load (default: 500) */
204
+ loadMoreThreshold?: number;
205
+ /** Callback to load more data (for infinite scroll) */
206
+ onLoadMore?: () => void;
207
+ }
208
+ /**
209
+ * API exposed to renderPagination function for building custom pagination UI.
210
+ */
211
+ export interface NvDatatableRenderPaginationAPI {
212
+ /** Current page index (zero-based) */
213
+ pageIndex: number;
214
+ /** Current page size */
215
+ pageSize: number;
216
+ /** Total number of pages */
217
+ pageCount: number;
218
+ /** Total number of rows */
219
+ rowCount: number;
220
+ /** Navigate to first page */
221
+ firstPage: () => void;
222
+ /** Navigate to previous page */
223
+ previousPage: () => void;
224
+ /** Navigate to next page */
225
+ nextPage: () => void;
226
+ /** Navigate to last page */
227
+ lastPage: () => void;
228
+ /** Navigate to specific page by index */
229
+ setPageIndex: (index: number) => void;
230
+ /** Change page size */
231
+ setPageSize: (size: number) => void;
232
+ /** Whether previous page is available */
233
+ canPreviousPage: boolean;
234
+ /** Whether next page is available */
235
+ canNextPage: boolean;
236
+ /** Loading state (only for infinite scroll) */
237
+ isLoading?: boolean;
238
+ /** Whether more items are available (only for infinite scroll) */
239
+ hasMore?: boolean;
240
+ }
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable jsdoc/require-jsdoc */
2
- import { defineComponent, computed, h } from 'vue';
3
- import { useVueTable, getCoreRowModel, } from '@tanstack/vue-table';
2
+ import { defineComponent, computed, h, ref, watch, watchEffect, onUnmounted, } from 'vue';
3
+ import { useVueTable, getCoreRowModel, getPaginationRowModel, } from '@tanstack/vue-table';
4
4
  import { NvTable } from '../generated/components';
5
5
  /**
6
6
  * Creates a typed NvDatatable component for a specific row type. This is the
@@ -23,8 +23,27 @@ export function createNvDatatable() {
23
23
  required: true,
24
24
  default: () => [],
25
25
  },
26
+ pagination: {
27
+ type: Object,
28
+ default: undefined,
29
+ },
30
+ renderPagination: {
31
+ type: Function,
32
+ default: undefined,
33
+ },
34
+ stickyHeader: {
35
+ type: Boolean,
36
+ default: false,
37
+ },
26
38
  },
27
- setup(props, { attrs }) {
39
+ setup(props, { attrs, slots }) {
40
+ // Pagination state for controlled pagination (server mode)
41
+ const paginationState = ref({
42
+ pageIndex: 0,
43
+ pageSize: props.pagination?.initialPageSize || 10,
44
+ });
45
+ // Ref for observing last row (infinite scroll)
46
+ const lastRowRef = ref(null);
28
47
  const tableColumns = computed(() => props.columns
29
48
  .filter((col) => !col.hidden)
30
49
  .map((col) => ({
@@ -36,12 +55,21 @@ export function createNvDatatable() {
36
55
  const value = context.getValue();
37
56
  const row = context.row.original;
38
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
+ }
39
67
  // Use custom renderCell if provided
40
68
  if (col.renderCell) {
41
69
  return col.renderCell({
42
70
  value,
43
71
  row,
44
- field: col.field,
72
+ field,
45
73
  rowIndex,
46
74
  });
47
75
  }
@@ -49,16 +77,169 @@ export function createNvDatatable() {
49
77
  return value;
50
78
  },
51
79
  })));
52
- const table = useVueTable({
53
- data: computed(() => props.rows),
54
- columns: tableColumns.value,
55
- getCoreRowModel: getCoreRowModel(),
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;
91
+ },
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
+ get data() {
99
+ return computed(() => props.rows);
100
+ },
101
+ get columns() {
102
+ return tableColumns.value;
103
+ },
104
+ getCoreRowModel: getCoreRowModel(),
105
+ getPaginationRowModel: getPaginationRowModel(),
106
+ initialState: {
107
+ pagination: {
108
+ pageIndex: 0,
109
+ pageSize: props.pagination.initialPageSize || 10,
110
+ },
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') {
127
+ 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
+ }
153
+ // Handle pagination changes for server mode
154
+ watch(paginationState, (newState) => {
155
+ if (props.pagination?.mode === 'server' &&
156
+ props.pagination.onPaginationChange) {
157
+ props.pagination.onPaginationChange({
158
+ pageIndex: newState.pageIndex,
159
+ pageSize: newState.pageSize,
160
+ });
161
+ }
162
+ }, { deep: true });
163
+ // Intersection observer for infinite scroll
164
+ let observer = null;
165
+ watchEffect((onCleanup) => {
166
+ // Disconnect previous observer if it exists
167
+ if (observer) {
168
+ observer.disconnect();
169
+ observer = null;
170
+ }
171
+ // Only set up observer for infinite scroll mode
172
+ if (props.pagination?.mode !== 'infinite' || !lastRowRef.value) {
173
+ return;
174
+ }
175
+ const threshold = props.pagination.loadMoreThreshold || 500;
176
+ observer = new IntersectionObserver((entries) => {
177
+ const entry = entries[0];
178
+ if (entry.isIntersecting &&
179
+ props.pagination?.mode === 'infinite' &&
180
+ props.pagination.hasMore &&
181
+ !props.pagination.isLoading &&
182
+ props.pagination.onLoadMore) {
183
+ props.pagination.onLoadMore();
184
+ }
185
+ }, {
186
+ rootMargin: `${threshold}px`,
187
+ root: null,
188
+ });
189
+ observer.observe(lastRowRef.value);
190
+ // Cleanup function
191
+ onCleanup(() => {
192
+ if (observer) {
193
+ observer.disconnect();
194
+ observer = null;
195
+ }
196
+ });
197
+ });
198
+ onUnmounted(() => {
199
+ if (observer) {
200
+ observer.disconnect();
201
+ }
202
+ });
203
+ // Build pagination API for render prop
204
+ const paginationAPI = computed(() => {
205
+ if (!props.pagination) {
206
+ return null;
207
+ }
208
+ const tablePaginationState = table.getState().pagination;
209
+ const pageCount = table.getPageCount();
210
+ const rowCount = props.pagination.mode === 'server'
211
+ ? props.pagination.totalRowCount || props.rows.length
212
+ : props.rows.length;
213
+ return {
214
+ pageIndex: tablePaginationState.pageIndex,
215
+ pageSize: tablePaginationState.pageSize,
216
+ pageCount,
217
+ 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(),
226
+ isLoading: props.pagination.mode === 'infinite'
227
+ ? props.pagination.isLoading
228
+ : undefined,
229
+ hasMore: props.pagination.mode === 'infinite'
230
+ ? props.pagination.hasMore
231
+ : undefined,
232
+ };
56
233
  });
57
234
  return () => {
58
- return h(NvTable, attrs, {
235
+ const tableRows = table.getRowModel().rows;
236
+ const isInfiniteScroll = props.pagination?.mode === 'infinite';
237
+ const tableElement = h(NvTable, attrs, {
59
238
  default: () => [
60
239
  h('table', {}, [
61
- h('thead', {}, [
240
+ h('thead', {
241
+ 'data-sticky-top': props.stickyHeader ? 'true' : undefined,
242
+ }, [
62
243
  ...table.getHeaderGroups().map((headerGroup) => h('tr', { key: headerGroup.id }, [
63
244
  ...headerGroup.headers.map((header) => h('th', {
64
245
  key: header.id,
@@ -74,31 +255,51 @@ export function createNvDatatable() {
74
255
  : true,
75
256
  }, header.isPlaceholder
76
257
  ? null
77
- : typeof header.column.columnDef.header === 'function'
258
+ : typeof header.column.columnDef.header ===
259
+ 'function'
78
260
  ? header.column.columnDef.header(header.getContext())
79
261
  : header.column.columnDef.header)),
80
262
  ])),
81
263
  ]),
82
264
  h('tbody', {}, [
83
- ...table.getRowModel().rows.map((row) => h('tr', {
84
- key: row.id,
85
- 'data-testid': `datatable-row-${row.id}`,
86
- }, [
87
- ...row.getVisibleCells().map((cell) => h('td', {
88
- key: cell.id,
89
- 'data-testid': `datatable-cell-${cell.id}`,
90
- style: {
91
- padding: '8px',
92
- borderBottom: '1px solid #eee',
93
- },
94
- }, typeof cell.column.columnDef.cell === 'function'
95
- ? cell.column.columnDef.cell(cell.getContext())
96
- : cell.getValue())),
97
- ])),
265
+ ...tableRows.map((row, index) => {
266
+ const isLastRow = isInfiniteScroll && index === tableRows.length - 1;
267
+ return h('tr', {
268
+ key: row.id,
269
+ 'data-testid': `datatable-row-${row.id}`,
270
+ ref: isLastRow ? lastRowRef : undefined,
271
+ }, [
272
+ ...row.getVisibleCells().map((cell) => h('td', {
273
+ key: cell.id,
274
+ 'data-testid': `datatable-cell-${cell.id}`,
275
+ }, typeof cell.column.columnDef.cell === 'function'
276
+ ? cell.column.columnDef.cell(cell.getContext())
277
+ : cell.getValue())),
278
+ ]);
279
+ }),
98
280
  ]),
99
281
  ]),
100
282
  ],
101
283
  });
284
+ // Return table with optional pagination
285
+ // Priority: slots.pagination > props.renderPagination > no pagination
286
+ if (paginationAPI.value) {
287
+ if (slots.pagination) {
288
+ // Use scoped slot (preferred)
289
+ return h('div', {}, [
290
+ tableElement,
291
+ slots.pagination({ api: paginationAPI.value }),
292
+ ]);
293
+ }
294
+ else if (props.renderPagination) {
295
+ // Fall back to render prop for backward compatibility
296
+ return h('div', {}, [
297
+ tableElement,
298
+ props.renderPagination(paginationAPI.value),
299
+ ]);
300
+ }
301
+ }
302
+ return tableElement;
102
303
  };
103
304
  },
104
305
  });
@@ -59,6 +59,7 @@ export declare const NvNotification: import("vue").DefineSetupFnComponent<JSX.Nv
59
59
  export declare const NvNotificationcontainer: import("vue").DefineSetupFnComponent<JSX.NvNotificationcontainer & import("./vue-component-lib/utils").InputProps<string | number | boolean>, {}, {}, JSX.NvNotificationcontainer & import("./vue-component-lib/utils").InputProps<string | number | boolean> & {}, import("vue").PublicProps>;
60
60
  export declare const NvPopover: import("vue").DefineSetupFnComponent<JSX.NvPopover & import("./vue-component-lib/utils").InputProps<boolean>, {}, {}, JSX.NvPopover & import("./vue-component-lib/utils").InputProps<boolean> & {}, import("vue").PublicProps>;
61
61
  export declare const NvRow: import("vue").DefineSetupFnComponent<JSX.NvRow & import("./vue-component-lib/utils").InputProps<string | number | boolean>, {}, {}, JSX.NvRow & import("./vue-component-lib/utils").InputProps<string | number | boolean> & {}, import("vue").PublicProps>;
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>;
62
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>;
63
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>;
64
65
  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>;
@@ -565,6 +565,13 @@ export const NvPopover = /*@__PURE__*/ defineContainer('nv-popover', undefined,
565
565
  'openChanged'
566
566
  ], 'open', 'open-changed');
567
567
  export const NvRow = /*@__PURE__*/ defineContainer('nv-row', undefined);
568
+ export const NvSplit = /*@__PURE__*/ defineContainer('nv-split', undefined, [
569
+ 'direction',
570
+ 'sizes',
571
+ 'minSizes',
572
+ 'gutterSize',
573
+ 'sizesChanged'
574
+ ], 'sizes', 'sizes-changed');
568
575
  export const NvStack = /*@__PURE__*/ defineContainer('nv-stack', undefined, [
569
576
  'gutter',
570
577
  'fill',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nova-design-system/nova-vue",
3
- "version": "3.16.0",
3
+ "version": "3.18.0-beta.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",