@veristone/nuxt-v-app 0.2.5 → 0.2.7

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.
@@ -32,20 +32,26 @@ The primary table component. Wraps Nuxt UI's `<UTable>` with a toolbar, filter c
32
32
  | `data` | `unknown[]` | **required** | Array of row data |
33
33
  | `columns` | `TableColumn[]` | **required** | TanStack column definitions |
34
34
  | `loading` | `boolean` | `false` | Shows loading skeleton on initial load |
35
- | `initialPageLimit` | `number` | `10` | Rows per page |
35
+ | `initialPageLimit` | `number` | `10` | Rows per page (client-side mode) |
36
36
  | `selectable` | `boolean` | `false` | Enable row selection checkboxes |
37
+ | `showSearch` | `boolean` | `false` | Show the built-in search input in the toolbar |
37
38
  | `filters` | `FilterDefinition[]` | — | Filter dropdown definitions |
38
39
  | `onRefresh` | `() => void \| Promise<void>` | — | Shows refresh button; called on click |
39
40
  | `onRowClick` | `(row: unknown) => void` | — | Called when a row is clicked |
40
41
  | `defaultSort` | `string` | — | Column accessorKey to sort by on mount |
41
42
  | `defaultSortDesc` | `boolean` | `false` | Sort descending by default |
43
+ | `manualPagination` | `boolean` | `false` | Enable server-side pagination mode (disables client-side sorting and filtering) |
44
+ | `total` | `number` | — | Total row count from server (required for manualPagination) |
42
45
 
43
46
  #### v-model Bindings
44
47
 
45
48
  | Model | Type | Description |
46
49
  |-------|------|-------------|
47
50
  | `sorting` | `SortingState` | TanStack sorting state |
48
- | `filterValues` | `Record<string, unknown>` | Active filter values |
51
+ | `filterValues` | `Record<string, unknown>` | Active filter values (for built-in filters) |
52
+ | `globalFilter` | `string` | Search/query filter (for built-in search) |
53
+ | `page` | `number` | Current page (1-based, server-side mode) |
54
+ | `itemsPerPage` | `number` | Page size (server-side mode) |
49
55
 
50
56
  #### Slots
51
57
 
@@ -371,6 +377,75 @@ const columns = [
371
377
  </template>
372
378
  ```
373
379
 
380
+ ### Server-side pagination
381
+
382
+ When `manualPagination` is enabled, the table disables client-side sorting and filtering. The parent component owns all query logic — it watches the table's `sorting`, `page`, and `itemsPerPage` models, builds API params, and fetches data.
383
+
384
+ ```vue
385
+ <script setup>
386
+ const columns = [
387
+ { accessorKey: 'name', header: 'Name' },
388
+ { accessorKey: 'email', header: 'Email' },
389
+ { accessorKey: 'status', header: 'Status' },
390
+ ]
391
+
392
+ const sorting = ref([])
393
+ const page = ref(1)
394
+ const itemsPerPage = ref(20)
395
+ const total = ref(0)
396
+ const users = ref([])
397
+ const loading = ref(false)
398
+
399
+ // Parent-owned search (not built into the table)
400
+ const search = ref('')
401
+
402
+ // Build API params from table state + parent state
403
+ const apiParams = computed(() => {
404
+ const params = { page: page.value, limit: itemsPerPage.value }
405
+ const sort = sorting.value[0]
406
+ if (sort?.id) {
407
+ params.sort_by = sort.id
408
+ params.sort_order = sort.desc ? 'desc' : 'asc'
409
+ }
410
+ if (search.value.trim()) {
411
+ params.search = search.value.trim()
412
+ }
413
+ return params
414
+ })
415
+
416
+ async function fetchUsers() {
417
+ loading.value = true
418
+ try {
419
+ const response = await $fetch('/api/users', { query: apiParams.value })
420
+ users.value = response.data
421
+ total.value = response.total
422
+ } finally {
423
+ loading.value = false
424
+ }
425
+ }
426
+
427
+ watch(apiParams, fetchUsers, { immediate: true, deep: true })
428
+ </script>
429
+
430
+ <template>
431
+ <!-- Parent-owned search input -->
432
+ <UInput v-model="search" placeholder="Search..." icon="i-lucide-search" class="mb-4 max-w-xs" />
433
+
434
+ <VATable
435
+ name="Users"
436
+ :data="users"
437
+ :columns="columns"
438
+ :loading="loading"
439
+ :manual-pagination="true"
440
+ :total="total"
441
+ v-model:sorting="sorting"
442
+ v-model:page="page"
443
+ v-model:items-per-page="itemsPerPage"
444
+ :on-refresh="fetchUsers"
445
+ />
446
+ </template>
447
+ ```
448
+
374
449
  ---
375
450
 
376
451
  ## Date Auto-Detection
@@ -40,6 +40,7 @@
40
40
  <!-- Normal Toolbar (shown when no items selected) -->
41
41
  <div v-else class="flex items-center gap-2">
42
42
  <UInput
43
+ v-if="showSearch"
43
44
  v-model="globalFilter"
44
45
  placeholder="Search..."
45
46
  icon="i-lucide-search"
@@ -97,7 +98,7 @@
97
98
  <UTable
98
99
  ref="table"
99
100
  v-model:sorting="sorting"
100
- v-model:global-filter="globalFilter"
101
+ v-model:global-filter="tableGlobalFilter"
101
102
  v-model:pagination="pagination"
102
103
  v-model:column-visibility="columnVisibility"
103
104
  v-model:row-selection="rowSelection"
@@ -105,8 +106,14 @@
105
106
  :columns="tableColumns"
106
107
  :loading="loading"
107
108
  :row-selection-options="{ enableRowSelection: props.selectable }"
108
- :sorting-options="{ getSortedRowModel: getSortedRowModel() }"
109
- :pagination-options="{ getPaginationRowModel: getPaginationRowModel() }"
109
+ :sorting-options="sortingOptions"
110
+ :pagination-options="{
111
+ getPaginationRowModel: getPaginationRowModel(),
112
+ manualPagination: props.manualPagination,
113
+ ...(props.manualPagination && props.total !== undefined
114
+ ? { pageCount: Math.ceil(props.total / pagination.pageSize) }
115
+ : {})
116
+ }"
110
117
  :ui="{
111
118
  th: 'px-3 py-2',
112
119
  td: 'px-3 py-2',
@@ -148,7 +155,7 @@
148
155
  :items-per-page="pagination.pageSize"
149
156
  :total="totalRows"
150
157
  size="sm"
151
- @update:page="(p) => (pagination.pageIndex = p - 1)"
158
+ @update:page="onPageChange"
152
159
  />
153
160
  </div>
154
161
  </div>
@@ -157,7 +164,7 @@
157
164
  </template>
158
165
 
159
166
  <script setup lang="ts">
160
- import { getCoreRowModel, getPaginationRowModel, getSortedRowModel } from "@tanstack/vue-table";
167
+ import { getPaginationRowModel } from "@tanstack/vue-table";
161
168
  import type { TableColumn, DropdownMenuItem } from "@nuxt/ui";
162
169
  import type { SortingState } from "@tanstack/vue-table";
163
170
  import { h, resolveComponent, useSlots } from 'vue';
@@ -177,15 +184,18 @@ const props = withDefaults(defineProps<{
177
184
  loading?: boolean;
178
185
  initialPageLimit?: number;
179
186
  selectable?: boolean;
187
+ showSearch?: boolean;
180
188
  filters?: FilterDefinition[];
181
189
  onRefresh?: () => void | Promise<void>;
182
190
  onRowClick?: (row: unknown) => void;
183
- /** Default column to sort by */
184
191
  defaultSort?: string;
185
- /** Default sort direction */
186
192
  defaultSortDesc?: boolean;
193
+ manualPagination?: boolean;
194
+ total?: number;
187
195
  }>(), {
188
196
  selectable: false,
197
+ showSearch: false,
198
+ manualPagination: false,
189
199
  });
190
200
 
191
201
  // Sorting state - v-model support
@@ -197,6 +207,41 @@ const filterValues = defineModel<Record<string, unknown>>("filterValues", {
197
207
  default: () => ({}),
198
208
  });
199
209
 
210
+ // Server-side pagination - v-model support (1-based page)
211
+ const page = defineModel<number>("page", { default: 1 });
212
+ const itemsPerPage = defineModel<number>("itemsPerPage", { default: 10 });
213
+
214
+ // Internal pagination state (0-based pageIndex for TanStack)
215
+ const pagination = ref({
216
+ pageIndex: 0,
217
+ pageSize: 10,
218
+ });
219
+
220
+ // Initialize pagination from props and sync with v-model
221
+ onMounted(() => {
222
+ const defaultPageSize = props.initialPageLimit ?? 10;
223
+ pagination.value.pageSize = defaultPageSize;
224
+
225
+ // Sync itemsPerPage if not already set by parent
226
+ if (itemsPerPage.value === 10 && defaultPageSize !== 10) {
227
+ itemsPerPage.value = defaultPageSize;
228
+ }
229
+ });
230
+
231
+ // Sync internal pagination with v-model when in manual mode
232
+ watch(
233
+ [page, itemsPerPage],
234
+ ([newPage, newItemsPerPage]) => {
235
+ if (props.manualPagination) {
236
+ pagination.value = {
237
+ pageIndex: newPage - 1, // Convert 1-based to 0-based
238
+ pageSize: newItemsPerPage,
239
+ };
240
+ }
241
+ },
242
+ { immediate: true }
243
+ );
244
+
200
245
  const refreshing = ref(false);
201
246
  const slots = useSlots()
202
247
 
@@ -239,14 +284,6 @@ function clearSelection() {
239
284
  rowSelection.value = {};
240
285
  }
241
286
 
242
- watch(
243
- () => filterValues.value,
244
- () => {
245
- clearSelection();
246
- },
247
- { deep: true }
248
- );
249
-
250
287
  // Table ref for accessing TanStack API
251
288
  const table = ref<{ tableApi?: any } | null>(null);
252
289
 
@@ -282,6 +319,30 @@ watch(
282
319
  // Global filter - v-model support
283
320
  const globalFilter = defineModel<string>("globalFilter", { default: "" });
284
321
 
322
+ // In manual mode, prevent TanStack from filtering client-side
323
+ const tableGlobalFilter = computed({
324
+ get: () => props.manualPagination ? undefined : globalFilter.value,
325
+ set: (v: string) => { globalFilter.value = v ?? '' },
326
+ });
327
+
328
+ // Reset page when sorting changes
329
+ watch(
330
+ sorting,
331
+ () => {
332
+ clearSelection();
333
+ if (props.manualPagination) {
334
+ page.value = 1;
335
+ } else {
336
+ pagination.value = { ...pagination.value, pageIndex: 0 };
337
+ }
338
+ },
339
+ { deep: true, flush: 'sync' }
340
+ );
341
+
342
+ const sortingOptions = computed(() =>
343
+ props.manualPagination ? { manualSorting: true } : undefined
344
+ );
345
+
285
346
  // Initialize default sorting if provided
286
347
  onMounted(() => {
287
348
  if (props.defaultSort && sorting.value.length === 0) {
@@ -289,12 +350,6 @@ onMounted(() => {
289
350
  }
290
351
  });
291
352
 
292
- // Pagination state
293
- const pagination = ref({
294
- pageIndex: 0,
295
- pageSize: props.initialPageLimit ?? 10,
296
- });
297
-
298
353
  // Column visibility state
299
354
  const columnVisibility = ref<Record<string, boolean>>({});
300
355
 
@@ -446,6 +501,9 @@ function handleRowClick(row: any) {
446
501
 
447
502
  // Computed pagination info
448
503
  const totalRows = computed(() => {
504
+ if (props.manualPagination && props.total !== undefined) {
505
+ return props.total;
506
+ }
449
507
  return table.value?.tableApi?.getFilteredRowModel().rows.length ?? 0;
450
508
  });
451
509
 
@@ -461,17 +519,35 @@ const endIndex = computed(() => {
461
519
 
462
520
  // Handle page size change
463
521
  const onPageSizeChange = (size: number) => {
464
- pagination.value = {
465
- pageIndex: 0,
466
- pageSize: size,
467
- };
522
+ if (props.manualPagination) {
523
+ // Emit to parent for server-side handling
524
+ itemsPerPage.value = size;
525
+ page.value = 1; // Reset to first page
526
+ } else {
527
+ pagination.value = {
528
+ pageIndex: 0,
529
+ pageSize: size,
530
+ };
531
+ }
468
532
  };
469
533
 
470
- // Reset pagination when data changes
534
+ // Handle page change from UPagination
535
+ const onPageChange = (newPage: number) => {
536
+ if (props.manualPagination) {
537
+ // Emit to parent for server-side handling
538
+ page.value = newPage;
539
+ } else {
540
+ pagination.value.pageIndex = newPage - 1;
541
+ }
542
+ };
543
+
544
+ // Reset pagination when data changes (client-side only)
471
545
  watch(
472
546
  () => props.data,
473
547
  () => {
474
- pagination.value.pageIndex = 0;
548
+ if (!props.manualPagination) {
549
+ pagination.value.pageIndex = 0;
550
+ }
475
551
  }
476
552
  );
477
553
 
@@ -1,247 +1,459 @@
1
1
  <script setup lang="ts">
2
- import type { TableColumn } from '@nuxt/ui'
2
+ import type { TableColumn } from "@nuxt/ui";
3
3
 
4
4
  definePageMeta({
5
- title: 'Table Components'
6
- })
5
+ title: "Table Components",
6
+ });
7
7
 
8
8
  // Sample data
9
9
  const users = ref([
10
- { id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin', status: 'active', department: 'Engineering', salary: 95000, createdAt: '2024-01-15T10:30:00Z' },
11
- { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'Editor', status: 'active', department: 'Marketing', salary: 72000, createdAt: '2024-02-20T14:00:00Z' },
12
- { id: 3, name: 'Bob Wilson', email: 'bob@example.com', role: 'Viewer', status: 'pending', department: 'Sales', salary: 68000, createdAt: '2024-03-10T09:15:00Z' },
13
- { id: 4, name: 'Alice Brown', email: 'alice@example.com', role: 'Admin', status: 'active', department: 'Engineering', salary: 105000, createdAt: '2024-04-05T11:45:00Z' },
14
- { id: 5, name: 'Charlie Davis', email: 'charlie@example.com', role: 'Editor', status: 'inactive', department: 'Design', salary: 78000, createdAt: '2024-05-01T16:20:00Z' },
15
- { id: 6, name: 'Diana Miller', email: 'diana@example.com', role: 'Viewer', status: 'active', department: 'HR', salary: 62000, createdAt: '2024-06-12T08:00:00Z' },
16
- { id: 7, name: 'Edward Lee', email: 'edward@example.com', role: 'Admin', status: 'active', department: 'Engineering', salary: 112000, createdAt: '2024-07-18T13:30:00Z' },
17
- { id: 8, name: 'Fiona Garcia', email: 'fiona@example.com', role: 'Editor', status: 'pending', department: 'Marketing', salary: 75000, createdAt: '2024-08-22T10:00:00Z' },
18
- { id: 9, name: 'George Martinez', email: 'george@example.com', role: 'Viewer', status: 'active', department: 'Sales', salary: 71000, createdAt: '2024-09-03T15:45:00Z' },
19
- { id: 10, name: 'Hannah Robinson', email: 'hannah@example.com', role: 'Editor', status: 'active', department: 'Design', salary: 82000, createdAt: '2024-10-14T12:10:00Z' },
20
- { id: 11, name: 'Ivan Thompson', email: 'ivan@example.com', role: 'Admin', status: 'inactive', department: 'Engineering', salary: 98000, createdAt: '2024-11-25T09:30:00Z' },
21
- { id: 12, name: 'Julia White', email: 'julia@example.com', role: 'Viewer', status: 'active', department: 'HR', salary: 58000, createdAt: '2024-12-01T17:00:00Z' }
22
- ])
10
+ {
11
+ id: 1,
12
+ name: "John Doe",
13
+ email: "john@example.com",
14
+ role: "Admin",
15
+ status: "active",
16
+ department: "Engineering",
17
+ salary: 95000,
18
+ createdAt: "2024-01-15T10:30:00Z",
19
+ },
20
+ {
21
+ id: 2,
22
+ name: "Jane Smith",
23
+ email: "jane@example.com",
24
+ role: "Editor",
25
+ status: "active",
26
+ department: "Marketing",
27
+ salary: 72000,
28
+ createdAt: "2024-02-20T14:00:00Z",
29
+ },
30
+ {
31
+ id: 3,
32
+ name: "Bob Wilson",
33
+ email: "bob@example.com",
34
+ role: "Viewer",
35
+ status: "pending",
36
+ department: "Sales",
37
+ salary: 68000,
38
+ createdAt: "2024-03-10T09:15:00Z",
39
+ },
40
+ {
41
+ id: 4,
42
+ name: "Alice Brown",
43
+ email: "alice@example.com",
44
+ role: "Admin",
45
+ status: "active",
46
+ department: "Engineering",
47
+ salary: 105000,
48
+ createdAt: "2024-04-05T11:45:00Z",
49
+ },
50
+ {
51
+ id: 5,
52
+ name: "Charlie Davis",
53
+ email: "charlie@example.com",
54
+ role: "Editor",
55
+ status: "inactive",
56
+ department: "Design",
57
+ salary: 78000,
58
+ createdAt: "2024-05-01T16:20:00Z",
59
+ },
60
+ {
61
+ id: 6,
62
+ name: "Diana Miller",
63
+ email: "diana@example.com",
64
+ role: "Viewer",
65
+ status: "active",
66
+ department: "HR",
67
+ salary: 62000,
68
+ createdAt: "2024-06-12T08:00:00Z",
69
+ },
70
+ {
71
+ id: 7,
72
+ name: "Edward Lee",
73
+ email: "edward@example.com",
74
+ role: "Admin",
75
+ status: "active",
76
+ department: "Engineering",
77
+ salary: 112000,
78
+ createdAt: "2024-07-18T13:30:00Z",
79
+ },
80
+ {
81
+ id: 8,
82
+ name: "Fiona Garcia",
83
+ email: "fiona@example.com",
84
+ role: "Editor",
85
+ status: "pending",
86
+ department: "Marketing",
87
+ salary: 75000,
88
+ createdAt: "2024-08-22T10:00:00Z",
89
+ },
90
+ {
91
+ id: 9,
92
+ name: "George Martinez",
93
+ email: "george@example.com",
94
+ role: "Viewer",
95
+ status: "active",
96
+ department: "Sales",
97
+ salary: 71000,
98
+ createdAt: "2024-09-03T15:45:00Z",
99
+ },
100
+ {
101
+ id: 10,
102
+ name: "Hannah Robinson",
103
+ email: "hannah@example.com",
104
+ role: "Editor",
105
+ status: "active",
106
+ department: "Design",
107
+ salary: 82000,
108
+ createdAt: "2024-10-14T12:10:00Z",
109
+ },
110
+ {
111
+ id: 11,
112
+ name: "Ivan Thompson",
113
+ email: "ivan@example.com",
114
+ role: "Admin",
115
+ status: "inactive",
116
+ department: "Engineering",
117
+ salary: 98000,
118
+ createdAt: "2024-11-25T09:30:00Z",
119
+ },
120
+ {
121
+ id: 12,
122
+ name: "Julia White",
123
+ email: "julia@example.com",
124
+ role: "Viewer",
125
+ status: "active",
126
+ department: "HR",
127
+ salary: 58000,
128
+ createdAt: "2024-12-01T17:00:00Z",
129
+ },
130
+ ]);
23
131
 
24
132
  // Basic columns using the new Nuxt UI TableColumn format
25
- const basicColumns: TableColumn<typeof users.value[0]>[] = [
26
- { accessorKey: 'name', header: 'Name' },
27
- { accessorKey: 'email', header: 'Email' },
28
- { accessorKey: 'role', header: 'Role' },
29
- { accessorKey: 'status', header: 'Status' }
30
- ]
133
+ const basicColumns: TableColumn<(typeof users.value)[0]>[] = [
134
+ { accessorKey: "name", header: "Name" },
135
+ { accessorKey: "email", header: "Email" },
136
+ { accessorKey: "role", header: "Role" },
137
+ { accessorKey: "status", header: "Status" },
138
+ ];
31
139
 
32
140
  // Full columns with more fields
33
- const fullColumns: TableColumn<typeof users.value[0]>[] = [
34
- { accessorKey: 'name', header: 'Name' },
35
- { accessorKey: 'email', header: 'Email' },
36
- { accessorKey: 'role', header: 'Role' },
37
- { accessorKey: 'department', header: 'Department' },
38
- { accessorKey: 'salary', header: 'Salary' },
39
- { accessorKey: 'status', header: 'Status' },
40
- { accessorKey: 'createdAt', header: 'Created' }
41
- ]
141
+ const fullColumns: TableColumn<(typeof users.value)[0]>[] = [
142
+ { accessorKey: "name", header: "Name" },
143
+ { accessorKey: "email", header: "Email" },
144
+ { accessorKey: "role", header: "Role" },
145
+ { accessorKey: "department", header: "Department" },
146
+ { accessorKey: "salary", header: "Salary" },
147
+ { accessorKey: "status", header: "Status" },
148
+ { accessorKey: "createdAt", header: "Created" },
149
+ ];
42
150
 
43
151
  // Columns using presets
44
- const { presets } = useXATableColumns()
152
+ const { presets } = useXATableColumns();
45
153
 
46
- const presetColumns: TableColumn<typeof users.value[0]>[] = [
47
- presets.text('name', 'Name'),
48
- presets.email('email', 'Email'),
49
- presets.badge('status', 'Status', {
50
- colorMap: { active: 'success', pending: 'warning', inactive: 'neutral' }
154
+ const presetColumns: TableColumn<(typeof users.value)[0]>[] = [
155
+ presets.text("name", "Name"),
156
+ presets.email("email", "Email"),
157
+ presets.badge("status", "Status", {
158
+ colorMap: { active: "success", pending: "warning", inactive: "neutral" },
51
159
  }),
52
- presets.currency('salary', 'Salary'),
53
- presets.date('createdAt', 'Joined', { format: 'relative' })
54
- ]
160
+ presets.currency("salary", "Salary"),
161
+ presets.date("createdAt", "Joined", { format: "relative" }),
162
+ ];
55
163
 
56
164
  // Products data for variety
57
165
  const products = ref([
58
- { id: 1, name: 'MacBook Pro 14"', category: 'Laptops', price: 1999, stock: 45, rating: 4.8 },
59
- { id: 2, name: 'iPhone 15 Pro', category: 'Phones', price: 999, stock: 120, rating: 4.9 },
60
- { id: 3, name: 'AirPods Pro', category: 'Audio', price: 249, stock: 200, rating: 4.7 },
61
- { id: 4, name: 'iPad Air', category: 'Tablets', price: 599, stock: 80, rating: 4.6 },
62
- { id: 5, name: 'Apple Watch Ultra', category: 'Wearables', price: 799, stock: 35, rating: 4.8 },
63
- { id: 6, name: 'Mac Mini M2', category: 'Desktops', price: 699, stock: 60, rating: 4.5 }
64
- ])
65
-
66
- const productColumns: TableColumn<typeof products.value[0]>[] = [
67
- { accessorKey: 'name', header: 'Product' },
68
- { accessorKey: 'category', header: 'Category' },
69
- { accessorKey: 'price', header: 'Price' },
70
- { accessorKey: 'stock', header: 'Stock' },
71
- { accessorKey: 'rating', header: 'Rating' }
72
- ]
166
+ {
167
+ id: 1,
168
+ name: 'MacBook Pro 14"',
169
+ category: "Laptops",
170
+ price: 1999,
171
+ stock: 45,
172
+ rating: 4.8,
173
+ },
174
+ {
175
+ id: 2,
176
+ name: "iPhone 15 Pro",
177
+ category: "Phones",
178
+ price: 999,
179
+ stock: 120,
180
+ rating: 4.9,
181
+ },
182
+ {
183
+ id: 3,
184
+ name: "AirPods Pro",
185
+ category: "Audio",
186
+ price: 249,
187
+ stock: 200,
188
+ rating: 4.7,
189
+ },
190
+ {
191
+ id: 4,
192
+ name: "iPad Air",
193
+ category: "Tablets",
194
+ price: 599,
195
+ stock: 80,
196
+ rating: 4.6,
197
+ },
198
+ {
199
+ id: 5,
200
+ name: "Apple Watch Ultra",
201
+ category: "Wearables",
202
+ price: 799,
203
+ stock: 35,
204
+ rating: 4.8,
205
+ },
206
+ {
207
+ id: 6,
208
+ name: "Mac Mini M2",
209
+ category: "Desktops",
210
+ price: 699,
211
+ stock: 60,
212
+ rating: 4.5,
213
+ },
214
+ ]);
215
+
216
+ const productColumns: TableColumn<(typeof products.value)[0]>[] = [
217
+ { accessorKey: "name", header: "Product" },
218
+ { accessorKey: "category", header: "Category" },
219
+ { accessorKey: "price", header: "Price" },
220
+ { accessorKey: "stock", header: "Stock" },
221
+ { accessorKey: "rating", header: "Rating" },
222
+ ];
73
223
 
74
224
  // Filter definitions
75
225
  const statusFilters = [
76
- { label: 'Status', key: 'status', options: [
77
- { label: 'Active', value: 'active' },
78
- { label: 'Pending', value: 'pending' },
79
- { label: 'Inactive', value: 'inactive' }
80
- ]},
81
- { label: 'Role', key: 'role', options: [
82
- { label: 'Admin', value: 'Admin' },
83
- { label: 'Editor', value: 'Editor' },
84
- { label: 'Viewer', value: 'Viewer' }
85
- ]}
86
- ]
226
+ {
227
+ label: "Status",
228
+ key: "status",
229
+ options: [
230
+ { label: "Active", value: "active" },
231
+ { label: "Pending", value: "pending" },
232
+ { label: "Inactive", value: "inactive" },
233
+ ],
234
+ },
235
+ {
236
+ label: "Role",
237
+ key: "role",
238
+ options: [
239
+ { label: "Admin", value: "Admin" },
240
+ { label: "Editor", value: "Editor" },
241
+ { label: "Viewer", value: "Viewer" },
242
+ ],
243
+ },
244
+ ];
87
245
 
88
246
  // Event handlers
89
247
  const handleRowClick = (row: any) => {
90
- console.log('Row clicked:', row)
91
- }
248
+ console.log("Row clicked:", row);
249
+ };
92
250
  </script>
93
251
 
94
252
  <template>
95
253
  <UDashboardPanel grow>
96
254
  <UContainer>
97
- <div class="py-8 space-y-12">
98
- <div>
99
- <h1 class="text-3xl font-bold mb-4">VATable Components Showcase</h1>
100
- <p class="text-gray-600 dark:text-gray-400">
101
- TanStack-powered data tables with search, sort, filter, export, selection, and pagination.
102
- </p>
103
- </div>
255
+ <div class="py-8 space-y-12">
256
+ <div>
257
+ <h1 class="text-3xl font-bold mb-4">VATable Components Showcase</h1>
258
+ <p class="text-gray-600 dark:text-gray-400">
259
+ TanStack-powered data tables with search, sort, filter, export,
260
+ selection, and pagination.
261
+ </p>
262
+ </div>
263
+
264
+ <!-- Basic Table -->
265
+ <section>
266
+ <h2 class="text-2xl font-semibold mb-4">Basic Table</h2>
267
+ <p class="text-gray-600 dark:text-gray-400 mb-4">
268
+ Simple table with minimal configuration.
269
+ </p>
270
+ <ClientOnly>
271
+ <VATable
272
+ name="Basic Users"
273
+ :data="users.slice(0, 5)"
274
+ :columns="basicColumns"
275
+ />
276
+ </ClientOnly>
277
+ </section>
278
+
279
+ <!-- Preset-Based Columns -->
280
+ <section>
281
+ <h2 class="text-2xl font-semibold mb-4">Preset-Based Columns</h2>
282
+ <p class="text-gray-600 dark:text-gray-400 mb-4">
283
+ Using useXATableColumns presets for automatic cell rendering (badge,
284
+ currency, date).
285
+ </p>
286
+ <ClientOnly>
287
+ <VATable
288
+ name="Users with Presets"
289
+ :data="users"
290
+ :columns="presetColumns"
291
+ />
292
+ </ClientOnly>
293
+ </section>
294
+
295
+ <!-- Table with Filters -->
296
+ <section>
297
+ <h2 class="text-2xl font-semibold mb-4">Table with Filters</h2>
298
+ <p class="text-gray-600 dark:text-gray-400 mb-4">
299
+ Inline filter dropdowns with active filter chips.
300
+ </p>
301
+ <ClientOnly>
302
+ <VATable
303
+ name="Filtered Users"
304
+ :data="users"
305
+ :show-search="true"
306
+ :columns="basicColumns"
307
+ :filters="statusFilters"
308
+ />
309
+ </ClientOnly>
310
+ </section>
311
+
312
+ <!-- Selectable Table -->
313
+ <section>
314
+ <h2 class="text-2xl font-semibold mb-4">Selectable Table</h2>
315
+ <p class="text-gray-600 dark:text-gray-400 mb-4">
316
+ Multi-select with checkbox column and bulk action support.
317
+ </p>
318
+ <ClientOnly>
319
+ <VATable
320
+ name="Select Users"
321
+ :data="users.slice(0, 6)"
322
+ :columns="basicColumns"
323
+ selectable
324
+ >
325
+ <template #bulk-actions="{ count, clear }">
326
+ <span class="text-sm text-muted">{{ count }} selected</span>
327
+ <UButton
328
+ size="xs"
329
+ color="neutral"
330
+ variant="outline"
331
+ label="Clear"
332
+ @click="clear"
333
+ />
334
+ <UButton
335
+ size="xs"
336
+ color="error"
337
+ variant="soft"
338
+ label="Delete Selected"
339
+ />
340
+ </template>
341
+ </VATable>
342
+ </ClientOnly>
343
+ </section>
104
344
 
105
- <!-- Basic Table -->
106
- <section>
107
- <h2 class="text-2xl font-semibold mb-4">Basic Table</h2>
108
- <p class="text-gray-600 dark:text-gray-400 mb-4">Simple table with minimal configuration.</p>
109
- <ClientOnly>
110
- <VATable
111
- name="Basic Users"
112
- :data="users.slice(0, 5)"
113
- :columns="basicColumns"
114
- />
115
- </ClientOnly>
116
- </section>
117
-
118
- <!-- Preset-Based Columns -->
119
- <section>
120
- <h2 class="text-2xl font-semibold mb-4">Preset-Based Columns</h2>
121
- <p class="text-gray-600 dark:text-gray-400 mb-4">Using useXATableColumns presets for automatic cell rendering (badge, currency, date).</p>
122
- <ClientOnly>
123
- <VATable
124
- name="Users with Presets"
125
- :data="users"
126
- :columns="presetColumns"
127
- />
128
- </ClientOnly>
129
- </section>
130
-
131
- <!-- Table with Filters -->
132
- <section>
133
- <h2 class="text-2xl font-semibold mb-4">Table with Filters</h2>
134
- <p class="text-gray-600 dark:text-gray-400 mb-4">Inline filter dropdowns with active filter chips.</p>
135
- <ClientOnly>
136
- <VATable
137
- name="Filtered Users"
138
- :data="users"
139
- :columns="basicColumns"
140
- :filters="statusFilters"
141
- />
142
- </ClientOnly>
143
- </section>
144
-
145
- <!-- Selectable Table -->
146
- <section>
147
- <h2 class="text-2xl font-semibold mb-4">Selectable Table</h2>
148
- <p class="text-gray-600 dark:text-gray-400 mb-4">Multi-select with checkbox column and bulk action support.</p>
149
- <ClientOnly>
150
- <VATable
151
- name="Select Users"
152
- :data="users.slice(0, 6)"
153
- :columns="basicColumns"
154
- selectable
155
- >
156
- <template #bulk-actions="{ count, clear }">
157
- <span class="text-sm text-muted">{{ count }} selected</span>
158
- <UButton size="xs" color="neutral" variant="outline" label="Clear" @click="clear" />
159
- <UButton size="xs" color="error" variant="soft" label="Delete Selected" />
160
- </template>
161
- </VATable>
162
- </ClientOnly>
163
- </section>
164
-
165
- <!-- Table with Row Actions -->
166
- <section>
167
- <h2 class="text-2xl font-semibold mb-4">Table with Row Actions</h2>
168
- <p class="text-gray-600 dark:text-gray-400 mb-4">Custom actions column via slot.</p>
169
- <ClientOnly>
170
- <VATable
171
- name="Users with Actions"
172
- :data="users.slice(0, 5)"
173
- :columns="basicColumns"
174
- :on-row-click="handleRowClick"
175
- >
176
- <template #actions-cell="{ row }">
177
- <div class="flex items-center justify-end gap-1">
178
- <UButton icon="i-lucide-eye" color="neutral" variant="ghost" size="xs" @click.stop="() => console.log('View', row.original)" />
179
- <UButton icon="i-lucide-pencil" color="neutral" variant="ghost" size="xs" @click.stop="() => console.log('Edit', row.original)" />
180
- <UButton icon="i-lucide-trash-2" color="error" variant="ghost" size="xs" @click.stop="() => console.log('Delete', row.original)" />
181
- </div>
182
- </template>
183
- </VATable>
184
- </ClientOnly>
185
- </section>
186
-
187
- <!-- Table with Refresh -->
188
- <section>
189
- <h2 class="text-2xl font-semibold mb-4">Table with Refresh</h2>
190
- <p class="text-gray-600 dark:text-gray-400 mb-4">Refresh button with loading state.</p>
191
- <ClientOnly>
192
- <VATable
193
- name="Products"
194
- :data="products"
195
- :columns="productColumns"
196
- :on-refresh="async () => { await new Promise(r => setTimeout(r, 1000)) }"
197
- />
198
- </ClientOnly>
199
- </section>
200
-
201
- <!-- Default Sort -->
202
- <section>
203
- <h2 class="text-2xl font-semibold mb-4">Default Sort</h2>
204
- <p class="text-gray-600 dark:text-gray-400 mb-4">Table pre-sorted by salary descending.</p>
205
- <ClientOnly>
206
- <VATable
207
- name="Sorted Users"
208
- :data="users"
209
- :columns="fullColumns"
210
- default-sort="salary"
211
- :default-sort-desc="true"
212
- />
213
- </ClientOnly>
214
- </section>
215
-
216
- <!-- Loading State -->
217
- <section>
218
- <h2 class="text-2xl font-semibold mb-4">Loading State</h2>
219
- <p class="text-gray-600 dark:text-gray-400 mb-4">Full-screen loading state for initial data fetch.</p>
220
- <ClientOnly>
221
- <VATable
222
- name="Loading Table"
223
- :data="[]"
224
- :columns="basicColumns"
225
- :loading="true"
226
- />
227
- </ClientOnly>
228
- </section>
229
-
230
- <!-- VADataTable Section -->
231
- <section>
232
- <h2 class="text-2xl font-semibold mb-4">VADataTable</h2>
233
- <p class="text-gray-600 dark:text-gray-400 mb-4">Dashboard-integrated table with UDashboardPanel wrapper.</p>
234
- <ClientOnly>
235
- <VADataTable
236
- title="Users (DataTable)"
237
- :rows="users.slice(0, 5)"
238
- :columns="basicColumns"
239
- :show-search="false"
240
- :show-pagination="false"
241
- />
242
- </ClientOnly>
243
- </section>
244
- </div>
345
+ <!-- Table with Row Actions -->
346
+ <section>
347
+ <h2 class="text-2xl font-semibold mb-4">Table with Row Actions</h2>
348
+ <p class="text-gray-600 dark:text-gray-400 mb-4">
349
+ Custom actions column via slot.
350
+ </p>
351
+ <ClientOnly>
352
+ <VATable
353
+ name="Users with Actions"
354
+ :data="users.slice(0, 5)"
355
+ :columns="basicColumns"
356
+ :on-row-click="handleRowClick"
357
+ >
358
+ <template #actions-cell="{ row }">
359
+ <div class="flex items-center justify-end gap-1">
360
+ <UButton
361
+ icon="i-lucide-eye"
362
+ color="neutral"
363
+ variant="ghost"
364
+ size="xs"
365
+ @click.stop="() => console.log('View', row.original)"
366
+ />
367
+ <UButton
368
+ icon="i-lucide-pencil"
369
+ color="neutral"
370
+ variant="ghost"
371
+ size="xs"
372
+ @click.stop="() => console.log('Edit', row.original)"
373
+ />
374
+ <UButton
375
+ icon="i-lucide-trash-2"
376
+ color="error"
377
+ variant="ghost"
378
+ size="xs"
379
+ @click.stop="() => console.log('Delete', row.original)"
380
+ />
381
+ </div>
382
+ </template>
383
+ </VATable>
384
+ </ClientOnly>
385
+ </section>
386
+
387
+ <!-- Table with Refresh -->
388
+ <section>
389
+ <h2 class="text-2xl font-semibold mb-4">Table with Refresh</h2>
390
+ <p class="text-gray-600 dark:text-gray-400 mb-4">
391
+ Refresh button with loading state.
392
+ </p>
393
+ <ClientOnly>
394
+ <VATable
395
+ name="Products"
396
+ :data="products"
397
+ :columns="productColumns"
398
+ :on-refresh="
399
+ async () => {
400
+ await new Promise((r) => setTimeout(r, 1000));
401
+ }
402
+ "
403
+ />
404
+ </ClientOnly>
405
+ </section>
406
+
407
+ <!-- Default Sort -->
408
+ <section>
409
+ <h2 class="text-2xl font-semibold mb-4">Default Sort</h2>
410
+ <p class="text-gray-600 dark:text-gray-400 mb-4">
411
+ Table pre-sorted by salary descending.
412
+ </p>
413
+ <ClientOnly>
414
+ <VATable
415
+ name="Sorted Users"
416
+ :data="users"
417
+ :columns="fullColumns"
418
+ default-sort="salary"
419
+ :default-sort-desc="true"
420
+ />
421
+ </ClientOnly>
422
+ </section>
423
+
424
+ <!-- Loading State -->
425
+ <section>
426
+ <h2 class="text-2xl font-semibold mb-4">Loading State</h2>
427
+ <p class="text-gray-600 dark:text-gray-400 mb-4">
428
+ Full-screen loading state for initial data fetch.
429
+ </p>
430
+ <ClientOnly>
431
+ <VATable
432
+ name="Loading Table"
433
+ :data="[]"
434
+ :columns="basicColumns"
435
+ :loading="true"
436
+ />
437
+ </ClientOnly>
438
+ </section>
439
+
440
+ <!-- VADataTable Section -->
441
+ <section>
442
+ <h2 class="text-2xl font-semibold mb-4">VADataTable</h2>
443
+ <p class="text-gray-600 dark:text-gray-400 mb-4">
444
+ Dashboard-integrated table with UDashboardPanel wrapper.
445
+ </p>
446
+ <ClientOnly>
447
+ <VADataTable
448
+ title="Users (DataTable)"
449
+ :rows="users.slice(0, 5)"
450
+ :columns="basicColumns"
451
+ :show-search="false"
452
+ :show-pagination="false"
453
+ />
454
+ </ClientOnly>
455
+ </section>
456
+ </div>
245
457
  </UContainer>
246
458
  </UDashboardPanel>
247
459
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veristone/nuxt-v-app",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Veristone Nuxt App Layer - Shared components, composables, and layouts",
5
5
  "type": "module",
6
6
  "private": false,