nuxt-ui-basekit 0.1.1 → 0.2.1
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/README.md +93 -79
- package/app/assets/css/basekit.css +23 -24
- package/app/components/BaseKitBackLink.vue +9 -9
- package/app/components/BaseKitChoiceCard.vue +19 -19
- package/app/components/BaseKitConfirmModal.vue +7 -7
- package/app/components/BaseKitDataTable.vue +30 -30
- package/app/components/BaseKitEmptyState.vue +7 -7
- package/app/components/BaseKitFileUpload.vue +5 -5
- package/app/components/BaseKitIconPicker.vue +8 -8
- package/app/components/BaseKitMarkdownEditor.vue +11 -9
- package/app/components/BaseKitPending.vue +15 -16
- package/app/components/BaseKitRecordPicker.vue +14 -14
- package/app/components/BaseKitSettingRow.vue +15 -14
- package/app/components/BaseKitStatTile.vue +8 -8
- package/app/components/BaseKitTabs.vue +33 -34
- package/app/components/BaseKitViewLink.vue +14 -14
- package/app/components/charts/BaseKitChartBars.vue +8 -9
- package/app/components/charts/BaseKitChartColumns.vue +30 -31
- package/app/components/charts/BaseKitChartDonut.vue +18 -18
- package/app/components/charts/BaseKitChartFigure.vue +10 -10
- package/app/components/charts/BaseKitChartMeter.vue +4 -4
- package/app/composables/useBaseKit.ts +68 -68
- package/app/composables/useChartPalette.ts +14 -14
- package/app/composables/useChartWidth.ts +10 -10
- package/app/composables/useConfirm.ts +3 -3
- package/app/stores/confirm.ts +16 -16
- package/app/utils/html-to-markdown.ts +14 -16
- package/app/utils/markdown.ts +8 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# nuxt-ui-basekit
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
in
|
|
3
|
+
Base components for Nuxt 4 on top of [Nuxt UI](https://ui.nuxt.com) — as a Nuxt
|
|
4
|
+
layer. Tables, tabs, charts, pickers, empty states: the parts that come up again
|
|
5
|
+
in every admin frontend and that Nuxt UI does not ship.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
The package knows nothing about your application. It has no stores beyond the
|
|
8
|
+
one behind the confirmation dialog, no endpoints, no translation keys and no
|
|
9
|
+
colours that belong to a brand. Whatever it displays is handed in.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**[→ Every component with a screenshot](https://github.com/McGo/nuxt-ui-basekit/blob/main/COMPONENTS.md)**
|
|
12
|
+
|
|
13
|
+
## Install
|
|
12
14
|
|
|
13
15
|
```bash
|
|
14
16
|
npm i -D nuxt-ui-basekit
|
|
@@ -21,30 +23,39 @@ export default defineNuxtConfig({
|
|
|
21
23
|
})
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
`@nuxt/ui`, `nuxt`
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
`@nuxt/ui`, `nuxt` and `pinia` are peers — they live in the consumer and are not
|
|
27
|
+
bundled along. The components are globally available afterwards, no import.
|
|
28
|
+
|
|
29
|
+
Nuxt UI itself still needs its stylesheet, the same as in any project that uses
|
|
30
|
+
it:
|
|
31
|
+
|
|
32
|
+
```css
|
|
33
|
+
/* app/assets/css/main.css, referenced from nuxt.config.ts */
|
|
34
|
+
@import "tailwindcss";
|
|
35
|
+
@import "@nuxt/ui";
|
|
36
|
+
```
|
|
27
37
|
|
|
28
|
-
##
|
|
38
|
+
## What is in it
|
|
29
39
|
|
|
30
|
-
|
|
|
40
|
+
| Group | Components |
|
|
31
41
|
|---|---|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
42
|
+
| Structure | `BaseKitTabs` · `BaseKitSettingRow` · `BaseKitEmptyState` · `BaseKitChoiceCard` · `BaseKitPending` |
|
|
43
|
+
| Lists | `BaseKitDataTable` · `BaseKitStatTile` |
|
|
44
|
+
| Pickers | `BaseKitRecordPicker` · `BaseKitIconPicker` · `BaseKitFileUpload` |
|
|
45
|
+
| Navigation | `BaseKitBackLink` · `BaseKitViewLink` |
|
|
46
|
+
| Confirmation | `BaseKitConfirmModal` + `useConfirm()` |
|
|
37
47
|
| Text | `BaseKitMarkdownEditor` |
|
|
38
|
-
|
|
|
48
|
+
| Charts | `BaseKitChartBars` · `-Columns` · `-Donut` · `-Meter` · `-Figure` |
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
Each component carries its reasoning in the file header — what it does, when it
|
|
51
|
+
is the right choice and when it is not. [COMPONENTS.md](https://github.com/McGo/nuxt-ui-basekit/blob/main/COMPONENTS.md) has the
|
|
52
|
+
short version of each, with a screenshot.
|
|
42
53
|
|
|
43
|
-
##
|
|
54
|
+
## Labels and language
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
The components never call `vue-i18n` and know no translation keys. Left alone
|
|
57
|
+
they render English defaults; anyone with their own wording or several
|
|
58
|
+
languages hands them in through a plugin:
|
|
48
59
|
|
|
49
60
|
```ts
|
|
50
61
|
// plugins/basekit.ts
|
|
@@ -65,95 +76,98 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
65
76
|
})
|
|
66
77
|
```
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
`locale` is a BCP-47 tag and drives `Intl` — thousands separators, percentages
|
|
80
|
+
and date formats in the charts follow it.
|
|
81
|
+
|
|
82
|
+
Two things that can cost an afternoon:
|
|
69
83
|
|
|
70
|
-
- **`useI18n()`
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- **
|
|
76
|
-
|
|
84
|
+
- **`useI18n()` does not belong in the plugin.** It requires a component setup
|
|
85
|
+
context and throws `MUST_BE_CALL_SETUP_TOP` (error 26) there. Under server-side
|
|
86
|
+
rendering that means a 500 on every page. Take the instance from
|
|
87
|
+
`nuxtApp.$i18n`, and only when the `computed` is read — then plugin order
|
|
88
|
+
stops mattering too.
|
|
89
|
+
- **Individual labels stay overridable as props.** The table above only decides
|
|
90
|
+
what comes out when nothing is passed.
|
|
77
91
|
|
|
78
|
-
##
|
|
92
|
+
## Colours
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
darauf:
|
|
94
|
+
The components only ever reach for `--basekit-*`. The defaults live in
|
|
95
|
+
`app/assets/css/basekit.css` and are loaded by the layer. A project with its own
|
|
96
|
+
theme puts its values on top, in its own stylesheet:
|
|
84
97
|
|
|
85
98
|
```css
|
|
86
99
|
:root {
|
|
87
|
-
--basekit-accent: var(--
|
|
88
|
-
--basekit-surface: var(--
|
|
100
|
+
--basekit-accent: var(--my-brand-colour, #2563eb);
|
|
101
|
+
--basekit-surface: var(--my-card-surface, #ffffff);
|
|
89
102
|
}
|
|
90
103
|
```
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
The five chart colours are validated as a set — lightness band, chroma and the
|
|
106
|
+
distance between neighbours, colour vision deficiency included. Override them as
|
|
107
|
+
a set rather than one at a time; if you have more than five series, group them
|
|
108
|
+
instead of inventing a sixth colour.
|
|
96
109
|
|
|
97
|
-
##
|
|
110
|
+
## Development
|
|
98
111
|
|
|
99
112
|
```bash
|
|
100
113
|
npm install
|
|
101
|
-
npx nuxi prepare #
|
|
114
|
+
npx nuxi prepare # writes .nuxt/tsconfig.json, without it the tests fail
|
|
102
115
|
npm test
|
|
103
116
|
npm run lint # nuxi typecheck
|
|
104
117
|
```
|
|
105
118
|
|
|
106
|
-
`tests/grenze.test.ts`
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
`tests/grenze.test.ts` pins down what the package is not allowed to do: no
|
|
120
|
+
imports from outside the package, no dependency that is missing from
|
|
121
|
+
`package.json`, no `vue-i18n`, no translation keys, no foreign CSS variables.
|
|
122
|
+
That test is the reason the package could be carved out at all — without it the
|
|
123
|
+
separation would have closed up again by the third feature.
|
|
124
|
+
|
|
125
|
+
`playground/` is a small Nuxt app that pulls the layer in the way a consumer
|
|
126
|
+
would. It is the place to look at a component, and the source of the
|
|
127
|
+
screenshots in [COMPONENTS.md](https://github.com/McGo/nuxt-ui-basekit/blob/main/COMPONENTS.md):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
cd playground && npm install && npm run dev
|
|
131
|
+
```
|
|
112
132
|
|
|
113
133
|
### Lockfile
|
|
114
134
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
`npm ci` im Workflow bricht ab:
|
|
135
|
+
Adding a dependency on macOS or Windows means pulling the lockfile through
|
|
136
|
+
Linux/x64 afterwards — otherwise the platform-specific optional packages
|
|
137
|
+
(`@emnapi/*` and relatives) are missing and `npm ci` fails in the workflow:
|
|
119
138
|
|
|
120
139
|
```bash
|
|
121
140
|
docker run --rm --platform linux/amd64 -v "$PWD:/app" -w /app \
|
|
122
141
|
node:24 npm install --package-lock-only
|
|
123
142
|
```
|
|
124
143
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
zurück.
|
|
129
|
-
|
|
130
|
-
## Veröffentlichen
|
|
144
|
+
The architecture matters: on Apple Silicon the container runs as arm64 without
|
|
145
|
+
`--platform`, and then exactly the x64 variants the runner needs are still
|
|
146
|
+
missing. Run `npm ci` locally afterwards to get your own binaries back.
|
|
131
147
|
|
|
132
|
-
|
|
133
|
-
jedem Pull Request und führt `nuxi prepare`, `nuxi typecheck` und die Tests
|
|
134
|
-
aus. `veroeffentlichen` hängt an einem Tag `vX.Y.Z`, prüft noch einmal und
|
|
135
|
-
schiebt das Paket dann nach npmjs.com. Ein Build-Schritt fehlt, weil der Layer
|
|
136
|
-
als Rohquelle ausgeliefert wird und Nuxt ihn im Konsumenten übersetzt.
|
|
148
|
+
## Releasing
|
|
137
149
|
|
|
138
|
-
|
|
150
|
+
Two workflows under `.github/workflows`. `pruefen` runs on `main` and on every
|
|
151
|
+
pull request and does `nuxi prepare`, `nuxi typecheck` and the tests.
|
|
152
|
+
`veroeffentlichen` hangs off a `vX.Y.Z` tag, checks again and publishes to
|
|
153
|
+
npmjs.com. There is no build step: the layer ships as raw source and Nuxt
|
|
154
|
+
compiles it in the consumer.
|
|
139
155
|
|
|
140
156
|
```bash
|
|
141
|
-
npm version patch #
|
|
157
|
+
npm version patch # bumps package.json and sets the tag
|
|
142
158
|
git push --follow-tags
|
|
143
159
|
```
|
|
144
160
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
Dafür braucht das Repo ein Secret `NPM_TOKEN` — ein Automation-Token aus dem
|
|
150
|
-
npm-Konto, einzutragen unter *Settings → Secrets and variables → Actions*.
|
|
161
|
+
The job compares the tag against the number in `package.json` and stops if they
|
|
162
|
+
disagree. Publishing runs over OIDC through npm's trusted publishing — no token
|
|
163
|
+
in the repository — and with `--provenance`, so npmjs records which commit the
|
|
164
|
+
package was built from.
|
|
151
165
|
|
|
152
|
-
##
|
|
166
|
+
## Origin
|
|
153
167
|
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
Grown inside an admin frontend and in use there for months before it became a
|
|
169
|
+
package of its own.
|
|
156
170
|
|
|
157
|
-
##
|
|
171
|
+
## License
|
|
158
172
|
|
|
159
|
-
MIT —
|
|
173
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
* BaseKit —
|
|
2
|
+
* BaseKit — design tokens
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The components only ever reach for `--basekit-*`, never for a variable
|
|
5
|
+
* belonging to one particular application. What follows are the defaults.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* An application with its own theme maps the tokens onto its own variables in
|
|
8
|
+
* one place — a block in its stylesheet, loaded after this one:
|
|
9
9
|
*
|
|
10
10
|
* :root {
|
|
11
|
-
* --basekit-accent: var(--
|
|
12
|
-
* --basekit-surface: var(--
|
|
11
|
+
* --basekit-accent: var(--my-brand-colour, #2563eb);
|
|
12
|
+
* --basekit-surface: var(--my-card-surface, #ffffff);
|
|
13
13
|
* }
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* Drop that block and the components keep rendering with the values here.
|
|
16
16
|
* ========================================================================== */
|
|
17
17
|
|
|
18
18
|
:root {
|
|
@@ -38,21 +38,20 @@
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/* --------------------------------------------------------------------------
|
|
41
|
-
*
|
|
41
|
+
* Chart colours (BaseKitChart*)
|
|
42
42
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
43
|
+
* Deliberately **not** derived from the brand colour. A theme may carry any
|
|
44
|
+
* accent — four shades of it side by side in a stacked area would give series
|
|
45
|
+
* nobody can tell apart, and under a red-green deficiency nothing at all. The
|
|
46
|
+
* five series colours are therefore fixed and validated against the card
|
|
47
|
+
* surface: lightness band, chroma, CVD distance ΔE ≥ 8 between neighbours,
|
|
48
|
+
* ΔE ≥ 15 for normal vision.
|
|
49
49
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* zusammen, statt eine Farbe zu erfinden.
|
|
50
|
+
* Order is part of that validation. Reordering them, or adding a sixth, means
|
|
51
|
+
* measuring again. Beyond five series, group them rather than invent a colour.
|
|
53
52
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* A project may override the values — as a validated set, though, not one at
|
|
54
|
+
* a time.
|
|
56
55
|
* ------------------------------------------------------------------------ */
|
|
57
56
|
:root {
|
|
58
57
|
--basekit-chart-1: #2a78d6;
|
|
@@ -61,13 +60,13 @@
|
|
|
61
60
|
--basekit-chart-4: #eda100;
|
|
62
61
|
--basekit-chart-5: #e87ba4;
|
|
63
62
|
|
|
64
|
-
/*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
63
|
+
/* The surface a chart sits on. It carries the 2px gap between stacked
|
|
64
|
+
* segments and the ring around points — which is why it has to match the
|
|
65
|
+
* card colour rather than being white-ish. */
|
|
67
66
|
--basekit-chart-surface: var(--ui-bg, #ffffff);
|
|
68
67
|
--basekit-chart-grid: rgb(0 0 0 / 8%);
|
|
69
68
|
--basekit-chart-axis: rgb(0 0 0 / 18%);
|
|
70
|
-
/*
|
|
69
|
+
/* Muted series: context, not a statement. */
|
|
71
70
|
--basekit-chart-muted: rgb(0 0 0 / 22%);
|
|
72
71
|
}
|
|
73
72
|
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* The way back from a detail page to its list.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Always in the same place: top right of the page header. Before this, every
|
|
6
|
+
* page solved it differently — sometimes a button above the heading, sometimes
|
|
7
|
+
* a small link, sometimes a button on the right, labelled either with the name
|
|
8
|
+
* of the list or with "Back". Anyone moving between two areas had to look for
|
|
9
|
+
* the way back anew each time.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The label is therefore fixed, and it names the direction rather than the
|
|
12
|
+
* target. `label` overrides it for the cases where the way back leads to a
|
|
13
|
+
* parent record instead of a list.
|
|
14
14
|
*/
|
|
15
15
|
import { computed } from 'vue'
|
|
16
16
|
import { useBaseKitLabels } from '../composables/useBaseKit'
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Pick one of a handful of variants, for the case where the variants can be
|
|
4
|
+
* shown — themes, layouts, templates. A dropdown gives you names only; here
|
|
5
|
+
* the preview sits right next to the label.
|
|
6
6
|
*
|
|
7
7
|
* ┌──────────────────────────┐
|
|
8
|
-
* │ │ ←
|
|
9
|
-
* │
|
|
8
|
+
* │ │ ← `preview` slot (a stylised rendering)
|
|
9
|
+
* │ Preview │
|
|
10
10
|
* ├──────────────────────────┤
|
|
11
|
-
* │
|
|
12
|
-
* │
|
|
11
|
+
* │ Title [ Active ] │ ← active: a badge, otherwise an Apply button
|
|
12
|
+
* │ Description │
|
|
13
13
|
* └──────────────────────────┘
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* Usage — cards in a grid, one per variant:
|
|
16
16
|
*
|
|
17
17
|
* <div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
18
18
|
* <BaseKitChoiceCard
|
|
@@ -25,28 +25,28 @@
|
|
|
25
25
|
* :pending="pending"
|
|
26
26
|
* @apply="apply(o.id)"
|
|
27
27
|
* >
|
|
28
|
-
* <template #preview><
|
|
28
|
+
* <template #preview><MyPreview :id="o.id" /></template>
|
|
29
29
|
* </BaseKitChoiceCard>
|
|
30
30
|
* </div>
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* The card itself triggers nothing — the button does the choosing. Where a
|
|
33
|
+
* choice takes effect for everyone at once, a stray click is otherwise made
|
|
34
|
+
* quickly.
|
|
35
35
|
*/
|
|
36
36
|
withDefaults(defineProps<{
|
|
37
|
-
/** Name
|
|
37
|
+
/** Name of the variant. */
|
|
38
38
|
title: string
|
|
39
|
-
/**
|
|
39
|
+
/** A short explanation below the title. */
|
|
40
40
|
description?: string
|
|
41
|
-
/**
|
|
41
|
+
/** This variant is the active one. */
|
|
42
42
|
active?: boolean
|
|
43
|
-
/**
|
|
43
|
+
/** A save is in flight — disables the button. */
|
|
44
44
|
pending?: boolean
|
|
45
|
-
/**
|
|
45
|
+
/** Label of the button that applies this variant. */
|
|
46
46
|
applyLabel?: string
|
|
47
|
-
/**
|
|
47
|
+
/** Label of the active badge. */
|
|
48
48
|
activeLabel?: string
|
|
49
|
-
/**
|
|
49
|
+
/** Aspect ratio of the preview area. */
|
|
50
50
|
ratio?: string
|
|
51
51
|
}>(), {
|
|
52
52
|
description: undefined,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* The globally mounted confirmation modal, driven by the `basekit:confirm`
|
|
4
|
+
* store — see `useConfirm()`. Exactly one instance per layout is enough; it
|
|
5
|
+
* answers every prompt.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Replaces native `window.confirm` dialogs: themed, dark-mode-capable, with a
|
|
8
|
+
* red confirm button for destructive actions. Closing without a click, by
|
|
9
|
+
* Escape or backdrop, counts as cancel.
|
|
10
10
|
*/
|
|
11
11
|
import { computed } from 'vue'
|
|
12
12
|
import { useBaseKitLabels } from '../composables/useBaseKit'
|
|
@@ -20,7 +20,7 @@ const opts = computed(() => store.options)
|
|
|
20
20
|
const open = computed({
|
|
21
21
|
get: () => store.open,
|
|
22
22
|
set: (value: boolean) => {
|
|
23
|
-
//
|
|
23
|
+
// Closing by backdrop or Escape without confirming means cancel.
|
|
24
24
|
if (!value) store.settle(false)
|
|
25
25
|
},
|
|
26
26
|
})
|
|
@@ -4,22 +4,22 @@ import { useBaseKitLabels } from '../composables/useBaseKit'
|
|
|
4
4
|
import { NuxtLink } from '#components'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* A reusable admin table: sortable columns, a text filter and a create button
|
|
8
|
+
* top right. Client-side throughout — the rows are handed in.
|
|
9
9
|
*
|
|
10
|
-
* - `columns`
|
|
11
|
-
* -
|
|
10
|
+
* - `columns` defines the columns; `sortable` makes a header clickable.
|
|
11
|
+
* - Cells render `row[key]` by default, overridable through the slot
|
|
12
12
|
* `#cell-<key>="{ row, value }"`.
|
|
13
|
-
* -
|
|
14
|
-
* - `row-link`
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
13
|
+
* - Row actions go into the slot `#actions="{ row }"`, the rightmost column.
|
|
14
|
+
* - `row-link` makes the name column clickable: the function receives the row
|
|
15
|
+
* and returns its target, or `null` where that row has none. Which column
|
|
16
|
+
* carries the link is `link-column`; without it, the first.
|
|
17
|
+
* - Extra filters go into the slot `#toolbar`, right of the search field.
|
|
18
|
+
* - `create-label` plus `@create` renders the button top right.
|
|
19
|
+
* - Pagination is client-side: `page-size` and `page-size-options` (default
|
|
20
|
+
* 25, offering 10/25/50/100/250/all).
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* Generic over the row type `T` — slots hand `row` back typed.
|
|
23
23
|
*/
|
|
24
24
|
export interface BaseKitDataColumn {
|
|
25
25
|
key: string
|
|
@@ -29,7 +29,7 @@ export interface BaseKitDataColumn {
|
|
|
29
29
|
class?: string
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
32
|
+
/** Page size: a fixed number of rows, or `'all'` for no limit. */
|
|
33
33
|
export type BaseKitPageSize = number | 'all'
|
|
34
34
|
|
|
35
35
|
const props = withDefaults(defineProps<{
|
|
@@ -37,23 +37,23 @@ const props = withDefaults(defineProps<{
|
|
|
37
37
|
rows: T[]
|
|
38
38
|
rowKey?: string
|
|
39
39
|
searchable?: boolean
|
|
40
|
-
/**
|
|
40
|
+
/** Fields the search looks through. Defaults to every column key. */
|
|
41
41
|
searchKeys?: string[]
|
|
42
42
|
searchPlaceholder?: string
|
|
43
43
|
createLabel?: string
|
|
44
44
|
loading?: boolean
|
|
45
45
|
emptyLabel?: string
|
|
46
|
-
/**
|
|
46
|
+
/** Page size selected initially. */
|
|
47
47
|
pageSize?: BaseKitPageSize
|
|
48
|
-
/**
|
|
48
|
+
/** The page sizes on offer. */
|
|
49
49
|
pageSizeOptions?: BaseKitPageSize[]
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* Target per row. Set, it makes the name clickable — the route people try
|
|
52
|
+
* first. The actions column stays regardless: it shows what there is besides
|
|
53
|
+
* opening.
|
|
54
54
|
*/
|
|
55
55
|
rowLink?: (row: T) => string | null | undefined
|
|
56
|
-
/**
|
|
56
|
+
/** The column carrying the link. Without it, the first. */
|
|
57
57
|
linkColumn?: string
|
|
58
58
|
}>(), {
|
|
59
59
|
rowKey: 'id',
|
|
@@ -75,7 +75,7 @@ const sortDir = ref<'asc' | 'desc'>('asc')
|
|
|
75
75
|
|
|
76
76
|
const searchFields = computed(() => props.searchKeys ?? props.columns.map(c => c.key))
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/** The linked column — explicitly set, or the first. */
|
|
79
79
|
const linkKey = computed(() => props.linkColumn ?? props.columns[0]?.key ?? null)
|
|
80
80
|
|
|
81
81
|
function rowTarget(row: T, key: string): string | null {
|
|
@@ -84,7 +84,7 @@ function rowTarget(row: T, key: string): string | null {
|
|
|
84
84
|
return target || null
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
/**
|
|
87
|
+
/** Value of a cell — internal string indexing over the generic row type. */
|
|
88
88
|
function cell(row: T, key: string): unknown {
|
|
89
89
|
return (row as Record<string, unknown>)[key]
|
|
90
90
|
}
|
|
@@ -107,7 +107,7 @@ const displayed = computed<T[]>(() => {
|
|
|
107
107
|
return [...filtered.value].sort((a, b) => compare(cell(a, key), cell(b, key)) * dir)
|
|
108
108
|
})
|
|
109
109
|
|
|
110
|
-
// —
|
|
110
|
+
// — Pagination -------------------------------------------------------------
|
|
111
111
|
const page = ref(1)
|
|
112
112
|
const pageSize = ref<BaseKitPageSize>(props.pageSize)
|
|
113
113
|
|
|
@@ -136,12 +136,12 @@ const pageSizeItems = computed(() =>
|
|
|
136
136
|
})),
|
|
137
137
|
)
|
|
138
138
|
|
|
139
|
-
//
|
|
139
|
+
// Search or page size changed, so go back to page 1.
|
|
140
140
|
watch([search, pageSize], () => {
|
|
141
141
|
page.value = 1
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
-
//
|
|
144
|
+
// The data shrank, through a filter say, so clamp the page into range.
|
|
145
145
|
watch(totalPages, (pages) => {
|
|
146
146
|
if (page.value > pages) page.value = pages
|
|
147
147
|
})
|
|
@@ -176,7 +176,7 @@ function sortIcon(col: BaseKitDataColumn): string | null {
|
|
|
176
176
|
|
|
177
177
|
<template>
|
|
178
178
|
<div class="space-y-4">
|
|
179
|
-
<!-- Toolbar:
|
|
179
|
+
<!-- Toolbar: search on the left, filters and create on the right -->
|
|
180
180
|
<div v-if="searchable || $slots.toolbar || createLabel" class="flex flex-wrap items-center justify-between gap-3">
|
|
181
181
|
<UInput
|
|
182
182
|
v-if="searchable"
|
|
@@ -195,7 +195,7 @@ function sortIcon(col: BaseKitDataColumn): string | null {
|
|
|
195
195
|
</div>
|
|
196
196
|
</div>
|
|
197
197
|
|
|
198
|
-
<!--
|
|
198
|
+
<!-- States -->
|
|
199
199
|
<div v-if="loading" class="py-12 text-center text-muted">
|
|
200
200
|
<UIcon name="i-lucide-loader-2" class="size-6 animate-spin" />
|
|
201
201
|
</div>
|
|
@@ -209,7 +209,7 @@ function sortIcon(col: BaseKitDataColumn): string | null {
|
|
|
209
209
|
</slot>
|
|
210
210
|
</div>
|
|
211
211
|
|
|
212
|
-
<!--
|
|
212
|
+
<!-- Table -->
|
|
213
213
|
<table v-else class="w-full text-sm">
|
|
214
214
|
<thead class="border-b border-neutral-200 text-left text-muted dark:border-neutral-800">
|
|
215
215
|
<tr>
|
|
@@ -268,7 +268,7 @@ function sortIcon(col: BaseKitDataColumn): string | null {
|
|
|
268
268
|
</tbody>
|
|
269
269
|
</table>
|
|
270
270
|
|
|
271
|
-
<!--
|
|
271
|
+
<!-- Footer: page size on the left, range and paging on the right -->
|
|
272
272
|
<div
|
|
273
273
|
v-if="!loading && displayed.length"
|
|
274
274
|
class="flex flex-wrap items-center justify-between gap-3 pt-1 text-sm text-muted"
|