@eifi1/ui-kit 0.16.1 → 0.18.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.
Files changed (186) hide show
  1. package/README.md +34 -9
  2. package/dist/chart.d.ts +10 -1
  3. package/dist/components/account-chips.d.ts +153 -0
  4. package/dist/components/account-chips.js +135 -0
  5. package/dist/components/account-chips.js.map +1 -0
  6. package/dist/components/account-settings-labels.d.ts +10 -2
  7. package/dist/components/account-settings-labels.js +3 -0
  8. package/dist/components/account-settings-labels.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +18 -1
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/button-group.d.ts +10 -1
  12. package/dist/components/calculator.d.ts +10 -1
  13. package/dist/components/chip.d.ts +25 -1
  14. package/dist/components/chip.js +143 -6
  15. package/dist/components/chip.js.map +1 -1
  16. package/dist/components/confirm-dialog.d.ts +42 -1
  17. package/dist/components/confirm-dialog.js +36 -5
  18. package/dist/components/confirm-dialog.js.map +1 -1
  19. package/dist/components/copy-button.d.ts +10 -1
  20. package/dist/components/danger-confirm.d.ts +55 -4
  21. package/dist/components/danger-confirm.js +67 -30
  22. package/dist/components/danger-confirm.js.map +1 -1
  23. package/dist/components/data-table-cells.d.ts +1 -1
  24. package/dist/components/data-table-filter-popover.d.ts +1 -1
  25. package/dist/components/data-table-filters.d.ts +1 -1
  26. package/dist/components/data-table.d.ts +1 -1
  27. package/dist/components/data-table.js +93 -3
  28. package/dist/components/data-table.js.map +1 -1
  29. package/dist/components/facing-pair.d.ts +10 -1
  30. package/dist/components/file-button.d.ts +10 -1
  31. package/dist/components/file-dropzone.d.ts +10 -1
  32. package/dist/components/form-actions.d.ts +10 -1
  33. package/dist/components/form-actions.js +35 -1
  34. package/dist/components/form-actions.js.map +1 -1
  35. package/dist/components/line-items.d.ts +30 -6
  36. package/dist/components/line-items.js +77 -33
  37. package/dist/components/line-items.js.map +1 -1
  38. package/dist/components/list.d.ts +13 -1
  39. package/dist/components/list.js.map +1 -1
  40. package/dist/components/money-field.d.ts +10 -1
  41. package/dist/components/number-field.d.ts +11 -2
  42. package/dist/components/number-field.js +23 -22
  43. package/dist/components/number-field.js.map +1 -1
  44. package/dist/components/number-input.d.ts +10 -1
  45. package/dist/components/numpad-sheet.d.ts +10 -1
  46. package/dist/components/passkeys-setting.d.ts +27 -1
  47. package/dist/components/passkeys-setting.js +31 -5
  48. package/dist/components/passkeys-setting.js.map +1 -1
  49. package/dist/components/progress-bar.d.ts +25 -1
  50. package/dist/components/progress-bar.js +21 -4
  51. package/dist/components/progress-bar.js.map +1 -1
  52. package/dist/components/reauth-dialog.d.ts +90 -0
  53. package/dist/components/reauth-dialog.js +158 -0
  54. package/dist/components/reauth-dialog.js.map +1 -0
  55. package/dist/components/series-chart.d.ts +10 -1
  56. package/dist/components/server-wake.d.ts +58 -0
  57. package/dist/components/server-wake.js +58 -0
  58. package/dist/components/server-wake.js.map +1 -0
  59. package/dist/components/settings-fields.d.ts +10 -1
  60. package/dist/components/share-card.d.ts +61 -0
  61. package/dist/components/share-card.js +350 -0
  62. package/dist/components/share-card.js.map +1 -0
  63. package/dist/components/signed-amount.d.ts +12 -3
  64. package/dist/components/signed-amount.js +6 -4
  65. package/dist/components/signed-amount.js.map +1 -1
  66. package/dist/components/status-dot.d.ts +2 -0
  67. package/dist/components/text-link.d.ts +10 -1
  68. package/dist/components/time-input.d.ts +10 -1
  69. package/dist/components/toast.d.ts +10 -1
  70. package/dist/components/toast.js +42 -2
  71. package/dist/components/toast.js.map +1 -1
  72. package/dist/components/toggle-group.js +5 -2
  73. package/dist/components/toggle-group.js.map +1 -1
  74. package/dist/components/ui.d.ts +10 -1
  75. package/dist/components/ui.js +67 -30
  76. package/dist/components/ui.js.map +1 -1
  77. package/dist/components/use-table-state.d.ts +1 -1
  78. package/dist/components/user-avatar.d.ts +2 -0
  79. package/dist/components/write-lock.d.ts +68 -0
  80. package/dist/components/write-lock.js +33 -0
  81. package/dist/components/write-lock.js.map +1 -0
  82. package/dist/{data-table-CfVXtCeG.d.ts → data-table-3mi2R3so.d.ts} +59 -1
  83. package/dist/data-table.d.ts +1 -1
  84. package/dist/hooks/use-file-drop.d.ts +10 -1
  85. package/dist/hooks/use-overlay-history.js +16 -0
  86. package/dist/hooks/use-overlay-history.js.map +1 -1
  87. package/dist/i18n/defaults.d.ts +10 -1
  88. package/dist/i18n/defaults.js +11 -1
  89. package/dist/i18n/defaults.js.map +1 -1
  90. package/dist/i18n/german.d.ts +78 -0
  91. package/dist/i18n/{locales/de.js → german.js} +59 -9
  92. package/dist/i18n/german.js.map +1 -0
  93. package/dist/i18n/kit-labels.d.ts +10 -1
  94. package/dist/i18n/kit-labels.js +5 -0
  95. package/dist/i18n/kit-labels.js.map +1 -1
  96. package/dist/i18n/locales/de-CH.d.ts +23 -8
  97. package/dist/i18n/locales/de-CH.js +7 -3
  98. package/dist/i18n/locales/de-CH.js.map +1 -1
  99. package/dist/i18n/locales/{de.d.ts → en.d.ts} +24 -10
  100. package/dist/i18n/locales/en.js +95 -0
  101. package/dist/i18n/locales/en.js.map +1 -0
  102. package/dist/i18n/locales/es.d.ts +10 -1
  103. package/dist/i18n/locales/es.js +51 -1
  104. package/dist/i18n/locales/es.js.map +1 -1
  105. package/dist/i18n/locales/fr.d.ts +10 -1
  106. package/dist/i18n/locales/fr.js +51 -1
  107. package/dist/i18n/locales/fr.js.map +1 -1
  108. package/dist/i18n/locales/hu.d.ts +10 -1
  109. package/dist/i18n/locales/hu.js +54 -1
  110. package/dist/i18n/locales/hu.js.map +1 -1
  111. package/dist/i18n/locales/it.d.ts +10 -1
  112. package/dist/i18n/locales/it.js +51 -1
  113. package/dist/i18n/locales/it.js.map +1 -1
  114. package/dist/i18n/locales/zh.d.ts +10 -1
  115. package/dist/i18n/locales/zh.js +50 -1
  116. package/dist/i18n/locales/zh.js.map +1 -1
  117. package/dist/i18n/swiss.d.ts +3 -3
  118. package/dist/i18n/swiss.js.map +1 -1
  119. package/dist/index.d.ts +11 -6
  120. package/dist/index.js +33 -1
  121. package/dist/index.js.map +1 -1
  122. package/dist/{kit-labels-DE--LmCk.d.ts → kit-labels-CPbsjlXr.d.ts} +282 -10
  123. package/dist/lib/server-wake.d.ts +130 -0
  124. package/dist/lib/server-wake.js +121 -0
  125. package/dist/lib/server-wake.js.map +1 -0
  126. package/dist/rhf/fields.d.ts +10 -1
  127. package/dist/rhf/form.d.ts +10 -1
  128. package/dist/rhf/line-items.js.map +1 -1
  129. package/dist/rhf.d.ts +10 -1
  130. package/dist/search/global-search.d.ts +24 -4
  131. package/dist/search/global-search.js +31 -3
  132. package/dist/search/global-search.js.map +1 -1
  133. package/dist/search.d.ts +1 -1
  134. package/dist/shell/app-shell.d.ts +10 -1
  135. package/dist/shell/top-bar-brand.d.ts +10 -1
  136. package/dist/shell/topbar-action-menu.d.ts +2 -0
  137. package/dist/shell.d.ts +10 -1
  138. package/dist/wizard/stepper-nav.d.ts +10 -1
  139. package/dist/wizard.d.ts +10 -1
  140. package/package.json +1 -1
  141. package/src/components/account-chips.tsx +302 -0
  142. package/src/components/account-settings-labels.ts +16 -2
  143. package/src/components/amount-input.tsx +8 -0
  144. package/src/components/chip.tsx +239 -6
  145. package/src/components/confirm-dialog.tsx +82 -7
  146. package/src/components/danger-confirm.tsx +118 -33
  147. package/src/components/data-table.tsx +185 -3
  148. package/src/components/form-actions.tsx +93 -4
  149. package/src/components/line-items.tsx +116 -40
  150. package/src/components/list.tsx +13 -1
  151. package/src/components/number-field.tsx +32 -26
  152. package/src/components/passkeys-setting.tsx +68 -7
  153. package/src/components/progress-bar.tsx +40 -4
  154. package/src/components/reauth-dialog.tsx +273 -0
  155. package/src/components/server-wake.tsx +124 -0
  156. package/src/components/share-card.tsx +615 -0
  157. package/src/components/signed-amount.tsx +16 -5
  158. package/src/components/toast.tsx +72 -2
  159. package/src/components/toggle-group.tsx +5 -2
  160. package/src/components/ui.tsx +145 -34
  161. package/src/components/write-lock.tsx +96 -0
  162. package/src/hooks/use-overlay-history.ts +51 -0
  163. package/src/i18n/defaults.ts +10 -0
  164. package/src/i18n/{locales/de.ts → german.ts} +68 -13
  165. package/src/i18n/kit-labels.tsx +22 -2
  166. package/src/i18n/locales/de-CH.ts +17 -8
  167. package/src/i18n/locales/en.ts +116 -0
  168. package/src/i18n/locales/es.ts +51 -0
  169. package/src/i18n/locales/fr.ts +52 -0
  170. package/src/i18n/locales/hu.ts +55 -0
  171. package/src/i18n/locales/it.ts +51 -0
  172. package/src/i18n/locales/zh.ts +50 -0
  173. package/src/i18n/swiss.ts +3 -3
  174. package/src/index.ts +23 -1
  175. package/src/lib/server-wake.ts +270 -0
  176. package/src/rhf/line-items.tsx +2 -1
  177. package/src/search/global-search.tsx +59 -6
  178. package/dist/i18n/locales/de-CH-informal.d.ts +0 -61
  179. package/dist/i18n/locales/de-CH-informal.js +0 -8
  180. package/dist/i18n/locales/de-CH-informal.js.map +0 -1
  181. package/dist/i18n/locales/de-informal.d.ts +0 -82
  182. package/dist/i18n/locales/de-informal.js +0 -37
  183. package/dist/i18n/locales/de-informal.js.map +0 -1
  184. package/dist/i18n/locales/de.js.map +0 -1
  185. package/src/i18n/locales/de-CH-informal.ts +0 -10
  186. package/src/i18n/locales/de-informal.ts +0 -61
@@ -38,6 +38,16 @@ import { useEffect, useRef } from "react";
38
38
  * link while the overlay was open, the router has pushed on top of us, and a blind
39
39
  * `history.back()` there would undo their navigation.
40
40
  *
41
+ * ## Not undoing the owner's own close
42
+ *
43
+ * Nor when the address under the entry changed in the same breath as the close
44
+ * (keksdose dev #584). A page that opens its editor by PUSHING `?row=x` and closes it
45
+ * by REPLACING the clean URL over the top entry rewrites OUR entry — the overlay sits
46
+ * above `?row=x` — and that rewrite is what unmounts the overlay. Going back from there
47
+ * lands on `?row=x` and the router opens the editor that was just closed. See
48
+ * {@link noteAddress}: an address the overlay LIVED THROUGH (a filter the table wrote
49
+ * while its sheet was up) is still unwound, because the user has acted since.
50
+ *
41
51
  * ## The traversal is deferred
42
52
  *
43
53
  * And it is *scheduled* rather than issued, because `history.go()` lands a task
@@ -78,6 +88,8 @@ const stack: OverlayEntry[] = [];
78
88
  */
79
89
  interface PushedEntry {
80
90
  id: string;
91
+ /** The full address of the entry (query and hash included) as of the last time the
92
+ * user acted while it was current — see {@link noteAddress}. */
81
93
  href: string;
82
94
  /** Its overlay is gone but the entry is not — someone above owes this pop. */
83
95
  dead: boolean;
@@ -383,6 +395,30 @@ function currentSentinel(): string | null {
383
395
  return standing;
384
396
  }
385
397
 
398
+ /**
399
+ * The user is about to act: what the current entry's address is NOW is what the
400
+ * overlay standing on it has lived through.
401
+ *
402
+ * The cleanup needs to tell two same-page query rewrites apart, and the address alone
403
+ * cannot. A `DataTable` with `urlSync` replaces `f.*` over the entry while its filter
404
+ * sheet is up, and the sheet is closed LATER, by a press of its own — that entry is
405
+ * still the sheet's and must be unwound (the audit's drift fix, below). A page whose
406
+ * `?row=` drives the overlay replaces the clean URL over the entry and the overlay
407
+ * unmounts BECAUSE of it (keksdose dev #584) — that entry is the owner's now, and
408
+ * unwinding lands on `?row=` beneath and reopens what was just closed. What separates
409
+ * them is whether the user acted in between: the rewrite that closes an overlay is
410
+ * made by the very press that closes it, so it postdates this snapshot.
411
+ *
412
+ * Capture phase, so the snapshot is taken before any handler of the press runs.
413
+ */
414
+ function noteAddress(): void {
415
+ const top = pushed[pushed.length - 1];
416
+ if (!top || top.dead || !stack.some((e) => e.id === top.id)) return;
417
+ const tag = taggedSentinel();
418
+ // Read without `currentSentinel`'s repair: an observation must not write history.
419
+ if (tag === top.id || (tag === null && standing === top.id && isStillOn(top))) top.href = currentHref();
420
+ }
421
+
386
422
  function handlePop() {
387
423
  // A pop MOVES us, so the belief is stale by definition: re-read it from the
388
424
  // entry we have landed on before anything below consults it, or the repair in
@@ -422,6 +458,8 @@ function closeOnPop() {
422
458
  function ensureListening() {
423
459
  if (listening || typeof window === "undefined") return;
424
460
  window.addEventListener("popstate", handlePop);
461
+ window.addEventListener("pointerdown", noteAddress, true);
462
+ window.addEventListener("keydown", noteAddress, true);
425
463
  listening = true;
426
464
  }
427
465
 
@@ -489,6 +527,19 @@ export function useOverlayHistory(open: boolean, onClose: () => void): void {
489
527
  if (consumed.delete(id)) return;
490
528
 
491
529
  const mine = pushed.findIndex((p) => p.id === id);
530
+ // Still our entry — same page, same router `idx` — but its address is not the one
531
+ // the user last acted on: the owner rewrote it as part of closing us (see
532
+ // {@link noteAddress}). The entry is the owner's page now, and the one beneath is
533
+ // the address it replaced — going back would restore exactly what was closed.
534
+ // Nothing to unwind; forget the record rather than re-stamp a page we left.
535
+ if (mine >= 0 && currentHref() !== pushed[mine].href) {
536
+ const tag = taggedSentinel();
537
+ if (tag === id || (tag === null && standing === id && isStillOn(pushed[mine]))) {
538
+ pushed.splice(mine, 1);
539
+ if (standing === id) standing = null;
540
+ return;
541
+ }
542
+ }
492
543
  // Only unwind an entry that is still THE current one. If a router push landed
493
544
  // on top of it, going back would undo the user's navigation instead — and if
494
545
  // a SIBLING overlay's sentinel landed on top, that overlay is about to unwind
@@ -46,6 +46,11 @@ import { DEFAULT_ERROR_BOUNDARY_LABELS } from "../components/error-boundary";
46
46
  import { DEFAULT_AUTHED_IMAGE_LABELS } from "../components/authed-image";
47
47
  import { DEFAULT_IMAGE_GRID_LABELS } from "../components/image-grid";
48
48
  import { DEFAULT_LIGHTBOX_LABELS } from "../components/lightbox";
49
+ import { DEFAULT_WRITE_LOCK_LABELS } from "../components/write-lock";
50
+ import { DEFAULT_ACCOUNT_STATE_LABELS } from "../components/account-chips";
51
+ import { DEFAULT_SHARE_CARD_LABELS } from "../components/share-card";
52
+ import { DEFAULT_REAUTH_DIALOG_LABELS } from "../components/reauth-dialog";
53
+ import { DEFAULT_SERVER_WAKE_LABELS } from "../components/server-wake";
49
54
  import {
50
55
  DEFAULT_APP_SHELL_LABELS,
51
56
  DEFAULT_CALCULATOR_LABELS,
@@ -128,4 +133,9 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
128
133
  authedImage: DEFAULT_AUTHED_IMAGE_LABELS,
129
134
  imageGrid: DEFAULT_IMAGE_GRID_LABELS,
130
135
  lightbox: DEFAULT_LIGHTBOX_LABELS,
136
+ writeLock: DEFAULT_WRITE_LOCK_LABELS,
137
+ accountState: DEFAULT_ACCOUNT_STATE_LABELS,
138
+ shareCard: DEFAULT_SHARE_CARD_LABELS,
139
+ reauthDialog: DEFAULT_REAUTH_DIALOG_LABELS,
140
+ serverWake: DEFAULT_SERVER_WAKE_LABELS,
131
141
  };
@@ -1,16 +1,20 @@
1
- import { formatFileSize } from "../kit-labels";
2
- import type { UiKitLabels } from "../kit-labels";
1
+ import { formatFileSize } from "./kit-labels";
2
+ import type { UiKitLabels } from "./kit-labels";
3
3
 
4
4
  /**
5
- * The kit's words in German: every namespace of {@link UiKitLabels}, for
6
- * `<UiKitProvider labels={UI_KIT_LABELS_DE}>`.
5
+ * The kit's German, formal ("Sie"), written in standard spelling with "ß" — the SOURCE
6
+ * the public `de-CH` catalogue is derived from (via `swiss()`), not a catalogue itself.
7
7
  *
8
- * Self-contained on purpose — nothing but the kit's own `formatFileSize` — so it works
9
- * as a starting point to copy and adjust. Counts and sizes are formatted with
10
- * `de-DE` digits; {@link uiKitLabelsDe} takes another number locale
11
- * (e.g. `"de-CH"` for 1’234) without touching the words.
8
+ * Internal on purpose: it lives outside `src/i18n/locales/` because every file there is
9
+ * a public `@eifi1/ui-kit/i18n/<code>` subpath, and the kit ships ONE German — Swiss,
10
+ * formal (0.18.0 removed `/i18n/de`, `/i18n/de-informal` and `/i18n/de-CH-informal`).
11
+ * The text stays written with "ß" because `swiss()` respells at runtime, so a Swiss app
12
+ * also gets an argument it passes in (a file name, a phrase to type) respelled; writing
13
+ * "ss" here instead would change that behaviour for every function label.
14
+ *
15
+ * Counts and sizes are formatted with `numberLocale`'s digits; the words never change.
12
16
  */
13
- export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
17
+ export function germanLabels(numberLocale: string): UiKitLabels {
14
18
  const num = new Intl.NumberFormat(numberLocale);
15
19
  const n = (value: number) => num.format(value);
16
20
 
@@ -40,8 +44,8 @@ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
40
44
  thread: "Kommentare",
41
45
  empty: "Noch keine Kommentare",
42
46
  loading: "Kommentare werden geladen…",
43
- // The reader's own messages. "Ich" rather than "Sie"/"Du": it is the same word in
44
- // both registers, so the informal catalogue inherits it unchanged.
47
+ // The reader's own messages. "Ich" rather than "Sie": it reads the same whether
48
+ // the app addresses its users with "Sie" or "du", so neither has to override it.
45
49
  you: "Ich",
46
50
  staff: "Team",
47
51
  attachments: "Anhänge",
@@ -91,6 +95,7 @@ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
91
95
  passkeys: {
92
96
  title: "Passkeys",
93
97
  description: "Mit Fingerabdruck, Gesichtserkennung oder Geräte-PIN statt Passwort anmelden.",
98
+ descriptionAlongside: "Mit Fingerabdruck, Gesichtserkennung oder Geräte-PIN anmelden. Das Passwort funktioniert weiterhin.",
94
99
  empty: "Noch keine Passkeys",
95
100
  loading: "Passkeys werden geladen…",
96
101
  list: "Registrierte Passkeys",
@@ -506,6 +511,7 @@ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
506
511
  confirmDialog: {
507
512
  confirm: "Bestätigen",
508
513
  cancel: "Abbrechen",
514
+ typed: (text) => `Geben Sie zur Bestätigung „${text}“ ein`,
509
515
  },
510
516
  floatingPanel: {
511
517
  close: "Schließen",
@@ -591,7 +597,56 @@ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
591
597
  noPreview: "Für diese Datei ist keine Vorschau verfügbar",
592
598
  openInNewTab: "In neuem Tab öffnen",
593
599
  },
600
+ writeLock: {
601
+ // Impersonal, like the rest of this catalogue where it can be: an app that
602
+ // addresses its users with "du" then has fewer keys to override.
603
+ reason: "Nur zum Ansehen – Änderungen sind hier nicht möglich.",
604
+ },
605
+ accountState: {
606
+ active: "Aktiv",
607
+ inactive: "Inaktiv",
608
+ invited: "Eingeladen",
609
+ registered: "Registriert",
610
+ unverified: "Nicht bestätigt",
611
+ passwordChange: "Passwortänderung erforderlich",
612
+ },
613
+ shareCard: {
614
+ dialogTitle: "Teilen",
615
+ close: "Schließen",
616
+ email: "E-Mail-Adresse",
617
+ emailOptional: "E-Mail-Adresse (optional)",
618
+ // example.com is reserved for examples (RFC 2606); a German-looking domain is
619
+ // somebody's real one.
620
+ emailPlaceholder: "name@example.com",
621
+ invalidEmail: "Bitte eine vollständige E-Mail-Adresse eingeben.",
622
+ role: "Rolle",
623
+ roleOf: (name) => `Rolle von ${name}`,
624
+ add: "Teilen",
625
+ whoHasAccess: "Wer hat Zugriff",
626
+ nobodyYet: "Bisher hat niemand sonst Zugriff.",
627
+ pending: "Ausstehend",
628
+ openInvite: "Offener Einladungslink",
629
+ copyLink: "Link kopieren",
630
+ team: "Team",
631
+ teamHint: (name) => `Alle in ${name} erhalten Zugriff.`,
632
+ remove: "Zugriff entfernen",
633
+ removeConfirm: (name) => `Zugriff für ${name} entfernen?`,
634
+ revokePending: "Einladung zurückziehen",
635
+ revokePendingConfirm: (name) => `Einladung für ${name} zurückziehen?`,
636
+ failed: "Das hat nicht geklappt. Bitte erneut versuchen.",
637
+ loading: "Wird geladen…",
638
+ },
639
+ reauthDialog: {
640
+ title: "Identität bestätigen",
641
+ description: "Zum Fortfahren das aktuelle Passwort eingeben.",
642
+ password: "Aktuelles Passwort",
643
+ submit: "Weiter",
644
+ cancel: "Abbrechen",
645
+ },
646
+ serverWake: {
647
+ slow: "Lädt noch – das dauert länger als sonst.",
648
+ waking: (appName) =>
649
+ `Der Server schläft ein, wenn ${appName ?? "die App"} gerade niemand benutzt. Die erste Anfrage nach einer Pause muss ihn erst wieder starten. Das kann einen Moment dauern – es geht nichts verloren, die Seite füllt sich von selbst.`,
650
+ },
594
651
  };
595
652
  }
596
-
597
- export const UI_KIT_LABELS_DE: UiKitLabels = uiKitLabelsDe();
@@ -46,6 +46,11 @@ import type { ErrorBoundaryLabels } from "../components/error-boundary";
46
46
  import type { AuthedImageLabels } from "../components/authed-image";
47
47
  import type { ImageGridLabels } from "../components/image-grid";
48
48
  import type { LightboxLabels } from "../components/lightbox";
49
+ import type { WriteLockLabels } from "../components/write-lock";
50
+ import type { AccountStateLabels } from "../components/account-chips";
51
+ import type { ShareCardLabels } from "../components/share-card";
52
+ import type { ReauthDialogLabels } from "../components/reauth-dialog";
53
+ import type { ServerWakeLabels } from "../components/server-wake";
49
54
 
50
55
  /**
51
56
  * EVERY string the kit renders, as one typed tree — and an optional provider that
@@ -274,6 +279,14 @@ export interface UiKitLabels {
274
279
  authedImage: AuthedImageLabels;
275
280
  imageGrid: ImageGridLabels;
276
281
  lightbox: LightboxLabels;
282
+ /** 0.18.0: `WriteLockProvider`'s fallback reason, `AccountStateChip`, `SharePanel` /
283
+ * `ShareCard` / `ShareDialog`, `ReauthDialog`. */
284
+ writeLock: WriteLockLabels;
285
+ accountState: AccountStateLabels;
286
+ shareCard: ShareCardLabels;
287
+ reauthDialog: ReauthDialogLabels;
288
+ /** 0.18.0: `ServerWakeNotice` — keksdose's cold-start notice, for every app. */
289
+ serverWake: ServerWakeLabels;
277
290
  }
278
291
 
279
292
  /**
@@ -621,17 +634,24 @@ export function useKitFileLabels(prop?: Partial<FileLabels>): FileLabels {
621
634
  * The dot paths of every key `labels` does NOT supply — the ones a translated app is
622
635
  * still showing in English. Assert on it in the app's own test:
623
636
  *
624
- * expect(missingKitLabels(de)).toEqual([]);
637
+ * expect(missingKitLabels(labels, DEFAULT_UI_KIT_LABELS)).toEqual([]);
625
638
  *
626
639
  * `reference` is the complete tree to check against — pass
627
640
  * {@link DEFAULT_UI_KIT_LABELS} (src/i18n/defaults.ts). It is a parameter rather than an import
628
641
  * because the defaults live beside their components, and this module is imported BY
629
- * those components.
642
+ * those components. Left out (a test file is not type-checked), it throws saying so
643
+ * rather than "Cannot convert undefined or null to object" (keksdose, 0.18).
630
644
  */
631
645
  export function missingKitLabels(
632
646
  labels: UiKitLabelOverrides | undefined,
633
647
  reference: UiKitLabels,
634
648
  ): string[] {
649
+ if (!isRecord(reference)) {
650
+ throw new TypeError(
651
+ "missingKitLabels(labels, reference): pass DEFAULT_UI_KIT_LABELS as the reference — " +
652
+ "it is the tree the labels are checked against.",
653
+ );
654
+ }
635
655
  const missing: string[] = [];
636
656
  for (const [ns, keys] of Object.entries(reference)) {
637
657
  const given = (labels as Record<string, Record<string, unknown> | undefined> | undefined)?.[ns];
@@ -1,15 +1,24 @@
1
1
  import type { UiKitLabels } from "../kit-labels";
2
+ import { germanLabels } from "../german";
2
3
  import { swiss } from "../swiss";
3
- import { uiKitLabelsDe } from "./de";
4
4
 
5
5
  /**
6
- * Swiss Standard German: the German labels with Swiss spelling — "ss" for every "ß"
7
- * (Schliessen, Grösse), which is the one systematic difference in UI text — and
6
+ * The kit's words in German — Swiss Standard German, formal ("Sie"), the kit's ONE German
7
+ * catalogue: `<UiKitProvider labels={UI_KIT_LABELS_DE_CH}>`. Swiss spelling — "ss" for
8
+ * every "ß" (Schliessen, Grösse), which is the one systematic difference in UI text — and
8
9
  * `de-CH` number formatting (1’234.5).
9
10
  *
10
- * Derived rather than duplicated, so a key added to the German labels is Swiss the
11
- * moment it lands. Function labels are wrapped: their RESULT is respelled, so an
12
- * argument the app passes in (a file name, a phrase to type) is respelled too — which
13
- * is what a Swiss app writing "ss" everywhere wants.
11
+ * Derived from the internal German source (`../german`) rather than written out, so a
12
+ * key added there is Swiss the moment it lands. Function labels are wrapped: their
13
+ * RESULT is respelled, so an argument the app passes in (a file name, a phrase to type)
14
+ * is respelled too — which is what a Swiss app writing "ss" everywhere wants.
15
+ *
16
+ * {@link uiKitLabelsDeCh} takes another number locale (e.g. `"de-DE"` for 1.234,5, or
17
+ * `"de-AT"`) without touching the words — the spelling stays Swiss, as the factories of
18
+ * the other catalogues keep their words.
14
19
  */
15
- export const UI_KIT_LABELS_DE_CH: UiKitLabels = swiss(uiKitLabelsDe("de-CH"));
20
+ export function uiKitLabelsDeCh(numberLocale = "de-CH"): UiKitLabels {
21
+ return swiss(germanLabels(numberLocale));
22
+ }
23
+
24
+ export const UI_KIT_LABELS_DE_CH: UiKitLabels = uiKitLabelsDeCh();
@@ -0,0 +1,116 @@
1
+ import { DEFAULT_UI_KIT_LABELS } from "../defaults";
2
+ import { formatFileSize } from "../kit-labels";
3
+ import type { UiKitLabels } from "../kit-labels";
4
+
5
+ /**
6
+ * The kit's English words with every count and file size formatted for
7
+ * `numberLocale` — `uiKitLabelsEn("en-CH")` reads "12’345 rows", where the defaults
8
+ * print "12345". For `<UiKitProvider labels={uiKitLabelsEn("en-CH")}>`.
9
+ *
10
+ * A factory and not a constant on purpose: {@link DEFAULT_UI_KIT_LABELS} prints counts
11
+ * raw and has since 0.1, so an app that passes nothing must keep seeing exactly that.
12
+ * Formatting is something an app asks for, by calling this.
13
+ *
14
+ * Derived rather than copied: every word is the default's own, spread in, and only the
15
+ * labels that print a number are restated — with the same wording and the same
16
+ * singular/plural split — so a key added to the defaults is here the moment it lands.
17
+ * Row and line numbers (`lineItems.row`, `measuredGrid.cell`) are positions, not
18
+ * counts, and stay as the defaults print them, as in the other catalogues.
19
+ */
20
+ export function uiKitLabelsEn(numberLocale = "en-US"): UiKitLabels {
21
+ const num = new Intl.NumberFormat(numberLocale);
22
+ const n = (value: number) => num.format(value);
23
+ const d = DEFAULT_UI_KIT_LABELS;
24
+
25
+ return {
26
+ ...d,
27
+ feedbackAttachment: {
28
+ ...d.feedbackAttachment,
29
+ attachmentLimit: (max) =>
30
+ `Up to ${n(max)} ${max === 1 ? "attachment" : "attachments"} — remove one to add another.`,
31
+ },
32
+ measuredGrid: {
33
+ ...d.measuredGrid,
34
+ points: (count) => (count === 1 ? "1 point" : `${n(count)} points`),
35
+ problems: (count) =>
36
+ count === 1 ? "1 cell is not a number" : `${n(count)} cells are not numbers`,
37
+ },
38
+ dataTable: {
39
+ ...d.dataTable,
40
+ filterResults: (shown, total) => `${n(shown)} of ${n(total)} rows`,
41
+ pageChanged: (page, totalPages) => `Page ${n(page)} of ${n(totalPages)}`,
42
+ pageRange: (from, to, total) => `${n(from)}–${n(to)} / ${n(total)}`,
43
+ rowCount: (total) => n(total),
44
+ columnsCount: (visible, total) => `Columns (${n(visible)}/${n(total)})`,
45
+ },
46
+ calendarHeatmap: {
47
+ ...d.calendarHeatmap,
48
+ truncated: (count) =>
49
+ `Showing the most recent days; ${n(count)} earlier ${count === 1 ? "day is" : "days are"} not shown.`,
50
+ },
51
+ combobox: {
52
+ ...d.combobox,
53
+ selectedCount: (count) => `${n(count)} selected`,
54
+ resultCount: (count) => (count === 1 ? "1 result" : `${n(count)} results`),
55
+ minChars: (count) =>
56
+ count === 1 ? "Type at least 1 character" : `Type at least ${n(count)} characters`,
57
+ },
58
+ multiSelect: {
59
+ ...d.multiSelect,
60
+ selectedCount: (count) => n(count),
61
+ },
62
+ chipInput: {
63
+ ...d.chipInput,
64
+ atLimit: (max) => `Limit of ${n(max)} reached`,
65
+ },
66
+ iconPicker: {
67
+ ...d.iconPicker,
68
+ resultCount: (count) => (count === 1 ? "1 icon" : `${n(count)} icons`),
69
+ },
70
+ bulkActionBar: {
71
+ ...d.bulkActionBar,
72
+ selected: (count) => `${n(count)} selected`,
73
+ },
74
+ file: {
75
+ // The kit's own `Intl` unit formatting, pinned to this locale ("3.4 MB").
76
+ size: (bytes) => formatFileSize(bytes, numberLocale),
77
+ },
78
+ filePicker: {
79
+ ...d.filePicker,
80
+ rejectedCount: (name, maxFiles) =>
81
+ `“${name}” was not added: at most ${n(maxFiles)} ${maxFiles === 1 ? "file" : "files"}`,
82
+ rejectedMany: (count) => `${n(count)} files were not added`,
83
+ rejectedPick: (count) =>
84
+ count === 1 ? "The file was not added" : `None of the ${n(count)} files were added`,
85
+ selected: (count, firstName) =>
86
+ count === 1 ? `“${firstName}” selected` : `${n(count)} files selected`,
87
+ },
88
+ wizard: {
89
+ ...d.wizard,
90
+ step: (current, total) => `Step ${n(current)} of ${n(total)}`,
91
+ },
92
+ tour: {
93
+ ...d.tour,
94
+ step: (current, total) => `${n(current)} / ${n(total)}`,
95
+ },
96
+ passwordStrength: {
97
+ ...d.passwordStrength,
98
+ ruleLength: (minLength) => `At least ${n(minLength)} characters`,
99
+ tooLong: (maxBytes) =>
100
+ `At most ${n(maxBytes)} characters (accents and emoji count for more than one).`,
101
+ },
102
+ floatingPanel: {
103
+ ...d.floatingPanel,
104
+ badge: (count) => `${n(count)} new`,
105
+ },
106
+ imageGrid: {
107
+ ...d.imageGrid,
108
+ item: (index, count) => `Image ${n(index)} of ${n(count)}`,
109
+ },
110
+ lightbox: {
111
+ ...d.lightbox,
112
+ counter: (index, count) => `${n(index)} / ${n(count)}`,
113
+ position: (index, count) => `Image ${n(index)} of ${n(count)}`,
114
+ },
115
+ };
116
+ }
@@ -88,6 +88,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
88
88
  passkeys: {
89
89
  title: "Llaves de acceso",
90
90
  description: "Inicia sesión con tu huella, tu cara o el PIN del dispositivo en lugar de una contraseña.",
91
+ descriptionAlongside: "Inicia sesión con tu huella, tu cara o el PIN del dispositivo. Tu contraseña sigue funcionando.",
91
92
  empty: "Aún no hay llaves de acceso",
92
93
  loading: "Cargando llaves de acceso…",
93
94
  list: "Tus llaves de acceso",
@@ -495,6 +496,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
495
496
  confirmDialog: {
496
497
  confirm: "Confirmar",
497
498
  cancel: "Cancelar",
499
+ typed: (text) => `Escribe «${text}» para confirmar`,
498
500
  },
499
501
  floatingPanel: {
500
502
  close: "Cerrar",
@@ -580,6 +582,55 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
580
582
  noPreview: "No hay vista previa disponible para este archivo",
581
583
  openInNewTab: "Abrir en una pestaña nueva",
582
584
  },
585
+ writeLock: {
586
+ reason: "Puedes verlo, pero no modificarlo.",
587
+ },
588
+ accountState: {
589
+ // Agreeing with "cuenta" (feminine): the chip is the state of an account.
590
+ active: "Activa",
591
+ inactive: "Inactiva",
592
+ invited: "Invitación enviada",
593
+ registered: "Registrada",
594
+ unverified: "Sin verificar",
595
+ passwordChange: "Debe cambiar la contraseña",
596
+ },
597
+ shareCard: {
598
+ dialogTitle: "Compartir",
599
+ close: "Cerrar",
600
+ email: "Correo electrónico",
601
+ emailOptional: "Correo electrónico (opcional)",
602
+ // example.com is reserved for examples (RFC 2606); a localised domain is real.
603
+ emailPlaceholder: "nombre@example.com",
604
+ invalidEmail: "Introduce una dirección de correo completa.",
605
+ role: "Rol",
606
+ roleOf: (name) => `Rol de ${name}`,
607
+ add: "Compartir",
608
+ whoHasAccess: "Quién tiene acceso",
609
+ nobodyYet: "Nadie más tiene acceso todavía.",
610
+ pending: "Pendiente",
611
+ openInvite: "Enlace de invitación abierto",
612
+ copyLink: "Copiar enlace",
613
+ team: "Equipo",
614
+ teamHint: (name) => `Todos los miembros de ${name} tendrán acceso.`,
615
+ remove: "Quitar acceso",
616
+ removeConfirm: (name) => `¿Quitar el acceso a ${name}?`,
617
+ revokePending: "Retirar invitación",
618
+ revokePendingConfirm: (name) => `¿Retirar la invitación para ${name}?`,
619
+ failed: "No ha funcionado. Inténtalo de nuevo.",
620
+ loading: "Cargando…",
621
+ },
622
+ reauthDialog: {
623
+ title: "Confirma que eres tú",
624
+ description: "Introduce tu contraseña actual para continuar.",
625
+ password: "Contraseña actual",
626
+ submit: "Continuar",
627
+ cancel: "Cancelar",
628
+ },
629
+ serverWake: {
630
+ slow: "Sigue cargando: está tardando más de lo habitual.",
631
+ waking: (appName) =>
632
+ `El servidor se duerme cuando nadie usa ${appName ?? "la aplicación"}, así que la primera solicitud tras una pausa tiene que volver a arrancarlo. Puede tardar un momento; no se pierde nada y la página se completará sola.`,
633
+ },
583
634
  };
584
635
  }
585
636
 
@@ -90,6 +90,8 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
90
90
  title: "Clés d’accès",
91
91
  description:
92
92
  "Connectez-vous avec votre empreinte, votre visage ou le code PIN de l’appareil plutôt qu’avec un mot de passe.",
93
+ descriptionAlongside:
94
+ "Connectez-vous avec votre empreinte, votre visage ou le code PIN de l’appareil. Votre mot de passe reste valable.",
93
95
  empty: "Aucune clé d’accès pour l’instant",
94
96
  loading: "Chargement des clés d’accès…",
95
97
  list: "Vos clés d’accès",
@@ -511,6 +513,7 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
511
513
  confirmDialog: {
512
514
  confirm: "Confirmer",
513
515
  cancel: "Annuler",
516
+ typed: (text) => `Saisissez «\u202f${text}\u202f» pour confirmer`,
514
517
  },
515
518
  floatingPanel: {
516
519
  close: "Fermer",
@@ -596,6 +599,55 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
596
599
  noPreview: "Aucun aperçu disponible pour ce fichier",
597
600
  openInNewTab: "Ouvrir dans un nouvel onglet",
598
601
  },
602
+ writeLock: {
603
+ reason: "Vous pouvez consulter ceci, mais pas le modifier.",
604
+ },
605
+ accountState: {
606
+ // Agreeing with "compte" (masculine): the chip is the state of an account.
607
+ active: "Actif",
608
+ inactive: "Inactif",
609
+ invited: "Invité",
610
+ registered: "Inscrit",
611
+ unverified: "Non vérifié",
612
+ passwordChange: "Doit changer de mot de passe",
613
+ },
614
+ shareCard: {
615
+ dialogTitle: "Partager",
616
+ close: "Fermer",
617
+ email: "Adresse e-mail",
618
+ emailOptional: "Adresse e-mail (facultatif)",
619
+ // example.com is reserved for examples (RFC 2606); a localised domain is real.
620
+ emailPlaceholder: "nom@example.com",
621
+ invalidEmail: "Saisissez une adresse e-mail complète.",
622
+ role: "Rôle",
623
+ roleOf: (name) => `Rôle de ${name}`,
624
+ add: "Partager",
625
+ whoHasAccess: "Qui a accès",
626
+ nobodyYet: "Personne d’autre n’y a encore accès.",
627
+ pending: "En attente",
628
+ openInvite: "Lien d’invitation ouvert",
629
+ copyLink: "Copier le lien",
630
+ team: "Équipe",
631
+ teamHint: (name) => `Tous les membres de ${name} y auront accès.`,
632
+ remove: "Retirer l’accès",
633
+ removeConfirm: (name) => `Retirer l’accès de ${name}\u202f?`,
634
+ revokePending: "Révoquer l’invitation",
635
+ revokePendingConfirm: (name) => `Révoquer l’invitation de ${name}\u202f?`,
636
+ failed: "Cela n’a pas fonctionné. Veuillez réessayer.",
637
+ loading: "Chargement…",
638
+ },
639
+ reauthDialog: {
640
+ title: "Confirmez votre identité",
641
+ description: "Saisissez votre mot de passe actuel pour continuer.",
642
+ password: "Mot de passe actuel",
643
+ submit: "Continuer",
644
+ cancel: "Annuler",
645
+ },
646
+ serverWake: {
647
+ slow: "Chargement en cours — cela prend plus de temps que d’habitude.",
648
+ waking: (appName) =>
649
+ `Le serveur se met en veille quand personne n’utilise ${appName ?? "l’application"}, donc la première requête après une pause doit le redémarrer. Cela peut prendre un moment — rien n’est perdu, la page se remplira d’elle-même.`,
650
+ },
599
651
  };
600
652
  }
601
653
 
@@ -88,6 +88,8 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
88
88
  title: "Hozzáférési kulcsok",
89
89
  description:
90
90
  "Jelentkezzen be ujjlenyomattal, arcfelismeréssel vagy az eszköz PIN-kódjával jelszó helyett.",
91
+ descriptionAlongside:
92
+ "Jelentkezzen be ujjlenyomattal, arcfelismeréssel vagy az eszköz PIN-kódjával. A jelszava továbbra is működik.",
91
93
  empty: "Még nincs hozzáférési kulcs",
92
94
  loading: "Hozzáférési kulcsok betöltése…",
93
95
  list: "Az Ön hozzáférési kulcsai",
@@ -497,6 +499,7 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
497
499
  confirmDialog: {
498
500
  confirm: "Megerősítés",
499
501
  cancel: "Mégse",
502
+ typed: (text) => `A megerősítéshez írja be: „${text}”`,
500
503
  },
501
504
  floatingPanel: {
502
505
  close: "Bezárás",
@@ -582,6 +585,58 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
582
585
  noPreview: "Ehhez a fájlhoz nincs előnézet",
583
586
  openInNewTab: "Megnyitás új lapon",
584
587
  },
588
+ writeLock: {
589
+ reason: "Ezt megtekintheti, de nem módosíthatja.",
590
+ },
591
+ accountState: {
592
+ active: "Aktív",
593
+ inactive: "Inaktív",
594
+ invited: "Meghívva",
595
+ registered: "Regisztrált",
596
+ unverified: "Nincs megerősítve",
597
+ passwordChange: "Jelszócsere szükséges",
598
+ },
599
+ shareCard: {
600
+ dialogTitle: "Megosztás",
601
+ close: "Bezárás",
602
+ email: "E-mail-cím",
603
+ emailOptional: "E-mail-cím (nem kötelező)",
604
+ // example.com is reserved for examples (RFC 2606); a localised domain is real.
605
+ emailPlaceholder: "nev@example.com",
606
+ invalidEmail: "Adjon meg egy teljes e-mail-címet.",
607
+ role: "Szerepkör",
608
+ // The name after a colon, never inflected: the article before it (a/az) and a
609
+ // possessive suffix on it both depend on how the name sounds. As `deleteConfirm`.
610
+ roleOf: (name) => `Szerepkör (${name})`,
611
+ add: "Megosztás",
612
+ whoHasAccess: "Hozzáféréssel rendelkezők",
613
+ nobodyYet: "Még senki más nem fér hozzá.",
614
+ pending: "Függőben",
615
+ openInvite: "Nyitott meghívólink",
616
+ copyLink: "Link másolása",
617
+ team: "Csapat",
618
+ teamHint: (name) => `A(z) ${name} minden tagja hozzáférést kap.`,
619
+ remove: "Hozzáférés megvonása",
620
+ removeConfirm: (name) => `Megvonja a hozzáférést: ${name}?`,
621
+ revokePending: "Meghívás visszavonása",
622
+ revokePendingConfirm: (name) => `Visszavonja a meghívást: ${name}?`,
623
+ failed: "Ez nem sikerült. Kérjük, próbálja újra.",
624
+ loading: "Betöltés…",
625
+ },
626
+ reauthDialog: {
627
+ title: "Személyazonosság megerősítése",
628
+ description: "A folytatáshoz adja meg jelenlegi jelszavát.",
629
+ password: "Jelenlegi jelszó",
630
+ submit: "Folytatás",
631
+ cancel: "Mégse",
632
+ },
633
+ serverWake: {
634
+ slow: "Még tölt – ez a szokásosnál tovább tart.",
635
+ // The name stays in the nominative behind "a(z) … alkalmazást", as in
636
+ // `confirmRemove`: a Hungarian case ending on an arbitrary product name is a guess.
637
+ waking: (appName) =>
638
+ `A szerver elalszik, ha senki sem használja ${appName ? `a(z) ${appName} alkalmazást` : "az alkalmazást"}, ezért egy szünet utáni első kérésnek újra el kell indítania. Ez eltarthat egy ideig – semmi sem vész el, az oldal magától betöltődik.`,
639
+ },
585
640
  };
586
641
  }
587
642