@eifi1/ui-kit 0.35.0 → 0.36.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 (265) hide show
  1. package/README.md +35 -5
  2. package/dist/admin/role-select.d.ts +3 -2
  3. package/dist/admin/role-select.js +2 -0
  4. package/dist/admin/role-select.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +2 -2
  6. package/dist/components/amount-input.js.map +1 -1
  7. package/dist/components/autocomplete.d.ts +2 -2
  8. package/dist/components/autocomplete.js +2 -1
  9. package/dist/components/autocomplete.js.map +1 -1
  10. package/dist/components/button-look.d.ts +1 -1
  11. package/dist/components/button-look.js +9 -4
  12. package/dist/components/button-look.js.map +1 -1
  13. package/dist/components/checkbox-group.d.ts +5 -2
  14. package/dist/components/checkbox-group.js +15 -4
  15. package/dist/components/checkbox-group.js.map +1 -1
  16. package/dist/components/column-mapper.d.ts +5 -3
  17. package/dist/components/column-mapper.js +2 -0
  18. package/dist/components/column-mapper.js.map +1 -1
  19. package/dist/components/combobox.d.ts +2 -2
  20. package/dist/components/combobox.js +2 -1
  21. package/dist/components/combobox.js.map +1 -1
  22. package/dist/components/country-select.d.ts +3 -3
  23. package/dist/components/country-select.js +3 -0
  24. package/dist/components/country-select.js.map +1 -1
  25. package/dist/components/currency-select.d.ts +3 -2
  26. package/dist/components/currency-select.js +10 -1
  27. package/dist/components/currency-select.js.map +1 -1
  28. package/dist/components/date-field.d.ts +2 -1
  29. package/dist/components/date-field.js.map +1 -1
  30. package/dist/components/date-picker.js +3 -0
  31. package/dist/components/date-picker.js.map +1 -1
  32. package/dist/components/date-range-picker.js +3 -0
  33. package/dist/components/date-range-picker.js.map +1 -1
  34. package/dist/components/entity-combobox.d.ts +2 -2
  35. package/dist/components/entity-combobox.js +2 -1
  36. package/dist/components/entity-combobox.js.map +1 -1
  37. package/dist/components/field-parts.d.ts +66 -11
  38. package/dist/components/field-parts.js +32 -10
  39. package/dist/components/field-parts.js.map +1 -1
  40. package/dist/components/field-strip.d.ts +3 -2
  41. package/dist/components/field-strip.js +2 -1
  42. package/dist/components/field-strip.js.map +1 -1
  43. package/dist/components/field.d.ts +3 -2
  44. package/dist/components/field.js +24 -15
  45. package/dist/components/field.js.map +1 -1
  46. package/dist/components/help-popover.d.ts +9 -7
  47. package/dist/components/help-popover.js.map +1 -1
  48. package/dist/components/icon-picker.d.ts +3 -2
  49. package/dist/components/icon-picker.js +3 -0
  50. package/dist/components/icon-picker.js.map +1 -1
  51. package/dist/components/inline-entity-combobox.d.ts +2 -2
  52. package/dist/components/inline-entity-combobox.js +2 -1
  53. package/dist/components/inline-entity-combobox.js.map +1 -1
  54. package/dist/components/input.d.ts +2 -2
  55. package/dist/components/input.js +2 -1
  56. package/dist/components/input.js.map +1 -1
  57. package/dist/components/month-picker.d.ts +2 -1
  58. package/dist/components/month-picker.js +8 -1
  59. package/dist/components/month-picker.js.map +1 -1
  60. package/dist/components/multi-entity-combobox.d.ts +2 -2
  61. package/dist/components/multi-entity-combobox.js +2 -1
  62. package/dist/components/multi-entity-combobox.js.map +1 -1
  63. package/dist/components/multi-select.d.ts +2 -2
  64. package/dist/components/multi-select.js +3 -0
  65. package/dist/components/multi-select.js.map +1 -1
  66. package/dist/components/number-input.d.ts +2 -2
  67. package/dist/components/number-input.js.map +1 -1
  68. package/dist/components/numeric-field.d.ts +5 -2
  69. package/dist/components/numeric-field.js +4 -1
  70. package/dist/components/numeric-field.js.map +1 -1
  71. package/dist/components/one-time-code-input.d.ts +2 -1
  72. package/dist/components/one-time-code-input.js +2 -1
  73. package/dist/components/one-time-code-input.js.map +1 -1
  74. package/dist/components/phone-input.d.ts +1 -1
  75. package/dist/components/phone-input.js +4 -0
  76. package/dist/components/phone-input.js.map +1 -1
  77. package/dist/components/popover.js +1 -1
  78. package/dist/components/popover.js.map +1 -1
  79. package/dist/components/select.d.ts +4 -3
  80. package/dist/components/select.js +2 -1
  81. package/dist/components/select.js.map +1 -1
  82. package/dist/components/slider.d.ts +3 -2
  83. package/dist/components/slider.js +7 -1
  84. package/dist/components/slider.js.map +1 -1
  85. package/dist/components/stat-tile.js +2 -11
  86. package/dist/components/stat-tile.js.map +1 -1
  87. package/dist/components/swatch-picker.d.ts +3 -2
  88. package/dist/components/swatch-picker.js +3 -0
  89. package/dist/components/swatch-picker.js.map +1 -1
  90. package/dist/components/text-link.js +3 -1
  91. package/dist/components/text-link.js.map +1 -1
  92. package/dist/components/textarea.d.ts +2 -1
  93. package/dist/components/textarea.js +9 -3
  94. package/dist/components/textarea.js.map +1 -1
  95. package/dist/components/toggle-group.d.ts +2 -2
  96. package/dist/components/toggle-group.js +7 -3
  97. package/dist/components/toggle-group.js.map +1 -1
  98. package/dist/eslint/index.js +1 -1
  99. package/dist/i18n/defaults.js +5 -1
  100. package/dist/i18n/defaults.js.map +1 -1
  101. package/dist/i18n/german.js +97 -1
  102. package/dist/i18n/german.js.map +1 -1
  103. package/dist/i18n/kit-labels.d.ts +15 -0
  104. package/dist/i18n/kit-labels.js +21 -1
  105. package/dist/i18n/kit-labels.js.map +1 -1
  106. package/dist/i18n/locales/en.js +103 -2
  107. package/dist/i18n/locales/en.js.map +3 -3
  108. package/dist/i18n/locales/es.js +95 -1
  109. package/dist/i18n/locales/es.js.map +1 -1
  110. package/dist/i18n/locales/fr.js +96 -1
  111. package/dist/i18n/locales/fr.js.map +1 -1
  112. package/dist/i18n/locales/hu.js +95 -1
  113. package/dist/i18n/locales/hu.js.map +1 -1
  114. package/dist/i18n/locales/it.js +96 -1
  115. package/dist/i18n/locales/it.js.map +1 -1
  116. package/dist/i18n/locales/zh.js +95 -1
  117. package/dist/i18n/locales/zh.js.map +1 -1
  118. package/dist/i18n/review.js +13 -0
  119. package/dist/i18n/review.js.map +1 -1
  120. package/dist/i18n/shared-labels.js +3 -1
  121. package/dist/i18n/shared-labels.js.map +1 -1
  122. package/dist/index.d.ts +21 -0
  123. package/dist/index.js +38 -0
  124. package/dist/index.js.map +1 -1
  125. package/dist/lib/device-settings.d.ts +72 -0
  126. package/dist/lib/device-settings.js +64 -0
  127. package/dist/lib/device-settings.js.map +1 -0
  128. package/dist/lib/plain-text.d.ts +11 -0
  129. package/dist/lib/plain-text.js +16 -0
  130. package/dist/lib/plain-text.js.map +1 -0
  131. package/dist/lib/safe-storage.d.ts +6 -0
  132. package/dist/lib/safe-storage.js +11 -0
  133. package/dist/lib/safe-storage.js.map +1 -1
  134. package/dist/notifications/notification-parts.d.ts +39 -0
  135. package/dist/notifications/notification-parts.js +47 -0
  136. package/dist/notifications/notification-parts.js.map +1 -0
  137. package/dist/notifications/notification-types-setting.d.ts +62 -0
  138. package/dist/notifications/notification-types-setting.js +192 -0
  139. package/dist/notifications/notification-types-setting.js.map +1 -0
  140. package/dist/notifications/notification-types.d.ts +75 -0
  141. package/dist/notifications/notification-types.js +2 -0
  142. package/dist/notifications/notification-types.js.map +1 -0
  143. package/dist/notifications/notifications-labels.d.ts +131 -0
  144. package/dist/notifications/notifications-labels.js +83 -0
  145. package/dist/notifications/notifications-labels.js.map +1 -0
  146. package/dist/notifications/push-device-setting.d.ts +37 -0
  147. package/dist/notifications/push-device-setting.js +164 -0
  148. package/dist/notifications/push-device-setting.js.map +1 -0
  149. package/dist/notifications/push-devices-setting.d.ts +44 -0
  150. package/dist/notifications/push-devices-setting.js +203 -0
  151. package/dist/notifications/push-devices-setting.js.map +1 -0
  152. package/dist/notifications/quiet-hours-setting.d.ts +47 -0
  153. package/dist/notifications/quiet-hours-setting.js +131 -0
  154. package/dist/notifications/quiet-hours-setting.js.map +1 -0
  155. package/dist/notifications/time-zone-select.d.ts +50 -0
  156. package/dist/notifications/time-zone-select.js +76 -0
  157. package/dist/notifications/time-zone-select.js.map +1 -0
  158. package/dist/notifications/unsubscribe-panel.d.ts +53 -0
  159. package/dist/notifications/unsubscribe-panel.js +139 -0
  160. package/dist/notifications/unsubscribe-panel.js.map +1 -0
  161. package/dist/notifications/use-push-device.d.ts +122 -0
  162. package/dist/notifications/use-push-device.js +230 -0
  163. package/dist/notifications/use-push-device.js.map +1 -0
  164. package/dist/rhf/fields.d.ts +26 -19
  165. package/dist/rhf/fields.js +51 -22
  166. package/dist/rhf/fields.js.map +1 -1
  167. package/dist/settings/device-reset-setting-labels.d.ts +3 -0
  168. package/dist/settings/device-reset-setting-labels.js +15 -0
  169. package/dist/settings/device-reset-setting-labels.js.map +1 -0
  170. package/dist/settings/device-reset-setting.d.ts +58 -0
  171. package/dist/settings/device-reset-setting.js +51 -0
  172. package/dist/settings/device-reset-setting.js.map +1 -0
  173. package/dist/shell/app-shell.js +5 -0
  174. package/dist/shell/app-shell.js.map +1 -1
  175. package/dist/theme/contrast-tokens.d.ts +42 -4
  176. package/dist/theme/contrast-tokens.js +56 -9
  177. package/dist/theme/contrast-tokens.js.map +1 -1
  178. package/dist/theme/contrast.d.ts +4 -2
  179. package/dist/theme/contrast.js +13 -0
  180. package/dist/theme/contrast.js.map +1 -1
  181. package/dist/theme/palette-derive.d.ts +15 -1
  182. package/dist/theme/palette-derive.js +37 -8
  183. package/dist/theme/palette-derive.js.map +1 -1
  184. package/dist/theme/palette-presets.d.ts +2 -1
  185. package/dist/theme/palette-presets.js +5 -5
  186. package/dist/theme/palette-presets.js.map +1 -1
  187. package/dist/theme/palette-store.js +6 -0
  188. package/dist/theme/palette-store.js.map +1 -1
  189. package/dist/theme/text-size.js +6 -0
  190. package/dist/theme/text-size.js.map +1 -1
  191. package/dist/theme/theme-store.js +6 -0
  192. package/dist/theme/theme-store.js.map +1 -1
  193. package/package.json +1 -1
  194. package/src/admin/role-select.tsx +4 -1
  195. package/src/components/amount-input.tsx +2 -1
  196. package/src/components/autocomplete.tsx +5 -2
  197. package/src/components/button-look.ts +15 -5
  198. package/src/components/checkbox-group.tsx +22 -4
  199. package/src/components/column-mapper.tsx +6 -2
  200. package/src/components/combobox.tsx +5 -2
  201. package/src/components/country-select.tsx +6 -1
  202. package/src/components/currency-select.tsx +12 -3
  203. package/src/components/date-field.tsx +2 -2
  204. package/src/components/date-picker.tsx +3 -0
  205. package/src/components/date-range-picker.tsx +3 -0
  206. package/src/components/entity-combobox.tsx +5 -2
  207. package/src/components/field-parts.tsx +107 -19
  208. package/src/components/field-strip.tsx +4 -3
  209. package/src/components/field.tsx +28 -15
  210. package/src/components/help-popover.tsx +9 -7
  211. package/src/components/icon-picker.tsx +8 -3
  212. package/src/components/inline-entity-combobox.tsx +5 -2
  213. package/src/components/input.tsx +4 -2
  214. package/src/components/month-picker.tsx +10 -3
  215. package/src/components/multi-entity-combobox.tsx +5 -2
  216. package/src/components/multi-select.tsx +6 -1
  217. package/src/components/number-input.tsx +2 -2
  218. package/src/components/numeric-field.ts +10 -2
  219. package/src/components/one-time-code-input.tsx +5 -3
  220. package/src/components/phone-input.tsx +4 -0
  221. package/src/components/popover.tsx +3 -1
  222. package/src/components/select.tsx +6 -3
  223. package/src/components/slider.tsx +10 -3
  224. package/src/components/stat-tile.tsx +3 -18
  225. package/src/components/swatch-picker.tsx +9 -4
  226. package/src/components/text-link.tsx +3 -1
  227. package/src/components/textarea.tsx +11 -4
  228. package/src/components/toggle-group.tsx +21 -5
  229. package/src/i18n/defaults.ts +4 -0
  230. package/src/i18n/german.ts +113 -0
  231. package/src/i18n/kit-labels.tsx +51 -1
  232. package/src/i18n/locales/en.ts +6 -0
  233. package/src/i18n/locales/es.ts +106 -0
  234. package/src/i18n/locales/fr.ts +106 -0
  235. package/src/i18n/locales/hu.ts +104 -0
  236. package/src/i18n/locales/it.ts +105 -0
  237. package/src/i18n/locales/zh.ts +96 -0
  238. package/src/i18n/review.ts +13 -0
  239. package/src/i18n/shared-labels.ts +2 -0
  240. package/src/index.ts +50 -0
  241. package/src/lib/device-settings.ts +146 -0
  242. package/src/lib/plain-text.ts +22 -0
  243. package/src/lib/safe-storage.ts +16 -0
  244. package/src/notifications/notification-parts.tsx +83 -0
  245. package/src/notifications/notification-types-setting.tsx +309 -0
  246. package/src/notifications/notification-types.ts +80 -0
  247. package/src/notifications/notifications-labels.ts +238 -0
  248. package/src/notifications/push-device-setting.tsx +224 -0
  249. package/src/notifications/push-devices-setting.tsx +306 -0
  250. package/src/notifications/quiet-hours-setting.tsx +185 -0
  251. package/src/notifications/time-zone-select.tsx +126 -0
  252. package/src/notifications/unsubscribe-panel.tsx +203 -0
  253. package/src/notifications/use-push-device.ts +392 -0
  254. package/src/rhf/fields.tsx +77 -29
  255. package/src/settings/device-reset-setting-labels.ts +20 -0
  256. package/src/settings/device-reset-setting.tsx +119 -0
  257. package/src/shell/app-shell.tsx +8 -0
  258. package/src/theme/contrast-tokens.ts +106 -12
  259. package/src/theme/contrast.ts +30 -4
  260. package/src/theme/palette-derive.ts +68 -7
  261. package/src/theme/palette-presets.ts +9 -6
  262. package/src/theme/palette-store.ts +12 -0
  263. package/src/theme/text-size.ts +8 -0
  264. package/src/theme/theme-store.ts +10 -0
  265. package/tokens.css +38 -4
@@ -148,6 +148,7 @@ export function PhoneInput({
148
148
  inputClassName,
149
149
  label,
150
150
  hint,
151
+ help,
151
152
  error,
152
153
  invalid,
153
154
  disabled,
@@ -225,6 +226,9 @@ export function PhoneInput({
225
226
  // since 0.34, not a copy of its rule (DUP-2, DUP-8).
226
227
  const messages = useFieldMessages({
227
228
  hint,
229
+ // The number's label line takes the help's "?" (0.36); the hint is then the caption.
230
+ help,
231
+ name: label ?? rest["aria-label"],
228
232
  error,
229
233
  invalid,
230
234
  describedBy: ariaDescribedBy,
@@ -10,7 +10,9 @@ import { dirOf } from "../lib/direction";
10
10
  import type { Direction } from "../lib/direction";
11
11
  import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
12
12
  import { DEFAULT_POPOVER_LABELS } from "./popover-labels";
13
- import { mergeRefs } from "./field-parts";
13
+ // From ../lib/refs, not field-parts (round 0.36): field-parts renders a HelpPopover for a
14
+ // field's `help`, so importing from there would close a cycle, as it did for Tooltip.
15
+ import { mergeRefs } from "../lib/refs";
14
16
 
15
17
  /** The panel's accessible name. A `role="dialog"` with no name announces as "dialog"
16
18
  * and nothing else, which is worse than the unlabelled div it replaced — so it is a
@@ -6,6 +6,7 @@ import {
6
6
  LockedReason,
7
7
  mergeDescribedBy,
8
8
  type DisabledReasonDisplay,
9
+ type FieldHelpProps,
9
10
  useFieldError,
10
11
  useFieldHint,
11
12
  useLockReason,
@@ -20,7 +21,7 @@ import {
20
21
  fieldBelow,
21
22
  } from "./field-base";
22
23
 
23
- export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "size"> {
24
+ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "size">, FieldHelpProps {
24
25
  label?: ReactNode;
25
26
  /** See {@link Input}'s `invalid`. */
26
27
  invalid?: boolean;
@@ -33,7 +34,8 @@ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement
33
34
  * set in the value's type on top of both the label and the value. On an
34
35
  * UNLABELLED Select (no label line) a FieldHint sits at the field's end edge,
35
36
  * outside the box. Since 0.34 a FieldHint is the caption at Large and on a touch
36
- * screen, as on Input (`useInlineHint`); a `HelpPopover` stays a "?". */
37
+ * screen, as on Input (`useInlineHint`); a `HelpPopover` stays a "?". Beside a
38
+ * `help` (0.36) the hint is the caption at every size: the help's "?" is the only one. */
37
39
  hint?: ReactNode;
38
40
  /**
39
41
  * `"sm"`: a 28px, 12px-type select for a toolbar or a table header, where the
@@ -109,6 +111,7 @@ export function Select(props: SelectProps) {
109
111
  invalid,
110
112
  error,
111
113
  hint: hintProp,
114
+ help,
112
115
  size,
113
116
  selectClassName,
114
117
  disabledReason,
@@ -138,7 +141,7 @@ export function Select(props: SelectProps) {
138
141
  // and on a touch screen is its words, a caption like any text (§4; a HelpPopover stays
139
142
  // on the label line, §10.23). See `hint` above. Through useFieldHint since 0.34, not a
140
143
  // copy of its rule (DUP-2, refactor audit 2026-10).
141
- const hintParts = useFieldHint(hintProp, rest["aria-describedby"]);
144
+ const hintParts = useFieldHint(hintProp, rest["aria-describedby"], { help, name: label ?? rest["aria-label"] });
142
145
  const hint = hintParts.labelHint;
143
146
  const { isInvalid, describedBy, errorEl } = useFieldError(
144
147
  error,
@@ -1,7 +1,7 @@
1
1
  import { useId } from "react";
2
2
  import type { CSSProperties, ChangeEvent, InputHTMLAttributes, ReactNode, Ref } from "react";
3
3
  import { cn } from "../lib/cn";
4
- import { useFieldMessages } from "./field-parts";
4
+ import { useFieldMessages, type FieldHelpProps } from "./field-parts";
5
5
 
6
6
  /**
7
7
  * A range control on a native `<input type="range">`.
@@ -123,7 +123,8 @@ export interface SliderProps
123
123
  extends Omit<
124
124
  InputHTMLAttributes<HTMLInputElement>,
125
125
  "type" | "value" | "defaultValue" | "onChange" | "min" | "max" | "step" | "children"
126
- > {
126
+ >,
127
+ FieldHelpProps {
127
128
  /** The value. On a log scale this is the real value, not the track position. */
128
129
  value: number;
129
130
  /** The new value — a number, not the DOM event, like `ToggleGroup`. Fired on every
@@ -201,6 +202,7 @@ export function Slider({
201
202
  formatValue,
202
203
  label,
203
204
  hint,
205
+ help,
204
206
  readout,
205
207
  marks,
206
208
  className,
@@ -254,7 +256,12 @@ export function Slider({
254
256
  const hasMarkLabels = normalizedMarks.some((m) => m.label !== undefined);
255
257
 
256
258
  // The label line's "?", or the caption under the track (DUP-2).
257
- const messages = useFieldMessages({ hint, describedBy: rest["aria-describedby"] });
259
+ const messages = useFieldMessages({
260
+ hint,
261
+ help,
262
+ name: label ?? rest["aria-label"],
263
+ describedBy: rest["aria-describedby"],
264
+ });
258
265
  const labelHint = messages.labelHint;
259
266
  const hasHeader = label !== undefined || labelHint !== undefined || readout !== undefined;
260
267
  const plainReadout = typeof readout === "string" || typeof readout === "number";
@@ -1,7 +1,9 @@
1
- import { createContext, isValidElement, useContext, useId, useLayoutEffect, useState } from "react";
1
+ import { createContext, useContext, useId, useLayoutEffect, useState } from "react";
2
2
  import type { ComponentPropsWithRef, CSSProperties, ReactElement, ReactNode } from "react";
3
3
  import { ArrowDown, ArrowUp, Minus } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
+ // The sparkline's name: the label's text (lib since 0.36, which a field's `help` "?" shares).
6
+ import { plainText } from "../lib/plain-text";
5
7
  import { useKitLabels, useKitLink, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
6
8
  import { pickLinkRenderer, RenderedKitLink } from "./text-link";
7
9
  import { Card } from "./card";
@@ -612,23 +614,6 @@ function FigureRow({
612
614
  );
613
615
  }
614
616
 
615
- /**
616
- * The text of a label node, for the sparkline's name: a string or number as is, an
617
- * element by its children, recursively. A `<Tooltip label="…">Spend</Tooltip>` label
618
- * yields "Spend" — the text the reader sees, not the bubble's. `undefined` when there
619
- * is no text to find (an icon), so the sparkline falls back to its unnamed summary.
620
- */
621
- function plainText(node: ReactNode): string | undefined {
622
- const walk = (n: ReactNode): string => {
623
- if (typeof n === "string" || typeof n === "number") return String(n);
624
- if (Array.isArray(n)) return n.map(walk).join("");
625
- if (isValidElement<{ children?: ReactNode }>(n)) return walk(n.props.children);
626
- return "";
627
- };
628
- const text = walk(node).replace(/\s+/g, " ").trim();
629
- return text === "" ? undefined : text;
630
- }
631
-
632
617
  /**
633
618
  * Whether `el`'s text is wider than its box — i.e. whether `truncate` is cutting it.
634
619
  * Re-measured whenever the box resizes (a grid reflowing, a translation loading), not
@@ -7,7 +7,7 @@ import type { TileItem, TileSize } from "./tile-radio";
7
7
  import { Tooltip } from "./tooltip";
8
8
  import { useCommitReason, type CommitScope } from "./write-lock";
9
9
  import { hasContent } from "../lib/presence";
10
- import { LabelStrip, mergeDescribedBy, useFieldMessages } from "./field-parts";
10
+ import { LabelStrip, mergeDescribedBy, useFieldMessages, type FieldHelpProps } from "./field-parts";
11
11
  import { DEFAULT_SWATCH_PICKER_LABELS } from "./swatch-picker-labels";
12
12
 
13
13
  export interface SwatchPickerLabels {
@@ -62,7 +62,8 @@ export interface SwatchOption<T extends string> {
62
62
  * is omitted rather than shadowed, as on {@link ToggleGroup}.
63
63
  */
64
64
  export interface SwatchPickerProps<T extends string>
65
- extends Omit<ComponentPropsWithRef<"div">, "onChange" | "children" | "defaultValue"> {
65
+ extends Omit<ComponentPropsWithRef<"div">, "onChange" | "children" | "defaultValue">,
66
+ FieldHelpProps {
66
67
  options: SwatchOption<T>[];
67
68
  /** The chosen colour, or `null` for none. */
68
69
  value: T | null;
@@ -145,6 +146,7 @@ export function SwatchPicker<T extends string>({
145
146
  labels,
146
147
  label,
147
148
  hint,
149
+ help,
148
150
  disabledReason: ownDisabledReason,
149
151
  commit,
150
152
  className,
@@ -157,10 +159,13 @@ export function SwatchPicker<T extends string>({
157
159
  const disabledReason = useCommitReason(commit, ownDisabledReason);
158
160
  const locked = hasContent(disabledReason);
159
161
  const labelled = hasContent(label);
160
- // The label line's "?", or the caption under the swatches (DUP-2). Only with a label.
161
- // The caller's ids, the caption, then the group's state: mixed, locked.
162
+ // The label line's "?", or the caption under the swatches (DUP-2). Only with a label,
163
+ // the help's "?" too (0.36). The caller's ids, the caption, then the group's state:
164
+ // mixed, locked.
162
165
  const messages = useFieldMessages({
163
166
  hint: labelled ? hint : undefined,
167
+ help: labelled ? help : undefined,
168
+ name: label,
164
169
  describedBy: rest["aria-describedby"],
165
170
  reasonId: mergeDescribedBy(mixed && mixedId, locked && reasonId),
166
171
  });
@@ -228,7 +228,9 @@ export interface TextLinkProps
228
228
  }
229
229
 
230
230
  const TONE: Record<TextLinkTone, string> = {
231
- brand: "text-brand hover:text-[var(--brand-hover)]",
231
+ // Under More contrast the brand as text, `--brand-muted`, at rest and hovered (0.36,
232
+ // colour-roles §13.3 decision 8): `--brand` is a mark's colour, solved to 3:1.
233
+ brand: "text-brand hover:text-[var(--brand-hover)] contrast:text-brand-muted contrast:hover:text-brand-muted",
232
234
  muted: "text-muted hover:text-primary",
233
235
  danger: "text-danger hover:text-[var(--danger-hover)]",
234
236
  inherit: "",
@@ -1,7 +1,14 @@
1
1
  import { useId, useLayoutEffect, useRef } from "react";
2
2
  import type { ChangeEvent, ReactNode, Ref, TextareaHTMLAttributes } from "react";
3
3
  import { cn } from "../lib/cn";
4
- import { EndHintRow, FieldCaption, mergeDescribedBy, useFieldError, useFieldHint } from "./field-parts";
4
+ import {
5
+ EndHintRow,
6
+ FieldCaption,
7
+ mergeDescribedBy,
8
+ useFieldError,
9
+ useFieldHint,
10
+ type FieldHelpProps,
11
+ } from "./field-parts";
5
12
  import {
6
13
  CharacterCount,
7
14
  FIELD_BASE,
@@ -221,7 +228,7 @@ function TextareaLabelStrip() {
221
228
  return <span ref={ref} aria-hidden data-slot="label-strip" className={TEXTAREA_LABEL_STRIP} />;
222
229
  }
223
230
 
224
- export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
231
+ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement>, FieldHelpProps {
225
232
  label?: ReactNode;
226
233
  /** See {@link Input}'s `invalid`. */
227
234
  invalid?: boolean;
@@ -240,13 +247,13 @@ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElemen
240
247
  }
241
248
 
242
249
  export function Textarea(props: TextareaProps) {
243
- const { ref, className, label, id, placeholder, invalid, error, hint, showCount, countLabels, ...rest } = props;
250
+ const { ref, className, label, id, placeholder, invalid, error, hint, help, showCount, countLabels, ...rest } = props;
244
251
  // See Input's `reserve`.
245
252
  const reserve = "error" in props || "hint" in props || "showCount" in props;
246
253
  const generated = useId();
247
254
  const fieldId = id ?? generated;
248
255
  const countId = useId();
249
- const hintParts = useFieldHint(hint, rest["aria-describedby"]);
256
+ const hintParts = useFieldHint(hint, rest["aria-describedby"], { help, name: label ?? rest["aria-label"] });
250
257
  const maxLength = rest.maxLength;
251
258
  const counting = Boolean(showCount) && typeof maxLength === "number" && maxLength > 0;
252
259
  const length = useTypedLength(rest.value, rest.defaultValue);
@@ -11,11 +11,13 @@ import {
11
11
  DISABLED_REASON_LINE_CLASS,
12
12
  FIELD_CAPTION_CLASS,
13
13
  FIELD_ERROR_CLASS,
14
+ fieldHelpMark,
14
15
  isTextHint,
15
16
  LabelStrip,
16
17
  useDisabledReasonLine,
17
18
  useInlineHint,
18
19
  type DisabledReasonDisplay,
20
+ type FieldHelpProps,
19
21
  } from "./field-parts";
20
22
  import { FOCUS_RING_WIDTH } from "./focus-ring";
21
23
 
@@ -35,7 +37,8 @@ export interface ToggleOption<T extends string> {
35
37
  * worse than one that does not compile.
36
38
  */
37
39
  export interface ToggleGroupBaseProps<T extends string>
38
- extends Omit<ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
40
+ extends Omit<ComponentPropsWithoutRef<"div">, "onChange" | "children">,
41
+ FieldHelpProps {
39
42
  options: ToggleOption<T>[];
40
43
  className?: string;
41
44
  /** Applied to every option button (e.g. to tune height/rounding to match
@@ -380,6 +383,7 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
380
383
  labelPlacement = "field",
381
384
  chromeClassName,
382
385
  hint,
386
+ help,
383
387
  error,
384
388
  disabledReason: ownDisabledReason,
385
389
  disabledReasonDisplay,
@@ -407,10 +411,22 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
407
411
  // §4: at Large and on touch the reason is a line under the group, not a bubble.
408
412
  const reasonLine = useDisabledReasonLine(disabledReasonDisplay) && locked;
409
413
  // §4: likewise a FieldHint "?" on the label line becomes a caption under the group at
410
- // Large and on touch; text is a caption at every size, as Select's (see `hint`).
411
- const inlineHint = useInlineHint(hint);
412
- const hintAsCaption = isTextHint(hint) ? hint : inlineHint !== hint ? inlineHint : undefined;
413
- const labelHint = hintAsCaption === undefined ? hint : undefined;
414
+ // Large and on touch; text is a caption at every size, as Select's (see `hint`). With
415
+ // a `help` (0.36, only with `label`, as `hint`), its "?" is the label line's only one,
416
+ // and the hint — a FieldHint's words included — is the caption at every size.
417
+ const helpMark = fieldHelpMark(hasContent(label) ? help : undefined, label);
418
+ const withHelp = helpMark !== undefined;
419
+ const inlineHint = useInlineHint(hint, withHelp);
420
+ const hintAsCaption = withHelp
421
+ ? hasContent(inlineHint)
422
+ ? inlineHint
423
+ : undefined
424
+ : isTextHint(hint)
425
+ ? hint
426
+ : inlineHint !== hint
427
+ ? inlineHint
428
+ : undefined;
429
+ const labelHint = helpMark ?? (hintAsCaption === undefined ? hint : undefined);
414
430
  // Locked looks disabled — the dimmed group, the not-allowed cursor, no hover offer —
415
431
  // but stays focusable; `disabled` alone also takes the segments out of the tab order.
416
432
  const dimmed = disabled || locked;
@@ -87,6 +87,7 @@ import { DEFAULT_DEMO_LABELS } from "../demo/demo-labels";
87
87
  import { DEFAULT_BILLING_LABELS } from "../billing/billing-labels";
88
88
  import { DEFAULT_PLAN_CHANGE_LABELS } from "../admin/plan-change-labels";
89
89
  import { DEFAULT_APPEARANCE_LABELS } from "../components/appearance-labels";
90
+ import { DEFAULT_DEVICE_RESET_LABELS } from "../settings/device-reset-setting-labels";
90
91
  import { DEFAULT_ROW_ACTIONS_LABELS } from "../components/row-actions-labels";
91
92
  import { DEFAULT_APP_SHELL_MORE_LABELS } from "../shell/app-shell-labels";
92
93
  import { DEFAULT_ACCOUNT_STATE_LABELS } from "../components/account-chips-labels";
@@ -100,6 +101,7 @@ import { DEFAULT_IBAN_INPUT_LABELS } from "../components/iban-input-labels";
100
101
  import { DEFAULT_PHONE_INPUT_LABELS } from "../components/phone-input-labels";
101
102
  import { DEFAULT_SIGN_CHIP_LABELS } from "../components/sign-chip-labels";
102
103
  import { DEFAULT_COLUMN_MAPPER_LABELS } from "../components/column-mapper-labels";
104
+ import { DEFAULT_NOTIFICATIONS_LABELS } from "../notifications/notifications-labels";
103
105
  import {
104
106
  DEFAULT_APP_SHELL_LABELS,
105
107
  DEFAULT_CALCULATOR_LABELS,
@@ -231,6 +233,7 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
231
233
  billing: DEFAULT_BILLING_LABELS,
232
234
  planChange: DEFAULT_PLAN_CHANGE_LABELS,
233
235
  appearance: DEFAULT_APPEARANCE_LABELS,
236
+ deviceReset: DEFAULT_DEVICE_RESET_LABELS,
234
237
  rowActions: DEFAULT_ROW_ACTIONS_LABELS,
235
238
  appShellMore: DEFAULT_APP_SHELL_MORE_LABELS,
236
239
  characterCount: DEFAULT_CHARACTER_COUNT_LABELS,
@@ -240,4 +243,5 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
240
243
  phoneInput: DEFAULT_PHONE_INPUT_LABELS,
241
244
  signChip: DEFAULT_SIGN_CHIP_LABELS,
242
245
  columnMapper: DEFAULT_COLUMN_MAPPER_LABELS,
246
+ notifications: DEFAULT_NOTIFICATIONS_LABELS,
243
247
  };
@@ -268,6 +268,8 @@ export function germanLabels(numberLocale: string): UiKitLabels {
268
268
  noResults: "Keine Ergebnisse",
269
269
  fieldValue: (field, value) => `${field}: ${value}`,
270
270
  opensInNewTab: "öffnet in einem neuen Tab",
271
+ aboutField: (field) => `Hilfe zu „${field}“`,
272
+ help: "Hilfe",
271
273
  },
272
274
  dataTable: {
273
275
  columns: "Spalten",
@@ -1072,6 +1074,102 @@ export function germanLabels(numberLocale: string): UiKitLabels {
1072
1074
  rateLimited,
1073
1075
  failed: "Das hat nicht geklappt. Bitte erneut versuchen.",
1074
1076
  },
1077
+ // 0.36.0 — docs/notifications-harmonization.md §4.2: the notification cards. The push
1078
+ // and device words follow keksdose's reviewed German, formal.
1079
+ notifications: {
1080
+ typesTitle: "Benachrichtigungen zu",
1081
+ typesDescription: "Wählen Sie, worüber Sie benachrichtigt werden, und wie.",
1082
+ byEmail: "Per E-Mail",
1083
+ emailUnverified: "Bestätigen Sie Ihre Adresse, um diese zu erhalten.",
1084
+ digestCadence: "Häufigkeit",
1085
+ digestWeekly: "Wöchentlich",
1086
+ digestMonthly: "Monatlich",
1087
+ typesLoading: "Benachrichtigungseinstellungen werden geladen…",
1088
+ typesEmpty: "Für dieses Konto gibt es keine Benachrichtigungen.",
1089
+ quietTitle: "Ruhezeiten",
1090
+ quietDescription: (inbox) =>
1091
+ inbox
1092
+ ? "In diesem Zeitfenster kommen keine Push-Benachrichtigungen. Im Posteingang kommen sie trotzdem an."
1093
+ : "In diesem Zeitfenster kommen keine Push-Benachrichtigungen.",
1094
+ quietFrom: "Von",
1095
+ quietUntil: "Bis",
1096
+ quietIncomplete: "Legen Sie eine Start- und eine Endzeit fest. Ruhezeiten gelten erst, wenn beide gesetzt sind.",
1097
+ timeZone: "Zeitzone",
1098
+ deviceZone: (zone) => `Dieses Gerät ist in der Zeitzone ${zone}.`,
1099
+ useDeviceZone: "Übernehmen",
1100
+ regions: {
1101
+ Africa: "Afrika",
1102
+ America: "Amerika",
1103
+ Antarctica: "Antarktis",
1104
+ Arctic: "Arktis",
1105
+ Asia: "Asien",
1106
+ Atlantic: "Atlantik",
1107
+ Australia: "Australien",
1108
+ Europe: "Europa",
1109
+ Indian: "Indischer Ozean",
1110
+ Pacific: "Pazifik",
1111
+ },
1112
+ pushTitle: "Push-Benachrichtigungen",
1113
+ pushDescription: "Benachrichtigungen auf diesem Gerät, auch wenn die App geschlossen ist.",
1114
+ unsupported: "Dieser Browser unterstützt keine Push-Benachrichtigungen.",
1115
+ iosInstall: (app) =>
1116
+ `Fügen Sie ${app} auf dem iPhone oder iPad zuerst zum Home-Bildschirm hinzu: iOS stellt Benachrichtigungen nur der installierten App zu.`,
1117
+ notConfigured: "Push-Benachrichtigungen sind auf diesem Server nicht eingerichtet.",
1118
+ blocked: "Benachrichtigungen sind in den Einstellungen dieses Browsers blockiert.",
1119
+ on: "Benachrichtigungen sind für dieses Gerät eingeschaltet.",
1120
+ off: "Benachrichtigungen sind für dieses Gerät ausgeschaltet.",
1121
+ otherAccount:
1122
+ "Dieser Browser erhält Benachrichtigungen für ein anderes Konto. Schalten Sie sie hier ein, um sie für das angemeldete Konto zu erhalten.",
1123
+ enable: "Benachrichtigungen einschalten",
1124
+ disable: "Ausschalten",
1125
+ sendTest: "Test senden",
1126
+ enableFailed: "Benachrichtigungen konnten nicht eingeschaltet werden.",
1127
+ noServiceWorker:
1128
+ "Der Service Worker der App ist nicht erreichbar. Laden Sie die Seite neu und versuchen Sie es erneut.",
1129
+ pushTimeout:
1130
+ "Der Push-Dienst des Browsers antwortet nicht. Prüfen Sie Ihre Verbindung und versuchen Sie es erneut.",
1131
+ testSent: (count) =>
1132
+ count === 1 ? `Test an ${n(count)} Gerät gesendet` : `Test an ${n(count)} Geräte gesendet`,
1133
+ testNone:
1134
+ "Kein Gerät hat den Test angenommen. Schalten Sie zuerst die Benachrichtigungen für dieses Gerät ein.",
1135
+ testFailed: "Die Test-Benachrichtigung konnte nicht gesendet werden.",
1136
+ explainerTitle: "Benachrichtigungen einschalten?",
1137
+ explainerBody: (app) =>
1138
+ `${app} sendet nur Benachrichtigungen zu dem, was Sie auswählen, und Sie können sie jederzeit ausschalten.`,
1139
+ explainerHint: "Als Nächstes fragt Ihr Browser nach der Berechtigung.",
1140
+ explainerContinue: "Weiter",
1141
+ devicesTitle: "Benachrichtigungsgeräte",
1142
+ devicesDescription:
1143
+ "Alle Geräte, die Push-Benachrichtigungen für Ihr Konto erhalten. Entfernen Sie die, die Sie nicht mehr nutzen.",
1144
+ devicesLoading: "Geräte werden geladen…",
1145
+ devicesEmpty: "Noch kein Gerät erhält Benachrichtigungen.",
1146
+ unknownDevice: "Unbekanntes Gerät",
1147
+ thisDevice: "Dieses Gerät",
1148
+ added: (date) => `Hinzugefügt am ${date}`,
1149
+ lastDelivery: (date) => `Letzte Zustellung am ${date}`,
1150
+ noDelivery: "Noch keine Zustellung",
1151
+ failures: (count) =>
1152
+ count === 1
1153
+ ? `${n(count)} fehlgeschlagene Zustellung in Folge`
1154
+ : `${n(count)} fehlgeschlagene Zustellungen in Folge`,
1155
+ remove: "Entfernen",
1156
+ // Starts with `remove`, the button's visible text, for voice control.
1157
+ removeItem: (device) => `Entfernen: ${device}`,
1158
+ removePrompt: "Keine Benachrichtigungen mehr an dieses Gerät senden?",
1159
+ unsubscribeTitle: "E-Mail-Benachrichtigungen",
1160
+ unsubscribeAbout: (type) => `Sie erhalten E-Mails zu: ${type}.`,
1161
+ unsubscribeGeneric: "Sie erhalten Benachrichtigungs-E-Mails.",
1162
+ stopThese: "Diese E-Mails abbestellen",
1163
+ stopAll: (app) => `Alle Benachrichtigungs-E-Mails von ${app} abbestellen`,
1164
+ stoppedThese: "Erledigt. Sie erhalten diese E-Mails nicht mehr.",
1165
+ stoppedAll: (app) => `Erledigt. Sie erhalten keine Benachrichtigungs-E-Mails von ${app} mehr.`,
1166
+ accountMail: "E-Mails zu Ihrem Konto erhalten Sie weiterhin.",
1167
+ turnBackOn: "Sie können sie jederzeit in den Einstellungen wieder einschalten.",
1168
+ openSettings: "Einstellungen öffnen",
1169
+ invalidLink:
1170
+ "Dieser Link funktioniert nicht mehr. Sie können E-Mails stattdessen in den Einstellungen abbestellen.",
1171
+ failed: "Das hat nicht geklappt. Bitte erneut versuchen.",
1172
+ },
1075
1173
  sessions: {
1076
1174
  title: "Sitzungen",
1077
1175
  description:
@@ -1634,6 +1732,21 @@ export function germanLabels(numberLocale: string): UiKitLabels {
1634
1732
  contrastModes: { system: "System", standard: "Standard", more: "Mehr" },
1635
1733
  saveFailed: "Konnte nicht in Ihrem Konto gespeichert werden. Auf diesem Gerät gilt es trotzdem.",
1636
1734
  },
1735
+ // 0.36.0 — docs/settings-harmonization.md §11.3: resetting this device's settings, the
1736
+ // last card of "Darstellung". "Theme", as `topBar.theme`; "Bereiche" for the panels
1737
+ // (the sidebar, folded groups).
1738
+ deviceReset: {
1739
+ title: "Dieses Gerät zurücksetzen",
1740
+ description:
1741
+ "Setzt die Darstellung auf diesem Gerät und die Ansicht seiner Tabellen und Bereiche auf die Standardwerte zurück. Ihr Konto, Ihre Daten und Ihre Anmeldung bleiben unberührt.",
1742
+ action: "Einstellungen dieses Geräts zurücksetzen",
1743
+ confirmTitle: "Einstellungen dieses Geräts zurücksetzen?",
1744
+ confirmBody:
1745
+ "Theme, Farben, Textgröße, Kontrast und Sprache kehren hier zu ihren Standardwerten zurück, und Tabellen und Bereiche öffnen sich wie beim ersten Mal. Wo Ihr Konto eine Sprache, eine Textgröße oder einen Kontrast festlegt, folgt dieses Gerät wieder dem Konto. Ihr Konto und Ihre anderen Geräte bleiben, wie sie sind.",
1746
+ confirmAction: "Zurücksetzen",
1747
+ done: "Die Einstellungen dieses Geräts sind auf die Standardwerte zurückgesetzt.",
1748
+ keywords: "zurücksetzen standard standardwerte gerät wiederherstellen darstellung tabellen layout",
1749
+ },
1637
1750
  rowActions: {
1638
1751
  actions: "Aktionen",
1639
1752
  actionsFor: (name) => `Aktionen für ${name}`,
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useMemo } from "react";
1
+ import { createContext, useContext, useEffect, useMemo } from "react";
2
2
  import {
3
3
  DEFAULT_APP_SHELL_LABELS,
4
4
  DEFAULT_CALCULATOR_LABELS,
@@ -98,6 +98,7 @@ import type { DemoLabels } from "../demo/demo-labels";
98
98
  import type { BillingLabels } from "../billing/billing-labels";
99
99
  import type { PlanChangeLabels } from "../admin/plan-change";
100
100
  import type { AppearanceLabels } from "../components/appearance-labels";
101
+ import type { DeviceResetLabels } from "../settings/device-reset-setting";
101
102
  import type { RowActionsLabels } from "../components/row-actions";
102
103
  import type { AppShellMoreLabels } from "../shell/app-shell";
103
104
  import type { AccountStateLabels } from "../components/account-chips";
@@ -111,6 +112,7 @@ import type { IbanInputLabels } from "../components/iban-input";
111
112
  import type { PhoneInputLabels } from "../components/phone-input";
112
113
  import type { SignChipLabels } from "../components/sign-chip";
113
114
  import type { ColumnMapperLabels } from "../components/column-mapper";
115
+ import type { NotificationsLabels } from "../notifications/notifications-labels";
114
116
 
115
117
  /**
116
118
  * EVERY string the kit renders, as one typed tree — and an optional provider that
@@ -168,6 +170,12 @@ export interface CommonLabels {
168
170
  /** Read after a link that opens a new tab (`<Button href external>`, TextLink):
169
171
  * nothing else tells a screen reader the page is about to change tabs. */
170
172
  opensInNewTab: string;
173
+ /** The name of a field's `help` "?" and its panel (round 0.36): "About <field>", the
174
+ * field's label as the argument — "About Jerk shape". A `HelpPopover` passed as
175
+ * `help` keeps its own name. */
176
+ aboutField: (field: string) => string;
177
+ /** …and of a `help` "?" on a field with no label and no `aria-label` to name it by. */
178
+ help: string;
171
179
  }
172
180
 
173
181
  /** `DatePicker` / `DateRangePicker` chrome. The calendar inside has its own
@@ -421,6 +429,9 @@ export interface UiKitLabels {
421
429
  planChange: PlanChangeLabels;
422
430
  /** 0.32.0: the text-size and contrast settings (docs/text-size-harmonization.md §6). */
423
431
  appearance: AppearanceLabels;
432
+ /** 0.36.0: resetting this device's settings, the appearance group's last card
433
+ * (docs/settings-harmonization.md §11.3). */
434
+ deviceReset: DeviceResetLabels;
424
435
  /** 0.32.0: a row's actions folded into a "⋯" menu at Large (text size §10.8). */
425
436
  rowActions: RowActionsLabels;
426
437
  /** 0.32.0: the phone bar's "More" cell and sheet (text size §10.7). */
@@ -440,6 +451,10 @@ export interface UiKitLabels {
440
451
  /** 0.23.0: `ColumnMapper` / `ColumnRoleTable` — a pasted or dropped table, a role per
441
452
  * column. */
442
453
  columnMapper: ColumnMapperLabels;
454
+ /** 0.36.0: the notification cards — the types, quiet hours and the zone picker, this
455
+ * device's push, the device list, and the unsubscribe page
456
+ * (docs/notifications-harmonization.md §4.2). */
457
+ notifications: NotificationsLabels;
443
458
  }
444
459
 
445
460
  /**
@@ -660,9 +675,44 @@ export function UiKitProvider({
660
675
  }),
661
676
  [outer, labels, locale, weekStartsOn, linkComponent, chartTooltipPlacement, formatDate, tableStorageKeyPrefix],
662
677
  );
678
+ useTableViewsJoin(value.tableStorageKeyPrefix ?? DEFAULT_TABLE_STORAGE_KEY_PREFIX);
663
679
  return <KitI18nContext.Provider value={value}>{children}</KitI18nContext.Provider>;
664
680
  }
665
681
 
682
+ /** The kit's own table prefix: `DEFAULT_PERSIST_PREFIX` of components/use-table-state,
683
+ * restated because that module imports the router (a test pins the two equal). */
684
+ const DEFAULT_TABLE_STORAGE_KEY_PREFIX = "hbui-table:";
685
+
686
+ /**
687
+ * Reset this device's settings (docs/settings-harmonization.md §11.4): the tables below a
688
+ * provider keep their views under its prefix, so the provider joins the registry with
689
+ * that prefix — a view, no `reset`, since a table reads its view when it next mounts —
690
+ * while it is mounted. Nested providers with one prefix hold one entry.
691
+ *
692
+ * Through a literal `import()`, not a static import: every catalogue reaches this module,
693
+ * and a catalogue may reach nothing outside i18n/ (scripts/check-module-graph.mjs,
694
+ * english-catalogue-graph.test.ts). The theme stores and the shell import the registry
695
+ * statically, so it is loaded already and the join lands a microtask after the mount.
696
+ */
697
+ function useTableViewsJoin(prefix: string): void {
698
+ useEffect(() => {
699
+ let leave: (() => void) | undefined;
700
+ let gone = false;
701
+ import("../lib/device-settings").then(
702
+ ({ registerDeviceSetting }) => {
703
+ if (!gone) leave = registerDeviceSetting({ key: prefix, prefix: true, kind: "view" });
704
+ },
705
+ // A module of the kit's own that fails to load has nothing to join; the provider
706
+ // and its tables work as before.
707
+ () => undefined,
708
+ );
709
+ return () => {
710
+ gone = true;
711
+ leave?.();
712
+ };
713
+ }, [prefix]);
714
+ }
715
+
666
716
  function isRecord(v: unknown): v is Record<string, unknown> {
667
717
  return typeof v === "object" && v !== null && !Array.isArray(v);
668
718
  }
@@ -198,6 +198,12 @@ export function uiKitLabelsEn(numberLocale = "en-US"): UiKitLabels {
198
198
  ? "1 item others can see will pass to an administrator."
199
199
  : `${n(count)} items others can see will pass to an administrator.`,
200
200
  },
201
+ notifications: {
202
+ ...d.notifications,
203
+ testSent: (count) => (count === 1 ? "Test sent to 1 device" : `Test sent to ${n(count)} devices`),
204
+ failures: (count) =>
205
+ count === 1 ? "1 failed delivery in a row" : `${n(count)} failed deliveries in a row`,
206
+ },
201
207
  settings: {
202
208
  ...d.settings,
203
209
  matchCount: (count) => (count === 1 ? "1 match" : `${n(count)} matches`),