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.
@@ -3,32 +3,32 @@ import { computed, ref } from 'vue'
3
3
  import { useBaseKitLabels } from '../composables/useBaseKit'
4
4
 
5
5
  /**
6
- * Auswahl eines Datensatzes über (potenziell mehrere) Modelle die Auswahl
7
- * läuft in einem Modal, das per „Auswählen" / „Ändern" aufgeht. Im Modal ist
8
- * die Liste nach Text UND Typ filterbar.
6
+ * Picks a record across one or several models. The choosing happens in a modal
7
+ * that opens through "Select" or "Change"; inside, the list filters by text
8
+ * AND by type.
9
9
  *
10
- * - **Nichts gewählt** Button „Auswählen".
11
- * - **Gewählt** Titel der Auswahl (Link zum Bearbeiten des Inhalts) + Typ-
12
- * Badge + Button „Ändern".
10
+ * - **Nothing selected** a "Select" button.
11
+ * - **Selected** the title of the choice, linking to its edit form, plus a
12
+ * type badge and a "Change" button.
13
13
  *
14
- * Modell-agnostisch die `items` liefert der Aufrufer. `v-model` = `value`
15
- * der Option (oder `null`).
14
+ * Model-agnostic: the caller supplies `items`. `v-model` is the option's
15
+ * `value`, or `null`.
16
16
  */
17
17
  export interface BaseKitRecordOption {
18
18
  value: number
19
19
  label: string
20
- /** Typ-Badge (bereits übersetzt). */
20
+ /** Type badge — already in the caller's language. */
21
21
  type?: string
22
- /** Zusatz rechts in der Liste (z. B. Slug). */
22
+ /** Extra detail on the right of the list, a slug for instance. */
23
23
  hint?: string
24
- /** Interner Link zum Bearbeiten des Inhalts. */
24
+ /** Internal link to the record's edit form. */
25
25
  editHref?: string
26
26
  }
27
27
 
28
28
  const props = withDefaults(defineProps<{
29
29
  modelValue?: number | null
30
30
  items: BaseKitRecordOption[]
31
- /** Titel des Auswahl-Modals. */
31
+ /** Title of the picker modal. */
32
32
  title?: string
33
33
  }>(), {
34
34
  modelValue: null,
@@ -72,7 +72,7 @@ function choose(item: BaseKitRecordOption): void {
72
72
 
73
73
  <template>
74
74
  <div>
75
- <!-- Gewählt: Titel (Bearbeiten-Link) + Typ-Badge + Ändern -->
75
+ <!-- Selected: title as an edit link, type badge, change button -->
76
76
  <div v-if="selected" class="flex items-center gap-2">
77
77
  <NuxtLink
78
78
  v-if="selected.editHref"
@@ -93,7 +93,7 @@ function choose(item: BaseKitRecordOption): void {
93
93
  </UButton>
94
94
  </div>
95
95
 
96
- <!-- Nichts gewählt: Auswählen -->
96
+ <!-- Nothing selected: the select button -->
97
97
  <UButton v-else color="neutral" variant="subtle" icon="i-lucide-list" @click="openModal">
98
98
  {{ labels.select }}
99
99
  </UButton>
@@ -1,35 +1,36 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Setting-Row Pattern für eine einzelne Einstellung in einer Settings-Card.
3
+ * One setting inside a settings card.
4
4
  *
5
5
  * ┌──────────────────────────────────────────────────────┐
6
- * │ Titel [Action] │
7
- * │ Beschreibungstext (optional, mehrzeilig)
6
+ * │ Title [Control] │
7
+ * │ Description (optional, may wrap)
8
8
  * └──────────────────────────────────────────────────────┘
9
9
  *
10
- * Titel + Action stehen auf einer Zeile (Action rechts, Titel links).
11
- * Die Beschreibung steht darunter, linksbündig zum Titel.
10
+ * Title and control share a line, title left and control right. The
11
+ * description sits underneath, aligned with the title.
12
12
  *
13
- * Verwendung:
13
+ * Usage:
14
14
  *
15
- * <BaseKitSettingRow title="Benachrichtigungen" description="E-Mails und Push erhalten">
15
+ * <BaseKitSettingRow title="Notifications" description="Receive email and push">
16
16
  * <USwitch v-model="notifications" />
17
17
  * </BaseKitSettingRow>
18
18
  *
19
- * <BaseKitSettingRow title="Sprache" description="Wird beim Speichern übernommen">
19
+ * <BaseKitSettingRow title="Language" description="Applied on save">
20
20
  * <USelect v-model="staged" :items="locales" />
21
21
  * </BaseKitSettingRow>
22
22
  *
23
- * Der `meta`-Slot nimmt eine Zusatzzeile unter der Beschreibung aufBestand,
24
- * Sperrgrund, Hinweis. Sie steht bewusst getrennt von `description`: das Feld
25
- * trägt den einen erklärenden Satz, und eine Zahl ist keine Erklärung.
23
+ * The `meta` slot takes an extra line below the description a count, a
24
+ * reason something is locked, a hint. Kept apart from `description` on
25
+ * purpose: that field carries the one explaining sentence, and a number is not
26
+ * an explanation.
26
27
  *
27
- * <BaseKitSettingRow title="E-Learning" description="Kurse und Nachweise.">
28
+ * <BaseKitSettingRow title="E-learning" description="Courses and certificates.">
28
29
  * <USwitch v-model="on" />
29
- * <template #meta><span>4 Kurse</span></template>
30
+ * <template #meta><span>4 courses</span></template>
30
31
  * </BaseKitSettingRow>
31
32
  *
32
- * Mehrere Rows in einer Card werden über Tailwind divider getrennt:
33
+ * Several rows in one card are separated with a Tailwind divider:
33
34
  *
34
35
  * <UCard>
35
36
  * <div class="divide-y divide-default">
@@ -1,13 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Eine Zahl mit Beschriftungdie richtige Form für einen einzelnen Wert.
3
+ * A number with a label the right shape for a single value.
4
4
  *
5
- * Ein Balkendiagramm mit genau einem Balken sagt nicht mehr als die Zahl
6
- * selbst und kostet die vierfache Fläche. Deshalb sind Bestand und
7
- * Handlungsliste Kacheln und keine Diagramme.
5
+ * A bar chart with exactly one bar says no more than the number itself and
6
+ * costs four times the space. Which is why counts and to-do figures are tiles
7
+ * here and not charts.
8
8
  *
9
- * `tone` färbt nur, wenn die Zahl über null liegt: 0 offene Vorgänge" ist
10
- * keine Warnung, sondern der Normalzustand.
9
+ * `tone` only colours when the number is above zero: "0 open items" is not a
10
+ * warning, it is the normal state.
11
11
  */
12
12
  import { computed } from 'vue'
13
13
  import { useChartFormat } from '../composables/useChartPalette'
@@ -33,8 +33,8 @@ const valueClass = computed(() => {
33
33
  return props.tone === 'alert' ? 'text-error' : 'text-highlighted'
34
34
  })
35
35
 
36
- /** Der Punkt neben der Zahl trägt dieselbe Aussage wie die Farbedie Farbe
37
- * allein wäre für eine Rot-Grün-Schwäche keine. */
36
+ /** The dot next to the number carries the same message as the colour the
37
+ * colour alone would carry none under a red-green deficiency. */
38
38
  const flagged = computed(() => props.value > 0 && props.tone === 'alert')
39
39
  </script>
40
40
 
@@ -3,43 +3,43 @@ import { computed, onMounted, ref, watch } from 'vue'
3
3
  import { useRoute, useRouter } from '#imports'
4
4
 
5
5
  /**
6
- * Tab-GruppierungPattern, um auf einer Seite mehrere Bereiche (z.B.
7
- * Einstellungs-Gruppen) zu gliedern, gerade bei vielen Feldern.
6
+ * Splits a page into sections settings groups, say, and anything else that
7
+ * gets unwieldy once there are many fields.
8
8
  *
9
- * Generisch: pro Tab ein benannter Slot (= dessen `key`). Aktiver Tab optional
10
- * über `v-model` steuerbar, sonst der erste.
9
+ * One named slot per tab, keyed by its `key`. The active tab is optionally
10
+ * driven through `v-model`; without it the first one wins.
11
11
  *
12
12
  * <BaseKitTabs :items="[
13
- * { key: 'general', label: 'Allgemein', icon: 'i-lucide-settings' },
14
- * { key: 'access', label: 'Zugriff', icon: 'i-lucide-lock' },
13
+ * { key: 'general', label: 'General', icon: 'i-lucide-settings' },
14
+ * { key: 'access', label: 'Access', icon: 'i-lucide-lock' },
15
15
  * ]">
16
16
  * <template #general> … </template>
17
17
  * <template #access> … </template>
18
18
  * </BaseKitTabs>
19
19
  *
20
- * Deep-Linking: mit `hash-nav` ist der aktive Tab über den URL-Hash adressierbar
21
- * (`…/seite#access`). Beim Laden gewinnt ein passender Hash; ein Tab-Wechsel
22
- * schreibt den Hash via `router.replace` (kein History-Spam). Bei mehreren
23
- * Tab-Gruppen auf einer Seite nur EINE mit `hash-nav` betreiben.
20
+ * Deep linking: with `hash-nav` the active tab becomes addressable through the
21
+ * URL fragment (`…/page#access`). On load a matching hash wins; switching tabs
22
+ * writes the hash through `router.replace`, so the history stays clean. With
23
+ * several tabsets on one page, run only ONE of them with `hash-nav`.
24
24
  *
25
- * Der Hash greift bewusst erst `onMounted`: der Server sieht ihn gar nicht
26
- * (Browser schicken ihn nicht mit), und ein davon abweichender erster Render
27
- * im Client zerlegt die Hydration dann stand die Markierung auf dem einen
28
- * Tab und der Inhalt kam vom anderen. Aus demselben Grund läuft er über
29
- * `select()`, damit ein gebundenes `v-model` den Tab aus dem Hash mitbekommt.
25
+ * The hash is applied `onMounted` on purpose. The server never sees it —
26
+ * browsers do not send it and a first client render that differs from the
27
+ * server's tears the hydration apart: the marker sat on one tab while the
28
+ * content came from another. For the same reason it goes through `select()`,
29
+ * so a bound `v-model` learns about the tab from the hash.
30
30
  *
31
- * A11y: tablist/tab/tabpanel-Rollen, Pfeiltasten links/rechts wechseln.
31
+ * Accessibility: tablist/tab/tabpanel roles, left and right arrow keys switch.
32
32
  *
33
- * Layout-Konvention: die Tab-Leiste NICHT in eine `UCard` wrappen Tabs +
34
- * Inhalt liegen direkt auf der Seite. Feinere Struktur je Tab-Inhalt (Gruppen,
35
- * Fieldset, einzelne Cards) nach Bedarf INNERHALB des jeweiligen Slots.
33
+ * Layout convention: do NOT wrap the tab bar in a `UCard` — tabs and content
34
+ * sit directly on the page. Finer structure per tab (groups, fieldsets,
35
+ * individual cards) belongs INSIDE the respective slot.
36
36
  */
37
37
  const props = withDefaults(defineProps<{
38
- /** Tab-Definitionen (Reihenfolge = Anzeigereihenfolge). */
38
+ /** Tab definitions; their order is the display order. */
39
39
  items: Array<{ key: string, label: string, icon?: string }>
40
- /** Aktiver Tab-Key (v-model). Ohne Bindung wird der erste Tab aktiv. */
40
+ /** Key of the active tab (v-model). Unbound, the first tab becomes active. */
41
41
  modelValue?: string
42
- /** Aktiven Tab über den URL-Hash adressierbar machen (`#<key>`). */
42
+ /** Make the active tab addressable through the URL fragment (`#<key>`). */
43
43
  hashNav?: boolean
44
44
  }>(), {
45
45
  modelValue: undefined,
@@ -53,7 +53,7 @@ const emit = defineEmits<{
53
53
  const route = useRoute()
54
54
  const router = useRouter()
55
55
 
56
- /** Gültiger Tab-Key aus dem aktuellen URL-Hash, sonst null. */
56
+ /** A valid tab key from the current URL fragment, otherwise null. */
57
57
  function keyFromHash(): string | null {
58
58
  if (!props.hashNav) return null
59
59
  const key = String(route.hash ?? '').replace(/^#/, '')
@@ -62,36 +62,35 @@ function keyFromHash(): string | null {
62
62
 
63
63
  const active = ref<string>(props.modelValue ?? props.items[0]?.key ?? '')
64
64
 
65
- /** Hat der Nutzer selbst gewählt? Dann gewinnt seine Wahl über den Hash. */
65
+ /** Did the user choose themselves? Then their choice beats the hash. */
66
66
  const picked = ref(false)
67
67
 
68
- // Hash erst im Client anwendensiehe Kommentar oben.
68
+ // Apply the hash on the client only see the comment at the top.
69
69
  onMounted(() => applyHash())
70
70
 
71
- // v-model rein: Auswahl von außen übernehmen.
71
+ // v-model coming in: take the selection from outside.
72
72
  watch(() => props.modelValue, (next) => {
73
73
  if (next !== undefined && next !== active.value) active.value = next
74
74
  })
75
75
 
76
- // Hash ändert sich (Direktlink, Zurück/Vorwärts) passenden Tab aktivieren.
76
+ // The hash changed — a direct link, back or forward so activate that tab.
77
77
  watch(() => route.hash, () => {
78
78
  const key = keyFromHash()
79
79
  if (key !== null && key !== active.value) active.value = key
80
80
  })
81
81
 
82
- // Items wechseln. Zwei Fälle: der Tab aus dem Hash taucht erst jetzt auf
83
- // (rechte-abhängige Tabs kommen mit dem Bootstrap nach) dann übernehmen,
84
- // solange der Nutzer nicht selbst gewählt hat. Sonst: fällt der aktive Tab
85
- // weg, zurück auf den ersten.
82
+ // The items changed. Two cases: the tab named in the hash only appears now —
83
+ // permission-dependent tabs arrive late in which case take it, as long as
84
+ // the user has not chosen themselves. Otherwise, if the active tab disappears,
85
+ // fall back to the first.
86
86
  watch(() => props.items, (items) => {
87
87
  if (!picked.value && applyHash()) return
88
88
  if (!items.some(i => i.key === active.value)) active.value = items[0]?.key ?? ''
89
89
  })
90
90
 
91
91
  /**
92
- * Tab aus dem Hash übernehmen, falls er zu einem Item passt und nicht schon
93
- * aktiv ist. Zählt nicht als Nutzer-Wahl. Liefert `true`, wenn gewechselt
94
- * wurde.
92
+ * Take the tab from the hash if it matches an item and is not already active.
93
+ * Does not count as a user choice. Returns `true` when it switched.
95
94
  */
96
95
  function applyHash(): boolean {
97
96
  const key = keyFromHash()
@@ -1,19 +1,19 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Weg vom Bearbeiten-Formular zur Ansicht derselben Sache.
3
+ * The way from an edit form to the public view of the same thing.
4
4
  *
5
- * Die Gegenrichtung gab es längst: In der Ansicht stehen die Kontext-Aktionen
6
- * („Bearbeiten", „Inhalt pflegen"). Der Rückweg fehlte wer eine Rubrik
7
- * bearbeitet hatte, kam nur über den Umweg Übersicht Frontend Rubrik
8
- * dorthin, um das Ergebnis zu sehen.
5
+ * The opposite direction had existed for a long time: the view carries the
6
+ * context actions ("Edit", "Manage content"). The way back was missing
7
+ * having edited a record, the only route to seeing the result was the detour
8
+ * through the overview and the front end.
9
9
  *
10
- * Steht neben dem Rücksprung im Seitenkopf, an derselben Stelle wie dort:
11
- * Erst die Ansicht, dann der Rücksprung zur Liste die Reihenfolge folgt der
12
- * Häufigkeit, mit der man beides braucht.
10
+ * Sits next to the back link in the page header, in the same spot: first the
11
+ * view, then the way back to the list. The order follows how often each is
12
+ * needed.
13
13
  *
14
- * **Nur zeigen, wenn es die Ansicht gibt.** Ein neu angelegter Datensatz hat
15
- * noch keine, und ein Link ins Leere ist schlechter als kein Link. Deshalb
16
- * gehört das `v-if` an die Aufrufstelle, wo bekannt ist, ob gespeichert wurde.
14
+ * **Only render it when the view exists.** A freshly created record has none,
15
+ * and a link into nowhere is worse than no link. That is why the `v-if`
16
+ * belongs at the call site, where it is known whether anything was saved.
17
17
  */
18
18
  import { computed } from 'vue'
19
19
  import { useBaseKitLabels } from '../composables/useBaseKit'
@@ -21,9 +21,9 @@ import { useBaseKitLabels } from '../composables/useBaseKit'
21
21
  const props = withDefaults(defineProps<{
22
22
  to: string
23
23
  /**
24
- * Öffnet die Ansicht in einem neuen Reiter. Voreingestellt, weil man beim
25
- * Bearbeiten meist nachsehen und weiterarbeiten willnicht wechseln und
26
- * die ungespeicherten Änderungen verlieren.
24
+ * Opens the view in a new tab. On by default, because while editing you
25
+ * usually want to look and carry on not switch away and lose the unsaved
26
+ * changes.
27
27
  */
28
28
  newTab?: boolean
29
29
  label?: string
@@ -1,12 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Waagerechte Balken für nominale KategorienRollen, Typen, Herkünfte.
3
+ * Horizontal bars for nominal categoriesroles, types, sources.
4
4
  *
5
- * Waagerecht, weil die Beschriftung aus Namen besteht und Namen unter einer
6
- * senkrechten Säule schräg gestellt werden müssten. Alle Balken tragen
7
- * **dieselbe** Farbe: die Kategorien haben keine eigene Ordnung, und ein
8
- * Farbverlauf nach Größe würde die Länge ein zweites Mal erzählen, statt eine
9
- * neue Auskunft zu geben.
5
+ * Horizontal because the labels are names, and names under a vertical column
6
+ * would have to be tilted. Every bar carries the **same** colour: the
7
+ * categories have no order of their own, and a gradient by size would tell the
8
+ * length a second time instead of adding anything.
10
9
  */
11
10
  import { computed, ref } from 'vue'
12
11
  import { baseKitChartColor, useChartFormat } from '../../composables/useChartPalette'
@@ -19,7 +18,7 @@ interface BarItem {
19
18
 
20
19
  const props = withDefaults(defineProps<{
21
20
  items: BarItem[]
22
- /** Sichtbare Zeilen; der Rest wandert in „Weitere". */
21
+ /** Rows shown; the rest is folded into a "more" row. */
23
22
  limit?: number
24
23
  moreLabel?: string
25
24
  emptyLabel?: string
@@ -65,8 +64,8 @@ const color = baseKitChartColor(0)
65
64
  >
66
65
  <span class="truncate text-sm text-muted" :title="row.label">{{ row.label }}</span>
67
66
 
68
- <!-- Die Spur ist die Fläche, der Balken die Aussage; deshalb trägt die
69
- Spur nur eine Andeutung und keine zweite Farbe. -->
67
+ <!-- The track is the surface, the bar is the statement; which is why
68
+ the track only hints and carries no second colour. -->
70
69
  <span class="h-2.5 w-full rounded-full bg-elevated">
71
70
  <span
72
71
  class="block h-2.5 rounded-full transition-opacity"
@@ -1,18 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Gestapelte Säulen über einer Zeitachse„was ist je Tag entstanden".
3
+ * Stacked columns over a time axis what accrued per day.
4
4
  *
5
- * Warum gestapelt und nicht mehrere Linien: die Frage ist Anteil am Ganzen,
6
- * nicht Verlauf einzelner Reihen. Und warum Säulen und keine Fläche: Tageswerte
7
- * sind gezählte Ereignisse, keine stetige Größe eine Fläche würde zwischen
8
- * zwei Tagen interpolieren und behaupten, dass um 12 Uhr nachts etwas
9
- * halb passiert ist.
5
+ * Stacked rather than several lines because the question is share of the
6
+ * whole, not the course of individual series. Columns rather than an area
7
+ * because daily values are counted events, not a continuous quantity: an area
8
+ * would interpolate between two days and claim something half-happened at
9
+ * midnight.
10
10
  *
11
- * Trennung zwischen den Segmenten passiert über eine 2-px-Lücke in der
12
- * Kartenfarbe, nicht über einen Rahmen: ein Rahmen ist Tinte, die keine Daten
13
- * trägt, und bei dünnen Segmenten überdeckt er den Wert.
11
+ * Segments are separated by a 2px gap in the card colour rather than a border.
12
+ * A border is ink that carries no data, and on thin segments it covers the
13
+ * value.
14
14
  *
15
- * Höchstens fünf Reihensiehe `useChartPalette`.
15
+ * Five series at most see `useChartPalette`.
16
16
  */
17
17
  import { computed, ref } from 'vue'
18
18
  import { baseKitChartColor, useChartFormat } from '../../composables/useChartPalette'
@@ -25,13 +25,13 @@ interface ColumnSeries {
25
25
  }
26
26
 
27
27
  const props = withDefaults(defineProps<{
28
- /** Beschriftung je Säule, z. B. ISO-Tage. */
28
+ /** Label per column, ISO dates for instance. */
29
29
  categories: string[]
30
30
  series: ColumnSeries[]
31
31
  height?: number
32
- /** Achsenbeschriftung (kurz) aus einer Kategorie. */
32
+ /** Short axis label derived from a category. */
33
33
  formatCategory?: (value: string) => string
34
- /** Ausführliche Fassung für den Tooltip. */
34
+ /** The long form, for the tooltip. */
35
35
  formatCategoryLong?: (value: string) => string
36
36
  ariaLabel?: string
37
37
  }>(), {
@@ -64,7 +64,7 @@ const plot = computed(() => {
64
64
  }
65
65
  })
66
66
 
67
- /** Tagessummensie bestimmen die Skala und stehen im Tooltip. */
67
+ /** Daily totals they set the scale and appear in the tooltip. */
68
68
  const totals = computed(() =>
69
69
  props.categories.map((_, index) =>
70
70
  props.series.reduce((sum, s) => sum + (s.points[index] ?? 0), 0),
@@ -72,9 +72,8 @@ const totals = computed(() =>
72
72
  )
73
73
 
74
74
  /**
75
- * Obere Achsengrenze auf eine runde Zahl aufgerundet. Ohne das steht die
76
- * Beschriftung auf krummen Werten wie 37, und die Gitterlinien tragen keine
77
- * ablesbare Auskunft mehr.
75
+ * Upper axis bound, rounded up to something even. Without it the labels sit on
76
+ * awkward values like 37 and the grid lines stop telling anyone anything.
78
77
  */
79
78
  const scaleMax = computed(() => {
80
79
  const peak = Math.max(0, ...totals.value)
@@ -100,7 +99,7 @@ function columnX(index: number): number {
100
99
  return PADDING.left + plot.value.band * (index + 0.5) - plot.value.columnWidth / 2
101
100
  }
102
101
 
103
- /** Runde Kappe nur obenunten sitzt die Säule auf der Grundlinie auf. */
102
+ /** Rounded cap on top only the column sits flat on the baseline. */
104
103
  function cappedPath(x: number, y: number, w: number, h: number): string {
105
104
  const r = Math.min(4, w / 2, h)
106
105
  if (h <= 0) return ''
@@ -129,7 +128,7 @@ const columns = computed(() => {
129
128
  const segments: Segment[] = []
130
129
  let stacked = 0
131
130
 
132
- // Von unten nach oben stapeln; die oberste Reihe bekommt die Kappe.
131
+ // Stack bottom to top; the topmost series gets the cap.
133
132
  const present = props.series
134
133
  .map((s, order) => ({ s, order, value: s.points[index] ?? 0 }))
135
134
  .filter(entry => entry.value > 0)
@@ -140,8 +139,8 @@ const columns = computed(() => {
140
139
  const top = baseline - (stacked / max) * innerHeight
141
140
 
142
141
  const isTop = position === present.length - 1
143
- // Lücke nach oben, außer beim obersten Segmentdort säße sie
144
- // zwischen Säule und Luft und würde die Säule nur kürzen.
142
+ // A gap above, except on the topmost segment there it would sit
143
+ // between the column and thin air and merely shorten it.
145
144
  const height = Math.max(0, bottom - top - (isTop ? 0 : GAP))
146
145
  if (height <= 0) return
147
146
 
@@ -159,9 +158,9 @@ const columns = computed(() => {
159
158
  })
160
159
 
161
160
  /**
162
- * Beschriftung ausdünnen, bis sie nicht mehr kollidiertund zwar durch
163
- * Weglassen, nicht durch Verstecken: ein ausgeblendeter Text bleibt im
164
- * Baum stehen und wird von Screenreadern trotzdem vorgelesen.
161
+ * Thin the labels out until they stop colliding by leaving them out, not by
162
+ * hiding them: hidden text stays in the tree and screen readers read it
163
+ * anyway.
165
164
  */
166
165
  const axisLabels = computed(() => {
167
166
  const perLabel = 56
@@ -194,7 +193,7 @@ const tooltip = computed(() => {
194
193
  value: s.points[index] ?? 0,
195
194
  }))
196
195
  .filter(row => row.value > 0),
197
- // Am Rand kippt der Kasten nach innen, statt aus der Karte zu laufen.
196
+ // Near the edge the box flips inward instead of running off the card.
198
197
  anchor: PADDING.left + plot.value.band * (index + 0.5),
199
198
  }
200
199
  })
@@ -218,7 +217,7 @@ function readout(index: number): string {
218
217
  :aria-label="props.ariaLabel"
219
218
  class="block max-w-full overflow-visible"
220
219
  >
221
- <!-- Gitter: durchgezogene Haarlinien, eine Stufe von der Fläche weg. -->
220
+ <!-- Grid: solid hairlines, one step away from the surface. -->
222
221
  <g>
223
222
  <line
224
223
  v-for="tick in ticks"
@@ -242,7 +241,7 @@ function readout(index: number): string {
242
241
  >{{ tick.label }}</text>
243
242
  </g>
244
243
 
245
- <!-- Datenmarken -->
244
+ <!-- Data marks -->
246
245
  <g
247
246
  v-for="column in columns"
248
247
  :key="column.category"
@@ -257,8 +256,8 @@ function readout(index: number): string {
257
256
  />
258
257
  </g>
259
258
 
260
- <!-- Trefferflächen: über die volle Höhe und über die ganze Bandbreite,
261
- damit niemand eine 14 px schmale Säule treffen muss. -->
259
+ <!-- Hit areas: full height and the whole band, so nobody has to hit a
260
+ 14px column. -->
262
261
  <g>
263
262
  <rect
264
263
  v-for="column in columns"
@@ -279,7 +278,7 @@ function readout(index: number): string {
279
278
  />
280
279
  </g>
281
280
 
282
- <!-- Achsenbeschriftung -->
281
+ <!-- Axis labels -->
283
282
  <text
284
283
  v-for="label in axisLabels"
285
284
  :key="`label-${label.category}`"
@@ -291,7 +290,7 @@ function readout(index: number): string {
291
290
  >{{ label.text }}</text>
292
291
  </svg>
293
292
 
294
- <!-- Tooltip: Wert führt, Reihenname folgt. -->
293
+ <!-- Tooltip: the value leads, the series name follows. -->
295
294
  <div
296
295
  v-if="tooltip"
297
296
  class="pointer-events-none absolute top-0 z-10 min-w-40 -translate-x-1/2 rounded-md border border-default bg-default p-2 shadow-lg"
@@ -1,14 +1,14 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Ring für Teil-vom-Ganzen bei wenigen Klassen.
3
+ * A ring for part-of-whole across a few classes.
4
4
  *
5
- * Bewusst eng eingesetzt: ein Ring beantwortet „wie verteilt sich das grob",
6
- * nicht „welcher von zweien ist größer". Sobald zwei Segmente nahe beieinander
7
- * liegen, ist ein Balken die ehrlichere Form. Höchstens vier Segmente, sonst
8
- * verlieren die kleinen ihre Beschriftung.
5
+ * Deliberately narrow in scope: a ring answers "roughly how does this split",
6
+ * not "which of these two is bigger". As soon as two segments sit close
7
+ * together, a bar is the more honest shape. Four segments at most, otherwise
8
+ * the small ones lose their labels.
9
9
  *
10
- * In der Mitte steht die Summedas ist die Zahl, die zuerst gesucht wird,
11
- * und sie füllt den Platz, den ein Ring ohnehin frei lässt.
10
+ * The total goes in the middleit is the number people look for first, and
11
+ * it fills the space a ring leaves empty anyway.
12
12
  */
13
13
  import { computed, ref } from 'vue'
14
14
  import { baseKitChartColor, useChartFormat } from '../../composables/useChartPalette'
@@ -21,7 +21,7 @@ interface DonutSlice {
21
21
 
22
22
  const props = withDefaults(defineProps<{
23
23
  slices: DonutSlice[]
24
- /** Beschriftung unter der Summe in der Mitte. */
24
+ /** Label below the total in the centre. */
25
25
  centerLabel?: string
26
26
  size?: number
27
27
  ariaLabel?: string
@@ -34,7 +34,7 @@ const props = withDefaults(defineProps<{
34
34
  const { number, percent } = useChartFormat()
35
35
 
36
36
  const THICKNESS = 18
37
- /** Lücke zwischen den Segmenten, in Graddas Gegenstück zur 2-px-Lücke. */
37
+ /** Gap between segments, in degreesthe counterpart to the 2px gap. */
38
38
  const GAP_DEGREES = 2
39
39
 
40
40
  const active = ref<string | null>(null)
@@ -61,8 +61,8 @@ const arcs = computed(() => {
61
61
  const start = cursor
62
62
  cursor += sweep
63
63
 
64
- // Ein Segment, das den ganzen Kreis füllt, hätte identischen Start- und
65
- // Endpunkt der Bogen verschwände. Dann zeichnen wir den vollen Ring.
64
+ // A segment filling the whole circle would have identical start and end
65
+ // points and the arc would vanish. Draw the full ring instead.
66
66
  const full = sweep >= 359.9
67
67
  const gap = full ? 0 : Math.min(GAP_DEGREES, sweep / 3)
68
68
  const from = start + gap / 2
@@ -95,10 +95,10 @@ const legend = computed(() =>
95
95
  </script>
96
96
 
97
97
  <template>
98
- <!-- Legende unter dem Ring, nicht daneben: die Karte steht auf der
99
- Übersicht in einer schmalen Spalte, und nebeneinander blieb für die
100
- Beschriftungen so wenig übrig, dass „Ausgeschieden" auf ein „A"
101
- zusammenschrumpfte. Untereinander hat jede Zeile die volle Breite. -->
98
+ <!-- Legend below the ring, not beside it: the card often sits in a narrow
99
+ column, and side by side so little was left for the labels that a word
100
+ like "Discontinued" shrank to a "D". Stacked, every row has the full
101
+ width. -->
102
102
  <div class="flex flex-col items-center gap-5">
103
103
  <svg
104
104
  :width="props.size"
@@ -108,8 +108,8 @@ const legend = computed(() =>
108
108
  :aria-label="props.ariaLabel"
109
109
  class="shrink-0"
110
110
  >
111
- <!-- Leerer Ring, wenn nichts da ist: die Form bleibt erkennbar, statt
112
- dass die Karte auf eine leere Fläche zusammenfällt. -->
111
+ <!-- An empty ring when there is nothing: the shape stays recognisable
112
+ instead of the card collapsing into a blank area. -->
113
113
  <circle
114
114
  v-if="!arcs.length"
115
115
  :cx="center"
@@ -156,7 +156,7 @@ const legend = computed(() =>
156
156
  >{{ props.centerLabel }}</text>
157
157
  </svg>
158
158
 
159
- <!-- Legende trägt die Identität, nicht die Farbe allein. -->
159
+ <!-- The legend carries the identity, not the colour alone. -->
160
160
  <ul class="w-full space-y-1.5">
161
161
  <li
162
162
  v-for="entry in legend"