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 CHANGED
@@ -1,14 +1,16 @@
1
1
  # nuxt-ui-basekit
2
2
 
3
- Basis-Komponenten für Nuxt 4 auf [Nuxt UI](https://ui.nuxt.com) — als Nuxt-Layer.
4
- Tabellen, Reiter, Diagramme, Auswahl-Dialoge, Leerzustände: die Bausteine, die
5
- in jedem Verwaltungs-Frontend wieder anfallen und die Nuxt UI nicht mitbringt.
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
- Das Paket kennt keine Anwendung. Es hat keine Stores außer einem für die
8
- Rückfrage, keine Endpunkte, keine Übersetzungsschlüssel und keine Farben, die
9
- zu einer Marke gehören. Was es anzeigt, bekommt es hereingereicht.
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
- ## Einbinden
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` und `pinia` sind Peers, sie stehen im Konsumenten und
25
- werden nicht mitgeliefert. Die Komponenten sind danach global verfügbar, ohne
26
- Import.
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
- ## Was drin ist
38
+ ## What is in it
29
39
 
30
- | Gruppe | Komponenten |
40
+ | Group | Components |
31
41
  |---|---|
32
- | Struktur | `BaseKitTabs` · `BaseKitSettingRow` · `BaseKitEmptyState` · `BaseKitChoiceCard` · `BaseKitPending` |
33
- | Listen | `BaseKitDataTable` · `BaseKitStatTile` |
34
- | Auswahl | `BaseKitRecordPicker` · `BaseKitIconPicker` · `BaseKitFileUpload` |
35
- | Wege | `BaseKitBackLink` · `BaseKitViewLink` |
36
- | Rückfrage | `BaseKitConfirmModal` + `useConfirm()` |
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
- | Diagramme | `BaseKitChartBars` · `-Columns` · `-Donut` · `-Meter` · `-Figure` |
48
+ | Charts | `BaseKitChartBars` · `-Columns` · `-Donut` · `-Meter` · `-Figure` |
39
49
 
40
- Jede Komponente trägt ihre Erklärung im Kopf der Dateiwas sie tut, wann sie
41
- die richtige Wahl ist und wann nicht.
50
+ Each component carries its reasoning in the file headerwhat 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
- ## Beschriftungen und Sprache
54
+ ## Labels and language
44
55
 
45
- Die Komponenten rufen kein `vue-i18n` auf und kennen keine
46
- Übersetzungsschlüssel. Ohne Zutun rendern sie deutsche Voreinstellungen; wer
47
- eigene Texte oder mehrere Sprachen führt, reicht sie über ein Plugin herein:
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
- Zwei Dinge, die dabei Zeit kosten können:
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()` gehört nicht ins Plugin.** Es verlangt einen
71
- Komponenten-Setup-Kontext und wirft dort `MUST_BE_CALL_SETUP_TOP`
72
- (Fehlercode 26). Beim serverseitigen Rendern heißt das: 500 auf jeder Seite.
73
- Die Instanz kommt über `nuxtApp.$i18n`, und zwar erst beim Lesen des
74
- `computed` dann ist auch die Plugin-Reihenfolge gleichgültig.
75
- - **Einzelne Beschriftungen** bleiben als Prop überschreibbar. Die Tabelle
76
- trägt nur, was ohne Angabe herauskommt.
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
- ## Farben
92
+ ## Colours
79
93
 
80
- Die Komponenten greifen ausschließlich auf `--basekit-*` zu. Die
81
- Voreinstellungen stehen in `app/assets/css/basekit.css` und werden vom Layer
82
- geladen. Ein Projekt mit eigenem Theme legt sie in seinem eigenen Stylesheet
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(--meine-markenfarbe, #2563eb);
88
- --basekit-surface: var(--meine-kartenflaeche, #ffffff);
100
+ --basekit-accent: var(--my-brand-colour, #2563eb);
101
+ --basekit-surface: var(--my-card-surface, #ffffff);
89
102
  }
90
103
  ```
91
104
 
92
- Die fünf Diagrammfarben sind als Satz geprüftLichtheitsband, Chroma und
93
- Abstand zwischen Nachbarn, auch unter Farbfehlsichtigkeit. Wer sie
94
- überschreibt, sollte das als Satz tun und nicht einzeln; wer mehr als fünf
95
- Reihen hat, fasst zusammen, statt eine sechste Farbe zu erfinden.
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
- ## Entwickeln
110
+ ## Development
98
111
 
99
112
  ```bash
100
113
  npm install
101
- npx nuxi prepare # erzeugt .nuxt/tsconfig.json, sonst laufen die Tests nicht
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` hält fest, was das Paket nicht darf: keine Importe
107
- außerhalb des Pakets, keine Abhängigkeit, die nicht in der `package.json`
108
- steht, kein `vue-i18n`, keine Übersetzungsschlüssel, keine fremden
109
- CSS-Variablen. Der Test ist der Grund, warum sich das Paket überhaupt
110
- herausschneiden ließ ohne ihn wäre die Trennung nach dem dritten Feature
111
- wieder zu.
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
- Wer auf macOS oder Windows eine Abhängigkeit hinzufügt, muss das Lockfile
116
- anschließend einmal unter Linux/x64 nachziehensonst fehlen die
117
- plattformspezifischen Optional-Pakete (`@emnapi/*` und Verwandtschaft), und
118
- `npm ci` im Workflow bricht ab:
135
+ Adding a dependency on macOS or Windows means pulling the lockfile through
136
+ Linux/x64 afterwardsotherwise 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
- Die Architektur ist nicht gleichgültig: auf Apple Silicon läuft der Container
126
- ohne `--platform` als arm64, und dann fehlen genau die x64-Varianten, die der
127
- Runner braucht. Danach lokal einmal `npm ci`, das holt die eigenen Binaries
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
- Zwei Workflows unter `.github/workflows`. `pruefen` läuft auf `main` und in
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
- Eine neue Fassung:
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 # hebt die package.json und setzt den Tag
157
+ npm version patch # bumps package.json and sets the tag
142
158
  git push --follow-tags
143
159
  ```
144
160
 
145
- Der Job vergleicht den Tag mit der Zahl in der `package.json` und bricht ab,
146
- wenn die beiden auseinanderlaufen. Veröffentlicht wird mit `--provenance`:
147
- auf npmjs steht dann nachprüfbar, aus welchem Commit das Paket stammt.
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
- ## Herkunft
166
+ ## Origin
153
167
 
154
- Entstanden in einem Verwaltungs-Frontend und dort über Monate in Gebrauch,
155
- bevor es hier ein eigenes Paket wurde.
168
+ Grown inside an admin frontend and in use there for months before it became a
169
+ package of its own.
156
170
 
157
- ## Lizenz
171
+ ## License
158
172
 
159
- MIT — siehe [LICENSE](LICENSE).
173
+ MIT — see [LICENSE](LICENSE).
@@ -1,18 +1,18 @@
1
1
  /* ==========================================================================
2
- * BaseKit — Gestaltungs-Token
2
+ * BaseKit — design tokens
3
3
  *
4
- * Die Komponenten greifen ausschließlich auf `--basekit-*` zu und nie auf
5
- * Variablen einer bestimmten Anwendung. Hier stehen die Voreinstellungen.
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
- * Eine Anwendung mit eigenem Theme legt die Token an einer Stelle auf ihre
8
- * eigenen Variablenein Block in ihrem Stylesheet, geladen nach diesem:
7
+ * An application with its own theme maps the tokens onto its own variables in
8
+ * one placea block in its stylesheet, loaded after this one:
9
9
  *
10
10
  * :root {
11
- * --basekit-accent: var(--meine-markenfarbe, #2563eb);
12
- * --basekit-surface: var(--meine-kartenflaeche, #ffffff);
11
+ * --basekit-accent: var(--my-brand-colour, #2563eb);
12
+ * --basekit-surface: var(--my-card-surface, #ffffff);
13
13
  * }
14
14
  *
15
- * Fällt der Block weg, rendern die Komponenten in den Werten von hier weiter.
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
- * Diagramm-Farben (BaseKitChart*)
41
+ * Chart colours (BaseKitChart*)
42
42
  *
43
- * Bewusst **nicht** aus der Markenfarbe abgeleitet. Ein Theme darf jede
44
- * Akzentfarbe tragen vier davon nebeneinander in einer gestapelten Fläche
45
- * ergäben Reihen, die man nicht mehr auseinanderhält, und bei einer
46
- * Rot-Grün-Schwäche gar nichts mehr. Die fünf Reihenfarben stehen deshalb
47
- * fest und sind gegen die Kartenfläche geprüft (Lichtheitsband, Chroma,
48
- * CVD-Abstand ΔE ≥ 8 zwischen Nachbarn, Normalsicht ΔE ≥ 15).
43
+ * Deliberately **not** derived from the brand colour. A theme may carry any
44
+ * accentfour 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
- * Reihenfolge ist Teil der Prüfung: wer sie umsortiert oder eine sechste
51
- * Farbe dazustellt, muss neu messen. Über fünf Reihen fasst die Übersicht
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
- * Ein Kundenprojekt darf die Werte überschreibendann aber bitte als
55
- * geprüften Satz und nicht einzeln.
53
+ * A project may override the valuesas 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
- /* Die Fläche, in der ein Diagramm sitzt. Trägt die 2-px-Lücke zwischen
65
- * gestapelten Segmenten und den Ring um Punkte deshalb muss sie der
66
- * Kartenfarbe entsprechen und nicht „irgendwie weiß" sein. */
63
+ /* The surface a chart sits on. It carries the 2px gap between stacked
64
+ * segments and the ring around pointswhich 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
- /* Zurückgenommene Reihe: Kontext, nicht Aussage. */
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
- * Rücksprung von einer Detailseite zur zugehörigen Liste.
3
+ * The way back from a detail page to its list.
4
4
  *
5
- * Steht immer an derselben Stelle: oben rechts im Seitenkopf. Vorher hatte
6
- * das jede Seite anders gelöst mal ein Knopf über der Überschrift, mal ein
7
- * kleiner Link darüber, mal ein Knopf rechts im Kopf, und beschriftet war er
8
- * mal mit dem Namen der Liste („Rubriken"), mal mit „Zurück". Wer zwischen
9
- * zwei Bereichen wechselt, sucht den Weg zurück dann jedes Mal neu.
5
+ * Always in the same place: top right of the page header. Before this, every
6
+ * page solved it differentlysometimes 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
- * Die Beschriftung ist deshalb fest und nennt nicht das Ziel, sondern die
12
- * Richtung. `label` überschreibt sie für die Fälle, in denen es nicht zu
13
- * einer Liste zurückgeht, sondern zum übergeordneten Datensatz.
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
- * Auswahl-Karte Pattern für „aus einer Handvoll Varianten eine wählen“,
4
- * wenn die Varianten sich zeigen lassen (Themes, Layouts, Vorlagen).
5
- * Ein Dropdown zeigt nur Namen; hier steht die Vorschau daneben.
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
- * │ │ ← Slot `preview` (stilisierte Vorschau)
9
- * │ Vorschau
8
+ * │ │ ← `preview` slot (a stylised rendering)
9
+ * │ Preview
10
10
  * ├──────────────────────────┤
11
- * │ Titel [ Aktiv ] │ ← aktiv: Badge, sonst Knopf „Übernehmen“
12
- * │ Beschreibung
11
+ * │ Title [ Active ] │ ← active: a badge, otherwise an Apply button
12
+ * │ Description
13
13
  * └──────────────────────────┘
14
14
  *
15
- * Verwendung (Karten in einem Grid, eine pro Variante):
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><MeineVorschau :id="o.id" /></template>
28
+ * <template #preview><MyPreview :id="o.id" /></template>
29
29
  * </BaseKitChoiceCard>
30
30
  * </div>
31
31
  *
32
- * Die Karte selbst löst nichts aus gewählt wird über den Knopf. Bei einer
33
- * Auswahl, die sofort für alle gilt, ist ein Klick daneben sonst schnell
34
- * passiert.
32
+ * The card itself triggers nothingthe 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 der Variante. */
37
+ /** Name of the variant. */
38
38
  title: string
39
- /** Kurze Erläuterung unter dem Titel. */
39
+ /** A short explanation below the title. */
40
40
  description?: string
41
- /** Diese Variante ist die aktive. */
41
+ /** This variant is the active one. */
42
42
  active?: boolean
43
- /** Läuft gerade ein Speichervorgang (sperrt den Knopf). */
43
+ /** A save is in flight disables the button. */
44
44
  pending?: boolean
45
- /** Beschriftung des Knopfes für „diese Variante übernehmen“. */
45
+ /** Label of the button that applies this variant. */
46
46
  applyLabel?: string
47
- /** Beschriftung des Aktiv-Kennzeichens. */
47
+ /** Label of the active badge. */
48
48
  activeLabel?: string
49
- /** Seitenverhältnis der Vorschaufläche. */
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
- * Global gemountetes Bestätigungs-Modal wird vom `basekit:confirm`-Store
4
- * gesteuert (siehe `useConfirm()`). Genau eine Instanz pro Layout genügt,
5
- * sie beantwortet alle Abfragen.
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
- * Ersetzt native `window.confirm`-Dialoge: gethemt, dark-mode-fähig,
8
- * destruktive Aktionen als roter Bestätigen-Button. Schließen ohne Klick
9
- * (Escape/Backdrop) zählt als Abbruch.
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
- // Backdrop-/Escape-Schließen ohne Bestätigung Abbruch.
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
- * Wiederverwendbare Admin-Tabelle: sortierbare Spalten, Textfilter und ein
8
- * „Anlegen"-Button oben rechts. Client-seitig (Daten werden übergeben).
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` definiert Spalten; `sortable` macht den Kopf klickbar.
11
- * - Zellen rendern per Default `row[key]`; überschreibbar via Slot
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
- * - Zeilen-Aktionen über den Slot `#actions="{ row }"` (rechte Spalte).
14
- * - `row-link` macht die Namensspalte anklickbar: die Funktion bekommt die
15
- * Zeile und gibt ihr Ziel zurück (oder `null`, wenn diese Zeile keins hat).
16
- * Welche Spalte den Link trägt, sagt `link-column` ohne Angabe die erste.
17
- * - Zusätzliche Filter über den Slot `#toolbar` (rechts neben der Suche).
18
- * - „Anlegen": `create-label` + `@create` rendert den Button oben rechts.
19
- * - Paginierung client-seitig: Seitengröße über `page-size` /
20
- * `page-size-options` (Default 25; Auswahl 10/25/50/100/250/Alle).
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
- * Generisch über den Zeilentyp `T` — Slots liefern `row` typisiert zurück.
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
- /** Seitengröße: feste Zeilenzahl oder `'all'` für „ohne Limit". */
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
- /** Felder, die die Suche durchsucht (Default: alle Spalten-Keys). */
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
- /** Anfangs gewählte Seitengröße. */
46
+ /** Page size selected initially. */
47
47
  pageSize?: BaseKitPageSize
48
- /** Auswahlmöglichkeiten für die Seitengröße. */
48
+ /** The page sizes on offer. */
49
49
  pageSizeOptions?: BaseKitPageSize[]
50
50
  /**
51
- * Ziel je Zeile. Gesetzt, macht es den Namen anklickbarder Weg, den man
52
- * zuerst probiert. Die Aktionsspalte bleibt trotzdem: sie zeigt, was es
53
- * außer „öffnen" noch gibt.
51
+ * Target per row. Set, it makes the name clickablethe 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
- /** Spalte, die den Link trägt. Ohne Angabe die erste. */
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
- /** Die verlinkte Spalteexplizit gesetzt oder die erste. */
78
+ /** The linked columnexplicitly 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
- /** Wert einer Zelleinternes String-Indexing über den generischen Zeilentyp. */
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
- // — Paginierung ------------------------------------------------------------
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
- // Suche oder Seitengröße geändert zurück auf Seite 1.
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
- // Datenbestand geschrumpft (z. B. Filter) Seite in gültigen Bereich klemmen.
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: Suche links, Filter/Anlegen rechts -->
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
- <!-- Zustände -->
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
- <!-- Tabelle -->
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
- <!-- Fußzeile: Seitengröße links, Bereich + Blättern rechts -->
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"