nuxt-unified-ui 0.5.11 → 0.5.12
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/app/assets/css/nuxt-ui-fixes.css +0 -11
- package/app/components/un-table.vue +215 -0
- package/app/elements/form-element-date.vue +3 -99
- package/i18n/locales/de.json +4 -0
- package/i18n/locales/en.json +4 -0
- package/package.json +5 -5
- package/skills/nuxt-unified-ui/SKILL.md +6 -3
- package/skills/nuxt-unified-ui/references/code-style.md +2 -1
- package/skills/nuxt-unified-ui/references/dialogs.md +1 -0
- package/skills/nuxt-unified-ui/references/public-surface.md +1 -0
- package/skills/nuxt-unified-ui/references/tables.md +303 -0
- package/skills/nuxt-unified-ui/references/toast-and-ui.md +4 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
/* interface */
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
columns: Array,
|
|
7
|
+
loading: Boolean,
|
|
8
|
+
data: Array,
|
|
9
|
+
hidePagination: Boolean,
|
|
10
|
+
totalItems: Number,
|
|
11
|
+
actions: Array,
|
|
12
|
+
extraActions: Array,
|
|
13
|
+
stickyActions: Boolean,
|
|
14
|
+
ui: Object,
|
|
15
|
+
meta: Object,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const currentPage = defineModel('currentPage', {
|
|
20
|
+
type: [
|
|
21
|
+
Number,
|
|
22
|
+
String,
|
|
23
|
+
],
|
|
24
|
+
default: '1',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const itemsPerPage = defineModel('itemsPerPage', {
|
|
28
|
+
type: [
|
|
29
|
+
Number,
|
|
30
|
+
String,
|
|
31
|
+
],
|
|
32
|
+
default: '25',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/* columns */
|
|
37
|
+
|
|
38
|
+
const hasActions = computed(() => {
|
|
39
|
+
return !!props.actions?.length || !!props.extraActions?.length;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const fullColumns = computed(() => {
|
|
43
|
+
return [
|
|
44
|
+
...(props.columns || []),
|
|
45
|
+
...(!hasActions.value ? [] : [
|
|
46
|
+
{
|
|
47
|
+
header: $t('un.table.actions'),
|
|
48
|
+
accessorKey: 'actions',
|
|
49
|
+
meta: {
|
|
50
|
+
class: {
|
|
51
|
+
th: 'text-end!',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
]),
|
|
56
|
+
];
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const columnPinning = computed(() => {
|
|
60
|
+
return {
|
|
61
|
+
right: props.stickyActions ? [
|
|
62
|
+
'actions',
|
|
63
|
+
] : undefined,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const tableUi = computed(() => {
|
|
68
|
+
return {
|
|
69
|
+
tr: 'data-[expanded=true]:bg-elevated!',
|
|
70
|
+
...(props.ui || {}),
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
/* actions */
|
|
76
|
+
|
|
77
|
+
function resolveActionValue(value, row) {
|
|
78
|
+
if (typeof value === 'function') {
|
|
79
|
+
return value(row.original);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getExtraActionItems(row) {
|
|
87
|
+
return (props.extraActions || [])
|
|
88
|
+
.filter(it => !it.vIf || it.vIf(row.original))
|
|
89
|
+
.map(it => {
|
|
90
|
+
return {
|
|
91
|
+
...radOmit(it, [
|
|
92
|
+
'vIf',
|
|
93
|
+
'actionType',
|
|
94
|
+
'tooltip',
|
|
95
|
+
'to',
|
|
96
|
+
'href',
|
|
97
|
+
'disabled',
|
|
98
|
+
'onClick',
|
|
99
|
+
]),
|
|
100
|
+
href: resolveActionValue(it.href, row),
|
|
101
|
+
to: resolveActionValue(it.to, row),
|
|
102
|
+
disabled: resolveActionValue(it.disabled, row),
|
|
103
|
+
onClick: () => it.onClick?.(row.original),
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
/* pagination */
|
|
110
|
+
|
|
111
|
+
const pageSizeItems = [
|
|
112
|
+
5,
|
|
113
|
+
10,
|
|
114
|
+
25,
|
|
115
|
+
50,
|
|
116
|
+
100,
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
<template>
|
|
123
|
+
<div>
|
|
124
|
+
|
|
125
|
+
<u-table
|
|
126
|
+
loading-color="neutral"
|
|
127
|
+
:columns="fullColumns"
|
|
128
|
+
:loading="props.loading"
|
|
129
|
+
:data="props.data || []"
|
|
130
|
+
:column-pinning="columnPinning"
|
|
131
|
+
:ui="tableUi"
|
|
132
|
+
:meta="props.meta">
|
|
133
|
+
|
|
134
|
+
<template v-for="(_, name) in $slots" #[name]="slotData">
|
|
135
|
+
<slot
|
|
136
|
+
:name="name"
|
|
137
|
+
v-bind="slotData"
|
|
138
|
+
/>
|
|
139
|
+
</template>
|
|
140
|
+
|
|
141
|
+
<template v-if="hasActions" #actions-cell="{ row }">
|
|
142
|
+
<div class="flex items-center justify-end gap-1">
|
|
143
|
+
|
|
144
|
+
<template v-for="(action, index) in props.actions" :key="index">
|
|
145
|
+
<template v-if="!action.vIf || action.vIf(row.original)">
|
|
146
|
+
|
|
147
|
+
<template v-if="!action.actionType || action.actionType === 'button'">
|
|
148
|
+
<u-tooltip :text="action.tooltip">
|
|
149
|
+
<u-button
|
|
150
|
+
variant="subtle"
|
|
151
|
+
v-bind="radOmit(action, [ 'actionType', 'vIf', 'tooltip', 'to', 'href', 'disabled', 'onClick' ])"
|
|
152
|
+
:to="resolveActionValue(action.to, row)"
|
|
153
|
+
:href="resolveActionValue(action.href, row)"
|
|
154
|
+
:disabled="resolveActionValue(action.disabled, row)"
|
|
155
|
+
loading-auto
|
|
156
|
+
@click="action.onClick?.(row.original)"
|
|
157
|
+
/>
|
|
158
|
+
</u-tooltip>
|
|
159
|
+
</template>
|
|
160
|
+
|
|
161
|
+
<template v-else-if="action.actionType === 'separator'">
|
|
162
|
+
<u-separator
|
|
163
|
+
orientation="vertical"
|
|
164
|
+
class="h-6 mx-1"
|
|
165
|
+
/>
|
|
166
|
+
</template>
|
|
167
|
+
|
|
168
|
+
</template>
|
|
169
|
+
</template>
|
|
170
|
+
|
|
171
|
+
<template v-if="!!props.extraActions?.length">
|
|
172
|
+
|
|
173
|
+
<template v-if="!!props.actions?.length">
|
|
174
|
+
<u-separator
|
|
175
|
+
orientation="vertical"
|
|
176
|
+
class="h-6 mx-1"
|
|
177
|
+
/>
|
|
178
|
+
</template>
|
|
179
|
+
|
|
180
|
+
<u-dropdown-menu :items="getExtraActionItems(row)">
|
|
181
|
+
<u-button
|
|
182
|
+
variant="subtle"
|
|
183
|
+
icon="lucide:ellipsis-vertical"
|
|
184
|
+
/>
|
|
185
|
+
</u-dropdown-menu>
|
|
186
|
+
|
|
187
|
+
</template>
|
|
188
|
+
|
|
189
|
+
</div>
|
|
190
|
+
</template>
|
|
191
|
+
|
|
192
|
+
</u-table>
|
|
193
|
+
|
|
194
|
+
<template v-if="!props.hidePagination">
|
|
195
|
+
<div class="flex items-center gap-2 p-3 border-t border-default">
|
|
196
|
+
<u-pagination
|
|
197
|
+
active-color="neutral"
|
|
198
|
+
:total="props.totalItems"
|
|
199
|
+
:items-per-page="Number(itemsPerPage)"
|
|
200
|
+
:page="Number(currentPage)"
|
|
201
|
+
@update:page="currentPage = $event"
|
|
202
|
+
/>
|
|
203
|
+
<div class="grow" />
|
|
204
|
+
<u-select
|
|
205
|
+
:items="pageSizeItems"
|
|
206
|
+
v-model="itemsPerPage"
|
|
207
|
+
/>
|
|
208
|
+
<span class="text-sm">
|
|
209
|
+
{{ $t('un.table.itemsPerPage') }}
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
</template>
|
|
213
|
+
|
|
214
|
+
</div>
|
|
215
|
+
</template>
|
|
@@ -41,19 +41,6 @@ const inputTitle = computed(() => {
|
|
|
41
41
|
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
const availableYears = computed(() => {
|
|
45
|
-
return Array.from({ length: 80 }, (_, i) => new Date().getFullYear() + 10 - i);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
function setMonthTo(month) {
|
|
50
|
-
internalModel.value = new CalendarDate(internalModel.value?.toDate()?.getFullYear(), month, internalModel.value?.toDate()?.getDate());
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function setYearTo(year) {
|
|
54
|
-
internalModel.value = new CalendarDate(year, internalModel.value?.toDate()?.getMonth() + 1, internalModel.value?.toDate()?.getDate());
|
|
55
|
-
}
|
|
56
|
-
|
|
57
44
|
</script>
|
|
58
45
|
|
|
59
46
|
|
|
@@ -70,93 +57,10 @@ function setYearTo(year) {
|
|
|
70
57
|
</u-form-field>
|
|
71
58
|
|
|
72
59
|
<template #content>
|
|
73
|
-
|
|
74
60
|
<u-calendar
|
|
75
|
-
class="
|
|
76
|
-
v-model="internalModel"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<template v-if="value?.split(' ')?.[0]">
|
|
80
|
-
<u-dropdown-menu
|
|
81
|
-
:items="[
|
|
82
|
-
{
|
|
83
|
-
label: 'January',
|
|
84
|
-
onClick: () => setMonthTo(1)
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
label: 'February',
|
|
88
|
-
onClick: () => setMonthTo(2)
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
label: 'March',
|
|
92
|
-
onClick: () => setMonthTo(3)
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
label: 'April',
|
|
96
|
-
onClick: () => setMonthTo(4),
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
label: 'May',
|
|
100
|
-
onClick: () => setMonthTo(5),
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
label: 'June',
|
|
104
|
-
onClick: () => setMonthTo(6),
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
label: 'July',
|
|
108
|
-
onClick: () => setMonthTo(7),
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
label: 'August',
|
|
112
|
-
onClick: () => setMonthTo(8),
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
label: 'September',
|
|
116
|
-
onClick: () => setMonthTo(9),
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
label: 'October',
|
|
120
|
-
onClick: () => setMonthTo(10),
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
label: 'November',
|
|
124
|
-
onClick: () => setMonthTo(11),
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
label: 'December',
|
|
128
|
-
onClick: () => setMonthTo(12),
|
|
129
|
-
},
|
|
130
|
-
]">
|
|
131
|
-
<u-button
|
|
132
|
-
variant="subtle"
|
|
133
|
-
size="sm"
|
|
134
|
-
:label="value?.split(' ')?.[0]"
|
|
135
|
-
/>
|
|
136
|
-
</u-dropdown-menu>
|
|
137
|
-
</template>
|
|
138
|
-
|
|
139
|
-
<template v-if="value?.split(' ')?.[1]">
|
|
140
|
-
<u-dropdown-menu
|
|
141
|
-
:items="availableYears.map(year => ({
|
|
142
|
-
label: String(year),
|
|
143
|
-
onClick: () => setYearTo(year),
|
|
144
|
-
}))"
|
|
145
|
-
:ui="{
|
|
146
|
-
content: 'max-h-40'
|
|
147
|
-
}">
|
|
148
|
-
<u-button
|
|
149
|
-
variant="subtle"
|
|
150
|
-
size="sm"
|
|
151
|
-
:label="value?.split(' ')?.[1]"
|
|
152
|
-
class="ms-1"
|
|
153
|
-
/>
|
|
154
|
-
</u-dropdown-menu>
|
|
155
|
-
</template>
|
|
156
|
-
|
|
157
|
-
</template>
|
|
158
|
-
</u-calendar>
|
|
159
|
-
|
|
61
|
+
class="p-2 [&_th]:text-center!"
|
|
62
|
+
v-model="internalModel"
|
|
63
|
+
/>
|
|
160
64
|
</template>
|
|
161
65
|
|
|
162
66
|
</u-popover>
|
package/i18n/locales/de.json
CHANGED
package/i18n/locales/en.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-unified-ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.12",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"types": "./index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@formkit/tempo": "1.1.0",
|
|
23
|
-
"@iconify-json/lucide": "1.2.
|
|
23
|
+
"@iconify-json/lucide": "1.2.135",
|
|
24
24
|
"@nuxt/kit": "4.5.2",
|
|
25
25
|
"@nuxt/ui": "4.11.1",
|
|
26
26
|
"@nuxtjs/i18n": "10.6.0",
|
|
27
27
|
"@types/canvas-confetti": "1.9.0",
|
|
28
|
-
"@vueuse/core": "
|
|
29
|
-
"@vueuse/nuxt": "
|
|
28
|
+
"@vueuse/core": "15.0.0",
|
|
29
|
+
"@vueuse/nuxt": "15.0.0",
|
|
30
30
|
"canvas-confetti": "1.9.4",
|
|
31
|
-
"radashi": "12.9.
|
|
31
|
+
"radashi": "12.9.5",
|
|
32
32
|
"unified-mongo-filter": "0.4.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
@@ -4,7 +4,8 @@ description: >-
|
|
|
4
4
|
Single skill for the nuxt-unified-ui Nuxt layer and mandatory Nuxt code style:
|
|
5
5
|
install/extend the layer, required CSS, modules/config, radashi radXxx
|
|
6
6
|
auto-imports, un-form / useForm, launchFormPickerDialog /
|
|
7
|
-
launchChoicePickerDialog, toast helpers,
|
|
7
|
+
launchChoicePickerDialog, toast helpers, un-card / un-typography / un-table
|
|
8
|
+
(attribute order, row actions, pagination), pages /
|
|
8
9
|
definePageMeta, page /* params */ and /* seo */ blocks, ufetch / useUFetch wrapping, unified resources
|
|
9
10
|
(server plugins, REST handleResource*, resource-manager dashboard /
|
|
10
11
|
customization), attribute order/defaults, and whitespace/formatting
|
|
@@ -24,7 +25,7 @@ This is the **only** installable skill in this repo. Deep topics live under `ref
|
|
|
24
25
|
## When to use
|
|
25
26
|
|
|
26
27
|
- Installing / extending `nuxt-unified-ui` as a Nuxt layer
|
|
27
|
-
- Using `un-form`, dialogs, toasts, `un-card`, radashi `radXxx`, etc.
|
|
28
|
+
- Using `un-form`, dialogs, toasts, `un-card`, `un-table`, radashi `radXxx`, etc.
|
|
28
29
|
- Declaring or customizing **unified resources** (plugins, REST, dashboard)
|
|
29
30
|
- **Whenever generating or editing Nuxt/Vue/server code** that must follow the unified look (whitespace, wrapping, template shape, sectioning)
|
|
30
31
|
|
|
@@ -44,6 +45,7 @@ This is the **only** installable skill in this repo. Deep topics live under `ref
|
|
|
44
45
|
| Dialogs / toasts / UI | [references/dialogs.md](references/dialogs.md) |
|
|
45
46
|
| Dialog implementation | [references/dialogs-impl.md](references/dialogs-impl.md) |
|
|
46
47
|
| Toast + `un-*` details | [references/toast-and-ui.md](references/toast-and-ui.md) |
|
|
48
|
+
| Tables (`un-table`) | [references/tables.md](references/tables.md) |
|
|
47
49
|
| Radashi `radXxx` catalog | [references/radashi.md](references/radashi.md) |
|
|
48
50
|
|
|
49
51
|
---
|
|
@@ -122,7 +124,7 @@ Published: `nuxt.config.ts`, `index.d.ts`, `app/`, `i18n/`, `modules/`.
|
|
|
122
124
|
|
|
123
125
|
| Path | Role |
|
|
124
126
|
|------|------|
|
|
125
|
-
| `app/components/` | `un-form`, `un-card`, `un-typography`, `un-spinner` |
|
|
127
|
+
| `app/components/` | `un-form`, `un-card`, `un-typography`, `un-spinner`, `un-table` |
|
|
126
128
|
| `app/composables/` | `useForm`, `useFormExtraElements` |
|
|
127
129
|
| `app/elements/` | Built-in form field renderers |
|
|
128
130
|
| `app/dialogs/` | Form / choice picker modal UIs |
|
|
@@ -144,6 +146,7 @@ From `nuxt.config.ts`: `@vueuse/nuxt`, `@nuxt/ui`, `@nuxtjs/i18n`; `ui.colorMode
|
|
|
144
146
|
| Confirm / choice | `launchChoicePickerDialog` + button `onClick` (avoid `value`) |
|
|
145
147
|
| Feedback | `toastSuccess` / `toastError` / `toastWarning` / `toastInfo` / `toast` |
|
|
146
148
|
| Page chrome | `un-typography` + `un-card` |
|
|
149
|
+
| Data table | `un-table` + parent-owned fetch / sort / filter → [tables.md](references/tables.md) |
|
|
147
150
|
| Custom field | `registerFormExtraElement` in a plugin |
|
|
148
151
|
| Utilities | `radXxx` → [radashi.md](references/radashi.md) |
|
|
149
152
|
| New page / route | [pages.md](references/pages.md) |
|
|
@@ -522,7 +522,8 @@ Practical shortcuts:
|
|
|
522
522
|
|
|
523
523
|
- `u-button`: `variant` → `color` → `size` → `icon` → label/value → `block` → `disabled` → `loading-auto` → events
|
|
524
524
|
- `u-input` / `u-select*`: user-facing props (`:placeholder`, `:label`) → transient state (`:loading`, `:disabled`) → data source (`:items`) → `class` → `v-model` → events
|
|
525
|
-
- Action objects (`:actions`, `:append-actions`, table row actions): `vIf` → `color` → `icon` → `label` → `tooltip` → `disabled` → `to` → `onClick`
|
|
525
|
+
- Action objects (`:actions`, `:append-actions`, table row actions): `vIf` → `actionType` → `color` → `icon` → `label` → `tooltip` → `disabled` → `to` → `href` → `onClick`
|
|
526
|
+
- `un-table`: `:columns` → `class` / `:ui` → `:loading` → `:data` → `hide-pagination` → `:total-items` → `v-model:items-per-page` → `v-model:current-page` → `sticky-actions` → `:actions` → `:extra-actions` → `:meta` (omit unused). Full table rules → [tables.md](tables.md)
|
|
526
527
|
- Tab / select item objects: `value` → `icon` → `label`
|
|
527
528
|
|
|
528
529
|
### Default attribute values (omit noise)
|
|
@@ -5,6 +5,7 @@ Overlay dialogs (Nuxt UI `useOverlay`), toast helpers, and layout primitives.
|
|
|
5
5
|
Deep refs:
|
|
6
6
|
- [dialogs-impl.md](dialogs-impl.md)
|
|
7
7
|
- [toast-and-ui.md](toast-and-ui.md)
|
|
8
|
+
- [tables.md](tables.md)
|
|
8
9
|
|
|
9
10
|
Follow [code-style.md](code-style.md) for all generated dialog/UI code.
|
|
10
11
|
|
|
@@ -12,6 +12,7 @@ Reference components in **kebab-case** in templates.
|
|
|
12
12
|
| `un-card` | `un-card.vue` | Card + typography + action rows |
|
|
13
13
|
| `un-typography` | `un-typography.vue` | Icon / title / subtitle / text / append |
|
|
14
14
|
| `un-spinner` | `un-spinner.vue` | Spinning Lucide loader icon |
|
|
15
|
+
| `un-table` | `un-table.vue` | `u-table` + row actions + pagination footer → [tables.md](tables.md) |
|
|
15
16
|
|
|
16
17
|
## Composables
|
|
17
18
|
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# `un-table`
|
|
2
|
+
|
|
3
|
+
`app/components/un-table.vue` wraps Nuxt UI `u-table` with an optional actions column and a pagination footer. It is a **presentational page of rows**. The parent owns fetching, sorting, filtering, and which slice of data is passed in.
|
|
4
|
+
|
|
5
|
+
Follow [code-style.md](code-style.md) for all generated table code. i18n: `$t` in both script and template (do not call `useI18n()`). Keys: `un.table.actions`, `un.table.itemsPerPage`.
|
|
6
|
+
|
|
7
|
+
## Mental model
|
|
8
|
+
|
|
9
|
+
| Belongs on `un-table` | Belongs on the parent |
|
|
10
|
+
|-----------------------|-----------------------|
|
|
11
|
+
| Columns, current page of `data`, loading flag | `useUFetch` / `ufetch`, query params |
|
|
12
|
+
| Footer page / page-size models | `skip` / `limit` (or client slice) from those models |
|
|
13
|
+
| Row `actions` / `extraActions` | Sort cycle, filter chips, toolbar |
|
|
14
|
+
| Slot forwarding | `{accessorKey}-header` / `-cell` contents |
|
|
15
|
+
|
|
16
|
+
Do **not** add sort, filter, or selection props to `un-table`. Resource dashboards put that above the table (`resource-explorer-table` pattern): filter bar + header slots, then `<un-table>`.
|
|
17
|
+
|
|
18
|
+
## Props
|
|
19
|
+
|
|
20
|
+
| Prop / model | Role |
|
|
21
|
+
|--------------|------|
|
|
22
|
+
| `columns` | TanStack / Nuxt UI column defs |
|
|
23
|
+
| `loading` | Passed to `u-table` (OR of list + count pending) |
|
|
24
|
+
| `data` | **Current page** of row objects, not the full collection |
|
|
25
|
+
| `hidePagination` | Drop the footer (dialogs, already-sliced lists) |
|
|
26
|
+
| `totalItems` | Count for `u-pagination` (server `/count` or `array.length`) |
|
|
27
|
+
| `actions` | Visible row buttons; adds the trailing `actions` column |
|
|
28
|
+
| `extraActions` | Overflow `u-dropdown-menu` (ellipsis); also creates the column |
|
|
29
|
+
| `stickyActions` | Pin the `actions` column to the right |
|
|
30
|
+
| `ui` | Merged into `u-table` `:ui` after the expanded-row `tr` class |
|
|
31
|
+
| `meta` | Passed through to `u-table` |
|
|
32
|
+
| `v-model:itemsPerPage` | Page size (default `'25'`; choices 5 / 10 / 25 / 50 / 100) |
|
|
33
|
+
| `v-model:currentPage` | Page number (default `'1'`) |
|
|
34
|
+
|
|
35
|
+
The actions column is added when **either** `actions` or `extraActions` has length. `#actions-cell` is then owned by the wrapper — do not override it.
|
|
36
|
+
|
|
37
|
+
`to` / `href` / `disabled` on an action may be a value or `(row) => …`. `vIf(row)` hides the item. `onClick(row)` receives the original row. Buttons use `loading-auto`.
|
|
38
|
+
|
|
39
|
+
## Attribute order on `<un-table>`
|
|
40
|
+
|
|
41
|
+
Omit unused props. When present, write them in this order:
|
|
42
|
+
|
|
43
|
+
1. `:columns`
|
|
44
|
+
2. `class` / `:ui` (presentation only)
|
|
45
|
+
3. `:loading`
|
|
46
|
+
4. `:data`
|
|
47
|
+
5. `hide-pagination`
|
|
48
|
+
6. `:total-items`
|
|
49
|
+
7. `v-model:items-per-page`
|
|
50
|
+
8. `v-model:current-page`
|
|
51
|
+
9. `sticky-actions`
|
|
52
|
+
10. `:actions`
|
|
53
|
+
11. `:extra-actions`
|
|
54
|
+
12. `:meta`
|
|
55
|
+
|
|
56
|
+
Models are always **page size, then page**. Closing `>` sits on the same line as the last attribute.
|
|
57
|
+
|
|
58
|
+
### Paged table
|
|
59
|
+
|
|
60
|
+
```vue
|
|
61
|
+
<un-table
|
|
62
|
+
:columns="columns"
|
|
63
|
+
:loading="isItemsLoading || isItemsCountLoading"
|
|
64
|
+
:data="itemsData"
|
|
65
|
+
:total-items="itemsCountData"
|
|
66
|
+
v-model:items-per-page="itemsPerPage"
|
|
67
|
+
v-model:current-page="currentPage"
|
|
68
|
+
:actions="itemActions"
|
|
69
|
+
:extra-actions="itemExtraActions">
|
|
70
|
+
|
|
71
|
+
<template #status-cell="{ row }">
|
|
72
|
+
...
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
</un-table>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Compact / dialog table
|
|
79
|
+
|
|
80
|
+
No footer, no page models, no `total-items`:
|
|
81
|
+
|
|
82
|
+
```vue
|
|
83
|
+
<un-card
|
|
84
|
+
:icon="icon"
|
|
85
|
+
:title="title"
|
|
86
|
+
fluid-body>
|
|
87
|
+
<un-table
|
|
88
|
+
:columns="columns"
|
|
89
|
+
:data="rows"
|
|
90
|
+
hide-pagination
|
|
91
|
+
:actions="tableActions"
|
|
92
|
+
/>
|
|
93
|
+
</un-card>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`class` / `:ui` stay immediately after `:columns` when a product theme overrides the table chrome. Do not invent extra layout props on `un-table` — put search / filter toolbars in the parent, above the table.
|
|
97
|
+
|
|
98
|
+
## Column defs
|
|
99
|
+
|
|
100
|
+
One object per column, **`accessorKey` then `header`**. Use `id` instead of `accessorKey` only when the cell is computed and there is no row field (duration, size, …).
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const columns = [
|
|
104
|
+
{
|
|
105
|
+
accessorKey: 'name',
|
|
106
|
+
header: $t('people.columnName'),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
accessorKey: 'status',
|
|
110
|
+
header: $t('people.columnStatus'),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'duration',
|
|
114
|
+
header: $t('people.columnDuration'),
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Keep column lists in the template when they are short and static. Move them to a script const / computed when they are reused or built from schema (`useResourceMeta` maps `key` → `accessorKey`, `radTitle(key)` → `header`).
|
|
120
|
+
|
|
121
|
+
## Slots
|
|
122
|
+
|
|
123
|
+
All `u-table` slots are forwarded.
|
|
124
|
+
|
|
125
|
+
| Slot | Use |
|
|
126
|
+
|------|-----|
|
|
127
|
+
| `{accessorKey}-cell` | Custom cell. Bind `{ row }` and read **`row.original`** |
|
|
128
|
+
| `{accessorKey}-header` | Custom header (sort button + filter popover) |
|
|
129
|
+
| `empty` | Empty state (`u-empty` or copy) |
|
|
130
|
+
| `actions-cell` | **Do not set** when `actions` / `extraActions` are passed |
|
|
131
|
+
|
|
132
|
+
```vue
|
|
133
|
+
<template #status-cell="{ row }">
|
|
134
|
+
<u-badge
|
|
135
|
+
variant="subtle"
|
|
136
|
+
:color="row.original.status === 'active' ? 'success' : undefined"
|
|
137
|
+
:label="radTitle(row.original.status)"
|
|
138
|
+
/>
|
|
139
|
+
</template>
|
|
140
|
+
|
|
141
|
+
<template v-for="column in columns" :key="column.accessorKey" #[column.accessorKey+'-cell']="{ row }">
|
|
142
|
+
<resource-explorer-cell
|
|
143
|
+
:column="column"
|
|
144
|
+
:row="row.original"
|
|
145
|
+
:data="row.original[column.accessorKey]"
|
|
146
|
+
/>
|
|
147
|
+
</template>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Status / role / type cells use `u-badge` (`variant="subtle"`, omit neutral `color`). Dates use `formatDate`. Several visual states → explicit `<template v-if>` / `v-else` badge variants, not nested ternaries.
|
|
151
|
+
|
|
152
|
+
## Row actions
|
|
153
|
+
|
|
154
|
+
Two channels:
|
|
155
|
+
|
|
156
|
+
| Prop | UI | Typical fields |
|
|
157
|
+
|------|----|----------------|
|
|
158
|
+
| `actions` | Inline icon (or labeled) buttons | `icon` + `tooltip`, or `label` |
|
|
159
|
+
| `extraActions` | Overflow menu | `icon` + `label` (menu items need text) |
|
|
160
|
+
|
|
161
|
+
Use `actions` for the one or two primary row operations (view, edit, delete). Use `extraActions` for the rest (assign, revoke, copy, archive). A vertical separator is inserted between the two groups automatically.
|
|
162
|
+
|
|
163
|
+
Field order on **every** action object (omit unused):
|
|
164
|
+
|
|
165
|
+
`vIf` → `actionType` → `color` → `icon` → `label` → `tooltip` → `disabled` → `to` → `href` → `onClick`
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
const itemActions = computed(() => {
|
|
169
|
+
return [
|
|
170
|
+
{
|
|
171
|
+
icon: 'lucide:eye',
|
|
172
|
+
tooltip: 'View',
|
|
173
|
+
onClick: handleItemView,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
icon: 'lucide:pencil',
|
|
177
|
+
tooltip: 'Edit',
|
|
178
|
+
onClick: handleItemUpdate,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
actionType: 'separator',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
vIf: it => it.status !== 'archived',
|
|
185
|
+
color: 'error',
|
|
186
|
+
icon: 'lucide:trash',
|
|
187
|
+
tooltip: 'Delete',
|
|
188
|
+
disabled: it => it.role === 'admin',
|
|
189
|
+
onClick: handleItemDelete,
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const itemExtraActions = computed(() => {
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
icon: 'lucide:copy',
|
|
198
|
+
label: 'Copy email',
|
|
199
|
+
onClick: handleItemCopyEmail,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
vIf: it => it.status !== 'archived',
|
|
203
|
+
icon: 'lucide:archive',
|
|
204
|
+
label: 'Archive',
|
|
205
|
+
onClick: handleItemArchive,
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Rules:
|
|
212
|
+
|
|
213
|
+
- Pass **handler references** (`onClick: handleItemUpdate`) unless the row must be adapted.
|
|
214
|
+
- Icon-only buttons: `icon` + `tooltip`, no `label`.
|
|
215
|
+
- Menu / text buttons: `label` (and `icon` when it helps).
|
|
216
|
+
- Destructive: `color: 'error'` before `icon`.
|
|
217
|
+
- Emphasized extra action: `color: 'primary'` before `icon`.
|
|
218
|
+
- `{ actionType: 'separator' }` is a lone-key object between visual groups in `actions`.
|
|
219
|
+
- Omit `variant: 'subtle'` — that is the button default.
|
|
220
|
+
- `vIf` / `disabled` / `to` / `href` take `(row) => …` when they depend on the row.
|
|
221
|
+
- Do not put toolbar Create / Refresh on the row. Those belong on the parent `un-card` (`:actions` / `:append-actions`).
|
|
222
|
+
|
|
223
|
+
Resource managers prepend custom row actions, then default Edit / Delete:
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
const resourceActions = computed(() => {
|
|
227
|
+
return [
|
|
228
|
+
...(props.resourceActions || []),
|
|
229
|
+
{
|
|
230
|
+
icon: 'lucide:pencil',
|
|
231
|
+
tooltip: 'Edit',
|
|
232
|
+
onClick: handleResourceUpdate,
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
color: 'error',
|
|
236
|
+
icon: 'lucide:trash',
|
|
237
|
+
tooltip: 'Delete',
|
|
238
|
+
onClick: handleResourceDelete,
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Pagination
|
|
245
|
+
|
|
246
|
+
Parent state:
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
const itemsPerPage = ref(20);
|
|
250
|
+
const currentPage = ref(1);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
- Layer default page size is `25` if the parent does not bind the model. Host pages often use `10` or `20` — set the ref explicitly.
|
|
254
|
+
- Server lists: `skip = (currentPage - 1) * itemsPerPage`, `limit = itemsPerPage`, `total-items` from the `/count` endpoint.
|
|
255
|
+
- Client lists: pass `data` already sliced; `total-items` is the uncut length.
|
|
256
|
+
- Reset `currentPage` to `1` when page size, filters, or the resource path change.
|
|
257
|
+
- After deletes, clamp `currentPage` if it is past the last page.
|
|
258
|
+
- `loading` should cover **both** the page request and the count request.
|
|
259
|
+
- `hide-pagination` still accepts `data` as the full in-memory list. Do not bind the page models just to hide them.
|
|
260
|
+
|
|
261
|
+
## Layout
|
|
262
|
+
|
|
263
|
+
Prefer an `un-card` with `fluid-body` so the table and footer are edge-to-edge:
|
|
264
|
+
|
|
265
|
+
```vue
|
|
266
|
+
<un-card
|
|
267
|
+
:title="`Manage ${titlePlural}`"
|
|
268
|
+
fluid-body
|
|
269
|
+
:append-actions="toolbarActions">
|
|
270
|
+
<un-table
|
|
271
|
+
:columns="columns"
|
|
272
|
+
:loading="isItemsLoading"
|
|
273
|
+
:data="itemsData"
|
|
274
|
+
:total-items="itemsCountData"
|
|
275
|
+
v-model:items-per-page="itemsPerPage"
|
|
276
|
+
v-model:current-page="currentPage"
|
|
277
|
+
:actions="itemActions"
|
|
278
|
+
/>
|
|
279
|
+
</un-card>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Search / filter chrome sits **inside** the card, **above** `<un-table>`, not on the table. `resource-explorer-table` is the reusable version of that stack (filter chips + sort headers + `<un-table>`).
|
|
283
|
+
|
|
284
|
+
`sticky-actions` is for wide column sets that scroll horizontally. Most pages do not need it.
|
|
285
|
+
|
|
286
|
+
## Do / don’t
|
|
287
|
+
|
|
288
|
+
**Do**
|
|
289
|
+
|
|
290
|
+
- Keep `<un-table>` dumb: current page of rows in, actions and slots out
|
|
291
|
+
- Follow the attribute order and action-field order above
|
|
292
|
+
- Use `$t` for user-facing strings; `$t` is legal in script computeds
|
|
293
|
+
- Forward cell work through `{key}-cell` and `row.original`
|
|
294
|
+
- Put Create / Refresh on the card, Edit / Delete on the row
|
|
295
|
+
|
|
296
|
+
**Don’t**
|
|
297
|
+
|
|
298
|
+
- Pass the full unpaged array while the footer is visible
|
|
299
|
+
- Override `#actions-cell`
|
|
300
|
+
- Implement sort / filter as `un-table` props
|
|
301
|
+
- Call `useI18n()` for `$t`
|
|
302
|
+
- Restate `variant: 'subtle'` on row actions
|
|
303
|
+
- Use `ghost` on row actions (Cancel-only rule still applies)
|