nuxt-ui-basekit 0.2.0 → 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.
@@ -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"
@@ -1,14 +1,14 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * BaseKitEmptyState einheitlicher Leerzustand: Icon + handlungsorientierte
4
- * Überschrift + optionaler Erklärtext + optionale Primäraktion (Default-Slot).
3
+ * One empty state for all of them: icon, an action-shaped heading, optional
4
+ * explanation, optional primary action in the default slot.
5
5
  *
6
- * `variant` trennt die beiden Fälle aus docs/18:
7
- * - `empty` „noch nichts angelegt" (führt zum ersten Schritt)
8
- * - `search` „kein Suchergebnis" (bestätigt die Suche, bietet Korrektur)
6
+ * `variant` separates the two cases that look alike and mean different things:
7
+ * - `empty` nothing has been created yet; leads to the first step.
8
+ * - `search` the query ran and found nothing; confirms it, offers a fix.
9
9
  *
10
- * Das passende Icon wird je Variante gewählt, kann per `icon` überschrieben
11
- * werden. Aktion (z. B. ein `UButton`) kommt in den Default-Slot.
10
+ * The icon follows the variant and can be overridden through `icon`. The
11
+ * action (a `UButton`, say) goes into the default slot.
12
12
  */
13
13
  withDefaults(defineProps<{
14
14
  title: string
@@ -3,9 +3,9 @@ import { ref } from 'vue'
3
3
  import { useBaseKitLabels } from '../composables/useBaseKit'
4
4
 
5
5
  /**
6
- * Datei-Auswahl per Klick oder Drag & Drop. Reicht die ausgewählten Dateien
7
- * über `select` nach oben kümmert sich NICHT selbst um den Upload (das macht
8
- * der Aufrufer, je nach Bild/Video unterschiedlich).
6
+ * File selection by click or drag and drop. Hands the chosen files up through
7
+ * `select` and does NOT upload them itself that stays with the caller, who
8
+ * knows whether this is an image or a video.
9
9
  */
10
10
  const props = withDefaults(defineProps<{
11
11
  accept?: string
@@ -31,7 +31,7 @@ function pick(): void {
31
31
  function onChange(event: Event): void {
32
32
  const files = Array.from((event.target as HTMLInputElement).files ?? [])
33
33
  if (files.length) emit('select', files)
34
- // Zurücksetzen, damit dieselbe Datei erneut gewählt werden kann.
34
+ // Reset, so the same file can be picked again.
35
35
  if (input.value) input.value.value = ''
36
36
  }
37
37
 
@@ -84,7 +84,7 @@ function onDrop(event: DragEvent): void {
84
84
  .basekit-upload__icon { font-size: 1.5rem; }
85
85
  .basekit-upload__label { font-size: 0.8125rem; font-weight: 500; }
86
86
 
87
- /* Dark-Mode: die Light-Fallbacks des Dropfelds überschreiben. */
87
+ /* Dark mode: override the drop zone's light fallbacks. */
88
88
  :where(.dark) .basekit-upload { border-color: #1f2937; background: #0f172a; color: #9ca3af; }
89
89
  :where(.dark) .basekit-upload:hover, :where(.dark) .basekit-upload--drag { background: #1f2937; }
90
90
  </style>
@@ -3,14 +3,14 @@ import { computed, ref } from 'vue'
3
3
  import { useBaseKitLabels } from '../composables/useBaseKit'
4
4
 
5
5
  /**
6
- * Icon-Auswahl als Form-Element für `i-lucide-*`-Namenreine Anzeige + Picker,
7
- * kein Freitext. Der Trigger zeigt das gewählte Icon (oder einen Platzhalter);
8
- * ein Klick öffnet ein Popover mit Suche und einem Raster kuratierter Icons.
9
- * `modelValue` ist der Icon-Name; leerer String bedeutet „kein Icon".
6
+ * A form control for `i-lucide-*` names display plus picker, no free text.
7
+ * The trigger shows the chosen icon, or a placeholder; a click opens a popover
8
+ * with a search field and a grid of curated icons. `modelValue` is the icon
9
+ * name, an empty string meaning no icon.
10
10
  *
11
- * Bewusst kuratiert statt des kompletten Iconify-Satzes das hält den Bundle
12
- * klein und braucht keine zusätzliche Datenquelle. Fehlt mal ein Icon, wird die
13
- * Liste unten einfach ergänzt.
11
+ * Curated rather than the full Iconify set, on purpose: it keeps the bundle
12
+ * small and needs no extra data source. When an icon is missing, the list
13
+ * below grows by a line.
14
14
  */
15
15
  const props = defineProps<{ modelValue: string }>()
16
16
  const emit = defineEmits<{ 'update:modelValue': [string] }>()
@@ -20,7 +20,7 @@ const labels = useBaseKitLabels()
20
20
  const open = ref(false)
21
21
  const search = ref('')
22
22
 
23
- // Kuratierte, für Navigation/Inhalte typische Lucide-Icons.
23
+ // Curated Lucide icons, the ones that come up in navigation and content.
24
24
  const ICONS: string[] = [
25
25
  'i-lucide-home', 'i-lucide-layout-dashboard', 'i-lucide-layout-grid', 'i-lucide-list',
26
26
  'i-lucide-file-text', 'i-lucide-file', 'i-lucide-files', 'i-lucide-folder',
@@ -8,14 +8,16 @@ import { renderMarkdown } from '../utils/markdown'
8
8
  import { htmlToMarkdown, prepareHtmlToMarkdown } from '../utils/html-to-markdown'
9
9
 
10
10
  /**
11
- * WYSIWYG-Editor für den Text-Block. v-model ist ein **Markdown-String**.
11
+ * A WYSIWYG editor whose v-model is a **Markdown string**.
12
12
  *
13
- * Tiptap arbeitet intern mit HTML; die Brücke nach Markdown läuft über zwei
14
- * etablierte Bibliotheken statt eines Tiptap-Markdown-Plugins (versionsrobust):
15
- * - Laden: Markdown HTML via markdown-it (`renderMarkdown`)
16
- * - Speichern: HTML Markdown via turndown
13
+ * Tiptap works in HTML internally. The bridge to Markdown runs through two
14
+ * established libraries rather than a Tiptap Markdown plugin, which survives
15
+ * version bumps better:
16
+ * - loading: Markdown to HTML through markdown-it (`renderMarkdown`)
17
+ * - saving: HTML to Markdown through turndown
17
18
  *
18
- * Der Editor wird erst `onMounted` erzeugt (ProseMirror braucht DOM, SSR-sicher).
19
+ * The editor is created `onMounted` ProseMirror needs a DOM, so this stays
20
+ * SSR-safe.
19
21
  */
20
22
  const props = defineProps<{ modelValue?: string | null }>()
21
23
  const emit = defineEmits<{ 'update:modelValue': [string] }>()
@@ -29,7 +31,7 @@ function toMarkdown(html: string): string {
29
31
  }
30
32
 
31
33
  onMounted(() => {
32
- // turndown im Browser nachladenDetails in html-to-markdown.ts.
34
+ // Pull turndown in on the client details in html-to-markdown.ts.
33
35
  prepareHtmlToMarkdown()
34
36
  editor.value = new Editor({
35
37
  extensions: [
@@ -43,7 +45,7 @@ onMounted(() => {
43
45
 
44
46
  onBeforeUnmount(() => editor.value?.destroy())
45
47
 
46
- // Externe Wertänderung übernehmen, ohne den Cursor zu stören (nur bei echtem Diff).
48
+ // Take an outside change without disturbing the cursor only on a real diff.
47
49
  watch(() => props.modelValue, (v) => {
48
50
  if (!editor.value) return
49
51
  if ((v ?? '') !== toMarkdown(editor.value.getHTML())) {
@@ -102,7 +104,7 @@ function isActive(name: string, attrs?: Record<string, unknown>): boolean {
102
104
  .md-content :deep(.ProseMirror code) { font-family: ui-monospace, monospace; font-size: 0.85em; background: var(--basekit-surface-muted, #f4f6fa); padding: 0.1em 0.35em; border-radius: 4px; }
103
105
  .md-content :deep(.ProseMirror a) { color: var(--basekit-accent, #2563eb); text-decoration: underline; }
104
106
 
105
- /* Dark-Mode: die hart hinterlegten Light-Fallbacks überschreiben. */
107
+ /* Dark mode: override the hard-coded light fallbacks. */
106
108
  :where(.dark) .md-editor { background: #101828; border-color: #1f2937; }
107
109
  :where(.dark) .md-toolbar { background: #0b1220; border-color: #1f2937; }
108
110
  :where(.dark) .md-toolbar button { color: #e5e7eb; }
@@ -1,35 +1,34 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Ladezustand für eine Seite oder einen Abschnitt.
3
+ * A loading state for a page or a section of one.
4
4
  *
5
- * Die Admin-Seiten holen ihre Daten in `onMounted` nach. Ohne Zwischenschritt
6
- * steht das Formular sofort da mit leeren Feldern, die sich einen Wimpernschlag
7
- * später von selbst füllen. Wer schnell tippt, schreibt in ein Feld, das gleich
8
- * überschrieben wird; wer langsam liest, hält den Datensatz für leer.
5
+ * Pages that fetch in `onMounted` show their form immediately otherwise — with
6
+ * empty fields that fill themselves a blink later. Whoever types fast writes
7
+ * into a field that is about to be overwritten; whoever reads slowly takes the
8
+ * record for empty.
9
9
  *
10
- * Deshalb: erst ein Kreisel, dann der Inhalt. Der Kreisel sagt „gleich", das
11
- * leere Formular sagt „nichts da" — und das eine ist wahr, das andere nicht.
10
+ * Hence: spinner first, content after. The spinner says "in a moment", the
11
+ * empty form says "nothing here" — and only one of those is true.
12
12
  *
13
- * Zwei Wege, beide gültig:
13
+ * Two ways, both valid:
14
14
  *
15
15
  * <BaseKitPending :pending="pending">
16
- * <UForm …> <!-- erscheint erst, wenn die Daten stehen -->
16
+ * <UForm …> <!-- appears once the data is in -->
17
17
  * </BaseKitPending>
18
18
  *
19
19
  * <BaseKitPending v-if="pending" />
20
20
  *
21
- * `label` nur setzen, wo der Vorgang länger dauert und einen Namen hat („Suche
22
- * läuft"). Bei einem gewöhnlichen Formular ist der Kreisel allein ehrlicher als
23
- * ein Wort, das niemand liest.
21
+ * Set `label` only where the operation takes a while and has a name ("Searching").
22
+ * On an ordinary form the bare spinner is more honest than a word nobody reads.
24
23
  */
25
24
  withDefaults(defineProps<{
26
- /** Solange wahr, steht der Kreisel statt des Inhalts. */
25
+ /** While true, the spinner stands in for the content. */
27
26
  pending?: boolean
28
- /** Optionale Beschriftung unter dem Kreisel. */
27
+ /** Optional label below the spinner. */
29
28
  label?: string
30
29
  /**
31
- * Höhe des Bereichs. `page` für eine ganze Seite, `inline` für einen
32
- * Abschnitt darin sonst springt das Layout, wenn der Inhalt erscheint.
30
+ * Height of the area. `page` for a whole page, `inline` for a section within
31
+ * oneotherwise the layout jumps when the content arrives.
33
32
  */
34
33
  size?: 'page' | 'inline'
35
34
  }>(), {