@eifi1/ui-kit 0.52.0 → 0.54.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 (290) hide show
  1. package/README.md +52 -59
  2. package/dist/account/delete-account-setting.d.ts +3 -2
  3. package/dist/account/delete-account-setting.js.map +1 -1
  4. package/dist/auth/auth-errors.d.ts +36 -0
  5. package/dist/auth/auth-errors.js +20 -0
  6. package/dist/auth/auth-errors.js.map +1 -1
  7. package/dist/billing/cancel-subscription.d.ts +5 -14
  8. package/dist/billing/cancel-subscription.js +1 -3
  9. package/dist/billing/cancel-subscription.js.map +1 -1
  10. package/dist/billing/plan-card.d.ts +5 -13
  11. package/dist/billing/plan-card.js +3 -11
  12. package/dist/billing/plan-card.js.map +1 -1
  13. package/dist/billing/plan-switch.d.ts +5 -14
  14. package/dist/billing/plan-switch.js +1 -3
  15. package/dist/billing/plan-switch.js.map +1 -1
  16. package/dist/chart.d.ts +3 -14
  17. package/dist/chart.js +64 -11
  18. package/dist/chart.js.map +1 -1
  19. package/dist/components/amount-input.d.ts +2 -1
  20. package/dist/components/amount-input.js.map +1 -1
  21. package/dist/components/calendar-heatmap.js +4 -2
  22. package/dist/components/calendar-heatmap.js.map +1 -1
  23. package/dist/components/currency-option-list.d.ts +2 -1
  24. package/dist/components/currency-option-list.js +10 -3
  25. package/dist/components/currency-option-list.js.map +1 -1
  26. package/dist/components/currency-select.d.ts +3 -1
  27. package/dist/components/currency-select.js.map +1 -1
  28. package/dist/components/danger-confirm.d.ts +22 -1
  29. package/dist/components/danger-confirm.js +50 -28
  30. package/dist/components/danger-confirm.js.map +1 -1
  31. package/dist/components/data-table-filter-popover.d.ts +2 -1
  32. package/dist/components/data-table-filter-popover.js +3 -2
  33. package/dist/components/data-table-filter-popover.js.map +1 -1
  34. package/dist/components/data-table-filters.d.ts +33 -3
  35. package/dist/components/data-table-filters.js +6 -4
  36. package/dist/components/data-table-filters.js.map +1 -1
  37. package/dist/components/date-field.d.ts +1 -1
  38. package/dist/components/date-field.js.map +1 -1
  39. package/dist/components/date-picker.d.ts +1 -1
  40. package/dist/components/date-picker.js.map +1 -1
  41. package/dist/components/date-range-picker.d.ts +4 -17
  42. package/dist/components/date-range-picker.js +2 -2
  43. package/dist/components/date-range-picker.js.map +1 -1
  44. package/dist/components/dialog-frame.d.ts +3 -1
  45. package/dist/components/dialog-frame.js +6 -2
  46. package/dist/components/dialog-frame.js.map +1 -1
  47. package/dist/components/mini-calendar.js +3 -2
  48. package/dist/components/mini-calendar.js.map +1 -1
  49. package/dist/components/modal.d.ts +27 -0
  50. package/dist/components/modal.js +5 -3
  51. package/dist/components/modal.js.map +1 -1
  52. package/dist/components/nav-pills.d.ts +1 -1
  53. package/dist/components/nav-pills.js.map +1 -1
  54. package/dist/components/number-input.js.map +1 -1
  55. package/dist/components/series-chart-axes.d.ts +1 -1
  56. package/dist/components/series-chart-axes.js.map +1 -1
  57. package/dist/components/series-chart.d.ts +64 -19
  58. package/dist/components/series-chart.js +2 -1
  59. package/dist/components/series-chart.js.map +1 -1
  60. package/dist/components/stat-tile.d.ts +21 -2
  61. package/dist/components/stat-tile.js +4 -2
  62. package/dist/components/stat-tile.js.map +1 -1
  63. package/dist/components/treemap-labels.d.ts +6 -1
  64. package/dist/components/treemap-labels.js +2 -2
  65. package/dist/components/treemap-labels.js.map +1 -1
  66. package/dist/components/treemap.d.ts +29 -4
  67. package/dist/components/treemap.js +22 -9
  68. package/dist/components/treemap.js.map +1 -1
  69. package/dist/currencies.d.ts +16 -2
  70. package/dist/currencies.js +2 -1
  71. package/dist/currencies.js.map +1 -1
  72. package/dist/data-table.d.ts +1 -6
  73. package/dist/data-table.js +1 -9
  74. package/dist/data-table.js.map +1 -1
  75. package/dist/demo/demo-countdown.d.ts +0 -5
  76. package/dist/demo/demo-countdown.js +0 -4
  77. package/dist/demo/demo-countdown.js.map +1 -1
  78. package/dist/eslint/index.js +1 -1
  79. package/dist/feedback/feedback-context-labels.js +2 -1
  80. package/dist/feedback/feedback-context-labels.js.map +1 -1
  81. package/dist/feedback/feedback-context.d.ts +19 -3
  82. package/dist/feedback/feedback-context.js +12 -1
  83. package/dist/feedback/feedback-context.js.map +1 -1
  84. package/dist/feedback/feedback-diagnostics.d.ts +17 -0
  85. package/dist/feedback/feedback-diagnostics.js +27 -0
  86. package/dist/feedback/feedback-diagnostics.js.map +1 -0
  87. package/dist/feedback/feedback-dialog.d.ts +10 -0
  88. package/dist/feedback/feedback-dialog.js +6 -4
  89. package/dist/feedback/feedback-dialog.js.map +1 -1
  90. package/dist/feedback/feedback-mutation.d.ts +11 -23
  91. package/dist/feedback/feedback-mutation.js +1 -9
  92. package/dist/feedback/feedback-mutation.js.map +1 -1
  93. package/dist/feedback/feedback-public-list.d.ts +1 -6
  94. package/dist/feedback/feedback-public-list.js +1 -2
  95. package/dist/feedback/feedback-public-list.js.map +1 -1
  96. package/dist/feedback/feedback-record.d.ts +5 -0
  97. package/dist/feedback/feedback-record.js.map +1 -1
  98. package/dist/feedback/feedback-row-detail-labels.js +1 -8
  99. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  100. package/dist/feedback/feedback-row-detail.d.ts +6 -50
  101. package/dist/feedback/feedback-row-detail.js +3 -8
  102. package/dist/feedback/feedback-row-detail.js.map +1 -1
  103. package/dist/feedback/feedback-status-undo.d.ts +3 -19
  104. package/dist/feedback/feedback-status-undo.js.map +1 -1
  105. package/dist/feedback/feedback-submit-dialog.d.ts +5 -1
  106. package/dist/feedback/feedback-submit-dialog.js +15 -3
  107. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  108. package/dist/feedback/feedback-submit.d.ts +30 -2
  109. package/dist/feedback/feedback-submit.js +5 -2
  110. package/dist/feedback/feedback-submit.js.map +1 -1
  111. package/dist/feedback/feedback-thread.d.ts +0 -9
  112. package/dist/feedback/feedback-thread.js.map +1 -1
  113. package/dist/feedback.d.ts +6 -40
  114. package/dist/feedback.js +88 -46
  115. package/dist/feedback.js.map +1 -1
  116. package/dist/hooks/use-hotkey.js +11 -1
  117. package/dist/hooks/use-hotkey.js.map +1 -1
  118. package/dist/i18n/defaults.js +0 -4
  119. package/dist/i18n/defaults.js.map +1 -1
  120. package/dist/i18n/german.js +5 -20
  121. package/dist/i18n/german.js.map +1 -1
  122. package/dist/i18n/kit-labels.d.ts +5 -14
  123. package/dist/i18n/kit-labels.js.map +1 -1
  124. package/dist/i18n/locales/en.js +5 -31
  125. package/dist/i18n/locales/en.js.map +3 -3
  126. package/dist/i18n/locales/es.js +5 -20
  127. package/dist/i18n/locales/es.js.map +1 -1
  128. package/dist/i18n/locales/fr.js +5 -20
  129. package/dist/i18n/locales/fr.js.map +1 -1
  130. package/dist/i18n/locales/hu.js +5 -20
  131. package/dist/i18n/locales/hu.js.map +1 -1
  132. package/dist/i18n/locales/it.js +5 -21
  133. package/dist/i18n/locales/it.js.map +1 -1
  134. package/dist/i18n/locales/zh.js +5 -20
  135. package/dist/i18n/locales/zh.js.map +1 -1
  136. package/dist/i18n/review.js +9 -4
  137. package/dist/i18n/review.js.map +1 -1
  138. package/dist/i18n/shared-labels.js +0 -1
  139. package/dist/i18n/shared-labels.js.map +1 -1
  140. package/dist/index.d.ts +18 -67
  141. package/dist/index.js +28 -103
  142. package/dist/index.js.map +1 -1
  143. package/dist/lib/dates.d.ts +7 -1
  144. package/dist/lib/dates.js +8 -1
  145. package/dist/lib/dates.js.map +1 -1
  146. package/dist/lib/format.d.ts +37 -2
  147. package/dist/lib/format.js +25 -1
  148. package/dist/lib/format.js.map +1 -1
  149. package/dist/lib/intl-cache.d.ts +4 -2
  150. package/dist/lib/intl-cache.js.map +1 -1
  151. package/dist/lib/translation-review.d.ts +26 -0
  152. package/dist/lib/translation-review.js +14 -0
  153. package/dist/lib/translation-review.js.map +1 -1
  154. package/dist/rhf/fields.d.ts +2 -2
  155. package/dist/rhf/fields.js.map +1 -1
  156. package/dist/search/search-index.d.ts +0 -6
  157. package/dist/search/search-index.js +0 -4
  158. package/dist/search/search-index.js.map +1 -1
  159. package/dist/search.d.ts +1 -8
  160. package/dist/search.js +2 -7
  161. package/dist/search.js.map +1 -1
  162. package/dist/shell/account-menu-language.d.ts +7 -21
  163. package/dist/shell/account-menu-language.js +1 -3
  164. package/dist/shell/account-menu-language.js.map +1 -1
  165. package/dist/shell/account-menu-text-size.d.ts +9 -23
  166. package/dist/shell/account-menu-text-size.js +2 -3
  167. package/dist/shell/account-menu-text-size.js.map +1 -1
  168. package/dist/shell/app-shell.d.ts +0 -10
  169. package/dist/shell/app-shell.js +1 -7
  170. package/dist/shell/app-shell.js.map +1 -1
  171. package/dist/shell/company-switcher.d.ts +1 -1
  172. package/dist/shell/company-switcher.js.map +1 -1
  173. package/dist/shell/dev-menu.d.ts +1 -1
  174. package/dist/shell/dev-menu.js.map +1 -1
  175. package/dist/shell/top-bar-parts-labels.d.ts +1 -1
  176. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  177. package/dist/shell.d.ts +7 -29
  178. package/dist/shell.js +12 -42
  179. package/dist/shell.js.map +1 -1
  180. package/dist/sync.d.ts +2 -6
  181. package/dist/sync.js +2 -15
  182. package/dist/sync.js.map +1 -1
  183. package/dist/theme/chart-palette.d.ts +0 -5
  184. package/dist/theme/chart-palette.js +0 -2
  185. package/dist/theme/chart-palette.js.map +1 -1
  186. package/package.json +1 -1
  187. package/src/account/delete-account-setting.tsx +3 -2
  188. package/src/auth/auth-errors.ts +69 -0
  189. package/src/billing/cancel-subscription.tsx +5 -16
  190. package/src/billing/plan-card.tsx +7 -24
  191. package/src/billing/plan-switch.tsx +5 -16
  192. package/src/chart.ts +62 -13
  193. package/src/components/amount-input.tsx +2 -1
  194. package/src/components/calendar-heatmap.tsx +8 -2
  195. package/src/components/currency-option-list.tsx +12 -4
  196. package/src/components/currency-select.tsx +3 -1
  197. package/src/components/danger-confirm.tsx +80 -27
  198. package/src/components/data-table-filter-popover.tsx +19 -11
  199. package/src/components/data-table-filters.ts +36 -8
  200. package/src/components/date-field.tsx +1 -1
  201. package/src/components/date-picker.tsx +0 -1
  202. package/src/components/date-range-picker.tsx +6 -22
  203. package/src/components/dialog-frame.tsx +12 -4
  204. package/src/components/mini-calendar.tsx +5 -2
  205. package/src/components/modal.tsx +36 -7
  206. package/src/components/nav-pills.tsx +1 -1
  207. package/src/components/number-input.tsx +4 -3
  208. package/src/components/series-chart-axes.ts +6 -1
  209. package/src/components/series-chart.tsx +78 -21
  210. package/src/components/stat-tile.tsx +32 -3
  211. package/src/components/treemap-labels.ts +11 -3
  212. package/src/components/treemap.tsx +65 -12
  213. package/src/currencies.ts +23 -3
  214. package/src/data-table.ts +1 -11
  215. package/src/demo/demo-countdown.ts +3 -8
  216. package/src/feedback/feedback-context-labels.ts +1 -0
  217. package/src/feedback/feedback-context.tsx +34 -2
  218. package/src/feedback/feedback-diagnostics.tsx +41 -0
  219. package/src/feedback/feedback-dialog.tsx +16 -4
  220. package/src/feedback/feedback-mutation.ts +12 -33
  221. package/src/feedback/feedback-public-list.tsx +1 -7
  222. package/src/feedback/feedback-record.ts +5 -0
  223. package/src/feedback/feedback-row-detail-labels.ts +1 -8
  224. package/src/feedback/feedback-row-detail.tsx +21 -58
  225. package/src/feedback/feedback-status-undo.ts +4 -18
  226. package/src/feedback/feedback-submit-dialog.tsx +15 -2
  227. package/src/feedback/feedback-submit.tsx +36 -3
  228. package/src/feedback/feedback-thread.tsx +0 -10
  229. package/src/feedback.ts +89 -48
  230. package/src/hooks/use-hotkey.ts +24 -1
  231. package/src/i18n/defaults.ts +0 -4
  232. package/src/i18n/german.ts +4 -19
  233. package/src/i18n/kit-labels.tsx +5 -14
  234. package/src/i18n/locales/es.ts +4 -19
  235. package/src/i18n/locales/fr.ts +4 -19
  236. package/src/i18n/locales/hu.ts +4 -19
  237. package/src/i18n/locales/it.ts +4 -20
  238. package/src/i18n/locales/zh.ts +4 -19
  239. package/src/i18n/review.ts +9 -4
  240. package/src/i18n/shared-labels.ts +0 -1
  241. package/src/index.ts +44 -114
  242. package/src/lib/dates.ts +24 -2
  243. package/src/lib/format.ts +66 -3
  244. package/src/lib/intl-cache.ts +4 -2
  245. package/src/lib/translation-review.ts +52 -0
  246. package/src/rhf/fields.tsx +2 -2
  247. package/src/search/search-index.ts +0 -15
  248. package/src/search.ts +8 -8
  249. package/src/shell/account-menu-language.tsx +7 -27
  250. package/src/shell/account-menu-text-size.tsx +13 -32
  251. package/src/shell/app-shell.tsx +0 -13
  252. package/src/shell/company-switcher.tsx +1 -1
  253. package/src/shell/dev-menu.tsx +1 -1
  254. package/src/shell/top-bar-parts-labels.ts +1 -1
  255. package/src/shell.ts +17 -44
  256. package/src/sync.ts +2 -15
  257. package/src/theme/chart-palette.ts +0 -2
  258. package/tokens.css +4 -4
  259. package/dist/billing/dialog-close.d.ts +0 -29
  260. package/dist/billing/dialog-close.js +0 -8
  261. package/dist/billing/dialog-close.js.map +0 -1
  262. package/dist/components/field-sync-labels.d.ts +0 -3
  263. package/dist/components/field-sync-labels.js +0 -12
  264. package/dist/components/field-sync-labels.js.map +0 -1
  265. package/dist/components/field-sync.d.ts +0 -186
  266. package/dist/components/field-sync.js +0 -269
  267. package/dist/components/field-sync.js.map +0 -1
  268. package/dist/components/section-nav.d.ts +0 -44
  269. package/dist/components/section-nav.js +0 -33
  270. package/dist/components/section-nav.js.map +0 -1
  271. package/dist/components/sign-chip-labels.d.ts +0 -3
  272. package/dist/components/sign-chip-labels.js +0 -11
  273. package/dist/components/sign-chip-labels.js.map +0 -1
  274. package/dist/components/sign-chip.d.ts +0 -77
  275. package/dist/components/sign-chip.js +0 -47
  276. package/dist/components/sign-chip.js.map +0 -1
  277. package/dist/lib/isin.d.ts +0 -38
  278. package/dist/lib/isin.js +0 -36
  279. package/dist/lib/isin.js.map +0 -1
  280. package/dist/shell/role-switcher.d.ts +0 -28
  281. package/dist/shell/role-switcher.js +0 -31
  282. package/dist/shell/role-switcher.js.map +0 -1
  283. package/src/billing/dialog-close.ts +0 -34
  284. package/src/components/field-sync-labels.ts +0 -14
  285. package/src/components/field-sync.tsx +0 -504
  286. package/src/components/section-nav.tsx +0 -72
  287. package/src/components/sign-chip-labels.ts +0 -13
  288. package/src/components/sign-chip.tsx +0 -133
  289. package/src/lib/isin.ts +0 -72
  290. package/src/shell/role-switcher.tsx +0 -53
@@ -8,7 +8,15 @@ export type ColumnFilter<T> =
8
8
  | { type: "text"; getValue: (row: T) => string }
9
9
  | { type: "select"; getValue: (row: T) => string; options?: { value: string; label?: string }[] }
10
10
  | { type: "date"; getValue: (row: T) => string | null | undefined }
11
- | { type: "number"; getValue: (row: T) => number | null | undefined };
11
+ | {
12
+ type: "number";
13
+ getValue: (row: T) => number | null | undefined;
14
+ /** `false`: no "Absolute value" box, for a figure that is never negative (a year, a
15
+ * count, a size), where the box only offers a choice that changes nothing. A state
16
+ * or a link that carries `abs` anyway is read without it. Default `true`. See
17
+ * {@link numberFilter}. */
18
+ abs?: boolean;
19
+ };
12
20
 
13
21
  /** The current state of a column's filter (what the user has entered). */
14
22
  export type FilterValue =
@@ -46,8 +54,15 @@ export function isFilterActive(v: FilterValue | undefined): boolean {
46
54
  * match every row while the column showed its filter as active; `Number(" ")` is 0, and
47
55
  * a blank bound kept every negative row out. The popover hands in the dot-decimal text
48
56
  * its locale's marks read (`readTyped`); anything else is no bound.
57
+ *
58
+ * Exported for a server-side table (`serverPagination`), which sends the bounds to its
59
+ * server instead of comparing here: send what this reads, and leave out a `null`. The box
60
+ * holds a half-typed "-" or "5e" while the user types, and kastlan's server tables sent
61
+ * that text on to `col >= :bound` until they wrote this rule again for themselves (kastlan
62
+ * reuse audit 2026-11-02, proposal 7). And send `abs` only beside a bound: on its own it
63
+ * narrows nothing.
49
64
  */
50
- function numberBound(raw: string): number | null {
65
+ export function numberBound(raw: string): number | null {
51
66
  if (raw.trim() === "") return null;
52
67
  const n = Number(raw);
53
68
  return Number.isFinite(n) ? n : null;
@@ -108,7 +123,9 @@ export function rowMatches<T>(col: DataTableColumn<T>, row: T, state: FilterValu
108
123
  const min = numberBound(state.min);
109
124
  const max = numberBound(state.max);
110
125
  if (raw == null || Number.isNaN(raw)) return min === null && max === null;
111
- const x = state.abs ? Math.abs(raw) : raw;
126
+ // A column without the box (`abs: false`) compares the figure as it is, whatever
127
+ // the state says: there is nothing on screen to untick.
128
+ const x = state.abs && filter.abs !== false ? Math.abs(raw) : raw;
112
129
  if (min !== null && x < min) return false;
113
130
  if (max !== null && x > max) return false;
114
131
  return true;
@@ -204,9 +221,12 @@ export function decodeFilterValueOfType(type: FilterValue["type"], raw: string):
204
221
  }
205
222
  }
206
223
 
207
- /** Inverse of {@link encodeFilterValue}, typed by the column's filter config. */
224
+ /** Inverse of {@link encodeFilterValue}, typed by the column's filter config. A number
225
+ * filter without the "Absolute value" box (`abs: false`) drops a link's `!`: kept, a
226
+ * server-side table would send `abs` that nobody can see or untick. */
208
227
  export function decodeFilterValue<T>(filter: ColumnFilter<T>, raw: string): FilterValue {
209
- return decodeFilterValueOfType(filter.type, raw);
228
+ const value = decodeFilterValueOfType(filter.type, raw);
229
+ return value.type === "number" && filter.type === "number" && filter.abs === false ? { ...value, abs: false } : value;
210
230
  }
211
231
 
212
232
  // ---------- Filter builders ----------
@@ -244,9 +264,17 @@ export function dateFilter<T>(getValue: (row: T) => string | null | undefined):
244
264
  return { type: "date", getValue };
245
265
  }
246
266
 
247
- /** A min/max (and absolute-value) filter over a number. */
248
- export function numberFilter<T>(getValue: (row: T) => number | null | undefined): ColumnFilter<T> {
249
- return { type: "number", getValue };
267
+ /**
268
+ * A min/max (and absolute-value) filter over a number. `{ abs: false }` leaves out the
269
+ * "Absolute value" box, for a figure that is never negative — a year, a count, a size —
270
+ * where it only offers a choice that changes nothing (kastlan reuse audit 2026-11-02,
271
+ * proposal 7: its server tables drew it under construction years and file sizes).
272
+ */
273
+ export function numberFilter<T>(
274
+ getValue: (row: T) => number | null | undefined,
275
+ options?: { abs?: boolean },
276
+ ): ColumnFilter<T> {
277
+ return options?.abs === false ? { type: "number", getValue, abs: false } : { type: "number", getValue };
250
278
  }
251
279
 
252
280
  /**
@@ -137,7 +137,7 @@ export interface DatePickerBaseProps extends Omit<ComponentPropsWithoutRef<"div"
137
137
  *
138
138
  * Off by default: the caption becomes one more stop in the panel's tab order, which a
139
139
  * 0.x minor does not spring on a form. Works in the popover and in
140
- * `DateRangePicker`'s phone sheet alike, beside presets and in `commit="apply"`
140
+ * `DateRangePicker`'s phone sheet alike, beside presets and with `apply`
141
141
  * (a preset picked while the months are open goes back to the days, on its start).
142
142
  *
143
143
  * Why not a two-month panel (`months={2}`) instead, or beside it: for a twelve-month
@@ -16,7 +16,6 @@ import { PanelCalendar } from "./jump-calendar";
16
16
  export type { DateTriggerAttributes } from "./date-field";
17
17
  export { DateRangePicker } from "./date-range-picker";
18
18
  export type {
19
- DateRangeCommit,
20
19
  DateRangeDraftSummary,
21
20
  DateRangePickerPreset,
22
21
  DateRangePickerProps,
@@ -40,17 +40,6 @@ export interface DateRangePickerPreset {
40
40
  to: string;
41
41
  }
42
42
 
43
- /**
44
- * The 0.x values of DateRangePicker's `commit`, when it was the panel's mode:
45
- * `"immediate"` is `apply` left out, `"apply"` is `apply`. See
46
- * {@link DateRangePickerProps.apply}.
47
- *
48
- * @deprecated 0.52 — DateRangePicker's `apply` (`commit="apply"` → `apply`, `"immediate"`
49
- * → nothing); removed in 0.53. `commit` is the write lock's opt-in on every other kit
50
- * control, and since 0.52 on this one too (refactor audit 2026-10-30, API-3).
51
- */
52
- export type DateRangeCommit = "immediate" | "apply";
53
-
54
43
  export interface DateRangePickerProps extends DatePickerBaseProps {
55
44
  from: string;
56
45
  to: string;
@@ -89,8 +78,7 @@ export interface DateRangePickerProps extends DatePickerBaseProps {
89
78
  * must never escape the panel.
90
79
  *
91
80
  * `commit="apply"` until 0.52 (refactor audit 2026-10-30, API-3): `commit` is the write
92
- * lock's prop on every other kit control. The old spelling still works for 0.52 and
93
- * goes in 0.53 ({@link DateRangeCommit}); `apply` wins where both are given.
81
+ * lock's prop on every other kit control. The old spelling went in 0.53.
94
82
  */
95
83
  apply?: boolean;
96
84
  /**
@@ -100,12 +88,10 @@ export interface DateRangePickerProps extends DatePickerBaseProps {
100
88
  * kit control since 0.33, and on this one since 0.52 (API-3). A range inside a form with
101
89
  * its own Save stays editable under the lock — leave this off there.
102
90
  *
103
- * The strings `"immediate"` and `"apply"` are the panel's mode, the 0.x meaning of this
104
- * prop: deprecated, read as `apply` for one minor, removed in 0.53 — see
105
- * {@link DateRangeCommit}. They cannot be taken for a scope, which is a boolean or an
106
- * object.
91
+ * The strings `"immediate"` and `"apply"`, the panel's mode in 0.x, went in 0.53
92
+ * (deprecated in 0.52): `apply` is the mode.
107
93
  */
108
- commit?: CommitScope | DateRangeCommit;
94
+ commit?: CommitScope;
109
95
  /**
110
96
  * Draw the trigger yourself. keksdose's report range field
111
97
  * (reports/report-range-field.tsx, "Why this is app-level") is why: its trigger
@@ -428,10 +414,8 @@ export function DateRangePicker(props: DateRangePickerProps) {
428
414
  const locale = useKitLocale(localeProp);
429
415
  const text = useDatePickerLabels(clearLabel, undefined, undefined);
430
416
  const render = useTriggerFormat("dateRangePicker", locale, formatOptions, formatValue);
431
- // `commit` is two props until 0.53: a string is the deprecated mode, anything else
432
- // the write lock's scope (API-3).
433
- const drafting = apply ?? commit === "apply";
434
- const lock = useLockReason(typeof commit === "string" ? undefined : commit, disabledReason);
417
+ const drafting = apply ?? false;
418
+ const lock = useLockReason(commit, disabledReason);
435
419
  const locked = lock.locked;
436
420
  const messages = useFieldMessages({
437
421
  hint,
@@ -26,7 +26,9 @@ export { DEFAULT_DIALOG_FRAME_LABELS };
26
26
  * `onKeyDown`, a `data-tour` anchor, still reaches the `Modal` — and since 0.35 `ref`
27
27
  * (round 0.35 R4): the panel, the `role="dialog"` element the frame's header, body and
28
28
  * actions sit in. `busy` too (DUP-3): it holds the frame's X and the `close` that
29
- * `actions` and `headerActions` receive, since both are the `Modal`'s own close.
29
+ * `actions` and `headerActions` receive, since both are the `Modal`'s own close. And
30
+ * `held` (keksdose audit follow-up, 2026-11-01), which holds the same and shows the X
31
+ * disabled — see {@link ModalProps.held}.
30
32
  */
31
33
  export interface DialogFrameProps extends Omit<ModalProps, "labelledBy" | "children" | "title"> {
32
34
  /** The heading, and therefore the dialog's accessible name (`aria-labelledby`). */
@@ -187,7 +189,7 @@ export function DialogFrame({
187
189
  <FrameHeaderActions actions={headerActions} onClose={modal.onClose} />
188
190
  )}
189
191
  </div>
190
- {closeButton && <FrameClose label={closeLabel} onClose={modal.onClose} />}
192
+ {closeButton && <FrameClose label={closeLabel} onClose={modal.onClose} held={modal.held} />}
191
193
  </div>
192
194
  {hasBody && (
193
195
  <div
@@ -210,8 +212,10 @@ export function DialogFrame({
210
212
  }
211
213
 
212
214
  /** The X. A component of its own so it can read the panel's animated close, which
213
- * only exists INSIDE the `Modal` (the frame's own body runs outside it). */
214
- function FrameClose({ label, onClose }: { label?: string; onClose: () => void }) {
215
+ * only exists INSIDE the `Modal` (the frame's own body runs outside it). Disabled
216
+ * while `held`; under `busy` it stays enabled and the `Modal` drops its press (see
217
+ * {@link ModalProps.held} for why the two differ). */
218
+ function FrameClose({ label, onClose, held = false }: { label?: string; onClose: () => void; held?: boolean }) {
215
219
  const close = useContext(ModalCloseContext) ?? onClose;
216
220
  const labels = useKitLabels(
217
221
  "dialogFrame",
@@ -222,9 +226,13 @@ function FrameClose({ label, onClose }: { label?: string; onClose: () => void })
222
226
  <button
223
227
  type="button"
224
228
  onClick={close}
229
+ disabled={held}
225
230
  aria-label={labels.close}
226
231
  className={cn(
227
232
  "-me-1.5 -mt-1 shrink-0 rounded p-1.5 text-muted outline-none hover:bg-hover hover:text-secondary",
233
+ // The kit's disabled look (Button, IconButton), and the resting look pinned under
234
+ // `disabled:hover:` so a refused X does not light up under the pointer.
235
+ "disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:text-muted",
228
236
  FOCUS_RING,
229
237
  )}
230
238
  >
@@ -8,6 +8,7 @@ import { FOCUS_RING, FOCUS_RING_WIDTH } from "./focus-ring";
8
8
  import { horizontalStep } from "../lib/direction";
9
9
  import { useAnnounce } from "../hooks/use-announce";
10
10
  import { addMonthsClamped, localeWeekStart, monthKey, parseIsoDate, sameYmd, toLocalIso } from "../lib/dates";
11
+ import { dateFormatter } from "../lib/intl-cache";
11
12
  import { useKitLabels, useKitLocale, useKitWeekStart, type LabelOverride } from "../i18n/kit-labels";
12
13
  import { DEFAULT_MINI_CALENDAR_LABELS } from "./mini-calendar-labels";
13
14
 
@@ -355,8 +356,10 @@ export function MiniCalendar({
355
356
  // The first letter only, not CSS `capitalize`, which upper-cased every word: Spanish
356
357
  // read "Septiembre De 2026" and Hungarian "2026. Szeptember" (refactor audit
357
358
  // 2026-10-30, DUP-11) — while the same panel with `monthJump` read them right.
358
- const monthLabel = upperFirst(firstOfMonth.toLocaleDateString(locale, { month: "long", year: "numeric" }), locale);
359
- const formatDay = (d: Date) => d.toLocaleDateString(locale, DAY_NAME_FORMAT);
359
+ // Both through the kit's formatter cache: every day cell names its date, which was a
360
+ // formatter per cell and render (keksdose audit follow-up, 2026-11-01).
361
+ const monthLabel = upperFirst(dateFormatter(locale, { month: "long", year: "numeric" }).format(firstOfMonth), locale);
362
+ const formatDay = (d: Date) => dateFormatter(locale, DAY_NAME_FORMAT).format(d);
360
363
 
361
364
  // Weekday labels via locale, from the week's first day: the narrow one for the
362
365
  // column head, the long one as its accessible name — "M" is the same letter for
@@ -257,8 +257,35 @@ export interface ModalProps extends Omit<ComponentPropsWithoutRef<"div">, "role"
257
257
  * the dialog asks for itself through `ModalCloseContext` while busy is refused too; one
258
258
  * made from an effect that depends on that close (ReauthDialog's on a resolved submit)
259
259
  * runs again once `busy` turns false, because the close changes with it.
260
+ *
261
+ * A dialog that must not be dismissed while NOTHING runs — a code shown once — wants
262
+ * {@link ModalProps.held} instead: the same hold without the request.
260
263
  */
261
264
  busy?: boolean;
265
+ /**
266
+ * Hold the dialog open: every dismissal `busy` refuses is refused — the backdrop,
267
+ * Escape, Back, the frame's X and an `actions(close)` button — but no request is
268
+ * claimed: the panel is not `aria-busy`. For a dialog the user must not lose by
269
+ * accident while nothing is in flight (keksdose audit follow-up, 2026-11-01): keksdose's
270
+ * one-time recovery code (auth/recovery-code-pane.tsx) refused in its own `onClose`,
271
+ * which comes after the exit has started and cannot hold Back — the first Back spent
272
+ * the dialog's history entry, and the second left the page, and the code with it.
273
+ * Held, a refused Back puts the entry back, so every Back is held, not only the first.
274
+ * The refusal is silent, as under `busy` — `onClose` is not called — so what the
275
+ * dialog is waiting for is said up front, not after a refused press.
276
+ *
277
+ * The way out is a close the CALLER decides on, which is never held: `open={false}`
278
+ * (the exit still plays) or unmounting, from the dialog's own "I have saved it" button.
279
+ * Not the `close` that `actions` and `headerActions` receive — that is the dialog's
280
+ * own dismissal, and held like the rest.
281
+ *
282
+ * `DialogFrame`'s X is DISABLED while held, where under `busy` it stays enabled and its
283
+ * press is dropped: a request is over in a moment, a hold lasts until the user acts,
284
+ * and an X that does nothing for that long reads as broken. Disabled rather than
285
+ * hidden, so the header does not reflow the moment the hold lifts; a caller that wants
286
+ * it gone passes `closeButton={!held}`. Independent of `busy`: either one holds.
287
+ */
288
+ held?: boolean;
262
289
  /**
263
290
  * The PANEL — the `role="dialog"` element the rest of the props go on, not the
264
291
  * backdrop around it (0.35, round 0.35 R4, the follow-up to API-2; as on
@@ -325,6 +352,7 @@ function ModalPanel({
325
352
  role = "dialog",
326
353
  urlParam: _urlParam,
327
354
  busy = false,
355
+ held = false,
328
356
  "aria-busy": ariaBusy,
329
357
  ownHistoryEntry = true,
330
358
  ref,
@@ -371,13 +399,14 @@ function ModalPanel({
371
399
  setDismissed(true);
372
400
  onClose();
373
401
  });
374
- // `busy` (DUP-3) refuses HERE, before `lower` starts the exit: a refusal in the
375
- // caller's `onClose` comes 220ms too late, after the panel has already gone down.
376
- // A new function when `busy` flips, so a close asked for from an effect while busy
402
+ // `busy` (DUP-3) and `held` refuse HERE, before `lower` starts the exit: a refusal in
403
+ // the caller's `onClose` comes 220ms too late, after the panel has already gone down.
404
+ // A new function when the hold flips, so a close asked for from an effect while busy
377
405
  // (a resolved action whose caller's `isPending` trails its promise) runs again.
406
+ const holding = busy || held;
378
407
  const requestClose = useCallback(() => {
379
- if (!busy) lower();
380
- }, [busy, lower]);
408
+ if (!holding) lower();
409
+ }, [holding, lower]);
381
410
  // A panel on its way out after `open` turned false is already closed: a press on
382
411
  // its fading backdrop must not report a second close.
383
412
  const backdropClose = useBackdropClose(active ? requestClose : () => {});
@@ -385,8 +414,8 @@ function ModalPanel({
385
414
  // this the only way out of a dialog is finding its close button — and Back, the
386
415
  // gesture everyone reaches for, navigated the page underneath instead (#172).
387
416
  // Held only while open: a closed `open={false}` dialog must not own a history entry.
388
- // `held`: a Back while busy is refused, and the entry it took is put back.
389
- useOverlayHistory(active && ownHistoryEntry, requestClose, { held: busy });
417
+ // `held`: a Back while busy or held is refused, and the entry it took is put back.
418
+ useOverlayHistory(active && ownHistoryEntry, requestClose, { held: holding });
390
419
 
391
420
  // Through the shared hook and not by hand: this component's own save/restore copy
392
421
  // was one half of the pair that left the page permanently unscrollable when a dialog
@@ -59,7 +59,7 @@ export interface NavPillsProps<T extends string> extends Omit<
59
59
  * 0.49: `wrap` (default) puts the pills on as many rows as they need. `scroll` keeps
60
60
  * them on ONE row that scrolls sideways — faded at a cut edge, the current pill
61
61
  * scrolled into view (the row only, never the page) — for a strip that must stay one
62
- * line high, as {@link SectionNav} under a page title on a phone.
62
+ * line high, as a section's pages under a page title on a phone.
63
63
  */
64
64
  layout?: "wrap" | "scroll";
65
65
  /** The `<nav>` (or the `<div role="group">`) — hence `HTMLElement` (0.34, API-2,
@@ -222,9 +222,10 @@ const asTyped = (text: string) => text;
222
222
 
223
223
  // ── The money guard (kastlan 5, Kurvenschmiede) ─────────────────────────────────
224
224
 
225
- // The codes the kit's own currency pickers offer, as whole words: "Amount (CHF)",
226
- // "EUR", "Kosten CHF". Not every ISO 4217 code — "ALL", "TOP" and "PEN" are codes too,
227
- // and an upper-case label would trip on them.
225
+ // The codes the kit's own currency pickers know, as whole words: "Amount (CHF)",
226
+ // "EUR", "Kosten CHF". A `retired` one too (BGN): a label that names it is still money,
227
+ // whatever the pickers offer. Not every ISO 4217 code — "ALL", "TOP" and "PEN" are codes
228
+ // too, and an upper-case label would trip on them.
228
229
  const CURRENCY_CODE = new RegExp(`(?:^|[^A-Za-z])(?:${CURRENCIES.map((c) => c.code).join("|")})(?![A-Za-z])`);
229
230
  // Symbols no unit of measure is written with. Not "kr", "R" or "Fr": those are words
230
231
  // and units as often as they are money.
@@ -212,7 +212,12 @@ export function anchoredBand(extent: readonly [number, number] | undefined): [nu
212
212
  * it in its neighbour's colour on each click. Resolving the colours on the full list
213
213
  * and filtering after keeps each series in the colour its legend entry shows.
214
214
  */
215
- export function visibleSeries(series: readonly SeriesChartSeries[], hidden: ReadonlySet<string>): SeriesChartSeries[] {
215
+ // Generic so a typed list comes back typed (0.53): a chart of `Month` rows takes it, and
216
+ // its `dot`s still read `point.row` as a `Month`.
217
+ export function visibleSeries<TRow extends object>(
218
+ series: readonly SeriesChartSeries<TRow>[],
219
+ hidden: ReadonlySet<string>,
220
+ ): SeriesChartSeries<TRow>[] {
216
221
  return series
217
222
  .map((entry, index) => ({ ...entry, color: entry.color ?? paletteFor(index) }))
218
223
  .filter((entry) => !hidden.has(entry.key));
@@ -129,7 +129,14 @@ import { useChartEdge, useEdgesInForce } from "./chart-edge";
129
129
  import { cn } from "../lib/cn";
130
130
  import { formatWithMinus } from "../lib/minus";
131
131
 
132
- export interface SeriesChartSeries {
132
+ /**
133
+ * One line, area or bar run of a {@link SeriesChart}.
134
+ *
135
+ * `TRow` is the chart's row type (0.53, keksdose after 0.52), and only `dot` reads it: its
136
+ * `point.row` is that type. Without a type argument a series is what it was, and a chart
137
+ * of typed rows still takes it — see {@link SeriesChartProps}.
138
+ */
139
+ export interface SeriesChartSeries<TRow extends object = object> {
133
140
  /** The row key this line reads — and the `--color-<key>` it is painted with, so it
134
141
  * must be a CSS identifier. Build keys out of data with {@link seriesKey}. */
135
142
  key: string;
@@ -177,8 +184,9 @@ export interface SeriesChartSeries {
177
184
  * point: `true` for the default dot, `false`/`null` for none, or any SVG node drawn
178
185
  * as-is at `point.cx`/`point.cy` — a buy/sell marker on keksdose's paper price.
179
186
  * Never called for a point with no value. Bars have no points, and ignore it.
187
+ * `point.row` is the caller's own row, typed as the chart's rows are (0.53).
180
188
  */
181
- dot?: boolean | ((point: SeriesChartPoint) => ReactNode);
189
+ dot?: boolean | ((point: SeriesChartPoint<TRow>) => ReactNode);
182
190
  /** Line (or area outline) weight in px. Default 2, or 1.5 for a `step`. keksdose
183
191
  * draws the subject of a chart at 2.5 and the reference it is read against at 1.5. */
184
192
  strokeWidth?: number;
@@ -213,14 +221,16 @@ export interface SeriesChartSeries {
213
221
  /** What a series draws. See {@link SeriesChartSeries.type}. */
214
222
  export type SeriesChartType = "line" | "bar" | "area";
215
223
 
216
- /** One point of a series, as a `dot` function is handed it. */
217
- export interface SeriesChartPoint {
224
+ /** One point of a series, as a `dot` function is handed it. `TRow` is the chart's row
225
+ * type (0.53); without one, `row` is a {@link SeriesChartRow}, as before. */
226
+ export interface SeriesChartPoint<TRow extends object = object> {
218
227
  /** The series' key. */
219
228
  key: string;
220
229
  /** The row's position in `rows`. */
221
230
  index: number;
222
- /** The caller's own row, with everything the chart did not plot still in it. */
223
- row: SeriesChartRow;
231
+ /** The caller's own row, with everything the chart did not plot still in it: the very
232
+ * object in `rows`, typed as `rows` is. */
233
+ row: SeriesChartRowOf<TRow>;
224
234
  /** The abscissa in the caller's terms: the number, the category, or (time) a Date. */
225
235
  x: SeriesChartXValue;
226
236
  value: number;
@@ -233,9 +243,32 @@ export interface SeriesChartPoint {
233
243
 
234
244
  /** A row of a series chart: one abscissa value and the series' values at it. Anything
235
245
  * else in it (a raw ISO period for a drilldown) rides along untouched and is handed
236
- * back in {@link SeriesChartPoint.row} and {@link SeriesChartHit.row}. */
246
+ * back in {@link SeriesChartPoint.row} and {@link SeriesChartHit.row} — typed as the
247
+ * caller's rows are since 0.53, and as this where their type names no keys. */
237
248
  export type SeriesChartRow = Readonly<Record<string, unknown>>;
238
249
 
250
+ /**
251
+ * The row a hit or a point hands back, for a chart whose rows are `TRow[]` (0.53, keksdose
252
+ * after 0.52): `TRow` itself, so a drilldown reads `hit.row.rawPeriod` as the string it
253
+ * is. A {@link SeriesChartRow} where `TRow` names no keys — `object`, the default, which
254
+ * keeps `SeriesChartHit` and `SeriesChartPoint` without a type argument what they were, and
255
+ * the `object[]` rows an app passes today; `{}`; and `any` or `never` (`rows={[]}`), which
256
+ * are both assignable to `null` while no object type is.
257
+ *
258
+ * Both tests take `TRow` in the CHECKED position, never in the `extends` one, so TypeScript
259
+ * measures every type below as covariant in it (`SeriesChartSeries`, through `dot`'s
260
+ * parameter, contravariant) and relates two of them by their type arguments alone: a
261
+ * `SeriesChartHit<Month>` is a `SeriesChartHit`, and a handler or a series typed without an
262
+ * argument fits a chart of `Month` rows, though an interface has no index signature and is
263
+ * not, field by field, a `SeriesChartRow`. Written `object extends TRow ?`, the type is
264
+ * invariant and both of those stop type-checking.
265
+ */
266
+ type SeriesChartRowOf<TRow> = [TRow] extends [null]
267
+ ? SeriesChartRow
268
+ : [keyof TRow] extends [never]
269
+ ? SeriesChartRow
270
+ : TRow;
271
+
239
272
  /** An abscissa value: a number (`x.type: "number"`), a category (a string, a number or
240
273
  * a Date standing for a period), or a time (a Date, epoch ms or an ISO string). */
241
274
  export type SeriesChartXValue = number | string | Date;
@@ -551,29 +584,42 @@ export interface SeriesChartMarker {
551
584
  r?: number;
552
585
  }
553
586
 
554
- /** What a click on the plot picked. */
555
- export interface SeriesChartHit {
587
+ /** What a click on the plot picked. `TRow` is the chart's row type (0.53); without one,
588
+ * `row` is a {@link SeriesChartRow}, as before. */
589
+ export interface SeriesChartHit<TRow extends object = object> {
556
590
  /** The row's position in `rows`. */
557
591
  index: number;
558
- /** The caller's own row — with its `rawPeriod`, id or whatever the drilldown needs. */
559
- row: SeriesChartRow;
592
+ /** The caller's own row — with its `rawPeriod`, id or whatever the drilldown needs: the
593
+ * very object in `rows`, typed as `rows` is, so it is read without a cast. */
594
+ row: SeriesChartRowOf<TRow>;
560
595
  x: SeriesChartXValue;
561
596
  /** The series, when the click landed ON a bar. A click anywhere else picks the slot
562
597
  * (the period), not a series. */
563
598
  key?: string;
564
599
  }
565
600
 
566
- export interface SeriesChartProps {
601
+ /**
602
+ * The props of {@link SeriesChart} and {@link StaticSeriesChart}, for rows of type `TRow`.
603
+ *
604
+ * `TRow` is inferred from `rows` (0.53, keksdose after 0.52), and what the chart hands back
605
+ * — `hit.row` in `onPointClick`, `point.row` in a series' `dot` — is that type, so a
606
+ * drilldown reads its own fields without narrowing an `unknown`. Without a type argument,
607
+ * `SeriesChartProps` is what it was: any object is a row, and a row comes back as a
608
+ * {@link SeriesChartRow}. So is `ComponentProps<typeof SeriesChart>`, which reads the
609
+ * constraint, `object`. A series, a handler or a `dot` typed without an argument fits a
610
+ * chart of typed rows too.
611
+ */
612
+ export interface SeriesChartProps<TRow extends object = object> {
567
613
  /**
568
614
  * One row per abscissa value. A missing key is a hole (see `connectNulls`).
569
615
  *
570
616
  * Any object (0.52, refactor audit 2026-10-30, API-2): an `interface` has no index
571
617
  * signature, so a typed sweep did not fit `SeriesChartRow` and Kurvenschmiede cast its
572
618
  * Ackermann rows `as unknown as Record<string, number>[]`. The chart reads the keys
573
- * `series` and `x` name, and hands the row back as {@link SeriesChartRow}.
619
+ * `series` and `x` name, and hands the row back typed as these are (0.53).
574
620
  */
575
- rows: readonly object[];
576
- series: readonly SeriesChartSeries[];
621
+ rows: readonly TRow[];
622
+ series: readonly SeriesChartSeries<TRow>[];
577
623
  /** Default: one untitled axis, `"y"`. */
578
624
  axes?: readonly SeriesChartAxis[];
579
625
  x?: SeriesChartX;
@@ -628,7 +674,7 @@ export interface SeriesChartProps {
628
674
  * a chart without bars, named "period — series: value", behind ONE tab stop. With this
629
675
  * they are buttons that Enter and Space activate; without, images to read.
630
676
  */
631
- onPointClick?: (hit: SeriesChartHit) => void;
677
+ onPointClick?: (hit: SeriesChartHit<TRow>) => void;
632
678
  /** Where the tooltip may go: for a chart inside a scroll wrapper. See
633
679
  * {@link SeriesChartTooltip}. */
634
680
  tooltip?: SeriesChartTooltip;
@@ -760,8 +806,10 @@ export interface SeriesLegendAxes {
760
806
  * series whose axis is not drawn says the unit that axis would have: "Velocity (mm/s)"
761
807
  * — see {@link SeriesChartAxis.unit}. Without them, the labels are as given.
762
808
  */
763
- export function seriesLegendEntries(
764
- series: readonly SeriesChartSeries[],
809
+ export function seriesLegendEntries<TRow extends object>(
810
+ // Generic only so a typed list fits (0.53): `SeriesChartSeries<Month>` is not a
811
+ // `SeriesChartSeries`, whose `dot` would be handed rows that are not `Month`s.
812
+ series: readonly SeriesChartSeries<TRow>[],
765
813
  { axes, budgeted }: SeriesLegendAxes = {},
766
814
  ): LegendEntry[] {
767
815
  const undrawn = new Map<string, string | undefined>();
@@ -1819,7 +1867,12 @@ const ZoomablePlot = withChartZoom(SeriesPlot);
1819
1867
  * category or time chart otherwise re-maps every row on every render, and `maxRows`'
1820
1868
  * envelope would be worked out again with it.
1821
1869
  */
1822
- function usePlotProps(props: SeriesChartProps): PlotProps {
1870
+ function usePlotProps<TRow extends object>(typed: SeriesChartProps<TRow>): PlotProps {
1871
+ // `TRow` types only what the caller is handed back, and that is its own row objects,
1872
+ // untouched (0.53): the plot reads them as the `object` rows they are. Through `unknown`
1873
+ // because the two are not comparable for an unresolved `TRow`: `SeriesChartSeries` is
1874
+ // contravariant in it (its `dot` is handed rows) and `rows` covariant.
1875
+ const props = typed as unknown as SeriesChartProps;
1823
1876
  const x = props.x ?? {};
1824
1877
  const { rows } = props;
1825
1878
  const { type, key } = x;
@@ -1833,7 +1886,7 @@ function usePlotProps(props: SeriesChartProps): PlotProps {
1833
1886
  * button. For a thumbnail, a print view, or a chart the consumer wraps in its own
1834
1887
  * interaction.
1835
1888
  */
1836
- export function StaticSeriesChart(props: SeriesChartProps) {
1889
+ export function StaticSeriesChart<TRow extends object = object>(props: SeriesChartProps<TRow>) {
1837
1890
  return <SeriesPlot {...usePlotProps(props)} />;
1838
1891
  }
1839
1892
 
@@ -1842,8 +1895,12 @@ export function StaticSeriesChart(props: SeriesChartProps) {
1842
1895
  * axes), double-click or press the reset button to go back; wrap several in
1843
1896
  * `SharedXZoom` to move their x windows together. Which axes zoom depends on the marks
1844
1897
  * — see {@link SeriesChartProps.zoomAxes}.
1898
+ *
1899
+ * Generic in its rows (0.53): `hit.row` and a `dot`'s `point.row` are typed as `rows` is —
1900
+ * see {@link SeriesChartProps}. A plain function, so the generic survives: a `memo` or a
1901
+ * `forwardRef` around it would read `TRow` as its constraint.
1845
1902
  */
1846
- export function SeriesChart(props: SeriesChartProps) {
1903
+ export function SeriesChart<TRow extends object = object>(props: SeriesChartProps<TRow>) {
1847
1904
  // The zoom fits the PLOTTED rows — slot indices and epoch ms, not the caller's labels.
1848
1905
  return <ZoomablePlot {...usePlotProps(props)} />;
1849
1906
  }
@@ -229,8 +229,27 @@ export interface StatTileProps extends Omit<ComponentPropsWithRef<"div">, "child
229
229
  /** Anything below the rest — a progress bar, a link row. */
230
230
  footer?: ReactNode;
231
231
  /** Tag every figure `data-private`, so the host's demo-mode rule blurs it — the
232
- * same attribute `Tooltip redact` and the chart tooltip's value cell carry. */
232
+ * same attribute `Tooltip redact` and the chart tooltip's value cell carry.
233
+ * The figures are the tile's own: value, delta, projection, sub-values and the
234
+ * trend. The `description` and `footer` are the app's nodes and stay readable;
235
+ * see {@link StatTileProps.descriptionSensitive} for the ones that carry an amount. */
233
236
  sensitive?: boolean;
237
+ /**
238
+ * Tag the `description` line `data-private` too, as `sensitive` tags the figures.
239
+ * For a description that names an amount: keksdose's tax reserve tile states its
240
+ * basis there ("…applied to €42,000"), and its report KpiCard puts a `hintSensitive`
241
+ * hint there — both tag their own `<span data-private>` today
242
+ * (reports/tabs/tax-tab.tsx, reports/charts/kpi-card.tsx). Separate from
243
+ * `sensitive`, so a description of words ("Last 12 months") on a money tile stays
244
+ * readable in demo mode. keksdose audit follow-up, 2026-11-01.
245
+ */
246
+ descriptionSensitive?: boolean;
247
+ /**
248
+ * Tag the `footer` `data-private`, as `sensitive` tags the figures — for a footer
249
+ * that carries an amount: keksdose's tax reserve tile's shortfall line ("Set aside
250
+ * €1,200 more…"), which it tags by hand. keksdose audit follow-up, 2026-11-01.
251
+ */
252
+ footerSensitive?: boolean;
234
253
  /** Skeleton in place of the value; delta, sub-values and trend are withheld. */
235
254
  loading?: boolean;
236
255
  /** `"sm"` for dense grids (a metrics wall); `"md"` the default KPI. */
@@ -317,6 +336,8 @@ export function StatTile({
317
336
  icon,
318
337
  footer,
319
338
  sensitive = false,
339
+ descriptionSensitive = false,
340
+ footerSensitive = false,
320
341
  loading = false,
321
342
  size = "md",
322
343
  variant = "card",
@@ -551,7 +572,11 @@ export function StatTile({
551
572
  )}
552
573
 
553
574
  {deltaNode}
554
- {description != null && <div className="mt-1 text-xs text-muted">{description}</div>}
575
+ {description != null && (
576
+ <div data-private={descriptionSensitive ? "" : undefined} className="mt-1 text-xs text-muted">
577
+ {description}
578
+ </div>
579
+ )}
555
580
  {!large && graphic && <div className="mt-2">{graphic}</div>}
556
581
 
557
582
  {!loading && ((large && projections.length > 0) || (subValues && subValues.length > 0)) && (
@@ -572,7 +597,11 @@ export function StatTile({
572
597
  </div>
573
598
  )}
574
599
 
575
- {footer != null && <div className="mt-2">{footer}</div>}
600
+ {footer != null && (
601
+ <div data-private={footerSensitive ? "" : undefined} className="mt-2">
602
+ {footer}
603
+ </div>
604
+ )}
576
605
  </Card>
577
606
  );
578
607
  }
@@ -13,13 +13,18 @@ export interface TreemapLabels {
13
13
  * `valueFormatter` when that returns text, else as the number in the provider's
14
14
  * locale; `share` is the tile's share of what is drawn, already a formatted percent
15
15
  * ("60 %" in German, "60%" in English) — the function only arranges them.
16
+ *
17
+ * `share` is `""` when the map's `shareInValue` says the formatter's text already holds
18
+ * it (0.53): leave out whatever frames it, as every catalogue drops its " (share)" —
19
+ * "Migros: CHF 412.30 · 12%", not "… · 12% ()".
16
20
  */
17
21
  tile: (name: string, value: string, share: string) => string;
18
22
  /**
19
23
  * A group's accessible name (0.52, `TreemapNode.children`): its frame is a stop of the
20
24
  * map's keyboard before its tiles, so a reader hears the group and its total once, on the
21
25
  * way in. `value` and `share` as for {@link TreemapLabels.tile}, of the group's tiles
22
- * together; the word "group" tells it from a tile of the same shape.
26
+ * together, the share `""` under `shareInValue` as there; the word "group" tells it from
27
+ * a tile of the same shape.
23
28
  */
24
29
  group: (name: string, value: string, share: string) => string;
25
30
  /**
@@ -32,8 +37,11 @@ export interface TreemapLabels {
32
37
  inGroup: (name: string, group: string) => string;
33
38
  }
34
39
 
40
+ // The share in brackets only when there is one: `""` under the map's `shareInValue` (0.53,
41
+ // keksdose after 0.52), which the catalogues in src/i18n/locales follow, each in its own
42
+ // typography.
35
43
  export const DEFAULT_TREEMAP_LABELS: TreemapLabels = {
36
- tile: (name, value, share) => `${name}: ${value} (${share})`,
37
- group: (name, value, share) => `${name}, group: ${value} (${share})`,
44
+ tile: (name, value, share) => `${name}: ${value}${share ? ` (${share})` : ""}`,
45
+ group: (name, value, share) => `${name}, group: ${value}${share ? ` (${share})` : ""}`,
38
46
  inGroup: (name, group) => `${name}, in ${group}`,
39
47
  };