@eifi1/ui-kit 0.6.1 → 0.7.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 (263) hide show
  1. package/README.md +69 -33
  2. package/dist/chart.d.ts +1 -0
  3. package/dist/components/account-settings.js +3 -2
  4. package/dist/components/account-settings.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +3 -0
  6. package/dist/components/amount-input.js +6 -4
  7. package/dist/components/amount-input.js.map +1 -1
  8. package/dist/components/autocomplete.d.ts +33 -6
  9. package/dist/components/autocomplete.js +62 -16
  10. package/dist/components/autocomplete.js.map +1 -1
  11. package/dist/components/calculator.d.ts +3 -0
  12. package/dist/components/calculator.js +2 -1
  13. package/dist/components/calculator.js.map +1 -1
  14. package/dist/components/chart-zoom.js +2 -1
  15. package/dist/components/chart-zoom.js.map +1 -1
  16. package/dist/components/chart.d.ts +33 -2
  17. package/dist/components/chart.js +20 -8
  18. package/dist/components/chart.js.map +1 -1
  19. package/dist/components/chip.d.ts +34 -14
  20. package/dist/components/chip.js +63 -28
  21. package/dist/components/chip.js.map +1 -1
  22. package/dist/components/choice-card.d.ts +18 -1
  23. package/dist/components/choice-card.js +21 -8
  24. package/dist/components/choice-card.js.map +1 -1
  25. package/dist/components/combobox-core.d.ts +61 -2
  26. package/dist/components/combobox-core.js +68 -17
  27. package/dist/components/combobox-core.js.map +1 -1
  28. package/dist/components/combobox.d.ts +11 -7
  29. package/dist/components/combobox.js +51 -28
  30. package/dist/components/combobox.js.map +1 -1
  31. package/dist/components/currency-select.js +3 -3
  32. package/dist/components/currency-select.js.map +1 -1
  33. package/dist/components/danger-confirm.d.ts +30 -6
  34. package/dist/components/danger-confirm.js +51 -32
  35. package/dist/components/danger-confirm.js.map +1 -1
  36. package/dist/components/data-table-filter-popover.d.ts +1 -1
  37. package/dist/components/data-table-filters.d.ts +1 -1
  38. package/dist/components/data-table-pagination.d.ts +6 -3
  39. package/dist/components/data-table-pagination.js +6 -5
  40. package/dist/components/data-table-pagination.js.map +1 -1
  41. package/dist/components/data-table.d.ts +1 -1
  42. package/dist/components/data-table.js +332 -248
  43. package/dist/components/data-table.js.map +1 -1
  44. package/dist/components/date-picker.js +17 -7
  45. package/dist/components/date-picker.js.map +1 -1
  46. package/dist/components/dialog-frame.d.ts +28 -3
  47. package/dist/components/dialog-frame.js +47 -11
  48. package/dist/components/dialog-frame.js.map +1 -1
  49. package/dist/components/disclosure.d.ts +22 -2
  50. package/dist/components/disclosure.js +41 -8
  51. package/dist/components/disclosure.js.map +1 -1
  52. package/dist/components/dropdown.d.ts +7 -3
  53. package/dist/components/dropdown.js +6 -2
  54. package/dist/components/dropdown.js.map +1 -1
  55. package/dist/components/entity-combobox.d.ts +17 -8
  56. package/dist/components/entity-combobox.js +7 -5
  57. package/dist/components/entity-combobox.js.map +1 -1
  58. package/dist/components/file-button.d.ts +36 -5
  59. package/dist/components/file-button.js +23 -2
  60. package/dist/components/file-button.js.map +1 -1
  61. package/dist/components/file-dropzone.d.ts +9 -5
  62. package/dist/components/file-dropzone.js +14 -19
  63. package/dist/components/file-dropzone.js.map +1 -1
  64. package/dist/components/grouped-picker.js +2 -2
  65. package/dist/components/grouped-picker.js.map +1 -1
  66. package/dist/components/hover-menu.d.ts +8 -3
  67. package/dist/components/hover-menu.js +88 -4
  68. package/dist/components/hover-menu.js.map +1 -1
  69. package/dist/components/icon-picker.d.ts +10 -1
  70. package/dist/components/icon-picker.js +5 -1
  71. package/dist/components/icon-picker.js.map +1 -1
  72. package/dist/components/measured-grid.d.ts +165 -0
  73. package/dist/components/measured-grid.js +559 -0
  74. package/dist/components/measured-grid.js.map +1 -0
  75. package/dist/components/mini-calendar.js +7 -8
  76. package/dist/components/mini-calendar.js.map +1 -1
  77. package/dist/components/month-picker.js +13 -9
  78. package/dist/components/month-picker.js.map +1 -1
  79. package/dist/components/multi-entity-combobox.js +2 -2
  80. package/dist/components/multi-entity-combobox.js.map +1 -1
  81. package/dist/components/multi-select.d.ts +9 -1
  82. package/dist/components/multi-select.js +22 -8
  83. package/dist/components/multi-select.js.map +1 -1
  84. package/dist/components/number-field.d.ts +3 -0
  85. package/dist/components/number-input.d.ts +3 -0
  86. package/dist/components/number-input.js +3 -3
  87. package/dist/components/number-input.js.map +1 -1
  88. package/dist/components/numpad-sheet.d.ts +3 -0
  89. package/dist/components/numpad-sheet.js +3 -3
  90. package/dist/components/numpad-sheet.js.map +1 -1
  91. package/dist/components/picker-sheet.js +1 -1
  92. package/dist/components/picker-sheet.js.map +1 -1
  93. package/dist/components/popover.js +11 -2
  94. package/dist/components/popover.js.map +1 -1
  95. package/dist/components/series-chart-ticks.d.ts +13 -0
  96. package/dist/components/series-chart-ticks.js +29 -0
  97. package/dist/components/series-chart-ticks.js.map +1 -0
  98. package/dist/components/series-chart.js +33 -27
  99. package/dist/components/series-chart.js.map +1 -1
  100. package/dist/components/swatch-picker.d.ts +10 -1
  101. package/dist/components/swatch-picker.js +2 -0
  102. package/dist/components/swatch-picker.js.map +1 -1
  103. package/dist/components/swipeable-row.d.ts +11 -2
  104. package/dist/components/swipeable-row.js +5 -3
  105. package/dist/components/swipeable-row.js.map +1 -1
  106. package/dist/components/tile-radio.js +5 -7
  107. package/dist/components/tile-radio.js.map +1 -1
  108. package/dist/components/toggle-group.d.ts +13 -3
  109. package/dist/components/toggle-group.js +21 -1
  110. package/dist/components/toggle-group.js.map +1 -1
  111. package/dist/components/toggle-legend.js +1 -2
  112. package/dist/components/toggle-legend.js.map +1 -1
  113. package/dist/components/treemap.d.ts +6 -1
  114. package/dist/components/treemap.js +16 -6
  115. package/dist/components/treemap.js.map +1 -1
  116. package/dist/components/ui.d.ts +31 -7
  117. package/dist/components/ui.js +26 -18
  118. package/dist/components/ui.js.map +1 -1
  119. package/dist/components/use-anchor-dir.d.ts +15 -0
  120. package/dist/components/use-anchor-dir.js +14 -0
  121. package/dist/components/use-anchor-dir.js.map +1 -0
  122. package/dist/components/use-mobile-reveal.d.ts +8 -1
  123. package/dist/components/use-mobile-reveal.js +3 -2
  124. package/dist/components/use-mobile-reveal.js.map +1 -1
  125. package/dist/components/use-table-state.d.ts +4 -4
  126. package/dist/components/use-table-state.js +12 -5
  127. package/dist/components/use-table-state.js.map +1 -1
  128. package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
  129. package/dist/data-table.d.ts +1 -1
  130. package/dist/data-table.js.map +1 -1
  131. package/dist/feedback/feedback-attachment.d.ts +22 -3
  132. package/dist/feedback/feedback-attachment.js +15 -7
  133. package/dist/feedback/feedback-attachment.js.map +1 -1
  134. package/dist/feedback/feedback-dialog.d.ts +3 -2
  135. package/dist/feedback/feedback-dialog.js.map +1 -1
  136. package/dist/feedback.d.ts +1 -1
  137. package/dist/hooks/use-windowed-rows.d.ts +29 -0
  138. package/dist/hooks/use-windowed-rows.js +31 -0
  139. package/dist/hooks/use-windowed-rows.js.map +1 -0
  140. package/dist/i18n/defaults.d.ts +3 -0
  141. package/dist/i18n/defaults.js +5 -1
  142. package/dist/i18n/defaults.js.map +1 -1
  143. package/dist/i18n/kit-labels.d.ts +5 -0
  144. package/dist/i18n/kit-labels.js.map +1 -1
  145. package/dist/i18n/locales/de-CH.d.ts +43 -0
  146. package/dist/i18n/locales/de-CH.js +21 -0
  147. package/dist/i18n/locales/de-CH.js.map +1 -0
  148. package/dist/i18n/locales/de.d.ts +43 -0
  149. package/dist/i18n/locales/de.js +344 -0
  150. package/dist/i18n/locales/de.js.map +1 -0
  151. package/dist/i18n/locales/es.d.ts +43 -0
  152. package/dist/i18n/locales/es.js +339 -0
  153. package/dist/i18n/locales/es.js.map +1 -0
  154. package/dist/i18n/locales/fr.d.ts +43 -0
  155. package/dist/i18n/locales/fr.js +344 -0
  156. package/dist/i18n/locales/fr.js.map +1 -0
  157. package/dist/i18n/locales/hu.d.ts +43 -0
  158. package/dist/i18n/locales/hu.js +343 -0
  159. package/dist/i18n/locales/hu.js.map +1 -0
  160. package/dist/i18n/locales/it.d.ts +43 -0
  161. package/dist/i18n/locales/it.js +342 -0
  162. package/dist/i18n/locales/it.js.map +1 -0
  163. package/dist/i18n/locales/zh.d.ts +43 -0
  164. package/dist/i18n/locales/zh.js +338 -0
  165. package/dist/i18n/locales/zh.js.map +1 -0
  166. package/dist/index.d.ts +6 -3
  167. package/dist/index.js +4 -1
  168. package/dist/index.js.map +1 -1
  169. package/dist/lib/direction.d.ts +24 -0
  170. package/dist/lib/direction.js +19 -0
  171. package/dist/lib/direction.js.map +1 -0
  172. package/dist/rhf/form.js.map +1 -1
  173. package/dist/search/command-palette.d.ts +5 -0
  174. package/dist/search/command-palette.js +80 -50
  175. package/dist/search/command-palette.js.map +1 -1
  176. package/dist/shell/app-shell.d.ts +13 -1
  177. package/dist/shell/app-shell.js +133 -84
  178. package/dist/shell/app-shell.js.map +1 -1
  179. package/dist/shell/topbar-action-menu.js +1 -1
  180. package/dist/shell/topbar-action-menu.js.map +1 -1
  181. package/dist/shell/topbar-controls.d.ts +7 -3
  182. package/dist/shell/topbar-controls.js +38 -31
  183. package/dist/shell/topbar-controls.js.map +1 -1
  184. package/dist/theme/chart-palette.d.ts +7 -1
  185. package/dist/theme/chart-palette.js +2 -1
  186. package/dist/theme/chart-palette.js.map +1 -1
  187. package/dist/tour/tour.d.ts +11 -5
  188. package/dist/tour/tour.js +124 -58
  189. package/dist/tour/tour.js.map +1 -1
  190. package/dist/wizard/use-wizard.js +30 -13
  191. package/dist/wizard/use-wizard.js.map +1 -1
  192. package/dist/wizard/wizard-summary.js +1 -1
  193. package/dist/wizard/wizard-summary.js.map +1 -1
  194. package/package.json +12 -2
  195. package/src/components/account-settings.tsx +6 -2
  196. package/src/components/amount-input.tsx +10 -8
  197. package/src/components/autocomplete.tsx +110 -24
  198. package/src/components/calculator.tsx +5 -1
  199. package/src/components/chart-zoom.tsx +8 -1
  200. package/src/components/chart.tsx +68 -7
  201. package/src/components/chip.tsx +131 -49
  202. package/src/components/choice-card.tsx +77 -36
  203. package/src/components/combobox-core.tsx +147 -20
  204. package/src/components/combobox.tsx +100 -44
  205. package/src/components/currency-select.tsx +6 -6
  206. package/src/components/danger-confirm.tsx +90 -36
  207. package/src/components/data-table-pagination.tsx +32 -19
  208. package/src/components/data-table.tsx +198 -36
  209. package/src/components/date-picker.tsx +24 -7
  210. package/src/components/dialog-frame.tsx +97 -22
  211. package/src/components/disclosure.tsx +75 -13
  212. package/src/components/dropdown.tsx +17 -5
  213. package/src/components/entity-combobox.tsx +26 -13
  214. package/src/components/file-button.tsx +74 -5
  215. package/src/components/file-dropzone.tsx +44 -35
  216. package/src/components/grouped-picker.tsx +2 -2
  217. package/src/components/hover-menu.tsx +139 -10
  218. package/src/components/icon-picker.tsx +14 -1
  219. package/src/components/measured-grid.tsx +852 -0
  220. package/src/components/mini-calendar.tsx +11 -8
  221. package/src/components/month-picker.tsx +18 -11
  222. package/src/components/multi-entity-combobox.tsx +2 -2
  223. package/src/components/multi-select.tsx +34 -7
  224. package/src/components/number-input.tsx +4 -4
  225. package/src/components/numpad-sheet.tsx +5 -3
  226. package/src/components/picker-sheet.tsx +1 -1
  227. package/src/components/popover.tsx +18 -3
  228. package/src/components/series-chart-ticks.ts +52 -0
  229. package/src/components/series-chart.tsx +39 -31
  230. package/src/components/swatch-picker.tsx +11 -0
  231. package/src/components/swipeable-row.tsx +21 -6
  232. package/src/components/tile-radio.tsx +5 -7
  233. package/src/components/toggle-group.tsx +39 -3
  234. package/src/components/toggle-legend.tsx +5 -2
  235. package/src/components/treemap.tsx +28 -5
  236. package/src/components/ui.tsx +74 -30
  237. package/src/components/use-anchor-dir.ts +20 -0
  238. package/src/components/use-mobile-reveal.ts +10 -2
  239. package/src/components/use-table-state.ts +44 -7
  240. package/src/data-table.ts +1 -0
  241. package/src/feedback/feedback-attachment.tsx +36 -9
  242. package/src/feedback/feedback-dialog.tsx +3 -2
  243. package/src/hooks/use-windowed-rows.ts +69 -0
  244. package/src/i18n/defaults.ts +4 -0
  245. package/src/i18n/kit-labels.tsx +4 -0
  246. package/src/i18n/locales/de-CH.ts +30 -0
  247. package/src/i18n/locales/de.ts +365 -0
  248. package/src/i18n/locales/es.ts +358 -0
  249. package/src/i18n/locales/fr.ts +371 -0
  250. package/src/i18n/locales/hu.ts +359 -0
  251. package/src/i18n/locales/it.ts +360 -0
  252. package/src/i18n/locales/zh.ts +348 -0
  253. package/src/index.ts +10 -1
  254. package/src/lib/direction.ts +37 -0
  255. package/src/rhf/form.tsx +21 -0
  256. package/src/search/command-palette.tsx +77 -19
  257. package/src/shell/app-shell.tsx +200 -129
  258. package/src/shell/topbar-action-menu.tsx +1 -1
  259. package/src/shell/topbar-controls.tsx +19 -8
  260. package/src/theme/chart-palette.ts +9 -2
  261. package/src/tour/tour.tsx +119 -18
  262. package/src/wizard/use-wizard.ts +55 -17
  263. package/src/wizard/wizard-summary.tsx +1 -1
@@ -2,12 +2,36 @@ import { cn } from "../lib/cn";
2
2
  import { useEffect, useRef, useState, type RefObject } from "react";
3
3
  import { Camera, FileText, Paperclip, X } from "lucide-react";
4
4
  import { Button } from "../components/ui";
5
- import { useKitFileLabels } from "../i18n/kit-labels";
5
+ import { useKitFileLabels, useKitLabels } from "../i18n/kit-labels";
6
6
  import type { FeedbackAttachmentLabels } from "./feedback-dialog";
7
7
 
8
8
  export const DEFAULT_ATTACHMENT_ACCEPT = ["image/png", "image/jpeg", "image/webp", "image/gif"];
9
9
  export const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
10
10
 
11
+ /**
12
+ * The field's own strings — the `feedbackAttachment` namespace of `<UiKitProvider
13
+ * labels>`. The keys are {@link FeedbackAttachmentLabels}' own, so the `labels` prop
14
+ * (and a whole `FeedbackDialogLabels` handed down by the dialog) merges over them
15
+ * one to one. The optional heading, `attachment`, stays a prop: a note editor puts
16
+ * the buttons straight under its textarea and wants none.
17
+ *
18
+ * Before 0.7.0 the two optional keys fell back to hard-coded English, so a German app
19
+ * that relied on its provider got "Capture screenshot" under a German form.
20
+ */
21
+ export interface FeedbackAttachmentFieldLabels {
22
+ attachmentAdd: string;
23
+ attachmentCapture: string;
24
+ attachmentPaste: string;
25
+ attachmentRemove: string;
26
+ }
27
+
28
+ export const DEFAULT_FEEDBACK_ATTACHMENT_LABELS: FeedbackAttachmentFieldLabels = {
29
+ attachmentAdd: "Attach image",
30
+ attachmentCapture: "Capture screenshot",
31
+ attachmentPaste: "…or paste a screenshot from the clipboard.",
32
+ attachmentRemove: "Remove attachment",
33
+ };
34
+
11
35
  /**
12
36
  * Picking one picture: the file dialog, a capture of the app view, or a paste.
13
37
  *
@@ -40,7 +64,7 @@ export const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
40
64
  export function FeedbackAttachmentField({
41
65
  value,
42
66
  onChange,
43
- labels,
67
+ labels: labelsProp,
44
68
  accept = DEFAULT_ATTACHMENT_ACCEPT,
45
69
  maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
46
70
  onError,
@@ -51,7 +75,9 @@ export function FeedbackAttachmentField({
51
75
  }: {
52
76
  value: File | null;
53
77
  onChange: (file: File | null) => void;
54
- labels: FeedbackAttachmentLabels;
78
+ /** Prop > `<UiKitProvider labels={{ feedbackAttachment }}>` > English. Optional
79
+ * since 0.7.0; `attachment` (the heading) is only ever read from here. */
80
+ labels?: Partial<FeedbackAttachmentLabels>;
55
81
  accept?: string[];
56
82
  maxBytes?: number;
57
83
  onError?: (kind: "type" | "size") => void;
@@ -73,6 +99,7 @@ export function FeedbackAttachmentField({
73
99
  const fileInputRef = useRef<HTMLInputElement>(null);
74
100
  // `file.size` from `<UiKitProvider labels>`, formatted in its locale — see FileDropzone.
75
101
  const fileText = useKitFileLabels();
102
+ const text = useKitLabels("feedbackAttachment", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);
76
103
 
77
104
  // Object-URL preview lifecycle (create on change, revoke on cleanup).
78
105
  useEffect(() => {
@@ -151,9 +178,9 @@ export function FeedbackAttachmentField({
151
178
  : (event) => takeImage(event.clipboardData, () => event.preventDefault())
152
179
  }
153
180
  >
154
- {labels.attachment && (
181
+ {labelsProp?.attachment && (
155
182
  <div className="mb-1 text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]">
156
- {labels.attachment}
183
+ {labelsProp.attachment}
157
184
  </div>
158
185
  )}
159
186
  {value ? (
@@ -180,7 +207,7 @@ export function FeedbackAttachmentField({
180
207
  <button
181
208
  type="button"
182
209
  onClick={() => onChange(null)}
183
- aria-label={labels.attachmentRemove}
210
+ aria-label={text.attachmentRemove}
184
211
  className="rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
185
212
  >
186
213
  <X className="size-4" />
@@ -190,7 +217,7 @@ export function FeedbackAttachmentField({
190
217
  <div className="space-y-1.5">
191
218
  <div className="flex flex-wrap gap-2">
192
219
  <Button type="button" variant="secondary" onClick={() => fileInputRef.current?.click()}>
193
- <Paperclip className="size-4" /> {labels.attachmentAdd}
220
+ <Paperclip className="size-4" /> {text.attachmentAdd}
194
221
  </Button>
195
222
  {onCaptureScreenshot && (
196
223
  <Button
@@ -208,14 +235,14 @@ export function FeedbackAttachmentField({
208
235
  }}
209
236
  >
210
237
  <Camera className="size-4" />{" "}
211
- {capturing ? "…" : (labels.attachmentCapture ?? "Capture screenshot")}
238
+ {capturing ? "…" : text.attachmentCapture}
212
239
  </Button>
213
240
  )}
214
241
  </div>
215
242
  {/* Said out loud, because a gesture with no affordance is a gesture
216
243
  nobody finds. */}
217
244
  <p className="text-xs text-[var(--text-muted)]">
218
- {labels.attachmentPaste ?? "…or paste a screenshot from the clipboard."}
245
+ {text.attachmentPaste}
219
246
  </p>
220
247
  </div>
221
248
  )}
@@ -19,10 +19,11 @@ export interface FeedbackAttachmentLabels {
19
19
  * straight under its textarea, where a second heading is noise. */
20
20
  attachment?: string;
21
21
  attachmentAdd: string;
22
- /** Label for the "capture screenshot" button. Optional — falls back to an English default. */
22
+ /** Label for the "capture screenshot" button. Optional — falls back to the provider's
23
+ * `feedbackAttachment.attachmentCapture`, then English. */
23
24
  attachmentCapture?: string;
24
25
  /** The line under the attachment buttons saying a screenshot can be pasted
25
- * straight in. Optional — falls back to an English default. */
26
+ * straight in. Optional — falls back to the provider's `feedbackAttachment`, then English. */
26
27
  attachmentPaste?: string;
27
28
  attachmentRemove: string;
28
29
  }
@@ -0,0 +1,69 @@
1
+ import { useEffect, useState } from "react";
2
+ import type { RefObject } from "react";
3
+
4
+ /** Rows kept mounted beyond each edge, so a flick-scroll has something to show
5
+ * before the next render lands. */
6
+ const OVERSCAN = 6;
7
+
8
+ /** What {@link useWindowedRows} answers: render rows `first` up to (not including)
9
+ * `last`, inside a scroll body `totalHeight` pixels tall. */
10
+ export interface WindowedRows {
11
+ first: number;
12
+ last: number;
13
+ totalHeight: number;
14
+ }
15
+
16
+ /**
17
+ * Which rows of a fixed-height list are worth rendering.
18
+ *
19
+ * Hand-rolled rather than pulled from a virtualization library, for two reasons. With
20
+ * a fixed row height the whole problem is two divisions — a library earns its keep on
21
+ * measured, variable-height rows, and there are none here. And the obvious library for
22
+ * it (`@tanstack/react-virtual`) returns functions that cannot be memoized safely, so
23
+ * the React Compiler skips optimising any component that calls it: a list that exists
24
+ * to stay fast would opt out of the consumer's main source of speed.
25
+ *
26
+ * `scrollRef` is the element that scrolls. The rows are assumed to start at its top;
27
+ * a sticky header above them costs a row or two of the overscan, not correctness.
28
+ *
29
+ * Moved from Lenkbank, where `MeasuredGrid` and a segment list window the same way over
30
+ * rows of different heights — two copies of this would be two places to fix the day
31
+ * the overscan turns out to be wrong.
32
+ */
33
+ export function useWindowedRows(
34
+ count: number,
35
+ rowHeight: number,
36
+ scrollRef: RefObject<HTMLElement | null>,
37
+ ): WindowedRows {
38
+ const [scrollTop, setScrollTop] = useState(0);
39
+ const [viewport, setViewport] = useState(0);
40
+
41
+ useEffect(() => {
42
+ const element = scrollRef.current;
43
+ if (!element) return;
44
+ const measure = () => {
45
+ setScrollTop(element.scrollTop);
46
+ setViewport(element.clientHeight);
47
+ };
48
+ measure();
49
+ element.addEventListener("scroll", measure, { passive: true });
50
+ // The pane usually sits in a responsive layout, so its height changes without the
51
+ // window's — a resize listener alone would miss every one of those. Guarded:
52
+ // jsdom and older embedded webviews have no ResizeObserver, and the list still
53
+ // has to render there.
54
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
55
+ observer?.observe(element);
56
+ return () => {
57
+ element.removeEventListener("scroll", measure);
58
+ observer?.disconnect();
59
+ };
60
+ }, [scrollRef]);
61
+
62
+ const first = Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN);
63
+ // A viewport of 0 is the first render, before the pane has been laid out (and every
64
+ // render under jsdom). Rendering nothing then would leave the list blank until
65
+ // something scrolled it, so fall back to a screenful.
66
+ const visible = Math.ceil((viewport || 600) / rowHeight) + OVERSCAN * 2;
67
+ const last = Math.min(count, first + visible);
68
+ return { first, last, totalHeight: count * rowHeight };
69
+ }
@@ -19,6 +19,8 @@ import { DEFAULT_SWATCH_PICKER_LABELS } from "../components/swatch-picker";
19
19
  import { DEFAULT_ICON_PICKER_LABELS } from "../components/icon-picker";
20
20
  import { DEFAULT_DIALOG_FRAME_LABELS } from "../components/dialog-frame";
21
21
  import { DEFAULT_FILE_PICKER_LABELS } from "../components/file-button";
22
+ import { DEFAULT_MEASURED_GRID_LABELS } from "../components/measured-grid";
23
+ import { DEFAULT_FEEDBACK_ATTACHMENT_LABELS } from "../feedback/feedback-attachment";
22
24
  import {
23
25
  DEFAULT_APP_SHELL_LABELS,
24
26
  DEFAULT_CALCULATOR_LABELS,
@@ -76,4 +78,6 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
76
78
  iconPicker: DEFAULT_ICON_PICKER_LABELS,
77
79
  dialogFrame: DEFAULT_DIALOG_FRAME_LABELS,
78
80
  filePicker: DEFAULT_FILE_PICKER_LABELS,
81
+ measuredGrid: DEFAULT_MEASURED_GRID_LABELS,
82
+ feedbackAttachment: DEFAULT_FEEDBACK_ATTACHMENT_LABELS,
79
83
  };
@@ -21,6 +21,8 @@ import type { SwatchPickerLabels } from "../components/swatch-picker";
21
21
  import type { IconPickerLabels } from "../components/icon-picker";
22
22
  import type { DialogFrameLabels } from "../components/dialog-frame";
23
23
  import type { FilePickerLabels } from "../components/file-button";
24
+ import type { MeasuredGridLabels } from "../components/measured-grid";
25
+ import type { FeedbackAttachmentFieldLabels } from "../feedback/feedback-attachment";
24
26
 
25
27
  /**
26
28
  * EVERY string the kit renders, as one typed tree — and an optional provider that
@@ -208,6 +210,8 @@ export interface UiKitLabels {
208
210
  iconPicker: IconPickerLabels;
209
211
  dialogFrame: DialogFrameLabels;
210
212
  filePicker: FilePickerLabels;
213
+ measuredGrid: MeasuredGridLabels;
214
+ feedbackAttachment: FeedbackAttachmentFieldLabels;
211
215
  }
212
216
 
213
217
  /** Any subset of the tree, one level deep — each namespace may be partial, and a
@@ -0,0 +1,30 @@
1
+ import type { UiKitLabels } from "../kit-labels";
2
+ import { uiKitLabelsDe } from "./de";
3
+
4
+ /**
5
+ * Swiss Standard German: the German labels with Swiss spelling — "ss" for every "ß"
6
+ * (Schliessen, Grösse), which is the one systematic difference in UI text — and
7
+ * `de-CH` number formatting (1’234.5).
8
+ *
9
+ * Derived rather than duplicated, so a key added to the German labels is Swiss the
10
+ * moment it lands. Function labels are wrapped: their RESULT is respelled, so an
11
+ * argument the app passes in (a file name, a phrase to type) is respelled too — which
12
+ * is what a Swiss app writing "ss" everywhere wants.
13
+ */
14
+ export const UI_KIT_LABELS_DE_CH: UiKitLabels = swiss(uiKitLabelsDe("de-CH"));
15
+
16
+ function respell(text: string): string {
17
+ return text.replace(/ß/g, "ss").replace(/ẞ/g, "SS");
18
+ }
19
+
20
+ function swiss<T>(value: T): T {
21
+ if (typeof value === "string") return respell(value) as T;
22
+ if (typeof value === "function") {
23
+ const fn = value as (...args: unknown[]) => unknown;
24
+ return ((...args: unknown[]) => swiss(fn(...args))) as T;
25
+ }
26
+ if (value && typeof value === "object") {
27
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, swiss(v)])) as T;
28
+ }
29
+ return value;
30
+ }
@@ -0,0 +1,365 @@
1
+ import { formatFileSize } from "../kit-labels";
2
+ import type { UiKitLabels } from "../kit-labels";
3
+
4
+ /**
5
+ * The kit's words in German: every namespace of {@link UiKitLabels}, for
6
+ * `<UiKitProvider labels={UI_KIT_LABELS_DE}>`.
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.
12
+ */
13
+ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
14
+ const num = new Intl.NumberFormat(numberLocale);
15
+ const n = (value: number) => num.format(value);
16
+
17
+ return {
18
+ feedbackAttachment: {
19
+ attachmentAdd: "Bild anhängen",
20
+ attachmentCapture: "Screenshot aufnehmen",
21
+ attachmentPaste: "…oder einen Screenshot aus der Zwischenablage einfügen.",
22
+ attachmentRemove: "Anhang entfernen",
23
+ },
24
+ measuredGrid: {
25
+ view: "Tabellenansicht",
26
+ cellsView: "Zellen",
27
+ textView: "Text",
28
+ addRow: "Zeile hinzufügen",
29
+ removeRow: (row) => `Zeile ${row} entfernen`,
30
+ clear: "Tabelle leeren",
31
+ pasteHint: "Einen Block aus einer Tabellenkalkulation in eine beliebige Zelle einfügen",
32
+ cell: (column, row) => `${column}, Zeile ${row}`,
33
+ rowNumber: "Zeile",
34
+ rowActions: "Zeilenaktionen",
35
+ keyboardHint:
36
+ "Pfeiltasten wechseln zwischen Zellen. Tippen ersetzt eine Zelle, F2 bearbeitet sie, Escape macht die Bearbeitung rückgängig. Enter geht nach unten und fügt am Ende eine Zeile hinzu.",
37
+ lineError: (line) => `Zeile ${line} konnte nicht gelesen werden`,
38
+ points: (count) => (count === 1 ? "1 Punkt" : `${count} Punkte`),
39
+ problems: (count) =>
40
+ count === 1 ? "1 Zelle ist keine Zahl" : `${count} Zellen sind keine Zahlen`,
41
+ },
42
+ pageContents: { title: "Auf dieser Seite" },
43
+ common: {
44
+ close: "Schließen",
45
+ clear: "Leeren",
46
+ search: "Suchen",
47
+ done: "Fertig",
48
+ cancel: "Abbrechen",
49
+ save: "Speichern",
50
+ back: "Zurück",
51
+ next: "Weiter",
52
+ remove: "Entfernen",
53
+ loading: "Wird geladen…",
54
+ noResults: "Keine Ergebnisse",
55
+ fieldValue: (field, value) => `${field}: ${value}`,
56
+ },
57
+ dataTable: {
58
+ columns: "Spalten",
59
+ selectAllRows: "Alle Zeilen auswählen",
60
+ sortHint: "Klicken zum Sortieren · Umschalt-Klick für weitere Sortierung",
61
+ filter: "Filter",
62
+ close: "Schließen",
63
+ selectRow: "Zeile auswählen",
64
+ autoSize: "Spaltenbreiten anpassen",
65
+ loading: "Wird geladen…",
66
+ filters: "Filter",
67
+ // Resets every active filter; "Alle löschen" would read as deleting the filters.
68
+ clearAll: "Alle zurücksetzen",
69
+ done: "Fertig",
70
+ pageSize: "Zeilen pro Seite",
71
+ // Sits among 10 / 25 / 50 counting "Zeilen", so the plural "Alle", not "Alles".
72
+ pageSizeAll: "Alle",
73
+ prevPage: "Vorherige Seite",
74
+ nextPage: "Nächste Seite",
75
+ clearFilter: "Filter zurücksetzen",
76
+ filterPlaceholder: "Filtern…",
77
+ selectFilter: "Auswählen",
78
+ selectAll: "Alle",
79
+ selectNone: "Keine",
80
+ dateFrom: "Von",
81
+ dateTo: "Bis",
82
+ numberMin: "Min.",
83
+ numberMax: "Max.",
84
+ numberAbs: "Betrag",
85
+ presets: {
86
+ today: "Heute",
87
+ yesterday: "Gestern",
88
+ this_week: "Diese Woche",
89
+ last_week: "Letzte Woche",
90
+ last_7_days: "Letzte 7 Tage",
91
+ last_30_days: "Letzte 30 Tage",
92
+ this_month: "Dieser Monat",
93
+ last_month: "Letzter Monat",
94
+ last_3_months: "Letzte 3 Monate",
95
+ ytd: "Seit Jahresbeginn",
96
+ last_year: "Letztes Jahr",
97
+ },
98
+ table: "Datentabelle",
99
+ filterResults: (shown, total) =>
100
+ `${n(shown)} von ${n(total)} ${total === 1 ? "Zeile" : "Zeilen"}`,
101
+ sortedAscending: (column) => `Sortiert nach ${column}, aufsteigend`,
102
+ sortedDescending: (column) => `Sortiert nach ${column}, absteigend`,
103
+ sortCleared: (column) => `Sortierung nach ${column} aufgehoben`,
104
+ pageChanged: (page, totalPages) => `Seite ${n(page)} von ${n(totalPages)}`,
105
+ pageRange: (from, to, total) => `${n(from)}–${n(to)} / ${n(total)}`,
106
+ rowCount: (total) => n(total),
107
+ columnsCount: (visible, total) => `Spalten (${n(visible)}/${n(total)})`,
108
+ },
109
+ miniCalendar: {
110
+ previousMonth: "Vorheriger Monat",
111
+ nextMonth: "Nächster Monat",
112
+ // Already formatted in the provider's locale ("Montag, 14. September 2026").
113
+ day: (date) => date,
114
+ chooseStart: "Startdatum wählen",
115
+ chooseEnd: "Enddatum wählen",
116
+ startSelected: (date) => `${date} als Startdatum gewählt. Wählen Sie jetzt ein Enddatum.`,
117
+ rangeSelected: (from, to) =>
118
+ `Zeitraum ${from} bis ${to} gewählt. Wählen Sie ein Startdatum, um neu zu beginnen.`,
119
+ },
120
+ datePicker: {
121
+ panel: "Datum wählen",
122
+ rangePanel: "Zeitraum wählen",
123
+ clear: "Leeren",
124
+ previousDay: "Vorheriger Tag",
125
+ nextDay: "Nächster Tag",
126
+ today: "Heute",
127
+ },
128
+ monthPicker: {
129
+ previousYear: "Vorheriges Jahr",
130
+ nextYear: "Nächstes Jahr",
131
+ panel: "Monat wählen",
132
+ month: (monthYear) => monthYear,
133
+ },
134
+ popover: {
135
+ panel: "Aufklappfenster",
136
+ },
137
+ combobox: {
138
+ search: "Suchen",
139
+ noResults: "Keine Ergebnisse",
140
+ clear: "Leeren",
141
+ loading: "Wird geladen…",
142
+ create: (query) => `„${query}“ anlegen`,
143
+ selectedCount: (count) => `${n(count)} ausgewählt`,
144
+ loadError: "Ergebnisse konnten nicht geladen werden",
145
+ resultCount: (count) => `${n(count)} ${count === 1 ? "Ergebnis" : "Ergebnisse"}`,
146
+ // "Zeichen" is the same in singular and plural.
147
+ minChars: (count) => `Mindestens ${n(count)} Zeichen eingeben`,
148
+ },
149
+ multiSelect: {
150
+ search: "Suchen",
151
+ selectAll: "Alle auswählen",
152
+ clear: "Leeren",
153
+ all: "Alle",
154
+ // The bare count, as in English: the trigger has always shown just the number.
155
+ selectedCount: (count) => n(count),
156
+ },
157
+ calculator: {
158
+ open: "Taschenrechner öffnen",
159
+ panel: "Taschenrechner",
160
+ calculation: "Rechnung",
161
+ backspace: "Rücktaste",
162
+ clear: "Löschen",
163
+ equals: "Ist gleich",
164
+ done: "Fertig",
165
+ plus: "Plus",
166
+ minus: "Minus",
167
+ times: "Mal",
168
+ divide: "Geteilt durch",
169
+ decimal: "Dezimaltrennzeichen",
170
+ },
171
+ currency: {
172
+ currency: "Währung",
173
+ search: "Währung suchen",
174
+ },
175
+ chipInput: {
176
+ // The value is arbitrary user text, quoted as a citation, so the participle agrees
177
+ // with the implied "Eintrag" whatever the chip says.
178
+ added: (value) => `„${value}“ hinzugefügt`,
179
+ removed: (value) => `„${value}“ entfernt`,
180
+ remove: "Entfernen",
181
+ // `von` governs the dative, visible only in the plural: "von 1 Eintrag", "von 2 Einträgen".
182
+ atLimit: (max) => `Grenze von ${n(max)} ${max === 1 ? "Eintrag" : "Einträgen"} erreicht`,
183
+ duplicate: (value) => `„${value}“ steht bereits in der Liste`,
184
+ },
185
+ swatchPicker: {
186
+ none: "Keine Farbe",
187
+ mixed: "Gemischt: Die ausgewählten Elemente haben unterschiedliche Farben",
188
+ },
189
+ iconPicker: {
190
+ none: "Kein Symbol",
191
+ mixed: "Gemischt: Die ausgewählten Elemente haben unterschiedliche Symbole",
192
+ search: "Symbole suchen",
193
+ noResults: "Keine passenden Symbole",
194
+ resultCount: (count) => `${n(count)} ${count === 1 ? "Symbol" : "Symbole"}`,
195
+ },
196
+ fieldSync: {
197
+ synced: "Gespeichert",
198
+ edited: "Nicht gespeicherte Änderungen",
199
+ pending: "Wird gespeichert…",
200
+ error: "Speichern fehlgeschlagen",
201
+ retry: "Erneut versuchen",
202
+ },
203
+ passwordReveal: {
204
+ show: "Passwort anzeigen",
205
+ hide: "Passwort verbergen",
206
+ },
207
+ dangerConfirm: {
208
+ arm: "Löschen…",
209
+ confirm: "Löschen",
210
+ cancel: "Abbrechen",
211
+ prompt: "Dies kann nicht rückgängig gemacht werden.",
212
+ password: "Passwort",
213
+ phrase: (phrase) => `Geben Sie zur Bestätigung „${phrase}“ ein`,
214
+ },
215
+ tabs: {
216
+ // "Tab" is what German UIs say; "Registerkarte" reads like a 1990s manual.
217
+ add: "Tab hinzufügen",
218
+ remove: (tab) => `${tab} entfernen`,
219
+ },
220
+ appShell: {
221
+ collapse: "Seitenleiste einklappen",
222
+ expand: "Seitenleiste ausklappen",
223
+ toggleGroup: (groupLabel) => `${groupLabel}: Seiten`,
224
+ },
225
+ topBar: {
226
+ theme: "Theme wechseln",
227
+ palette: "Darstellungsvorlage",
228
+ language: "Sprache",
229
+ switchRole: "Rolle wechseln",
230
+ role: (value) => `Rolle: ${value}`,
231
+ },
232
+ pickerSheet: {
233
+ close: "Schließen",
234
+ },
235
+ dialogFrame: {
236
+ close: "Schließen",
237
+ },
238
+ swipeableRow: {
239
+ actions: "Zeilenaktionen",
240
+ },
241
+ file: {
242
+ // The kit's own `Intl` unit formatting, pinned to this locale ("3,4 MB").
243
+ size: (bytes) => formatFileSize(bytes, numberLocale),
244
+ },
245
+ filePicker: {
246
+ rejectedPick: (count) =>
247
+ count === 1
248
+ ? "Die Datei wurde nicht hinzugefügt"
249
+ : `Keine der ${count} Dateien wurde hinzugefügt`,
250
+ rejectedType: (name) => `„${name}“ hat einen nicht unterstützten Dateityp`,
251
+ rejectedTypeOnly: (accept) => `Nur ${accept}-Dateien`,
252
+ rejectedSize: (name, maxSize) => `„${name}“ ist größer als ${maxSize}`,
253
+ rejectedCount: (name, maxFiles) =>
254
+ `„${name}“ wurde nicht hinzugefügt: höchstens ${n(maxFiles)} ${maxFiles === 1 ? "Datei" : "Dateien"}`,
255
+ rejectedInvalid: (name) => `„${name}“ kann hier nicht verwendet werden`,
256
+ rejectedMany: (count) =>
257
+ count === 1
258
+ ? "1 Datei wurde nicht hinzugefügt"
259
+ : `${n(count)} Dateien wurden nicht hinzugefügt`,
260
+ selected: (count, firstName) =>
261
+ count === 1 ? `„${firstName}“ ausgewählt` : `${n(count)} Dateien ausgewählt`,
262
+ remove: (name) => `„${name}“ entfernen`,
263
+ clearAll: "Alle Dateien entfernen",
264
+ removed: (name) => `„${name}“ entfernt`,
265
+ cleared: "Alle Dateien entfernt",
266
+ },
267
+ wizard: {
268
+ cancel: "Abbrechen",
269
+ back: "Zurück",
270
+ next: "Weiter",
271
+ skip: "Überspringen",
272
+ finish: "Abschließen",
273
+ submitting: "Wird erstellt…",
274
+ steps: "Schritte",
275
+ step: (current, total) => `Schritt ${n(current)} von ${n(total)}`,
276
+ cancelTitle: "Formular verwerfen?",
277
+ confirmCancel: "Ihre Eingaben gehen verloren.",
278
+ cancelConfirmLabel: "Verwerfen",
279
+ cancelDismissLabel: "Weiter bearbeiten",
280
+ reviewTitle: "Überprüfen",
281
+ edit: "Bearbeiten",
282
+ missingRequired: "Bitte füllen Sie alle Pflichtfelder aus.",
283
+ genericError: "Ein Fehler ist aufgetreten",
284
+ },
285
+ tour: {
286
+ next: "Weiter",
287
+ back: "Zurück",
288
+ skip: "Überspringen",
289
+ done: "Fertig",
290
+ awaitClickHint: "Klicken Sie auf das hervorgehobene Element, um fortzufahren",
291
+ step: (current, total) => `${n(current)} / ${n(total)}`,
292
+ },
293
+ commandPalette: {
294
+ placeholder: "Suchen…",
295
+ empty: "Keine Ergebnisse",
296
+ loading: "Wird gesucht…",
297
+ dialog: "Suche",
298
+ error: "Die Suche ist fehlgeschlagen. Bitte erneut versuchen.",
299
+ },
300
+ sparkline: {
301
+ rising: (first, last) => `Steigend von ${first} auf ${last}`,
302
+ falling: (first, last) => `Fallend von ${first} auf ${last}`,
303
+ flat: (value) => `Unverändert bei ${value}`,
304
+ single: (value) => `Ein Wert: ${value}`,
305
+ noData: "Keine Daten",
306
+ named: (name, summary) => `${name}: ${summary}`,
307
+ },
308
+ statTile: {
309
+ // `amount` arrives formatted and unsigned ("5 %", "1.200 €").
310
+ increase: (amount) => `Um ${amount} gestiegen`,
311
+ decrease: (amount) => `Um ${amount} gesunken`,
312
+ unchanged: "Keine Veränderung",
313
+ better: (change) => `${change} (besser)`,
314
+ worse: (change) => `${change} (schlechter)`,
315
+ noValue: "Keine Daten",
316
+ loading: "Wird geladen…",
317
+ },
318
+ signaturePad: {
319
+ label: "Unterschrift",
320
+ instructions: "Unterschreiben Sie im Feld mit der Maus, dem Finger oder einem Stift.",
321
+ typedFallbackHint: "Wenn Sie nicht zeichnen können, geben Sie stattdessen Ihren Namen ein.",
322
+ empty: "Noch nichts gezeichnet",
323
+ signed: "Unterschrift gezeichnet",
324
+ undo: "Letzten Strich rückgängig machen",
325
+ clear: "Leeren",
326
+ save: "Unterschrift speichern",
327
+ useTyped: "Stattdessen Namen eingeben",
328
+ useDrawn: "Stattdessen zeichnen",
329
+ typedName: "Vollständiger Name",
330
+ cleared: "Unterschrift gelöscht",
331
+ undone: "Letzter Strich entfernt",
332
+ viewEmpty: "Nicht unterschrieben",
333
+ viewDrawn: "Handschriftliche Unterschrift",
334
+ viewTyped: (name) => `Unterschrieben mit dem eingegebenen Namen ${name}`,
335
+ },
336
+ passwordStrength: {
337
+ tooShort: "Zu kurz",
338
+ weak: "Schwach",
339
+ fair: "Mittel",
340
+ good: "Gut",
341
+ strong: "Stark",
342
+ announcement: (level) => `Passwortstärke: ${level}`,
343
+ // "Zeichen" is the same in singular and plural. The English default does not
344
+ // format these counts, so neither does this file.
345
+ ruleLength: (minLength) => `Mindestens ${minLength} Zeichen`,
346
+ ruleCase: "Groß- und Kleinbuchstaben",
347
+ ruleDigit: "Eine Ziffer",
348
+ ruleSymbol: "Ein Sonderzeichen",
349
+ optional: (rule) => `${rule} (optional)`,
350
+ met: "Erfüllt:",
351
+ notMet: "Nicht erfüllt:",
352
+ tooLong: (maxBytes) =>
353
+ `Höchstens ${maxBytes} Zeichen (Buchstaben mit Akzent und Emojis zählen mehrfach).`,
354
+ },
355
+ seriesChart: {
356
+ resetZoom: "Zoom zurücksetzen",
357
+ zoomHint:
358
+ "Zum Zoomen ziehen: Eine annähernd quadratische Auswahl zoomt beide Achsen, eine lange, schmale nur ihre eigene. Doppelklick setzt zurück.",
359
+ empty: "Keine Daten",
360
+ legend: "Datenreihen",
361
+ },
362
+ };
363
+ }
364
+
365
+ export const UI_KIT_LABELS_DE: UiKitLabels = uiKitLabelsDe();