@veristone/nuxt-v-app 0.2.2 → 0.2.4

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.
@@ -0,0 +1,140 @@
1
+ <template>
2
+ <div class="flex flex-wrap items-center gap-2">
3
+ <!-- Search Input -->
4
+ <UInput
5
+ v-if="showSearch"
6
+ v-model="searchQuery"
7
+ :placeholder="searchPlaceholder"
8
+ icon="i-lucide-search"
9
+ size="sm"
10
+ class="w-64"
11
+ @update:model-value="onSearchChange"
12
+ />
13
+
14
+ <!-- Filter Dropdowns -->
15
+ <template v-for="filter in filters" :key="filter.key">
16
+ <USelectMenu
17
+ v-if="filter.type === 'select'"
18
+ v-model="filterValues[filter.key]"
19
+ :items="filter.options || []"
20
+ :placeholder="filter.placeholder || filter.label"
21
+ size="sm"
22
+ class="w-40"
23
+ @update:model-value="onFilterChange(filter.key, $event)"
24
+ />
25
+
26
+ <UInput
27
+ v-else-if="filter.type === 'text'"
28
+ v-model="filterValues[filter.key]"
29
+ :placeholder="filter.placeholder || filter.label"
30
+ size="sm"
31
+ class="w-40"
32
+ @update:model-value="onFilterChange(filter.key, $event)"
33
+ />
34
+
35
+ <UInput
36
+ v-else-if="filter.type === 'date'"
37
+ v-model="filterValues[filter.key]"
38
+ type="date"
39
+ :placeholder="filter.placeholder || filter.label"
40
+ size="sm"
41
+ class="w-40"
42
+ @update:model-value="onFilterChange(filter.key, $event)"
43
+ />
44
+
45
+ <UInput
46
+ v-else-if="filter.type === 'number'"
47
+ v-model="filterValues[filter.key]"
48
+ type="number"
49
+ :placeholder="filter.placeholder || filter.label"
50
+ size="sm"
51
+ class="w-32"
52
+ @update:model-value="onFilterChange(filter.key, $event)"
53
+ />
54
+ </template>
55
+
56
+ <!-- Clear Filters Button -->
57
+ <UButton
58
+ v-if="hasActiveFilters"
59
+ label="Clear"
60
+ color="neutral"
61
+ variant="ghost"
62
+ size="sm"
63
+ icon="i-lucide-x"
64
+ @click="clearFilters"
65
+ />
66
+
67
+ <!-- Additional actions slot -->
68
+ <slot name="actions" />
69
+ </div>
70
+ </template>
71
+
72
+ <script setup>
73
+ defineOptions({
74
+ name: 'VATableFilterBar'
75
+ })
76
+
77
+ const props = defineProps({
78
+ filters: {
79
+ type: Array,
80
+ default: () => []
81
+ },
82
+ showSearch: {
83
+ type: Boolean,
84
+ default: true
85
+ },
86
+ searchPlaceholder: {
87
+ type: String,
88
+ default: 'Search...'
89
+ },
90
+ debounce: {
91
+ type: Number,
92
+ default: 300
93
+ }
94
+ })
95
+
96
+ const emit = defineEmits(['update:search', 'update:filters', 'filter', 'clear'])
97
+
98
+ // Search state
99
+ const searchQuery = ref('')
100
+
101
+ // Filter values
102
+ const filterValues = ref({})
103
+
104
+ // Debounced search
105
+ let searchTimeout = null
106
+
107
+ function onSearchChange(value) {
108
+ if (searchTimeout) clearTimeout(searchTimeout)
109
+
110
+ searchTimeout = setTimeout(() => {
111
+ emit('update:search', value)
112
+ }, props.debounce)
113
+ }
114
+
115
+ function onFilterChange(key, value) {
116
+ emit('filter', key, value)
117
+ emit('update:filters', { ...filterValues.value })
118
+ }
119
+
120
+ function clearFilters() {
121
+ searchQuery.value = ''
122
+ filterValues.value = {}
123
+ emit('update:search', '')
124
+ emit('update:filters', {})
125
+ emit('clear')
126
+ }
127
+
128
+ // Check if any filters are active
129
+ const hasActiveFilters = computed(() => {
130
+ if (searchQuery.value) return true
131
+ return Object.values(filterValues.value).some(v => v !== undefined && v !== '' && v !== null)
132
+ })
133
+
134
+ // Expose for parent access
135
+ defineExpose({
136
+ searchQuery,
137
+ filterValues,
138
+ clearFilters
139
+ })
140
+ </script>
@@ -0,0 +1,107 @@
1
+ <template>
2
+ <div v-if="activeFilters.length > 0" class="flex flex-wrap items-center gap-2">
3
+ <span class="text-sm text-neutral-500">Filters:</span>
4
+
5
+ <TransitionGroup name="chip">
6
+ <div
7
+ v-for="filter in activeFilters"
8
+ :key="filter.key"
9
+ class="inline-flex items-center gap-1 px-2 py-1 text-sm bg-neutral-100 dark:bg-neutral-800 rounded-full"
10
+ >
11
+ <span class="text-neutral-600 dark:text-neutral-400">{{ filter.label }}:</span>
12
+ <span class="font-medium text-neutral-900 dark:text-white">{{ formatValue(filter.value) }}</span>
13
+ <button
14
+ type="button"
15
+ class="ml-1 p-0.5 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors"
16
+ @click="removeFilter(filter.key)"
17
+ >
18
+ <UIcon name="i-lucide-x" class="w-3 h-3 text-neutral-500" />
19
+ </button>
20
+ </div>
21
+ </TransitionGroup>
22
+
23
+ <UButton
24
+ v-if="activeFilters.length > 1"
25
+ label="Clear all"
26
+ color="neutral"
27
+ variant="link"
28
+ size="xs"
29
+ @click="clearAll"
30
+ />
31
+ </div>
32
+ </template>
33
+
34
+ <script setup>
35
+ defineOptions({
36
+ name: 'VATableFilterChips'
37
+ })
38
+
39
+ const props = defineProps({
40
+ filters: {
41
+ type: Object,
42
+ required: true
43
+ },
44
+ labels: {
45
+ type: Object,
46
+ default: () => ({})
47
+ }
48
+ })
49
+
50
+ const emit = defineEmits(['remove', 'clear'])
51
+
52
+ // Convert filters object to array with labels
53
+ const activeFilters = computed(() => {
54
+ return Object.entries(props.filters)
55
+ .filter(([_, value]) => value !== undefined && value !== '' && value !== null)
56
+ .map(([key, value]) => ({
57
+ key,
58
+ label: props.labels[key] || formatLabel(key),
59
+ value
60
+ }))
61
+ })
62
+
63
+ // Format camelCase/snake_case to readable label
64
+ function formatLabel(key) {
65
+ return key
66
+ .replace(/([A-Z])/g, ' $1')
67
+ .replace(/[_-]/g, ' ')
68
+ .replace(/^\s/, '')
69
+ .split(' ')
70
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
71
+ .join(' ')
72
+ }
73
+
74
+ // Format value for display
75
+ function formatValue(value) {
76
+ if (value === true) return 'Yes'
77
+ if (value === false) return 'No'
78
+ if (value instanceof Date) return value.toLocaleDateString()
79
+ if (Array.isArray(value)) return value.join(', ')
80
+ return String(value)
81
+ }
82
+
83
+ function removeFilter(key) {
84
+ emit('remove', key)
85
+ }
86
+
87
+ function clearAll() {
88
+ emit('clear')
89
+ }
90
+ </script>
91
+
92
+ <style scoped>
93
+ .chip-enter-active,
94
+ .chip-leave-active {
95
+ transition: all 0.2s ease;
96
+ }
97
+
98
+ .chip-enter-from {
99
+ opacity: 0;
100
+ transform: scale(0.8);
101
+ }
102
+
103
+ .chip-leave-to {
104
+ opacity: 0;
105
+ transform: scale(0.8);
106
+ }
107
+ </style>
@@ -0,0 +1,380 @@
1
+ # VATable Component System
2
+
3
+ TanStack-powered data table components for Nuxt 4 with built-in search, sorting, filtering, pagination, selection, and preset-based cell rendering.
4
+
5
+ ## Components
6
+
7
+ ### `<VATable>` — Main Table
8
+
9
+ The primary table component. Wraps Nuxt UI's `<UTable>` with a toolbar, filter chips, pagination footer, and automatic column processing.
10
+
11
+ ```vue
12
+ <VATable
13
+ name="Users"
14
+ :data="users"
15
+ :columns="columns"
16
+ :loading="loading"
17
+ :filters="filterDefs"
18
+ :selectable="true"
19
+ :on-refresh="refresh"
20
+ :on-row-click="handleClick"
21
+ default-sort="name"
22
+ :default-sort-desc="false"
23
+ :initial-page-limit="20"
24
+ />
25
+ ```
26
+
27
+ #### Props
28
+
29
+ | Prop | Type | Default | Description |
30
+ |------|------|---------|-------------|
31
+ | `name` | `string` | — | Table title shown in the card header |
32
+ | `data` | `unknown[]` | **required** | Array of row data |
33
+ | `columns` | `TableColumn[]` | **required** | TanStack column definitions |
34
+ | `loading` | `boolean` | `false` | Shows loading skeleton on initial load |
35
+ | `initialPageLimit` | `number` | `10` | Rows per page |
36
+ | `selectable` | `boolean` | `false` | Enable row selection checkboxes |
37
+ | `filters` | `FilterDefinition[]` | — | Filter dropdown definitions |
38
+ | `onRefresh` | `() => void \| Promise<void>` | — | Shows refresh button; called on click |
39
+ | `onRowClick` | `(row: unknown) => void` | — | Called when a row is clicked |
40
+ | `defaultSort` | `string` | — | Column accessorKey to sort by on mount |
41
+ | `defaultSortDesc` | `boolean` | `false` | Sort descending by default |
42
+
43
+ #### v-model Bindings
44
+
45
+ | Model | Type | Description |
46
+ |-------|------|-------------|
47
+ | `sorting` | `SortingState` | TanStack sorting state |
48
+ | `filterValues` | `Record<string, unknown>` | Active filter values |
49
+
50
+ #### Slots
51
+
52
+ | Slot | Props | Description |
53
+ |------|-------|-------------|
54
+ | `header-right` | — | Content placed right of the table name |
55
+ | `bulk-actions` | `{ selected, count, clear }` | Shown when rows are selected |
56
+ | `actions-cell` | `{ row }` | Custom actions column (auto-added) |
57
+ | `[column]-cell` | `{ row }` | Custom cell for a specific column |
58
+ | `cell-[column]` | `{ row }` | Alias for above (auto-mapped) |
59
+
60
+ #### Exposed
61
+
62
+ | Property | Type | Description |
63
+ |----------|------|-------------|
64
+ | `selectedRows` | `computed<any[]>` | Currently selected row data |
65
+ | `rowSelection` | `ref<Record<string, boolean>>` | Selection state |
66
+ | `clearSelection` | `() => void` | Clears all selections |
67
+
68
+ ---
69
+
70
+ ### `<VACrudTable>` — CRUD-Integrated Table
71
+
72
+ Full CRUD table that integrates with `useXCrud` composable. Provides toolbar, search, column toggle, export, pagination, and delete confirmation.
73
+
74
+ ```vue
75
+ <VACrudTable
76
+ endpoint="/api/users"
77
+ :columns="columns"
78
+ :config="{
79
+ toolbar: { search: true, export: true },
80
+ actions: { create: true, view: true, edit: true, delete: true },
81
+ pagination: { enabled: true, pageSize: 20 }
82
+ }"
83
+ @create="openCreate"
84
+ @view="openView"
85
+ @edit="openEdit"
86
+ @delete="onDeleted"
87
+ />
88
+ ```
89
+
90
+ #### Props
91
+
92
+ | Prop | Type | Default | Description |
93
+ |------|------|---------|-------------|
94
+ | `endpoint` | `string` | — | API endpoint for useXCrud |
95
+ | `columns` | `Array` | **required** | Column definitions |
96
+ | `rowKey` | `string` | `'id'` | Row identifier key |
97
+ | `crud` | `Object` | — | Inject existing crud instance |
98
+ | `config` | `Object` | see below | Grouped configuration |
99
+ | `emptyState` | `Object` | `{ title: 'No data' }` | Empty state config |
100
+ | `initialFilters` | `Object` | `{}` | Initial filter values |
101
+
102
+ **Config structure:**
103
+
104
+ ```ts
105
+ {
106
+ toolbar: {
107
+ search: true,
108
+ searchPlaceholder: 'Search...',
109
+ columns: true, // show column toggle
110
+ export: true,
111
+ exportFilename: 'export'
112
+ },
113
+ actions: {
114
+ create: true,
115
+ createLabel: 'Create',
116
+ view: true,
117
+ edit: true,
118
+ delete: true,
119
+ viewRoute: '/users/:id', // auto-navigate
120
+ editRoute: '/users/:id/edit'
121
+ },
122
+ selection: { enabled: false },
123
+ pagination: { enabled: true, pageSize: 20 }
124
+ }
125
+ ```
126
+
127
+ #### Events
128
+
129
+ | Event | Payload | Description |
130
+ |-------|---------|-------------|
131
+ | `create` | — | Create button clicked |
132
+ | `view` | `row` | View action (if no viewRoute) |
133
+ | `edit` | `row` | Edit action (if no editRoute) |
134
+ | `delete` | `row` | After successful delete |
135
+ | `select` | `rows[]` | Selection changed |
136
+ | `row-click` | `row` | Row clicked |
137
+
138
+ #### Slots
139
+
140
+ | Slot | Description |
141
+ |------|-------------|
142
+ | `toolbar-left` | Left side of toolbar |
143
+ | `toolbar-right` | Right side of toolbar |
144
+ | `toolbar-actions` | After the create button |
145
+ | `row-actions` | Override row action buttons |
146
+ | `extra-actions` | Additional action buttons per row |
147
+ | `empty` | Empty state content |
148
+
149
+ ---
150
+
151
+ ## Sub-Components
152
+
153
+ ### `<VATableCellRenderer>`
154
+
155
+ Renders cell content based on `column.meta.preset`. Used automatically by VATable when columns have presets.
156
+
157
+ **Presets:**
158
+
159
+ | Preset | Description | Meta Options |
160
+ |--------|-------------|--------------|
161
+ | `text` | Plain text (default) | — |
162
+ | `email` | Clickable mailto link | — |
163
+ | `link` | External link with icon | — |
164
+ | `badge` | Colored status badge | `colorMap` |
165
+ | `boolean` | Check/X circle icon | — |
166
+ | `avatar` | Avatar image + optional name | `size`, `showName` |
167
+ | `date` | Formatted date | `format: 'date' \| 'time' \| 'datetime' \| 'relative'` |
168
+ | `currency` | Formatted currency | `currency`, `locale` |
169
+ | `number` | Formatted number | `decimals`, `locale` |
170
+
171
+ ### `<VATableActions>`
172
+
173
+ Row action buttons with optional route navigation.
174
+
175
+ ```vue
176
+ <VATableActions
177
+ :row="row"
178
+ :show-view="true"
179
+ :show-edit="true"
180
+ :show-delete="true"
181
+ view-route="/users/:id"
182
+ @view="onView"
183
+ @edit="onEdit"
184
+ @delete="onDelete"
185
+ />
186
+ ```
187
+
188
+ ### `<VATableActionColumn>`
189
+
190
+ Smart action column with primary actions + overflow dropdown menu.
191
+
192
+ ```vue
193
+ <VATableActionColumn
194
+ :row="row"
195
+ :show-view="true"
196
+ :show-edit="true"
197
+ :show-delete="true"
198
+ :max-visible="2"
199
+ :actions="[
200
+ { name: 'archive', label: 'Archive', icon: 'i-lucide-archive' }
201
+ ]"
202
+ />
203
+ ```
204
+
205
+ ### `<VATableToolbar>`
206
+
207
+ Standalone toolbar with search, filter chips, column toggle, and export. Used by VACrudTable.
208
+
209
+ ### `<VATableFilterBar>`
210
+
211
+ Filter input row with support for `select`, `text`, `date`, and `number` filter types.
212
+
213
+ ### `<VATableFilterChips>`
214
+
215
+ Displays active filters as animated chips with remove/clear buttons.
216
+
217
+ ### `<VATableColumnToggle>`
218
+
219
+ Popover with per-column visibility checkboxes, show all, and reset.
220
+
221
+ ### `<VATableExport>`
222
+
223
+ Dropdown button for CSV/JSON export with nested path and custom formatter support.
224
+
225
+ ### `<VATableEditableCell>`
226
+
227
+ Inline editing triggered by double-click. Supports text, email, number, textarea, select, and boolean input types.
228
+
229
+ ```vue
230
+ <VATableEditableCell
231
+ :value="row.name"
232
+ type="text"
233
+ :editable="true"
234
+ @save="(newVal, oldVal) => updateField(row.id, 'name', newVal)"
235
+ />
236
+ ```
237
+
238
+ ---
239
+
240
+ ## Composables
241
+
242
+ ### `useXATableColumns()`
243
+
244
+ Preset-based column factory. Returns column definitions with `meta.preset` that CellRenderer auto-renders.
245
+
246
+ ```ts
247
+ const { presets } = useXATableColumns()
248
+
249
+ const columns = [
250
+ presets.text('name', 'Name'),
251
+ presets.email('email'),
252
+ presets.badge('status', 'Status', {
253
+ colorMap: { active: 'success', pending: 'warning', inactive: 'neutral' }
254
+ }),
255
+ presets.date('createdAt', 'Created', { format: 'relative' }),
256
+ presets.currency('price', 'Price', { currency: 'USD' }),
257
+ presets.number('quantity', 'Qty', { decimals: 0 }),
258
+ presets.boolean('isActive', 'Active'),
259
+ presets.avatar('avatar', 'Photo', { size: 'sm', showName: true }),
260
+ presets.link('website', 'Website'),
261
+ presets.editable('notes', 'Notes', { inputType: 'textarea' }),
262
+ presets.actions({ showView: true, showEdit: true, showDelete: true }),
263
+ presets.custom('custom', 'Custom Column'),
264
+ ]
265
+ ```
266
+
267
+ ### `useDataTable(options?)`
268
+
269
+ State management composable for server-side table patterns. Manages pagination, sorting, filtering, selection, and builds API query params.
270
+
271
+ ```ts
272
+ const {
273
+ // Pagination
274
+ page, pageSize, total, totalPages, startIndex, endIndex,
275
+
276
+ // Sorting
277
+ sortKey, sortDirection, toggleSort, setSort, clearSort,
278
+
279
+ // Filtering
280
+ filters, searchQuery, activeFilters, setFilter, removeFilter, clearFilters,
281
+
282
+ // Selection
283
+ selectedRows, selectRow, deselectRow, toggleRowSelection,
284
+ selectAll, deselectAll, isRowSelected,
285
+
286
+ // API query
287
+ queryParams, // { page, limit, sort, search, ...filters }
288
+
289
+ // Reset
290
+ reset,
291
+ } = useDataTable({
292
+ defaultPageSize: 20,
293
+ defaultSortKey: 'createdAt',
294
+ defaultSortDirection: 'desc',
295
+ })
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Quick Start
301
+
302
+ ### Basic table
303
+
304
+ ```vue
305
+ <script setup>
306
+ const { data, pending } = await useFetch('/api/users')
307
+
308
+ const columns = [
309
+ { accessorKey: 'name', header: 'Name' },
310
+ { accessorKey: 'email', header: 'Email' },
311
+ { accessorKey: 'status', header: 'Status' },
312
+ ]
313
+ </script>
314
+
315
+ <template>
316
+ <VATable name="Users" :data="data" :columns="columns" :loading="pending" />
317
+ </template>
318
+ ```
319
+
320
+ ### Preset columns with actions
321
+
322
+ ```vue
323
+ <script setup>
324
+ const { presets } = useXATableColumns()
325
+
326
+ const columns = [
327
+ presets.text('name', 'Name'),
328
+ presets.email('email'),
329
+ presets.badge('status', 'Status'),
330
+ presets.date('createdAt', 'Created', { format: 'relative' }),
331
+ ]
332
+
333
+ const { data } = await useFetch('/api/users')
334
+ </script>
335
+
336
+ <template>
337
+ <VATable name="Users" :data="data" :columns="columns">
338
+ <template #actions-cell="{ row }">
339
+ <VATableActions
340
+ :row="row.original"
341
+ view-route="/users/:id"
342
+ @edit="openEdit"
343
+ @delete="confirmDelete"
344
+ />
345
+ </template>
346
+ </VATable>
347
+ </template>
348
+ ```
349
+
350
+ ### CRUD table (auto-fetches data)
351
+
352
+ ```vue
353
+ <script setup>
354
+ const columns = [
355
+ { accessorKey: 'name', header: 'Name' },
356
+ { accessorKey: 'email', header: 'Email' },
357
+ { accessorKey: 'status', header: 'Status', meta: { preset: 'badge' } },
358
+ { accessorKey: 'createdAt', header: 'Created' },
359
+ ]
360
+ </script>
361
+
362
+ <template>
363
+ <VACrudTable
364
+ endpoint="/api/users"
365
+ :columns="columns"
366
+ :config="{
367
+ actions: { viewRoute: '/users/:id', editRoute: '/users/:id/edit' }
368
+ }"
369
+ @create="navigateTo('/users/new')"
370
+ />
371
+ </template>
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Date Auto-Detection
377
+
378
+ VATable automatically detects date columns by accessorKey and applies relative time formatting. Recognized patterns:
379
+
380
+ `createdAt`, `updatedAt`, `deletedAt`, `publishedAt`, `startedAt`, `endedAt`, `dueDate`, `due_at`, `expiresAt`, `completedAt`, `created_at`, `updated_at`