nuxt-unified-ui 0.5.12 → 0.5.14

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.
@@ -4,14 +4,16 @@
4
4
 
5
5
  const props = defineProps({
6
6
  columns: Array,
7
+ ui: Object,
7
8
  loading: Boolean,
8
9
  data: Array,
9
10
  hidePagination: Boolean,
10
11
  totalItems: Number,
12
+ itemsPerPageItems: Array,
13
+ rowTo: Function,
14
+ stickyActions: Boolean,
11
15
  actions: Array,
12
16
  extraActions: Array,
13
- stickyActions: Boolean,
14
- ui: Object,
15
17
  meta: Object,
16
18
  });
17
19
 
@@ -66,14 +68,32 @@ const columnPinning = computed(() => {
66
68
 
67
69
  const tableUi = computed(() => {
68
70
  return {
69
- tr: 'data-[expanded=true]:bg-elevated!',
70
- ...(props.ui || {}),
71
+ ...(props.ui ?? {}),
72
+ tr: [
73
+ 'data-[expanded=true]:bg-elevated!',
74
+ props.rowTo ? 'cursor-pointer' : '',
75
+ (props.ui ?? {}).tr,
76
+ ].filter(Boolean).join(' '),
71
77
  };
72
78
  });
73
79
 
74
80
 
75
81
  /* actions */
76
82
 
83
+ const actionButtonOmitKeys = [
84
+ 'actionType',
85
+ 'vIf',
86
+ 'tooltip',
87
+ 'warning',
88
+ 'to',
89
+ 'href',
90
+ 'disabled',
91
+ 'onClick',
92
+ 'label',
93
+ 'items',
94
+ ];
95
+
96
+
77
97
  function resolveActionValue(value, row) {
78
98
  if (typeof value === 'function') {
79
99
  return value(row.original);
@@ -83,6 +103,23 @@ function resolveActionValue(value, row) {
83
103
  }
84
104
  }
85
105
 
106
+ function getActionWarning(action, row) {
107
+ return resolveActionValue(action.warning, row);
108
+ }
109
+
110
+ function getSplitActionItems(action, row) {
111
+ return (resolveActionValue(action.items, row) || []).map(it => {
112
+ return {
113
+ ...radOmit(it, [
114
+ 'onClick',
115
+ 'onSelect',
116
+ ]),
117
+ label: resolveActionValue(it.label, row),
118
+ onSelect: () => it.onSelect?.(row.original),
119
+ };
120
+ });
121
+ }
122
+
86
123
  function getExtraActionItems(row) {
87
124
  return (props.extraActions || [])
88
125
  .filter(it => !it.vIf || it.vIf(row.original))
@@ -92,6 +129,7 @@ function getExtraActionItems(row) {
92
129
  'vIf',
93
130
  'actionType',
94
131
  'tooltip',
132
+ 'warning',
95
133
  'to',
96
134
  'href',
97
135
  'disabled',
@@ -108,13 +146,20 @@ function getExtraActionItems(row) {
108
146
 
109
147
  /* pagination */
110
148
 
111
- const pageSizeItems = [
112
- 5,
113
- 10,
114
- 25,
115
- 50,
116
- 100,
117
- ];
149
+ const pageSizeItems = computed(() => {
150
+ if (props.itemsPerPageItems?.length) {
151
+ return props.itemsPerPageItems;
152
+ }
153
+ else {
154
+ return [
155
+ 5,
156
+ 10,
157
+ 25,
158
+ 50,
159
+ 100,
160
+ ];
161
+ }
162
+ });
118
163
 
119
164
  </script>
120
165
 
@@ -145,17 +190,61 @@ const pageSizeItems = [
145
190
  <template v-if="!action.vIf || action.vIf(row.original)">
146
191
 
147
192
  <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>
193
+ <div class="flex flex-col items-end gap-1.5">
194
+ <u-tooltip :text="resolveActionValue(action.tooltip, row)">
195
+ <span class="inline-flex">
196
+ <u-button
197
+ variant="subtle"
198
+ v-bind="radOmit(action, actionButtonOmitKeys)"
199
+ :label="resolveActionValue(action.label, row)"
200
+ :to="resolveActionValue(action.to, row)"
201
+ :href="resolveActionValue(action.href, row)"
202
+ :disabled="resolveActionValue(action.disabled, row)"
203
+ loading-auto
204
+ @click.stop="action.onClick?.(row.original)"
205
+ />
206
+ </span>
207
+ </u-tooltip>
208
+ <template v-if="getActionWarning(action, row)">
209
+ <span class="flex items-center gap-1.5 text-sm text-warning max-w-sm">
210
+ <u-icon
211
+ name="lucide:triangle-alert"
212
+ class="shrink-0 size-4"
213
+ />
214
+ <span>
215
+ {{ getActionWarning(action, row) }}
216
+ </span>
217
+ </span>
218
+ </template>
219
+ </div>
220
+ </template>
221
+
222
+ <template v-else-if="action.actionType === 'split'">
223
+ <div class="flex">
224
+ <u-tooltip :text="resolveActionValue(action.tooltip, row)">
225
+ <span class="inline-flex">
226
+ <u-button
227
+ variant="subtle"
228
+ v-bind="radOmit(action, actionButtonOmitKeys)"
229
+ class="rounded-e-none"
230
+ :label="resolveActionValue(action.label, row)"
231
+ :disabled="resolveActionValue(action.disabled, row)"
232
+ loading-auto
233
+ @click.stop="action.onClick?.(row.original)"
234
+ />
235
+ </span>
236
+ </u-tooltip>
237
+ <u-dropdown-menu :items="getSplitActionItems(action, row)">
238
+ <u-button
239
+ variant="subtle"
240
+ :color="action.color"
241
+ icon="lucide:chevron-down"
242
+ class="rounded-s-none"
243
+ :disabled="resolveActionValue(action.disabled, row)"
244
+ @click.stop
245
+ />
246
+ </u-dropdown-menu>
247
+ </div>
159
248
  </template>
160
249
 
161
250
  <template v-else-if="action.actionType === 'separator'">
@@ -181,6 +270,7 @@ const pageSizeItems = [
181
270
  <u-button
182
271
  variant="subtle"
183
272
  icon="lucide:ellipsis-vertical"
273
+ @click.stop
184
274
  />
185
275
  </u-dropdown-menu>
186
276
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-unified-ui",
3
3
  "type": "module",
4
- "version": "0.5.12",
4
+ "version": "0.5.14",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./index.d.ts",
7
7
  "exports": {
@@ -522,8 +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` → `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)
525
+ - Action objects (`:actions`, `:append-actions`, table row actions): `vIf` → `actionType` → `color` → `icon` → `label` → `tooltip` → `warning` → `disabled` → `to` → `href` → `onClick` → `items`
526
+ - `un-table`: `:columns` → `class` / `:ui` → `:loading` → `:data` → `hide-pagination` → `:total-items` → `:items-per-page-items` → `:row-to` → `v-model:items-per-page` → `v-model:current-page` → `sticky-actions` → `:actions` → `:extra-actions` → `:meta` (omit unused). Full table rules → [tables.md](tables.md)
527
527
  - Tab / select item objects: `value` → `icon` → `label`
528
528
 
529
529
  ### Default attribute values (omit noise)
@@ -12,7 +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
+ | `un-table` | `un-table.vue` | `u-table` + row actions (button / split / warning) + pagination footer → [tables.md](tables.md) |
16
16
 
17
17
  ## Composables
18
18
 
@@ -27,14 +27,16 @@ Do **not** add sort, filter, or selection props to `un-table`. Resource dashboar
27
27
  | `actions` | Visible row buttons; adds the trailing `actions` column |
28
28
  | `extraActions` | Overflow `u-dropdown-menu` (ellipsis); also creates the column |
29
29
  | `stickyActions` | Pin the `actions` column to the right |
30
- | `ui` | Merged into `u-table` `:ui` after the expanded-row `tr` class |
30
+ | `rowTo` | Adds `cursor-pointer` on rows; parent still owns navigation |
31
+ | `ui` | Merged into `u-table` `:ui`. Default `tr` classes (`data-[expanded=true]:bg-elevated!`, plus `cursor-pointer` when `rowTo` is set) are prepended to `ui.tr` |
31
32
  | `meta` | Passed through to `u-table` |
32
33
  | `v-model:itemsPerPage` | Page size (default `'25'`; choices 5 / 10 / 25 / 50 / 100) |
34
+ | `itemsPerPageItems` | Overrides the page-size select options |
33
35
  | `v-model:currentPage` | Page number (default `'1'`) |
34
36
 
35
37
  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
38
 
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`.
39
+ `to` / `href` / `disabled` / `label` / `tooltip` / `warning` on an action may be a value or `(row) => …`. `vIf(row)` hides the item. `onClick(row)` receives the original row. Action clicks use `@click.stop` so they do not select the row. Buttons use `loading-auto`.
38
40
 
39
41
  ## Attribute order on `<un-table>`
40
42
 
@@ -46,12 +48,14 @@ Omit unused props. When present, write them in this order:
46
48
  4. `:data`
47
49
  5. `hide-pagination`
48
50
  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`
51
+ 7. `:items-per-page-items`
52
+ 8. `:row-to`
53
+ 9. `v-model:items-per-page`
54
+ 10. `v-model:current-page`
55
+ 11. `sticky-actions`
56
+ 12. `:actions`
57
+ 13. `:extra-actions`
58
+ 14. `:meta`
55
59
 
56
60
  Models are always **page size, then page**. Closing `>` sits on the same line as the last attribute.
57
61
 
@@ -160,9 +164,19 @@ Two channels:
160
164
 
161
165
  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
166
 
167
+ `actionType` values on `actions`:
168
+
169
+ | `actionType` | UI |
170
+ |--------------|----|
171
+ | omitted / `'button'` | Inline `u-button` |
172
+ | `'split'` | Primary button plus a chevron `u-dropdown-menu` |
173
+ | `'separator'` | Vertical rule between button groups |
174
+
175
+ `warning` is a string or `(row) => string | undefined` rendered under the button (triangle + text). Split `items` may be an array or `(row) => array`. Each item uses `label` (value or `(row) => …`) and `onSelect(row)` — do not put `onClick` on split items.
176
+
163
177
  Field order on **every** action object (omit unused):
164
178
 
165
- `vIf` → `actionType` → `color` → `icon` → `label` → `tooltip` → `disabled` → `to` → `href` → `onClick`
179
+ `vIf` → `actionType` → `color` → `icon` → `label` → `tooltip` → `warning` → `disabled` → `to` → `href` → `onClick` → `items`
166
180
 
167
181
  ```js
168
182
  const itemActions = computed(() => {
@@ -180,11 +194,30 @@ const itemActions = computed(() => {
180
194
  {
181
195
  actionType: 'separator',
182
196
  },
197
+ {
198
+ actionType: 'split',
199
+ icon: 'lucide:download',
200
+ tooltip: 'Download',
201
+ onClick: handleItemDownloadFile,
202
+ items: [
203
+ {
204
+ icon: 'lucide:file-text',
205
+ label: 'Download file',
206
+ onSelect: handleItemDownloadFile,
207
+ },
208
+ {
209
+ icon: 'lucide:file-archive',
210
+ label: 'Download archive',
211
+ onSelect: handleItemDownloadArchive,
212
+ },
213
+ ],
214
+ },
183
215
  {
184
216
  vIf: it => it.status !== 'archived',
185
217
  color: 'error',
186
218
  icon: 'lucide:trash',
187
- tooltip: 'Delete',
219
+ tooltip: it => it.role === 'admin' ? 'Admins cannot be deleted' : 'Delete',
220
+ warning: it => it.role === 'admin' ? 'Admins cannot be deleted' : undefined,
188
221
  disabled: it => it.role === 'admin',
189
222
  onClick: handleItemDelete,
190
223
  },
@@ -216,8 +249,9 @@ Rules:
216
249
  - Destructive: `color: 'error'` before `icon`.
217
250
  - Emphasized extra action: `color: 'primary'` before `icon`.
218
251
  - `{ actionType: 'separator' }` is a lone-key object between visual groups in `actions`.
252
+ - `{ actionType: 'split', items, onClick }` is a default click plus overflow choices; item handlers are `onSelect`.
219
253
  - Omit `variant: 'subtle'` — that is the button default.
220
- - `vIf` / `disabled` / `to` / `href` take `(row) => …` when they depend on the row.
254
+ - `vIf` / `disabled` / `to` / `href` / `label` / `tooltip` / `warning` take `(row) => …` when they depend on the row.
221
255
  - Do not put toolbar Create / Refresh on the row. Those belong on the parent `un-card` (`:actions` / `:append-actions`).
222
256
 
223
257
  Resource managers prepend custom row actions, then default Edit / Delete:
@@ -251,6 +285,7 @@ const currentPage = ref(1);
251
285
  ```
252
286
 
253
287
  - 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.
288
+ - Page-size choices default to `5 / 10 / 25 / 50 / 100`. Pass `:items-per-page-items` to replace that list.
254
289
  - Server lists: `skip = (currentPage - 1) * itemsPerPage`, `limit = itemsPerPage`, `total-items` from the `/count` endpoint.
255
290
  - Client lists: pass `data` already sliced; `total-items` is the uncut length.
256
291
  - Reset `currentPage` to `1` when page size, filters, or the resource path change.