@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
@@ -3,8 +3,11 @@ import type { ComponentPropsWithoutRef, ComponentPropsWithRef, ReactNode } from
3
3
  import { HelpCircle } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { hasContent } from "../lib/presence";
6
+ import { plainText } from "../lib/plain-text";
7
+ import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
6
8
  import { useInlineFacts } from "../hooks/use-large-text";
7
9
  import { FOCUS_RING } from "./focus-ring";
10
+ import { HelpPopover } from "./help-popover";
8
11
  import { Tooltip, type TooltipSide } from "./tooltip";
9
12
  import { useCommitReason, type CommitScope } from "./write-lock";
10
13
 
@@ -202,16 +205,82 @@ export function FieldHint({
202
205
  * The kit's fields call it through {@link useFieldHint} / {@link useFieldMessages}; a
203
206
  * field that resolves its own `hint` (an app's, or one of the kit's own outside this
204
207
  * file) calls it first and then treats the result as it treated a text hint.
208
+ *
209
+ * `withHelp` (round 0.36): the field has a `help`, whose "?" is the field's only one
210
+ * ({@link fieldHelpMark}) — so a FieldHint is its words at EVERY size, not only at Large
211
+ * and on touch.
205
212
  */
206
- export function useInlineHint(hint: ReactNode): ReactNode {
213
+ export function useInlineHint(hint: ReactNode, withHelp = false): ReactNode {
207
214
  const inline = useInlineFacts();
208
- if (inline && isValidElement(hint) && hint.type === FieldHint) {
215
+ if ((inline || withHelp) && isValidElement(hint) && hint.type === FieldHint) {
209
216
  const words = (hint.props as FieldHintProps).label;
210
217
  if (words) return words;
211
218
  }
212
219
  return hint;
213
220
  }
214
221
 
222
+ /* ── help (round 0.36, docs/text-size-harmonization.md §4, §10.23) ─────────── */
223
+
224
+ /**
225
+ * The doc every field's `help` prop points at.
226
+ *
227
+ * A field may carry two kinds of words (§10.23): a FACT — a format, a limit, a
228
+ * consequence — which stays visible, and HELP — why the field exists, how to choose,
229
+ * what the options mean — which the field is usable without and which may sit behind a
230
+ * "?". Until 0.36 a field had one slot, `hint`, so a field with both had to drop one or
231
+ * print a paragraph (Kurvenschmiede's "How it was measured", round 0.36 H). Now:
232
+ *
233
+ * - `hint` is the fact, the caption under the field, on its `aria-describedby`;
234
+ * - `help` is the help, a {@link HelpPopover} "?" on the label line (an unlabelled
235
+ * field's at its end edge, as a hint's), named "About <label>" (`common.aboutField`,
236
+ * from the label's text, else the `aria-label`, else `common.help`). Pass a
237
+ * `HelpPopover` as `help` to name it yourself ("What do the statuses mean?").
238
+ *
239
+ * The help's "?" is then the field's ONLY "?": a `FieldHint` passed as `hint` beside it
240
+ * draws none of its own and shows its words as the caption at every size, as it does at
241
+ * Large and on touch; any other `hint` is the caption too. Two "?" side by side would
242
+ * look the same and open different things.
243
+ */
244
+ export interface FieldHelpProps {
245
+ /**
246
+ * Help the field is usable without — why it exists, how to choose, what the options
247
+ * mean — behind the field's one "?" on its label line: a {@link HelpPopover} named
248
+ * "About <label>" that opens on a tap (round 0.36, docs/text-size-harmonization.md §4).
249
+ * Pass a `HelpPopover` to name it yourself. `hint` stays the visible fact: with `help`
250
+ * set it is the caption under the field at every size, a `FieldHint` included.
251
+ */
252
+ help?: ReactNode;
253
+ }
254
+
255
+ /**
256
+ * The "?" for a field's `help` — the field's own name, "About <label>", in the reader's
257
+ * language. A component so that a field without `help` asks the labels nothing.
258
+ */
259
+ function FieldHelp({ name, children }: { name: ReactNode; children: ReactNode }) {
260
+ const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
261
+ const text = plainText(name);
262
+ return <HelpPopover label={text === undefined ? common.help : common.aboutField(text)}>{children}</HelpPopover>;
263
+ }
264
+
265
+ /**
266
+ * A field's `help` as the "?" for its label line, or `undefined` when there is none. A
267
+ * {@link HelpPopover} is used as it is (it carries its own name); anything else is put
268
+ * in one named "About <name>" — `name` is the field's label, else its `aria-label`.
269
+ */
270
+ export function fieldHelpMark(help: ReactNode, name: ReactNode): ReactNode | undefined {
271
+ if (!hasContent(help)) return undefined;
272
+ if (isValidElement(help) && help.type === HelpPopover) return help;
273
+ return <FieldHelp name={name}>{help}</FieldHelp>;
274
+ }
275
+
276
+ /** What {@link useFieldHint} needs to draw a field's `help`. */
277
+ export interface FieldHelpSource {
278
+ /** The field's `help`. */
279
+ help?: ReactNode;
280
+ /** What names its "?": the field's label, else its `aria-label`. */
281
+ name?: ReactNode;
282
+ }
283
+
215
284
 
216
285
  /**
217
286
  * Whether a `hint` is a CAPTION (plain text: a non-empty string or a number) or
@@ -236,26 +305,43 @@ export interface FieldHintParts {
236
305
  describedBy: string | undefined;
237
306
  /** The caption's id while there is one. */
238
307
  captionId: string | undefined;
239
- /** The caption itself, or `undefined` for a hint that is not text. */
240
- captionText: string | number | undefined;
241
- /** A non-text hint (a FieldHint) for the label line — or, on a field with no label
242
- * line, the end edge outside the box. `undefined` when there is none. */
308
+ /** The caption itself — a text hint, or any hint beside a `help` — or `undefined`. */
309
+ captionText: ReactNode | undefined;
310
+ /** What rides the label line — the `help`'s "?" ({@link fieldHelpMark}), else a
311
+ * non-text hint (a FieldHint) — or, on a field with no label line, the end edge
312
+ * outside the box. `undefined` when there is none. */
243
313
  labelHint: ReactNode | undefined;
244
314
  }
245
315
 
316
+ /**
317
+ * Whether a resolved `hint` goes under the field as its caption: text always (see
318
+ * {@link isTextHint}); with a `help` beside it, whatever it is — the label line's "?"
319
+ * is the help's (round 0.36, §4).
320
+ */
321
+ export function isHintCaption(hint: ReactNode, withHelp: boolean): boolean {
322
+ return isTextHint(hint) || (withHelp && hasContent(hint));
323
+ }
324
+
246
325
  /**
247
326
  * Split a field's `hint` into the caption under it (attached with `aria-describedby`)
248
- * or the "?" on its label line. See {@link isTextHint}.
327
+ * or the "?" on its label line. See {@link isTextHint}. With a `help` ({@link
328
+ * FieldHelpProps}), the help's "?" takes the label line and the hint is the caption.
249
329
  */
250
- export function useFieldHint(hintProp: ReactNode, describedBy: string | undefined): FieldHintParts {
251
- const hint = useInlineHint(hintProp);
330
+ export function useFieldHint(
331
+ hintProp: ReactNode,
332
+ describedBy: string | undefined,
333
+ { help, name }: FieldHelpSource = {},
334
+ ): FieldHintParts {
335
+ const helpMark = fieldHelpMark(help, name);
336
+ const withHelp = helpMark !== undefined;
337
+ const hint = useInlineHint(hintProp, withHelp);
252
338
  const captionId = useId();
253
- const text = isTextHint(hint);
339
+ const caption = isHintCaption(hint, withHelp);
254
340
  return {
255
- describedBy: text ? mergeDescribedBy(describedBy, captionId) : describedBy,
256
- captionId: text ? captionId : undefined,
257
- captionText: text ? hint : undefined,
258
- labelHint: !text && hasContent(hint) ? hint : undefined,
341
+ describedBy: caption ? mergeDescribedBy(describedBy, captionId) : describedBy,
342
+ captionId: caption ? captionId : undefined,
343
+ captionText: caption ? hint : undefined,
344
+ labelHint: helpMark ?? (!caption && hasContent(hint) ? hint : undefined),
259
345
  };
260
346
  }
261
347
 
@@ -575,24 +661,26 @@ export interface FieldMessages {
575
661
  describedBy: string | undefined;
576
662
  /** The caption and the error, for under the field; `null` when there is neither. */
577
663
  below: ReactNode;
578
- /** The hint for the LABEL line (a `FieldHint`), or `undefined` when the hint is a
579
- * caption or there is none. */
664
+ /** What rides the LABEL line — the `help`'s "?", else a `FieldHint` — or `undefined`
665
+ * when the hint is a caption and there is no help. */
580
666
  labelHint: ReactNode;
581
667
  }
582
668
 
583
669
  /**
584
- * `hint` / `error` / `invalid` resolved the way `Select` resolves them —
670
+ * `hint` / `help` / `error` / `invalid` resolved the way `Select` resolves them —
585
671
  * {@link useFieldHint} and {@link useFieldError} together, for a field that draws what
586
672
  * goes under it in one place (a picker, a group, a field built on another).
587
673
  */
588
674
  export function useFieldMessages({
589
675
  hint,
676
+ help,
677
+ name,
590
678
  error,
591
679
  invalid,
592
680
  describedBy,
593
681
  ariaInvalid,
594
682
  reasonId,
595
- }: {
683
+ }: FieldHelpSource & {
596
684
  hint?: ReactNode;
597
685
  error?: ReactNode;
598
686
  invalid?: boolean;
@@ -602,7 +690,7 @@ export function useFieldMessages({
602
690
  * caption and before the error, as on Select. */
603
691
  reasonId?: string | false;
604
692
  }): FieldMessages {
605
- const hintParts = useFieldHint(hint, describedBy);
693
+ const hintParts = useFieldHint(hint, describedBy, { help, name });
606
694
  const field = useFieldError(error, invalid, mergeDescribedBy(hintParts.describedBy, reasonId), ariaInvalid);
607
695
  return {
608
696
  isInvalid: field.isInvalid,
@@ -1,6 +1,6 @@
1
1
  import { useId } from "react";
2
2
  import type { ComponentPropsWithRef, ReactNode } from "react";
3
- import { LabelStrip, useFieldMessages } from "./field-parts";
3
+ import { LabelStrip, useFieldMessages, type FieldHelpProps } from "./field-parts";
4
4
 
5
5
  /** What a {@link FieldStrip} hands render-prop children. */
6
6
  export interface FieldStripIds {
@@ -18,7 +18,7 @@ export interface FieldStripIds {
18
18
  * it in 0.34, API-2): the ref is the root — the group, or the plain box under
19
19
  * `group={false}` — which a caller scrolls into view or measures.
20
20
  */
21
- export interface FieldStripProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
21
+ export interface FieldStripProps extends Omit<ComponentPropsWithRef<"div">, "children">, FieldHelpProps {
22
22
  /** The 11px static label, on the line a labelled field's label sits on. */
23
23
  label: ReactNode;
24
24
  /**
@@ -86,6 +86,7 @@ export interface FieldStripProps extends Omit<ComponentPropsWithRef<"div">, "chi
86
86
  export function FieldStrip({
87
87
  label,
88
88
  hint,
89
+ help,
89
90
  error,
90
91
  disabled = false,
91
92
  group = true,
@@ -94,7 +95,7 @@ export function FieldStrip({
94
95
  ...rest
95
96
  }: FieldStripProps) {
96
97
  const labelId = useId();
97
- const messages = useFieldMessages({ hint, error });
98
+ const messages = useFieldMessages({ hint, help, name: label, error });
98
99
  const content = typeof children === "function" ? children({ labelId, describedBy: messages.describedBy }) : children;
99
100
  // The caller's own description first, then the caption, then the error — the order
100
101
  // every kit field reads them in.
@@ -40,7 +40,7 @@
40
40
  import { useId, type ReactNode } from "react";
41
41
  import { cn } from "../lib/cn";
42
42
  import { hasContent } from "../lib/presence";
43
- import { useInlineHint } from "./field-parts";
43
+ import { fieldHelpMark, useInlineHint, type FieldHelpProps } from "./field-parts";
44
44
  import { Label } from "./field-base";
45
45
 
46
46
  /** What a `Field` hands its render-prop children, to spread on the control. */
@@ -91,7 +91,7 @@ const LABEL_VISIBILITY: Record<FieldLabelVisibility, string | undefined> = {
91
91
  "below-lg": "lg:sr-only",
92
92
  };
93
93
 
94
- export interface FieldProps {
94
+ export interface FieldProps extends FieldHelpProps {
95
95
  /** The label above the control. Omit it for a group whose control is labelled
96
96
  * some other way; the hint and error still render. */
97
97
  label?: ReactNode;
@@ -123,6 +123,7 @@ export function Field({
123
123
  label,
124
124
  required,
125
125
  hint: hintProp,
126
+ help,
126
127
  error,
127
128
  htmlFor,
128
129
  disabled,
@@ -138,7 +139,10 @@ export function Field({
138
139
  const labelId = `${generated}-label`;
139
140
  // §4: a FieldHint under the control is its words at Large and on a touch screen, not
140
141
  // a lone "?" (DUP-2, refactor audit 2026-10). A HelpPopover stays a "?".
141
- const hint = useInlineHint(hintProp);
142
+ // With a `help` (0.36) its "?" follows the label, and a FieldHint is its words at
143
+ // every size: the help's is the field's only "?".
144
+ const helpMark = fieldHelpMark(help, label);
145
+ const hint = useInlineHint(hintProp, helpMark !== undefined);
142
146
  const hasHint = hasContent(hint);
143
147
  const hasError = hasContent(error);
144
148
  const describedBy = [hasHint && hintId, hasError && errorId].filter(Boolean).join(" ");
@@ -148,6 +152,19 @@ export function Field({
148
152
  "aria-invalid": hasError || undefined,
149
153
  "aria-required": required || undefined,
150
154
  };
155
+ const labelEl = label !== undefined && (
156
+ <Label
157
+ id={labelId}
158
+ htmlFor={id}
159
+ required={required}
160
+ disabled={disabled}
161
+ size={labelSize}
162
+ data-error={hasError || undefined}
163
+ className={cn("data-[error=true]:text-danger", LABEL_VISIBILITY[labelVisibility])}
164
+ >
165
+ {label}
166
+ </Label>
167
+ );
151
168
  return (
152
169
  <div
153
170
  data-slot="field"
@@ -162,18 +179,14 @@ export function Field({
162
179
  className,
163
180
  )}
164
181
  >
165
- {label !== undefined && (
166
- <Label
167
- id={labelId}
168
- htmlFor={id}
169
- required={required}
170
- disabled={disabled}
171
- size={labelSize}
172
- data-error={hasError || undefined}
173
- className={cn("data-[error=true]:text-danger", LABEL_VISIBILITY[labelVisibility])}
174
- >
175
- {label}
176
- </Label>
182
+ {helpMark === undefined && labelEl}
183
+ {helpMark !== undefined && (
184
+ // The label and its "?" on one line; the "?" stays visible where the label is
185
+ // hidden (`labelVisibility`) — a hidden button would still take the focus.
186
+ <div className="flex items-center gap-1">
187
+ {labelEl}
188
+ <span className="flex shrink-0 items-center">{helpMark}</span>
189
+ </div>
177
190
  )}
178
191
  {typeof children === "function"
179
192
  ? children(control, { labelId: label !== undefined ? labelId : undefined })
@@ -42,12 +42,14 @@ const HELP_WIDTH: RemLength = "20rem";
42
42
  * new row on a phone). `useInlineHint` turns only a FieldHint into a caption, so this
43
43
  * one stays on the label line at every size.
44
44
  *
45
- * **On a field.** Pass it as `hint`, as a FieldHint is passed: every kit field that
46
- * puts a FieldHint on its label line puts this there too, as it does any `hint` that is
47
- * not text (Input, Select, Textarea, NumberInput, AmountInput, the comboboxes and
48
- * pickers, ToggleGroup, {@link FieldStrip}) — and an unlabelled one at its end edge,
49
- * outside the box. The `@eifi1/ui-kit/rhf` bindings draw every `hint` under the field,
50
- * this one included. It also stands on its own, beside a heading or a column title.
45
+ * **On a field: its `help`** (0.36, round 0.36 decision 1). Every kit field that takes a
46
+ * `hint` takes a `help` beside it, and the `@eifi1/ui-kit/rhf` bindings too: pass the
47
+ * help's words and the field draws this "?" on its label line (an unlabelled one at its
48
+ * end edge, outside the box), named "About <label>" — or pass a HelpPopover to name it
49
+ * yourself. `hint` stays the FACT, printed under the field; while `help` is set a
50
+ * FieldHint there draws no "?" of its own, so the field has one. Passed as `hint`, as in
51
+ * 0.34, it still rides the label line. It also stands on its own, beside a heading or a
52
+ * column title.
51
53
  *
52
54
  * **A popover, not a tooltip**: a tooltip bubble is for a line, and opens on hover a
53
55
  * finger never makes. This is the kit {@link Popover}: it opens and closes on a tap or a
@@ -60,7 +62,7 @@ const HELP_WIDTH: RemLength = "20rem";
60
62
  * accepted and cleared?", keksdose live #247); "!" is a warning.
61
63
  *
62
64
  * ```tsx
63
- * <Select label="Status" hint={
65
+ * <Select label="Status" hint="Cleared and reconciled rows are locked." help={
64
66
  * <HelpPopover label="What do the statuses mean?">
65
67
  * Pending: … Accepted: … Cleared: … Reconciled: …
66
68
  * </HelpPopover>
@@ -7,7 +7,7 @@ import { TILE_SIZE, TileRadioGroup } from "./tile-radio";
7
7
  import type { TileItem, TileSize } from "./tile-radio";
8
8
  import { hasContent } from "../lib/presence";
9
9
  import { foldForSearch } from "../lib/text-match";
10
- import { LabelStrip, useFieldMessages } from "./field-parts";
10
+ import { LabelStrip, useFieldMessages, type FieldHelpProps } from "./field-parts";
11
11
  import { DEFAULT_ICON_PICKER_LABELS } from "./icon-picker-labels";
12
12
 
13
13
  export interface IconPickerLabels {
@@ -52,7 +52,8 @@ export interface IconOption<T extends string> {
52
52
  }
53
53
 
54
54
  export interface IconPickerProps<T extends string>
55
- extends Omit<ComponentPropsWithRef<"div">, "onChange" | "children" | "defaultValue"> {
55
+ extends Omit<ComponentPropsWithRef<"div">, "onChange" | "children" | "defaultValue">,
56
+ FieldHelpProps {
56
57
  options: IconOption<T>[];
57
58
  /** The chosen icon, or `null` for none. */
58
59
  value: T | null;
@@ -122,6 +123,7 @@ export function IconPicker<T extends string>({
122
123
  labels,
123
124
  label,
124
125
  hint,
126
+ help,
125
127
  className,
126
128
  "aria-label": ariaLabel,
127
129
  "aria-labelledby": ariaLabelledBy,
@@ -134,9 +136,12 @@ export function IconPicker<T extends string>({
134
136
  const mixedId = useId();
135
137
  const labelId = useId();
136
138
  const labelled = hasContent(label);
137
- // The label line's "?", or the caption under the grid (DUP-2). Only with a label.
139
+ // The label line's "?", or the caption under the grid (DUP-2). Only with a label, the
140
+ // help's "?" too (0.36).
138
141
  const messages = useFieldMessages({
139
142
  hint: labelled ? hint : undefined,
143
+ help: labelled ? help : undefined,
144
+ name: label,
140
145
  describedBy: ariaDescribedBy,
141
146
  reasonId: mixed && mixedId,
142
147
  });
@@ -34,6 +34,7 @@ import {
34
34
  useFieldHint,
35
35
  useLockReason,
36
36
  type DisabledReasonDisplay,
37
+ type FieldHelpProps,
37
38
  } from "./field-parts";
38
39
  import { hasContent } from "../lib/presence";
39
40
  import { useCommitReason, type CommitScope } from "./write-lock";
@@ -47,7 +48,8 @@ const CREATE_ROW_CLASS = "flex items-center gap-2 font-medium text-brand";
47
48
  /** `id` is the INPUT's and `onChange` is the field's — see {@link ComboboxProps}
48
49
  * for why the div's spellings of them are omitted. */
49
50
  export interface InlineEntityComboboxProps<V extends string | number, C extends ComboClearValue = null>
50
- extends Omit<ComponentPropsWithoutRef<"div">, "id" | "onChange"> {
51
+ extends Omit<ComponentPropsWithoutRef<"div">, "id" | "onChange">,
52
+ FieldHelpProps {
51
53
  /** Selected option id; `null` or `clearValue` when nothing is selected. */
52
54
  value: V | C | null;
53
55
  /** Required and unanswered — see {@link Combobox}'s `invalid`. */
@@ -226,6 +228,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
226
228
  invalid,
227
229
  error,
228
230
  hint,
231
+ help,
229
232
  ref,
230
233
  "aria-label": ariaLabel,
231
234
  // Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s
@@ -239,7 +242,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
239
242
  // The clear value reads as "nothing selected", whichever one the caller picked —
240
243
  // so from here down `value` is the id or `null`, as it always was.
241
244
  const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);
242
- const hintParts = useFieldHint(hint, ariaDescribedBy);
245
+ const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });
243
246
  const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);
244
247
  const generated = useId();
245
248
  const fieldId = id ?? generated;
@@ -12,6 +12,7 @@ import {
12
12
  LockedReason,
13
13
  mergeDescribedBy,
14
14
  type DisabledReasonDisplay,
15
+ type FieldHelpProps,
15
16
  useFieldError,
16
17
  useFieldHint,
17
18
  useLockReason,
@@ -38,7 +39,7 @@ import {
38
39
  // open the picker on a click anywhere in the field instead (feedback #224).
39
40
  const PICKER_TYPES = new Set(["date", "datetime-local", "month", "time", "week"]);
40
41
 
41
- export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
42
+ export interface InputProps extends InputHTMLAttributes<HTMLInputElement>, FieldHelpProps {
42
43
  label?: ReactNode;
43
44
  /** Classes for the `<input>` itself, as distinct from `className`, which
44
45
  * styles the field WRAPPER once a `label` turns this into a FloatingField.
@@ -156,6 +157,7 @@ export function Input(props: InputProps) {
156
157
  invalid,
157
158
  error,
158
159
  hint,
160
+ help,
159
161
  showCount,
160
162
  countLabels,
161
163
  passwordLabels,
@@ -171,7 +173,7 @@ export function Input(props: InputProps) {
171
173
  const generated = useId();
172
174
  const fieldId = id ?? generated;
173
175
  const countId = useId();
174
- const hintParts = useFieldHint(hint, rest["aria-describedby"]);
176
+ const hintParts = useFieldHint(hint, rest["aria-describedby"], { help, name: label ?? rest["aria-label"] });
175
177
  const maxLength = rest.maxLength;
176
178
  const counting = Boolean(showCount) && typeof maxLength === "number" && maxLength > 0;
177
179
  const length = useTypedLength(rest.value, rest.defaultValue);
@@ -13,7 +13,7 @@ import { splitTriggerAria } from "./trigger-aria";
13
13
  import type { TriggerAria } from "./trigger-aria";
14
14
  import { useKitDateFormatter, useKitLabels, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
15
15
  import { hasContent } from "../lib/presence";
16
- import { FieldBox, FieldLabelLine, LABEL_ABOVE_BOX, mergeRefs, useFieldMessages } from "./field-parts";
16
+ import { FieldBox, FieldLabelLine, LABEL_ABOVE_BOX, mergeRefs, useFieldMessages, type FieldHelpProps } from "./field-parts";
17
17
  import { DEFAULT_MONTH_PICKER_LABELS } from "./month-picker-labels";
18
18
 
19
19
  /**
@@ -63,7 +63,7 @@ function useMonthPickerLabels(partial?: Partial<MonthPickerLabels>): MonthPicker
63
63
  * a `FormEvent`, and this one answers with a month key. Everything else reaches the
64
64
  * field's root, so a test id or a `data-tour` anchor can find it.
65
65
  */
66
- export interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
66
+ export interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange">, FieldHelpProps {
67
67
  /**
68
68
  * The selected month as a `"YYYY-MM"` key, or `""` for none.
69
69
  *
@@ -831,6 +831,7 @@ export function MonthPicker(props: MonthPickerProps) {
831
831
  invalid,
832
832
  error,
833
833
  hint,
834
+ help,
834
835
  size = "md",
835
836
  mode = "month",
836
837
  formatOptions,
@@ -877,7 +878,13 @@ export function MonthPicker(props: MonthPickerProps) {
877
878
  // keksdose K4: the caption and the error, on the trigger after the caller's own
878
879
  // description. Painted for `invalid` and a message — not for a bare `aria-invalid`,
879
880
  // which this field has never painted for.
880
- const messages = useFieldMessages({ hint, error, describedBy: rest["aria-describedby"] });
881
+ const messages = useFieldMessages({
882
+ hint,
883
+ help,
884
+ name: label ?? rest["aria-label"],
885
+ error,
886
+ describedBy: rest["aria-describedby"],
887
+ });
881
888
  const [ownAria, wrapperRest] = splitTriggerAria(rest);
882
889
  // The field's trigger is described by the caption and the error too; the stepper
883
890
  // draws neither, so its heading keeps the caller's own description.
@@ -28,13 +28,15 @@ import {
28
28
  useFieldHint,
29
29
  useLockReason,
30
30
  type DisabledReasonDisplay,
31
+ type FieldHelpProps,
31
32
  } from "./field-parts";
32
33
  import { useCommitReason, type CommitScope } from "./write-lock";
33
34
 
34
35
  /** `onChange` is the kit's — "a selection was made", carrying values — rather
35
36
  * than the div's form event, so the DOM's spelling of it is omitted. */
36
37
  export interface MultiEntityComboboxProps<V extends string | number>
37
- extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
38
+ extends Omit<ComponentPropsWithoutRef<"div">, "onChange">,
39
+ FieldHelpProps {
38
40
  /** Selected ids. */
39
41
  value: V[];
40
42
  onChange: (value: V[]) => void;
@@ -144,6 +146,7 @@ export function MultiEntityCombobox<V extends string | number>(props: MultiEntit
144
146
  invalid,
145
147
  error,
146
148
  hint,
149
+ help,
147
150
  filter,
148
151
  minChars,
149
152
  debounceMs,
@@ -169,7 +172,7 @@ export function MultiEntityCombobox<V extends string | number>(props: MultiEntit
169
172
  minChars,
170
173
  debounceMs,
171
174
  });
172
- const hintParts = useFieldHint(hint, ariaDescribedBy);
175
+ const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });
173
176
  const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);
174
177
  // The props are the per-instance overrides, the provider the app-wide ones; a
175
178
  // prop left `undefined` falls through to the provider rather than masking it.
@@ -13,6 +13,7 @@ import {
13
13
  useFieldMessages,
14
14
  useLockReason,
15
15
  type DisabledReasonDisplay,
16
+ type FieldHelpProps,
16
17
  } from "./field-parts";
17
18
  import type { CommitScope } from "./write-lock";
18
19
  import {
@@ -46,7 +47,8 @@ export interface MultiSelectOption<V extends string | number = string | number>
46
47
  * inferred from `options` and `value`, so `onChange` hands back `number[]` for number
47
48
  * ids and `string[]` for string ones, and no call site casts. */
48
49
  export interface MultiSelectProps<V extends string | number = string | number>
49
- extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
50
+ extends Omit<ComponentPropsWithoutRef<"div">, "onChange">,
51
+ FieldHelpProps {
50
52
  options: MultiSelectOption<V>[];
51
53
  /** The chosen values — the select family's `value` (API-4). */
52
54
  value?: V[];
@@ -140,6 +142,7 @@ export function MultiSelect<V extends string | number = string | number>(props:
140
142
  invalid,
141
143
  error,
142
144
  hint,
145
+ help,
143
146
  disabled,
144
147
  disabledReason,
145
148
  disabledReasonDisplay,
@@ -195,6 +198,8 @@ export function MultiSelect<V extends string | number = string | number>(props:
195
198
  // The caller's ids, the caption, a lock's reason, the error — Select's order.
196
199
  const field = useFieldMessages({
197
200
  hint,
201
+ help,
202
+ name: label ?? ariaLabel,
198
203
  error,
199
204
  invalid,
200
205
  describedBy: ariaDescribedBy,
@@ -11,7 +11,7 @@ import {
11
11
  } from "./field-base";
12
12
  // The data alone (0.35): not the picker, which this field does not render.
13
13
  import { CURRENCIES } from "../currencies";
14
- import { FieldCaption, LockedReason, mergeRefs, type DisabledReasonDisplay } from "./field-parts";
14
+ import { FieldCaption, LockedReason, mergeRefs, type DisabledReasonDisplay, type FieldHelpProps } from "./field-parts";
15
15
  import { useEndWidth, useNumericField } from "./numeric-field";
16
16
  import type { CommitScope } from "./write-lock";
17
17
  import { cn } from "../lib/cn";
@@ -19,7 +19,7 @@ import { commitExpression, formatResult } from "../lib/calc";
19
19
 
20
20
  /** {@link NumberInput}'s props — exported since 0.34 (refactor audit 2026-10, API-9): a
21
21
  * wrapper typed itself off `ComponentProps<typeof NumberInput>` until then. */
22
- export interface NumberInputProps {
22
+ export interface NumberInputProps extends FieldHelpProps {
23
23
  /** The text, DOT-decimal ("1234.5", or a calculation being typed: "12+5"), as
24
24
  * `onChange` reports it — so a caller's `Number(value)` reads it. The field SHOWS it
25
25
  * in the locale's mark (see `locale`). A comma here is read as the decimal mark too,
@@ -8,6 +8,7 @@ import {
8
8
  useFieldError,
9
9
  useFieldHint,
10
10
  useLockReason,
11
+ type FieldHelpProps,
11
12
  type FieldHintParts,
12
13
  type LockReason,
13
14
  } from "./field-parts";
@@ -28,7 +29,9 @@ import type { CommitScope } from "./write-lock";
28
29
  */
29
30
 
30
31
  /** The props both fields take and this engine reads. */
31
- export interface NumericFieldProps {
32
+ export interface NumericFieldProps extends FieldHelpProps {
33
+ /** What names the `help` "?" — "About <label>" — with the `aria-label`. */
34
+ label?: ReactNode;
32
35
  hint?: ReactNode;
33
36
  error?: ReactNode;
34
37
  invalid?: boolean;
@@ -39,6 +42,7 @@ export interface NumericFieldProps {
39
42
  calculator?: boolean;
40
43
  variant?: "field" | "display";
41
44
  locale?: string;
45
+ "aria-label"?: string;
42
46
  "aria-describedby"?: string;
43
47
  "aria-invalid"?: boolean | "true" | "false";
44
48
  }
@@ -94,7 +98,11 @@ export function useNumericField(props: NumericFieldProps, { settle, display }: N
94
98
  const lock = useLockReason(props.commit, props.disabledReason);
95
99
  const locked = lock.locked;
96
100
  // §4: a FieldHint is the caption at Large and on touch, as on every field.
97
- const hint = useFieldHint(props.hint, props["aria-describedby"]);
101
+ // A `help` is the label line's one "?" and turns the hint into the caption (0.36).
102
+ const hint = useFieldHint(props.hint, props["aria-describedby"], {
103
+ help: props.help,
104
+ name: props.label ?? props["aria-label"],
105
+ });
98
106
  // The standing advice first, the news second: the caller's ids, the caption, the
99
107
  // error — a lock's reason between the last two, as on Select.
100
108
  const field = useFieldError(
@@ -1,7 +1,7 @@
1
1
  import { useId, useState } from "react";
2
2
  import type { ChangeEvent, ClipboardEvent, FocusEvent, InputHTMLAttributes, ReactNode, Ref } from "react";
3
3
  import { cn } from "../lib/cn";
4
- import { EndHintRow, FieldCaption, useFieldError, useFieldHint } from "./field-parts";
4
+ import { EndHintRow, FieldCaption, useFieldError, useFieldHint, type FieldHelpProps } from "./field-parts";
5
5
  import {
6
6
  FIELD_BASE,
7
7
  FIELD_INVALID,
@@ -60,7 +60,8 @@ export interface OneTimeCodeInputProps
60
60
  extends Omit<
61
61
  InputHTMLAttributes<HTMLInputElement>,
62
62
  "value" | "defaultValue" | "onChange" | "type" | "inputMode" | "autoComplete" | "maxLength" | "pattern"
63
- > {
63
+ >,
64
+ FieldHelpProps {
64
65
  /** The digits so far — a string, so leading zeros survive. */
65
66
  value: string;
66
67
  /** The cleaned code after every edit: digits only, never longer than `length`. */
@@ -130,6 +131,7 @@ export function OneTimeCodeInput(props: OneTimeCodeInputProps) {
130
131
  onComplete,
131
132
  label,
132
133
  hint,
134
+ help,
133
135
  error,
134
136
  invalid: invalidProp,
135
137
  className,
@@ -151,7 +153,7 @@ export function OneTimeCodeInput(props: OneTimeCodeInputProps) {
151
153
  // Text is a caption under the field; a FieldHint rides the label line, and is that
152
154
  // caption at Large and on touch (§4). See `hint`. Through useFieldHint and
153
155
  // useFieldError since 0.34, not copies of their rules (DUP-2, DUP-8).
154
- const hintParts = useFieldHint(hint, ariaDescribedBy);
156
+ const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? rest["aria-label"] });
155
157
  const {
156
158
  isInvalid: invalid,
157
159
  describedBy,