nuxt-ui-basekit 0.1.0
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/LICENSE +21 -0
- package/README.md +159 -0
- package/app/assets/css/basekit.css +86 -0
- package/app/components/BaseKitBackLink.vue +41 -0
- package/app/components/BaseKitChoiceCard.vue +107 -0
- package/app/components/BaseKitConfirmModal.vue +55 -0
- package/app/components/BaseKitDataTable.vue +293 -0
- package/app/components/BaseKitEmptyState.vue +39 -0
- package/app/components/BaseKitFileUpload.vue +90 -0
- package/app/components/BaseKitIconPicker.vue +130 -0
- package/app/components/BaseKitMarkdownEditor.vue +112 -0
- package/app/components/BaseKitPending.vue +54 -0
- package/app/components/BaseKitRecordPicker.vue +154 -0
- package/app/components/BaseKitSettingRow.vue +66 -0
- package/app/components/BaseKitStatTile.vue +64 -0
- package/app/components/BaseKitTabs.vue +161 -0
- package/app/components/BaseKitViewLink.vue +53 -0
- package/app/components/charts/BaseKitChartBars.vue +87 -0
- package/app/components/charts/BaseKitChartColumns.vue +319 -0
- package/app/components/charts/BaseKitChartDonut.vue +175 -0
- package/app/components/charts/BaseKitChartFigure.vue +87 -0
- package/app/components/charts/BaseKitChartMeter.vue +57 -0
- package/app/composables/useBaseKit.ts +155 -0
- package/app/composables/useChartPalette.ts +89 -0
- package/app/composables/useChartWidth.ts +41 -0
- package/app/composables/useConfirm.ts +16 -0
- package/app/stores/confirm.ts +56 -0
- package/app/utils/html-to-markdown.ts +47 -0
- package/app/utils/markdown.ts +24 -0
- package/nuxt.config.ts +43 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mirko Haaser
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# nuxt-ui-basekit
|
|
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.
|
|
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.
|
|
10
|
+
|
|
11
|
+
## Einbinden
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i -D nuxt-ui-basekit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// nuxt.config.ts
|
|
19
|
+
export default defineNuxtConfig({
|
|
20
|
+
extends: ['nuxt-ui-basekit'],
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
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.
|
|
27
|
+
|
|
28
|
+
## Was drin ist
|
|
29
|
+
|
|
30
|
+
| Gruppe | Komponenten |
|
|
31
|
+
|---|---|
|
|
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()` |
|
|
37
|
+
| Text | `BaseKitMarkdownEditor` |
|
|
38
|
+
| Diagramme | `BaseKitChartBars` · `-Columns` · `-Donut` · `-Meter` · `-Figure` |
|
|
39
|
+
|
|
40
|
+
Jede Komponente trägt ihre Erklärung im Kopf der Datei — was sie tut, wann sie
|
|
41
|
+
die richtige Wahl ist und wann nicht.
|
|
42
|
+
|
|
43
|
+
## Beschriftungen und Sprache
|
|
44
|
+
|
|
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:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// plugins/basekit.ts
|
|
51
|
+
import { computed } from 'vue'
|
|
52
|
+
import { baseKitKey, BASEKIT_DEFAULTS } from 'nuxt-ui-basekit/labels'
|
|
53
|
+
|
|
54
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
55
|
+
const config = computed(() => {
|
|
56
|
+
const { t, locale } = nuxtApp.$i18n as Composer
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
locale: locale.value,
|
|
60
|
+
labels: { ...BASEKIT_DEFAULTS.labels, search: t('common.search') },
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
nuxtApp.vueApp.provide(baseKitKey, config)
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Zwei Dinge, die dabei Zeit kosten können:
|
|
69
|
+
|
|
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.
|
|
77
|
+
|
|
78
|
+
## Farben
|
|
79
|
+
|
|
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:
|
|
84
|
+
|
|
85
|
+
```css
|
|
86
|
+
:root {
|
|
87
|
+
--basekit-accent: var(--meine-markenfarbe, #2563eb);
|
|
88
|
+
--basekit-surface: var(--meine-kartenflaeche, #ffffff);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Die fünf Diagrammfarben sind als Satz geprüft — Lichtheitsband, 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.
|
|
96
|
+
|
|
97
|
+
## Entwickeln
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm install
|
|
101
|
+
npx nuxi prepare # erzeugt .nuxt/tsconfig.json, sonst laufen die Tests nicht
|
|
102
|
+
npm test
|
|
103
|
+
npm run lint # nuxi typecheck
|
|
104
|
+
```
|
|
105
|
+
|
|
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.
|
|
112
|
+
|
|
113
|
+
### Lockfile
|
|
114
|
+
|
|
115
|
+
Wer auf macOS oder Windows eine Abhängigkeit hinzufügt, muss das Lockfile
|
|
116
|
+
anschließend einmal unter Linux/x64 nachziehen — sonst fehlen die
|
|
117
|
+
plattformspezifischen Optional-Pakete (`@emnapi/*` und Verwandtschaft), und
|
|
118
|
+
`npm ci` im Workflow bricht ab:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
docker run --rm --platform linux/amd64 -v "$PWD:/app" -w /app \
|
|
122
|
+
node:24 npm install --package-lock-only
|
|
123
|
+
```
|
|
124
|
+
|
|
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
|
|
131
|
+
|
|
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.
|
|
137
|
+
|
|
138
|
+
Eine neue Fassung:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm version patch # hebt die package.json und setzt den Tag
|
|
142
|
+
git push --follow-tags
|
|
143
|
+
```
|
|
144
|
+
|
|
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*.
|
|
151
|
+
|
|
152
|
+
## Herkunft
|
|
153
|
+
|
|
154
|
+
Entstanden in einem Verwaltungs-Frontend und dort über Monate in Gebrauch,
|
|
155
|
+
bevor es hier ein eigenes Paket wurde.
|
|
156
|
+
|
|
157
|
+
## Lizenz
|
|
158
|
+
|
|
159
|
+
MIT — siehe [LICENSE](LICENSE).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
* BaseKit — Gestaltungs-Token
|
|
3
|
+
*
|
|
4
|
+
* Die Komponenten greifen ausschließlich auf `--basekit-*` zu und nie auf
|
|
5
|
+
* Variablen einer bestimmten Anwendung. Hier stehen die Voreinstellungen.
|
|
6
|
+
*
|
|
7
|
+
* Eine Anwendung mit eigenem Theme legt die Token an einer Stelle auf ihre
|
|
8
|
+
* eigenen Variablen — ein Block in ihrem Stylesheet, geladen nach diesem:
|
|
9
|
+
*
|
|
10
|
+
* :root {
|
|
11
|
+
* --basekit-accent: var(--meine-markenfarbe, #2563eb);
|
|
12
|
+
* --basekit-surface: var(--meine-kartenflaeche, #ffffff);
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* Fällt der Block weg, rendern die Komponenten in den Werten von hier weiter.
|
|
16
|
+
* ========================================================================== */
|
|
17
|
+
|
|
18
|
+
:root {
|
|
19
|
+
--basekit-surface: #ffffff;
|
|
20
|
+
--basekit-surface-muted: #f4f6fa;
|
|
21
|
+
--basekit-border: #e5e8ee;
|
|
22
|
+
--basekit-border-strong: #d6dbe4;
|
|
23
|
+
--basekit-text: #101828;
|
|
24
|
+
--basekit-text-muted: #667085;
|
|
25
|
+
--basekit-accent: #2563eb;
|
|
26
|
+
--basekit-accent-text: #ffffff;
|
|
27
|
+
--basekit-chrome: #0b2239;
|
|
28
|
+
--basekit-chrome-text: rgb(255 255 255 / 82%);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.dark {
|
|
32
|
+
--basekit-surface: #101828;
|
|
33
|
+
--basekit-surface-muted: #0b1220;
|
|
34
|
+
--basekit-border: #1f2937;
|
|
35
|
+
--basekit-border-strong: #374151;
|
|
36
|
+
--basekit-text: #e5e7eb;
|
|
37
|
+
--basekit-text-muted: #9ca3af;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* --------------------------------------------------------------------------
|
|
41
|
+
* Diagramm-Farben (BaseKitChart*)
|
|
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).
|
|
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.
|
|
53
|
+
*
|
|
54
|
+
* Ein Kundenprojekt darf die Werte überschreiben — dann aber bitte als
|
|
55
|
+
* geprüften Satz und nicht einzeln.
|
|
56
|
+
* ------------------------------------------------------------------------ */
|
|
57
|
+
:root {
|
|
58
|
+
--basekit-chart-1: #2a78d6;
|
|
59
|
+
--basekit-chart-2: #eb6834;
|
|
60
|
+
--basekit-chart-3: #1baf7a;
|
|
61
|
+
--basekit-chart-4: #eda100;
|
|
62
|
+
--basekit-chart-5: #e87ba4;
|
|
63
|
+
|
|
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. */
|
|
67
|
+
--basekit-chart-surface: var(--ui-bg, #ffffff);
|
|
68
|
+
--basekit-chart-grid: rgb(0 0 0 / 8%);
|
|
69
|
+
--basekit-chart-axis: rgb(0 0 0 / 18%);
|
|
70
|
+
/* Zurückgenommene Reihe: Kontext, nicht Aussage. */
|
|
71
|
+
--basekit-chart-muted: rgb(0 0 0 / 22%);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.dark {
|
|
75
|
+
--basekit-chart-1: #3987e5;
|
|
76
|
+
--basekit-chart-2: #d95926;
|
|
77
|
+
--basekit-chart-3: #199e70;
|
|
78
|
+
--basekit-chart-4: #c98500;
|
|
79
|
+
--basekit-chart-5: #d55181;
|
|
80
|
+
|
|
81
|
+
--basekit-chart-surface: var(--ui-bg, #1e293b);
|
|
82
|
+
--basekit-chart-grid: rgb(255 255 255 / 10%);
|
|
83
|
+
--basekit-chart-axis: rgb(255 255 255 / 22%);
|
|
84
|
+
--basekit-chart-muted: rgb(255 255 255 / 28%);
|
|
85
|
+
}
|
|
86
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Rücksprung von einer Detailseite zur zugehörigen Liste.
|
|
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.
|
|
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.
|
|
14
|
+
*/
|
|
15
|
+
import { computed } from 'vue'
|
|
16
|
+
import { useBaseKitLabels } from '../composables/useBaseKit'
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(defineProps<{
|
|
19
|
+
to: string
|
|
20
|
+
label?: string
|
|
21
|
+
}>(), {
|
|
22
|
+
label: undefined,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const labels = useBaseKitLabels()
|
|
26
|
+
|
|
27
|
+
const text = computed(() => props.label ?? labels.value.back)
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<UButton
|
|
32
|
+
:to="props.to"
|
|
33
|
+
icon="i-lucide-arrow-left"
|
|
34
|
+
size="sm"
|
|
35
|
+
color="neutral"
|
|
36
|
+
variant="ghost"
|
|
37
|
+
class="shrink-0"
|
|
38
|
+
>
|
|
39
|
+
{{ text }}
|
|
40
|
+
</UButton>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
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.
|
|
6
|
+
*
|
|
7
|
+
* ┌──────────────────────────┐
|
|
8
|
+
* │ │ ← Slot `preview` (stilisierte Vorschau)
|
|
9
|
+
* │ Vorschau │
|
|
10
|
+
* ├──────────────────────────┤
|
|
11
|
+
* │ Titel [ Aktiv ] │ ← aktiv: Badge, sonst Knopf „Übernehmen“
|
|
12
|
+
* │ Beschreibung │
|
|
13
|
+
* └──────────────────────────┘
|
|
14
|
+
*
|
|
15
|
+
* Verwendung (Karten in einem Grid, eine pro Variante):
|
|
16
|
+
*
|
|
17
|
+
* <div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
18
|
+
* <BaseKitChoiceCard
|
|
19
|
+
* v-for="o in options"
|
|
20
|
+
* :key="o.id"
|
|
21
|
+
* :title="o.label"
|
|
22
|
+
* :active="o.id === activeId"
|
|
23
|
+
* :active-label="t('common.active')"
|
|
24
|
+
* :apply-label="t('common.apply')"
|
|
25
|
+
* :pending="pending"
|
|
26
|
+
* @apply="apply(o.id)"
|
|
27
|
+
* >
|
|
28
|
+
* <template #preview><MeineVorschau :id="o.id" /></template>
|
|
29
|
+
* </BaseKitChoiceCard>
|
|
30
|
+
* </div>
|
|
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.
|
|
35
|
+
*/
|
|
36
|
+
withDefaults(defineProps<{
|
|
37
|
+
/** Name der Variante. */
|
|
38
|
+
title: string
|
|
39
|
+
/** Kurze Erläuterung unter dem Titel. */
|
|
40
|
+
description?: string
|
|
41
|
+
/** Diese Variante ist die aktive. */
|
|
42
|
+
active?: boolean
|
|
43
|
+
/** Läuft gerade ein Speichervorgang (sperrt den Knopf). */
|
|
44
|
+
pending?: boolean
|
|
45
|
+
/** Beschriftung des Knopfes für „diese Variante übernehmen“. */
|
|
46
|
+
applyLabel?: string
|
|
47
|
+
/** Beschriftung des Aktiv-Kennzeichens. */
|
|
48
|
+
activeLabel?: string
|
|
49
|
+
/** Seitenverhältnis der Vorschaufläche. */
|
|
50
|
+
ratio?: string
|
|
51
|
+
}>(), {
|
|
52
|
+
description: undefined,
|
|
53
|
+
active: false,
|
|
54
|
+
pending: false,
|
|
55
|
+
applyLabel: 'Übernehmen',
|
|
56
|
+
activeLabel: 'Aktiv',
|
|
57
|
+
ratio: 'aspect-[16/10]',
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
defineEmits<{ apply: [] }>()
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<div
|
|
65
|
+
class="basekit-choice-card overflow-hidden rounded-lg border bg-white transition-colors dark:bg-neutral-900"
|
|
66
|
+
:class="active
|
|
67
|
+
? 'border-primary-500 ring-1 ring-primary-500'
|
|
68
|
+
: 'border-neutral-200 dark:border-neutral-800'"
|
|
69
|
+
:aria-current="active ? 'true' : undefined"
|
|
70
|
+
>
|
|
71
|
+
<div :class="['overflow-hidden border-b border-neutral-200 dark:border-neutral-800', ratio]">
|
|
72
|
+
<slot name="preview" />
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="flex items-start justify-between gap-3 p-3">
|
|
76
|
+
<div class="min-w-0">
|
|
77
|
+
<h3 class="truncate text-sm font-medium">
|
|
78
|
+
{{ title }}
|
|
79
|
+
</h3>
|
|
80
|
+
<p v-if="description" class="mt-0.5 text-xs text-neutral-500">
|
|
81
|
+
{{ description }}
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<UBadge
|
|
86
|
+
v-if="active"
|
|
87
|
+
color="primary"
|
|
88
|
+
variant="subtle"
|
|
89
|
+
icon="i-lucide-check"
|
|
90
|
+
class="shrink-0"
|
|
91
|
+
>
|
|
92
|
+
{{ activeLabel }}
|
|
93
|
+
</UBadge>
|
|
94
|
+
<UButton
|
|
95
|
+
v-else
|
|
96
|
+
size="xs"
|
|
97
|
+
color="neutral"
|
|
98
|
+
variant="subtle"
|
|
99
|
+
class="shrink-0"
|
|
100
|
+
:loading="pending"
|
|
101
|
+
@click="$emit('apply')"
|
|
102
|
+
>
|
|
103
|
+
{{ applyLabel }}
|
|
104
|
+
</UButton>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</template>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
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.
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
import { computed } from 'vue'
|
|
12
|
+
import { useBaseKitLabels } from '../composables/useBaseKit'
|
|
13
|
+
import { useConfirmStore } from '../stores/confirm'
|
|
14
|
+
|
|
15
|
+
const store = useConfirmStore()
|
|
16
|
+
const labels = useBaseKitLabels()
|
|
17
|
+
|
|
18
|
+
const opts = computed(() => store.options)
|
|
19
|
+
|
|
20
|
+
const open = computed({
|
|
21
|
+
get: () => store.open,
|
|
22
|
+
set: (value: boolean) => {
|
|
23
|
+
// Backdrop-/Escape-Schließen ohne Bestätigung → Abbruch.
|
|
24
|
+
if (!value) store.settle(false)
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<UModal
|
|
31
|
+
:open="open"
|
|
32
|
+
:title="opts.title ?? labels.confirmTitle"
|
|
33
|
+
:ui="{ content: 'max-w-md' }"
|
|
34
|
+
@update:open="open = $event"
|
|
35
|
+
>
|
|
36
|
+
<template #body>
|
|
37
|
+
<p class="text-sm text-muted">
|
|
38
|
+
{{ opts.description ?? labels.confirmBody }}
|
|
39
|
+
</p>
|
|
40
|
+
<div class="mt-6 flex justify-end gap-2">
|
|
41
|
+
<UButton type="button" color="neutral" variant="ghost" @click="store.settle(false)">
|
|
42
|
+
{{ opts.cancelLabel ?? labels.cancel }}
|
|
43
|
+
</UButton>
|
|
44
|
+
<UButton
|
|
45
|
+
type="button"
|
|
46
|
+
:color="opts.color ?? 'primary'"
|
|
47
|
+
:icon="opts.icon"
|
|
48
|
+
@click="store.settle(true)"
|
|
49
|
+
>
|
|
50
|
+
{{ opts.confirmLabel ?? labels.confirm }}
|
|
51
|
+
</UButton>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
54
|
+
</UModal>
|
|
55
|
+
</template>
|