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.
@@ -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
  }>(), {
@@ -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"