@eifi1/ui-kit 0.35.0 → 0.37.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 (269) 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/eslint/role-utilities.d.ts +34 -2
  100. package/dist/eslint/role-utilities.js +30 -9
  101. package/dist/eslint/role-utilities.js.map +1 -1
  102. package/dist/i18n/defaults.js +5 -1
  103. package/dist/i18n/defaults.js.map +1 -1
  104. package/dist/i18n/german.js +97 -1
  105. package/dist/i18n/german.js.map +1 -1
  106. package/dist/i18n/kit-labels.d.ts +16 -0
  107. package/dist/i18n/kit-labels.js +21 -1
  108. package/dist/i18n/kit-labels.js.map +1 -1
  109. package/dist/i18n/locales/en.js +103 -2
  110. package/dist/i18n/locales/en.js.map +3 -3
  111. package/dist/i18n/locales/es.js +95 -1
  112. package/dist/i18n/locales/es.js.map +1 -1
  113. package/dist/i18n/locales/fr.js +96 -1
  114. package/dist/i18n/locales/fr.js.map +1 -1
  115. package/dist/i18n/locales/hu.js +95 -1
  116. package/dist/i18n/locales/hu.js.map +1 -1
  117. package/dist/i18n/locales/it.js +96 -1
  118. package/dist/i18n/locales/it.js.map +1 -1
  119. package/dist/i18n/locales/zh.js +95 -1
  120. package/dist/i18n/locales/zh.js.map +1 -1
  121. package/dist/i18n/review.js +13 -0
  122. package/dist/i18n/review.js.map +1 -1
  123. package/dist/i18n/shared-labels.js +3 -1
  124. package/dist/i18n/shared-labels.js.map +1 -1
  125. package/dist/index.d.ts +21 -0
  126. package/dist/index.js +38 -0
  127. package/dist/index.js.map +1 -1
  128. package/dist/lib/device-settings.d.ts +72 -0
  129. package/dist/lib/device-settings.js +64 -0
  130. package/dist/lib/device-settings.js.map +1 -0
  131. package/dist/lib/plain-text.d.ts +11 -0
  132. package/dist/lib/plain-text.js +16 -0
  133. package/dist/lib/plain-text.js.map +1 -0
  134. package/dist/lib/safe-storage.d.ts +6 -0
  135. package/dist/lib/safe-storage.js +11 -0
  136. package/dist/lib/safe-storage.js.map +1 -1
  137. package/dist/notifications/notification-parts.d.ts +39 -0
  138. package/dist/notifications/notification-parts.js +47 -0
  139. package/dist/notifications/notification-parts.js.map +1 -0
  140. package/dist/notifications/notification-types-setting.d.ts +62 -0
  141. package/dist/notifications/notification-types-setting.js +192 -0
  142. package/dist/notifications/notification-types-setting.js.map +1 -0
  143. package/dist/notifications/notification-types.d.ts +75 -0
  144. package/dist/notifications/notification-types.js +2 -0
  145. package/dist/notifications/notification-types.js.map +1 -0
  146. package/dist/notifications/notifications-labels.d.ts +131 -0
  147. package/dist/notifications/notifications-labels.js +83 -0
  148. package/dist/notifications/notifications-labels.js.map +1 -0
  149. package/dist/notifications/push-device-setting.d.ts +37 -0
  150. package/dist/notifications/push-device-setting.js +164 -0
  151. package/dist/notifications/push-device-setting.js.map +1 -0
  152. package/dist/notifications/push-devices-setting.d.ts +44 -0
  153. package/dist/notifications/push-devices-setting.js +203 -0
  154. package/dist/notifications/push-devices-setting.js.map +1 -0
  155. package/dist/notifications/quiet-hours-setting.d.ts +47 -0
  156. package/dist/notifications/quiet-hours-setting.js +131 -0
  157. package/dist/notifications/quiet-hours-setting.js.map +1 -0
  158. package/dist/notifications/time-zone-select.d.ts +50 -0
  159. package/dist/notifications/time-zone-select.js +76 -0
  160. package/dist/notifications/time-zone-select.js.map +1 -0
  161. package/dist/notifications/unsubscribe-panel.d.ts +53 -0
  162. package/dist/notifications/unsubscribe-panel.js +139 -0
  163. package/dist/notifications/unsubscribe-panel.js.map +1 -0
  164. package/dist/notifications/use-push-device.d.ts +122 -0
  165. package/dist/notifications/use-push-device.js +230 -0
  166. package/dist/notifications/use-push-device.js.map +1 -0
  167. package/dist/rhf/fields.d.ts +26 -19
  168. package/dist/rhf/fields.js +51 -22
  169. package/dist/rhf/fields.js.map +1 -1
  170. package/dist/settings/device-reset-setting-labels.d.ts +3 -0
  171. package/dist/settings/device-reset-setting-labels.js +15 -0
  172. package/dist/settings/device-reset-setting-labels.js.map +1 -0
  173. package/dist/settings/device-reset-setting.d.ts +58 -0
  174. package/dist/settings/device-reset-setting.js +51 -0
  175. package/dist/settings/device-reset-setting.js.map +1 -0
  176. package/dist/shell/app-shell.js +5 -0
  177. package/dist/shell/app-shell.js.map +1 -1
  178. package/dist/theme/contrast-tokens.d.ts +42 -4
  179. package/dist/theme/contrast-tokens.js +56 -9
  180. package/dist/theme/contrast-tokens.js.map +1 -1
  181. package/dist/theme/contrast.d.ts +4 -2
  182. package/dist/theme/contrast.js +13 -0
  183. package/dist/theme/contrast.js.map +1 -1
  184. package/dist/theme/palette-derive.d.ts +15 -1
  185. package/dist/theme/palette-derive.js +37 -8
  186. package/dist/theme/palette-derive.js.map +1 -1
  187. package/dist/theme/palette-presets.d.ts +2 -1
  188. package/dist/theme/palette-presets.js +5 -5
  189. package/dist/theme/palette-presets.js.map +1 -1
  190. package/dist/theme/palette-store.js +6 -0
  191. package/dist/theme/palette-store.js.map +1 -1
  192. package/dist/theme/text-size.js +6 -0
  193. package/dist/theme/text-size.js.map +1 -1
  194. package/dist/theme/theme-store.js +6 -0
  195. package/dist/theme/theme-store.js.map +1 -1
  196. package/package.json +1 -1
  197. package/src/admin/role-select.tsx +4 -1
  198. package/src/components/amount-input.tsx +2 -1
  199. package/src/components/autocomplete.tsx +5 -2
  200. package/src/components/button-look.ts +15 -5
  201. package/src/components/checkbox-group.tsx +22 -4
  202. package/src/components/column-mapper.tsx +6 -2
  203. package/src/components/combobox.tsx +5 -2
  204. package/src/components/country-select.tsx +6 -1
  205. package/src/components/currency-select.tsx +12 -3
  206. package/src/components/date-field.tsx +2 -2
  207. package/src/components/date-picker.tsx +3 -0
  208. package/src/components/date-range-picker.tsx +3 -0
  209. package/src/components/entity-combobox.tsx +5 -2
  210. package/src/components/field-parts.tsx +107 -19
  211. package/src/components/field-strip.tsx +4 -3
  212. package/src/components/field.tsx +28 -15
  213. package/src/components/help-popover.tsx +9 -7
  214. package/src/components/icon-picker.tsx +8 -3
  215. package/src/components/inline-entity-combobox.tsx +5 -2
  216. package/src/components/input.tsx +4 -2
  217. package/src/components/month-picker.tsx +10 -3
  218. package/src/components/multi-entity-combobox.tsx +5 -2
  219. package/src/components/multi-select.tsx +6 -1
  220. package/src/components/number-input.tsx +2 -2
  221. package/src/components/numeric-field.ts +10 -2
  222. package/src/components/one-time-code-input.tsx +5 -3
  223. package/src/components/phone-input.tsx +4 -0
  224. package/src/components/popover.tsx +3 -1
  225. package/src/components/select.tsx +6 -3
  226. package/src/components/slider.tsx +10 -3
  227. package/src/components/stat-tile.tsx +3 -18
  228. package/src/components/swatch-picker.tsx +9 -4
  229. package/src/components/text-link.tsx +3 -1
  230. package/src/components/textarea.tsx +11 -4
  231. package/src/components/toggle-group.tsx +21 -5
  232. package/src/eslint/role-utilities.ts +67 -11
  233. package/src/i18n/defaults.ts +4 -0
  234. package/src/i18n/german.ts +113 -0
  235. package/src/i18n/kit-labels.tsx +52 -1
  236. package/src/i18n/locales/en.ts +6 -0
  237. package/src/i18n/locales/es.ts +106 -0
  238. package/src/i18n/locales/fr.ts +106 -0
  239. package/src/i18n/locales/hu.ts +104 -0
  240. package/src/i18n/locales/it.ts +105 -0
  241. package/src/i18n/locales/zh.ts +96 -0
  242. package/src/i18n/review.ts +13 -0
  243. package/src/i18n/shared-labels.ts +2 -0
  244. package/src/index.ts +50 -0
  245. package/src/lib/device-settings.ts +146 -0
  246. package/src/lib/plain-text.ts +22 -0
  247. package/src/lib/safe-storage.ts +16 -0
  248. package/src/notifications/notification-parts.tsx +83 -0
  249. package/src/notifications/notification-types-setting.tsx +309 -0
  250. package/src/notifications/notification-types.ts +80 -0
  251. package/src/notifications/notifications-labels.ts +238 -0
  252. package/src/notifications/push-device-setting.tsx +224 -0
  253. package/src/notifications/push-devices-setting.tsx +306 -0
  254. package/src/notifications/quiet-hours-setting.tsx +185 -0
  255. package/src/notifications/time-zone-select.tsx +126 -0
  256. package/src/notifications/unsubscribe-panel.tsx +203 -0
  257. package/src/notifications/use-push-device.ts +392 -0
  258. package/src/rhf/fields.tsx +77 -29
  259. package/src/settings/device-reset-setting-labels.ts +20 -0
  260. package/src/settings/device-reset-setting.tsx +119 -0
  261. package/src/shell/app-shell.tsx +8 -0
  262. package/src/theme/contrast-tokens.ts +106 -12
  263. package/src/theme/contrast.ts +30 -4
  264. package/src/theme/palette-derive.ts +68 -7
  265. package/src/theme/palette-presets.ts +9 -6
  266. package/src/theme/palette-store.ts +12 -0
  267. package/src/theme/text-size.ts +8 -0
  268. package/src/theme/theme-store.ts +10 -0
  269. package/tokens.css +38 -4
@@ -91,7 +91,7 @@ import {
91
91
  type InlineEntityComboboxProps,
92
92
  } from "../components/combobox";
93
93
  import type { ComboClearValue } from "../components/combobox-core";
94
- import { mergeRefs, useInlineHint } from "../components/field-parts";
94
+ import { fieldHelpMark, mergeRefs, useInlineHint } from "../components/field-parts";
95
95
  import { useKitLocale } from "../i18n/kit-labels";
96
96
  import { cn } from "../lib/cn";
97
97
  import { decimalMark, readTyped } from "../lib/decimal-marks";
@@ -115,8 +115,14 @@ export interface RhfFieldBaseProps<
115
115
  label?: ReactNode;
116
116
  /** Standing advice under the field ("As printed on the card"), attached to the
117
117
  * control with `aria-describedby`. A `FieldHint` is its words at Large and on a
118
- * touch screen (§4, DUP-2), not a lone "?"; a `HelpPopover` stays a "?". */
118
+ * touch screen (§4, DUP-2), not a lone "?"; a `HelpPopover` stays a "?". Beside a
119
+ * `help` it is that caption at every size. */
119
120
  hint?: ReactNode;
121
+ /** Help the field is usable without — why, how to choose — behind the field's one
122
+ * "?" after the label: a `HelpPopover` named "About “<label>”" (round 0.36,
123
+ * docs/text-size-harmonization.md §4); pass a `HelpPopover` to name it yourself.
124
+ * `hint` stays the visible fact under the field. */
125
+ help?: ReactNode;
120
126
  /** Draws the label's required mark and sets `aria-required` where the control
121
127
  * takes it. Whether the field IS required is `rules`' or the resolver's to say. */
122
128
  required?: boolean;
@@ -229,6 +235,7 @@ export function RhfField<
229
235
  rules,
230
236
  label,
231
237
  hint,
238
+ help,
232
239
  required,
233
240
  disabled,
234
241
  excludeWhenDisabled,
@@ -240,8 +247,11 @@ export function RhfField<
240
247
  }: RhfFieldProps<TFieldValues, TName, TTransformed>) {
241
248
  // §4: a FieldHint is a fact, so at Large and on a touch screen it is its words under
242
249
  // the field, not a lone "?" there (DUP-2, refactor audit 2026-10). A HelpPopover stays
243
- // a "?" at every size.
244
- const shownHint = useInlineHint(hint);
250
+ // a "?" at every size. With a `help` (0.36) its "?" follows the label and is the
251
+ // field's only one, so a FieldHint is its words at every size.
252
+ const helpMark = fieldHelpMark(help, label);
253
+ const shownHint = useInlineHint(hint, helpMark !== undefined);
254
+ const labelEl = showLabel && hasContent(label) && <RhfLabel required={required}>{label}</RhfLabel>;
245
255
  return (
246
256
  <FormField
247
257
  control={control}
@@ -250,7 +260,14 @@ export function RhfField<
250
260
  disabled={excludeWhenDisabled ? disabled : undefined}
251
261
  render={({ field, fieldState }) => (
252
262
  <FormItem className={className}>
253
- {showLabel && hasContent(label) && <RhfLabel required={required}>{label}</RhfLabel>}
263
+ {helpMark === undefined ? (
264
+ labelEl
265
+ ) : (
266
+ <div className="flex items-center gap-1">
267
+ {labelEl}
268
+ <span className="flex shrink-0 items-center">{helpMark}</span>
269
+ </div>
270
+ )}
254
271
  <ControlSlot
255
272
  field={disabled && !field.disabled ? { ...field, disabled: true } : field}
256
273
  fieldState={fieldState}
@@ -298,6 +315,7 @@ export function RhfTextField<
298
315
  rules,
299
316
  label,
300
317
  hint,
318
+ help,
301
319
  required,
302
320
  disabled,
303
321
  excludeWhenDisabled,
@@ -306,7 +324,7 @@ export function RhfTextField<
306
324
  }: RhfTextFieldProps<TFieldValues, TName, TTransformed>) {
307
325
  return (
308
326
  <RhfField
309
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
327
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
310
328
  render={({ field, invalid }) => (
311
329
  <Input
312
330
  aria-required={required || undefined}
@@ -349,6 +367,7 @@ export function RhfTextarea<
349
367
  rules,
350
368
  label,
351
369
  hint,
370
+ help,
352
371
  required,
353
372
  disabled,
354
373
  excludeWhenDisabled,
@@ -358,7 +377,7 @@ export function RhfTextarea<
358
377
  }: RhfTextareaProps<TFieldValues, TName, TTransformed>) {
359
378
  return (
360
379
  <RhfField
361
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
380
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
362
381
  render={({ field, invalid }) => (
363
382
  <Textarea
364
383
  aria-required={required || undefined}
@@ -435,6 +454,7 @@ export function RhfNumberField<
435
454
  rules,
436
455
  label,
437
456
  hint,
457
+ help,
438
458
  required,
439
459
  disabled,
440
460
  excludeWhenDisabled,
@@ -445,7 +465,7 @@ export function RhfNumberField<
445
465
  }: RhfNumberFieldProps<TFieldValues, TName, TTransformed>) {
446
466
  return (
447
467
  <RhfField
448
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
468
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
449
469
  render={({ field, invalid, id }) => (
450
470
  // FormControl clones this element with `id` / `aria-describedby` /
451
471
  // `aria-invalid`, which NumberControl hands on to the field.
@@ -647,6 +667,7 @@ export function RhfMoneyField<
647
667
  rules,
648
668
  label,
649
669
  hint,
670
+ help,
650
671
  required,
651
672
  disabled,
652
673
  excludeWhenDisabled,
@@ -655,7 +676,7 @@ export function RhfMoneyField<
655
676
  }: RhfMoneyFieldProps<TFieldValues, TName, TTransformed>) {
656
677
  return (
657
678
  <RhfField
658
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
679
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
659
680
  asControl={false}
660
681
  render={({ field, invalid, id, describedBy }) => (
661
682
  <MoneyControl
@@ -743,6 +764,7 @@ export function RhfDateField<
743
764
  rules,
744
765
  label,
745
766
  hint,
767
+ help,
746
768
  required,
747
769
  disabled,
748
770
  excludeWhenDisabled,
@@ -753,7 +775,7 @@ export function RhfDateField<
753
775
  }: RhfDateFieldProps<TFieldValues, TName, TTransformed>) {
754
776
  return (
755
777
  <RhfField
756
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
778
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
757
779
  render={({ field, invalid, id }) => (
758
780
  // FormControl clones this element with `aria-describedby` / `aria-invalid`,
759
781
  // which DateControl hands on to the picker's trigger.
@@ -853,6 +875,7 @@ export function RhfDateRangePicker<
853
875
  toRules,
854
876
  label,
855
877
  hint,
878
+ help,
856
879
  required,
857
880
  disabled,
858
881
  excludeWhenDisabled,
@@ -886,6 +909,7 @@ export function RhfDateRangePicker<
886
909
  toState={to.fieldState}
887
910
  label={label}
888
911
  hint={hint}
912
+ help={help}
889
913
  required={required}
890
914
  disabled={Boolean(disabled)}
891
915
  emptyValue={emptyValue}
@@ -905,6 +929,7 @@ function RangeBody({
905
929
  toState,
906
930
  label,
907
931
  hint,
932
+ help,
908
933
  required,
909
934
  disabled,
910
935
  emptyValue,
@@ -917,6 +942,7 @@ function RangeBody({
917
942
  toState: ControllerFieldState;
918
943
  label: ReactNode;
919
944
  hint: ReactNode;
945
+ help: ReactNode;
920
946
  required?: boolean;
921
947
  disabled: boolean;
922
948
  emptyValue: "" | null;
@@ -924,8 +950,10 @@ function RangeBody({
924
950
  pickerProps: OwnDateRangeProps;
925
951
  }) {
926
952
  const { id, formItemId, formMessageId, describedBy } = useFormField();
927
- // RhfField's rule: a FieldHint is its words at Large and on touch (DUP-2).
928
- const shownHint = useInlineHint(hint);
953
+ // RhfField's rule: a FieldHint is its words at Large and on touch (DUP-2) — and at
954
+ // every size beside a `help`, whose "?" follows the label (0.36).
955
+ const helpMark = fieldHelpMark(help, label);
956
+ const shownHint = useInlineHint(hint, helpMark !== undefined);
929
957
  // Both fields' refs go to the one trigger (API-3, refactor audit 2026-10), so
930
958
  // `shouldFocusError` lands there whichever of the two is first in error.
931
959
  const triggerRef = useMemo(() => mergeRefs(from.ref, to.ref), [from.ref, to.ref]);
@@ -936,14 +964,22 @@ function RangeBody({
936
964
  const messageIds = messages.map((_, i) => `${formMessageId}-${i}`);
937
965
  const fromValue: unknown = from.value;
938
966
  const toValue: unknown = to.value;
967
+ const labelEl = hasContent(label) && (
968
+ // FormLabel colours from the START's error alone (its context is that field); the
969
+ // pair's state says it here.
970
+ <FormLabel id={`${id}-label`} required={required} data-error={invalid || undefined}>
971
+ {label}
972
+ </FormLabel>
973
+ );
939
974
  return (
940
975
  <>
941
- {hasContent(label) && (
942
- // FormLabel colours from the START's error alone (its context is that field);
943
- // the pair's state says it here.
944
- <FormLabel id={`${id}-label`} required={required} data-error={invalid || undefined}>
945
- {label}
946
- </FormLabel>
976
+ {helpMark === undefined ? (
977
+ labelEl
978
+ ) : (
979
+ <div className="flex items-center gap-1">
980
+ {labelEl}
981
+ <span className="flex shrink-0 items-center">{helpMark}</span>
982
+ </div>
947
983
  )}
948
984
  <DateRangePicker
949
985
  {...pickerProps}
@@ -1034,6 +1070,7 @@ export function RhfTimeInput<
1034
1070
  rules,
1035
1071
  label,
1036
1072
  hint,
1073
+ help,
1037
1074
  required,
1038
1075
  disabled,
1039
1076
  excludeWhenDisabled,
@@ -1043,7 +1080,7 @@ export function RhfTimeInput<
1043
1080
  }: RhfTimeInputProps<TFieldValues, TName, TTransformed>) {
1044
1081
  return (
1045
1082
  <RhfField
1046
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
1083
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
1047
1084
  render={({ field, invalid }) => {
1048
1085
  const value: unknown = field.value;
1049
1086
  return (
@@ -1105,6 +1142,7 @@ export function RhfIbanInput<
1105
1142
  rules,
1106
1143
  label,
1107
1144
  hint,
1145
+ help,
1108
1146
  required,
1109
1147
  disabled,
1110
1148
  excludeWhenDisabled,
@@ -1113,7 +1151,7 @@ export function RhfIbanInput<
1113
1151
  }: RhfIbanInputProps<TFieldValues, TName, TTransformed>) {
1114
1152
  return (
1115
1153
  <RhfField
1116
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
1154
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
1117
1155
  render={({ field, invalid }) => {
1118
1156
  const value: unknown = field.value;
1119
1157
  return (
@@ -1156,6 +1194,7 @@ export function RhfPhoneInput<
1156
1194
  rules,
1157
1195
  label,
1158
1196
  hint,
1197
+ help,
1159
1198
  required,
1160
1199
  disabled,
1161
1200
  excludeWhenDisabled,
@@ -1164,7 +1203,7 @@ export function RhfPhoneInput<
1164
1203
  }: RhfPhoneInputProps<TFieldValues, TName, TTransformed>) {
1165
1204
  return (
1166
1205
  <RhfField
1167
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
1206
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
1168
1207
  render={({ field, invalid }) => {
1169
1208
  const value: unknown = field.value;
1170
1209
  return (
@@ -1251,6 +1290,7 @@ export function RhfCountrySelect<
1251
1290
  rules,
1252
1291
  label,
1253
1292
  hint,
1293
+ help,
1254
1294
  required,
1255
1295
  disabled,
1256
1296
  excludeWhenDisabled,
@@ -1267,7 +1307,7 @@ export function RhfCountrySelect<
1267
1307
  const ownLabels = typeof label === "string" ? { country: label, ...labels } : labels;
1268
1308
  return (
1269
1309
  <RhfField
1270
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
1310
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
1271
1311
  render={({ field, invalid, labelId }) => {
1272
1312
  const value: unknown = field.value;
1273
1313
  return (
@@ -1403,6 +1443,7 @@ export function RhfMonthPicker<
1403
1443
  rules,
1404
1444
  label,
1405
1445
  hint,
1446
+ help,
1406
1447
  required,
1407
1448
  disabled,
1408
1449
  excludeWhenDisabled,
@@ -1414,7 +1455,7 @@ export function RhfMonthPicker<
1414
1455
  }: RhfMonthPickerProps<TFieldValues, TName, TTransformed>) {
1415
1456
  return (
1416
1457
  <RhfField
1417
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
1458
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
1418
1459
  render={({ field, invalid, id }) => (
1419
1460
  // FormControl clones this element with `aria-describedby` / `aria-invalid`,
1420
1461
  // which MonthControl hands on to the picker's trigger.
@@ -1473,6 +1514,7 @@ export function RhfSelect<
1473
1514
  rules,
1474
1515
  label,
1475
1516
  hint,
1517
+ help,
1476
1518
  required,
1477
1519
  disabled,
1478
1520
  excludeWhenDisabled,
@@ -1486,7 +1528,7 @@ export function RhfSelect<
1486
1528
  }: RhfSelectProps<TFieldValues, TName, TTransformed>) {
1487
1529
  return (
1488
1530
  <RhfField
1489
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
1531
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
1490
1532
  render={({ field, invalid }) => (
1491
1533
  <Select
1492
1534
  aria-required={required || undefined}
@@ -1523,7 +1565,9 @@ export type RhfCheckboxProps<
1523
1565
  TFieldValues extends FieldValues = FieldValues,
1524
1566
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
1525
1567
  TTransformed = TFieldValues,
1526
- > = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
1568
+ > = // No `help` (0.36): a box has no label line — its label sits beside it, inside the
1569
+ // `<label>` a "?" cannot join — and Checkbox takes none.
1570
+ Omit<RhfFieldBaseProps<TFieldValues, TName, TTransformed>, "help"> &
1527
1571
  // Checkbox's own — its lock (`commit`, `disabledReason`) among them, which a `Pick`
1528
1572
  // list dropped until 0.34 (DUP-9) — less the box's state the form holds, and its
1529
1573
  // `description`, which is the twin's `hint`.
@@ -1646,6 +1690,7 @@ export function RhfToggleGroup<
1646
1690
  rules,
1647
1691
  label,
1648
1692
  hint,
1693
+ help,
1649
1694
  required,
1650
1695
  disabled,
1651
1696
  excludeWhenDisabled,
@@ -1655,7 +1700,7 @@ export function RhfToggleGroup<
1655
1700
  const labelled = hasContent(label);
1656
1701
  return (
1657
1702
  <RhfField
1658
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
1703
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
1659
1704
  asControl={false}
1660
1705
  render={({ field, invalid, id, labelId, describedBy }) => (
1661
1706
  <ToggleControl<T>
@@ -1846,6 +1891,7 @@ export function RhfCombobox<
1846
1891
  rules,
1847
1892
  label,
1848
1893
  hint,
1894
+ help,
1849
1895
  required,
1850
1896
  disabled,
1851
1897
  excludeWhenDisabled,
@@ -1856,7 +1902,7 @@ export function RhfCombobox<
1856
1902
  }: RhfComboboxProps<TFieldValues, TName, V, TTransformed>) {
1857
1903
  return (
1858
1904
  <RhfField
1859
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
1905
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
1860
1906
  asControl={false}
1861
1907
  message={false}
1862
1908
  render={({ field, invalid, error, id, describedBy }) => (
@@ -1956,6 +2002,7 @@ export function RhfInlineEntityCombobox<
1956
2002
  rules,
1957
2003
  label,
1958
2004
  hint,
2005
+ help,
1959
2006
  required,
1960
2007
  disabled,
1961
2008
  excludeWhenDisabled,
@@ -1966,7 +2013,7 @@ export function RhfInlineEntityCombobox<
1966
2013
  }: RhfInlineEntityComboboxProps<TFieldValues, TName, V, TTransformed>) {
1967
2014
  return (
1968
2015
  <RhfField
1969
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
2016
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
1970
2017
  render={({ field, invalid }) => {
1971
2018
  const value: unknown = field.value;
1972
2019
  return (
@@ -2021,6 +2068,7 @@ export function RhfTextCombobox<
2021
2068
  rules,
2022
2069
  label,
2023
2070
  hint,
2071
+ help,
2024
2072
  required,
2025
2073
  disabled,
2026
2074
  excludeWhenDisabled,
@@ -2030,7 +2078,7 @@ export function RhfTextCombobox<
2030
2078
  }: RhfTextComboboxProps<TFieldValues, TName, TTransformed>) {
2031
2079
  return (
2032
2080
  <RhfField
2033
- {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
2081
+ {...{ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className }}
2034
2082
  asControl={false}
2035
2083
  message={false}
2036
2084
  render={({ field, invalid, error, id, describedBy }) => (
@@ -0,0 +1,20 @@
1
+ // The English words of ./device-reset-setting, in a module with no component code: the
2
+ // English catalogue (src/i18n/defaults.ts) imports them without the component, so `en`
3
+ // loads as one chunk like the other languages (0.34, KS U8). ./device-reset-setting
4
+ // re-exports them.
5
+
6
+ import type { DeviceResetLabels } from "./device-reset-setting";
7
+
8
+ /** docs/settings-harmonization.md §11.3, word for word. */
9
+ export const DEFAULT_DEVICE_RESET_LABELS: DeviceResetLabels = {
10
+ title: "Reset this device",
11
+ description:
12
+ "Puts this device's appearance, and the way its tables and panels were left, back to the defaults. Your account, your data and your sign-in are not touched.",
13
+ action: "Reset this device's settings",
14
+ confirmTitle: "Reset this device's settings?",
15
+ confirmBody:
16
+ "Theme, colours, text size, contrast and language go back to their defaults here, and tables and panels open as they first did. Where your account has a language, a text size or a contrast, this device follows it again. Your account and your other devices stay as they are.",
17
+ confirmAction: "Reset",
18
+ done: "This device's settings are back to their defaults.",
19
+ keywords: "reset defaults device restore appearance tables layout",
20
+ };
@@ -0,0 +1,119 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
4
+ import { resetDeviceSettings } from "../lib/device-settings";
5
+ import { Button } from "../components/button";
6
+ import { useConfirm } from "../components/confirm-dialog";
7
+ import { toast } from "../components/toast";
8
+ import { SettingsSection, type SettingsSectionProps } from "./settings-section";
9
+ import { DEFAULT_DEVICE_RESET_LABELS } from "./device-reset-setting-labels";
10
+
11
+ /* ── Labels ──────────────────────────────────────────────────────────────── */
12
+
13
+ /** Every string {@link DeviceResetSetting} renders — the `deviceReset` namespace of
14
+ * `<UiKitProvider labels>`, overridable per instance through `labels`
15
+ * (docs/settings-harmonization.md §11.3). The confirm's Cancel is `confirmDialog.cancel`. */
16
+ export interface DeviceResetLabels {
17
+ /** The card's title, and the app's catalogue entry's. */
18
+ title: string;
19
+ /** The line under it: what goes back, and that the account is not touched. */
20
+ description: string;
21
+ /** The button that asks first. */
22
+ action: string;
23
+ /** The question it asks. */
24
+ confirmTitle: string;
25
+ /** What goes, what follows the account again, and what stays. */
26
+ confirmBody: string;
27
+ /** The button that answers it. */
28
+ confirmAction: string;
29
+ /** The toast once it is done. */
30
+ done: string;
31
+ /** For the app's catalogue entry (`keywords`); the card never reads it. */
32
+ keywords: string;
33
+ }
34
+
35
+ // Defined in ./device-reset-setting-labels, which holds no component code.
36
+ export { DEFAULT_DEVICE_RESET_LABELS };
37
+
38
+ /* ── The card ────────────────────────────────────────────────────────────── */
39
+
40
+ /** The card's DOM id: its catalogue entry's `anchor`, the `?focus=` target. */
41
+ export const DEVICE_RESET_ANCHOR = "settings-device-reset";
42
+
43
+ // `onReset` is the card's own: the DOM's form-reset handler never fires on a div.
44
+ export interface DeviceResetSettingProps
45
+ extends Omit<SettingsSectionProps, "anchor" | "title" | "description" | "children" | "onReset"> {
46
+ /** The app's sentence after the kit's description: what of its own goes back, and what
47
+ * stays (keksdose: the date format, the feature set and the gestures go; presentation
48
+ * mode and the home position stay). */
49
+ description?: ReactNode;
50
+ labels?: LabelOverride<DeviceResetLabels>;
51
+ /** Runs after the reset and before the toast, with the keys reset or removed. */
52
+ onReset?: (keys: string[]) => void;
53
+ }
54
+
55
+ /**
56
+ * "Reset this device's settings" (docs/settings-harmonization.md §11): the LAST card of
57
+ * the appearance group. It puts this device's appearance — theme, palette, language, text
58
+ * size, contrast and the app's own appearance choices — and its views — table views, the
59
+ * sidebar, folded groups, saved filters — back to what a new device gets, through
60
+ * {@link resetDeviceSettings}. The account, the sign-in, the session and anything waiting
61
+ * to be sent are never touched: they never join the registry.
62
+ *
63
+ * - **A confirm, not an undo** (§11.2 decision 9): `useConfirm` in the neutral tone,
64
+ * then a toast. An app mounts a `ConfirmProvider` and a `Toaster` already.
65
+ * - **Not a commit**: nothing is written to the account, so a demo or a lapsed plan's
66
+ * write lock does not disable it (decision 3).
67
+ * - **Live, no reload** (decision 10): each registered store resets through its own
68
+ * setter; the card stays where it is.
69
+ *
70
+ * The app adds the catalogue entry, `{ id, group: "appearance", anchor: DEVICE_RESET_ANCHOR,
71
+ * title, keywords }` (the id in its catalogue's own style, e.g. `device_reset` or
72
+ * `deviceReset`; `title` and `keywords` from these labels), last in its appearance group:
73
+ * inside a `SettingsLayout`, `SettingsSection` throws in development without it.
74
+ */
75
+ export function DeviceResetSetting({
76
+ description,
77
+ labels: labelsProp,
78
+ onReset,
79
+ ...rest
80
+ }: DeviceResetSettingProps) {
81
+ const labels = useKitLabels("deviceReset", DEFAULT_DEVICE_RESET_LABELS, labelsProp);
82
+ const confirm = useConfirm();
83
+
84
+ const reset = async () => {
85
+ const ok = await confirm({
86
+ title: labels.confirmTitle,
87
+ body: labels.confirmBody,
88
+ confirmLabel: labels.confirmAction,
89
+ tone: "neutral",
90
+ });
91
+ if (!ok) return;
92
+ const keys = resetDeviceSettings();
93
+ onReset?.(keys);
94
+ toast.success(labels.done);
95
+ };
96
+
97
+ return (
98
+ <SettingsSection
99
+ {...rest}
100
+ anchor={DEVICE_RESET_ANCHOR}
101
+ title={labels.title}
102
+ description={
103
+ description != null ? (
104
+ <>
105
+ {labels.description} {description}
106
+ </>
107
+ ) : (
108
+ labels.description
109
+ )
110
+ }
111
+ >
112
+ <div>
113
+ <Button variant="secondary" onClick={() => void reset()}>
114
+ {labels.action}
115
+ </Button>
116
+ </div>
117
+ </SettingsSection>
118
+ );
119
+ }
@@ -8,6 +8,7 @@ import { cn } from "../lib/cn";
8
8
  import { scrollIntoStrip, useStripFade } from "../lib/strip-fade";
9
9
  import { dirOf, type Direction } from "../lib/direction";
10
10
  import { readStored, writeStored } from "../lib/safe-storage";
11
+ import { registerDeviceSetting } from "../lib/device-settings";
11
12
  import { Tooltip } from "../components/tooltip";
12
13
  import { pickLinkRenderer } from "../components/text-link";
13
14
  import { DialogFrame } from "../components/dialog-frame";
@@ -394,6 +395,13 @@ export function AppShell({
394
395
  useEffect(() => {
395
396
  if (collapseStorageKey !== null) writeStored(collapseStorageKey, collapsed ? "1" : "0");
396
397
  }, [collapsed, collapseStorageKey]);
398
+ // Reset this device's settings (docs/settings-harmonization.md §11.4): the sidebar
399
+ // unfolds, live, and the effect above writes "0". An embedded shell without a key of
400
+ // its own stores nothing and joins nothing.
401
+ useEffect(() => {
402
+ if (collapseStorageKey === null) return;
403
+ return registerDeviceSetting({ key: collapseStorageKey, kind: "view", reset: () => setCollapsed(false) });
404
+ }, [collapseStorageKey]);
397
405
  // §4: at Large and Extra large the sidebar never collapses to icons only — a glyph
398
406
  // with its name in a tooltip is the very thing a reader who asked for bigger type
399
407
  // cannot use. The persisted preference is left alone, so Normal gets it back, and the