@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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/theme/theme-store.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport { logger } from \"../lib/logger\";\n\nexport type ThemeMode = \"light\" | \"dark\";\n/** What the user chose; \"system\" tracks the OS `prefers-color-scheme`. */\nexport type ThemePreference = \"light\" | \"dark\" | \"system\";\n\nexport interface ThemeState {\n /** The user's choice (persisted). */\n preference: ThemePreference;\n /** The RESOLVED light/dark actually in effect (system → OS). Read this for\n * anything that needs the concrete theme (e.g. token selection, toasts). */\n mode: ThemeMode;\n setPreference: (preference: ThemePreference) => void;\n /** Set an explicit light/dark choice (preference follows). */\n setMode: (mode: ThemeMode) => void;\n /** Flip the effective theme to the opposite explicit choice. */\n toggle: () => void;\n}\n\nfunction systemPreference(): ThemeMode {\n try {\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n } catch {\n return \"light\";\n }\n}\n\nfunction resolve(preference: ThemePreference): ThemeMode {\n return preference === \"system\" ? systemPreference() : preference;\n}\n\n/** Normalize a persisted theme-state blob to a valid preference. Older blobs\n * persisted the concrete `mode` (light/dark); carry that over as an explicit\n * preference so the choice survives. Shared by the store's `migrate` and the\n * pre-hydration {@link applyPersistedTheme}. */\nfunction preferenceFromState(\n s: { mode?: string; preference?: string } | null | undefined,\n): ThemePreference {\n if (s?.preference === \"light\" || s?.preference === \"dark\" || s?.preference === \"system\") {\n return s.preference;\n }\n if (s?.mode === \"light\" || s?.mode === \"dark\") return s.mode;\n return \"system\";\n}\n\n/** Toggle the `.dark` class on <html> for a resolved light/dark mode. */\nfunction applyThemeClass(mode: ThemeMode): void {\n const root = document.documentElement;\n if (mode === \"dark\") root.classList.add(\"dark\");\n else root.classList.remove(\"dark\");\n}\n\n/**\n * Pre-hydration: read the theme PREFERENCE persisted under `storageKey` straight\n * from localStorage, resolve it to a concrete light/dark mode, and toggle the\n * `.dark` class on <html> — BEFORE React and the store mount, so the first paint\n * already has the right theme (no flash). Returns the resolved mode so the caller\n * can chain {@link applyPersistedPalette}. Encapsulates the zustand-persist\n * envelope + the legacy `mode`→`preference` fallback so apps never re-type the\n * storage shape in their entrypoint.\n */\nexport function applyPersistedTheme(storageKey: string): ThemeMode {\n let preference: ThemePreference = \"system\";\n try {\n const blob = JSON.parse(localStorage.getItem(storageKey) ?? \"{}\") as {\n state?: { mode?: string; preference?: string };\n };\n preference = preferenceFromState(blob.state);\n } catch {\n /* malformed storage → system */\n }\n const mode = resolve(preference);\n applyThemeClass(mode);\n return mode;\n}\n\n/**\n * Build a theme store bound to a consumer-supplied localStorage key. Tracks a\n * light/dark/system PREFERENCE (feedback #330) and exposes the resolved `mode`.\n * Returns the `useTheme` hook plus a `useApplyTheme` effect that toggles the\n * `.dark` class on <html> and, while on \"system\", follows OS theme changes live.\n */\nexport function createThemeStore(storageKey: string) {\n const useTheme = create<ThemeState>()(\n logger(\n persist(\n (set, get) => ({\n preference: \"system\" as ThemePreference,\n mode: systemPreference(),\n setPreference: (preference) => set({ preference, mode: resolve(preference) }),\n setMode: (mode) => set({ preference: mode, mode }),\n toggle: () => {\n const next: ThemeMode = get().mode === \"dark\" ? \"light\" : \"dark\";\n set({ preference: next, mode: next });\n },\n }),\n {\n name: storageKey,\n version: 2,\n // Only the preference is persisted; `mode` is derived on load.\n partialize: (s) => ({ preference: s.preference }),\n migrate: (persistedState): { preference: ThemePreference } => ({\n preference: preferenceFromState(\n persistedState as { mode?: string; preference?: string } | null,\n ),\n }),\n onRehydrateStorage: () => (state) => {\n // The persisted preference decides the concrete mode at load.\n if (state) state.mode = resolve(state.preference);\n },\n },\n ),\n \"theme\",\n ),\n );\n\n function useApplyTheme(): void {\n const mode = useTheme((s) => s.mode);\n const preference = useTheme((s) => s.preference);\n useEffect(() => {\n applyThemeClass(mode);\n }, [mode]);\n // While tracking the system, follow OS theme changes live.\n useEffect(() => {\n if (preference !== \"system\") return;\n let mq: MediaQueryList;\n try {\n mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n } catch {\n return;\n }\n const onChange = () => useTheme.setState({ mode: mq.matches ? \"dark\" : \"light\" });\n onChange();\n mq.addEventListener?.(\"change\", onChange);\n return () => mq.removeEventListener?.(\"change\", onChange);\n }, [preference]);\n }\n\n return { useTheme, useApplyTheme };\n}\n\nexport type ThemeStore = ReturnType<typeof createThemeStore>;\n"],"mappings":";AAAA,SAAS,iBAAiB;AAC1B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,cAAc;AAmBvB,SAAS,mBAA8B;AACrC,MAAI;AACF,WAAO,OAAO,WAAW,8BAA8B,EAAE,UAAU,SAAS;AAAA,EAC9E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,QAAQ,YAAwC;AACvD,SAAO,eAAe,WAAW,iBAAiB,IAAI;AACxD;AAMA,SAAS,oBACP,GACiB;AACjB,MAAI,GAAG,eAAe,WAAW,GAAG,eAAe,UAAU,GAAG,eAAe,UAAU;AACvF,WAAO,EAAE;AAAA,EACX;AACA,MAAI,GAAG,SAAS,WAAW,GAAG,SAAS,OAAQ,QAAO,EAAE;AACxD,SAAO;AACT;AAGA,SAAS,gBAAgB,MAAuB;AAC9C,QAAM,OAAO,SAAS;AACtB,MAAI,SAAS,OAAQ,MAAK,UAAU,IAAI,MAAM;AAAA,MACzC,MAAK,UAAU,OAAO,MAAM;AACnC;AAWO,SAAS,oBAAoB,YAA+B;AACjE,MAAI,aAA8B;AAClC,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,aAAa,QAAQ,UAAU,KAAK,IAAI;AAGhE,iBAAa,oBAAoB,KAAK,KAAK;AAAA,EAC7C,QAAQ;AAAA,EAER;AACA,QAAM,OAAO,QAAQ,UAAU;AAC/B,kBAAgB,IAAI;AACpB,SAAO;AACT;AAQO,SAAS,iBAAiB,YAAoB;AACnD,QAAM,WAAW,OAAmB;AAAA,IAClC;AAAA,MACE;AAAA,QACE,CAAC,KAAK,SAAS;AAAA,UACb,YAAY;AAAA,UACZ,MAAM,iBAAiB;AAAA,UACvB,eAAe,CAAC,eAAe,IAAI,EAAE,YAAY,MAAM,QAAQ,UAAU,EAAE,CAAC;AAAA,UAC5E,SAAS,CAAC,SAAS,IAAI,EAAE,YAAY,MAAM,KAAK,CAAC;AAAA,UACjD,QAAQ,MAAM;AACZ,kBAAM,OAAkB,IAAI,EAAE,SAAS,SAAS,UAAU;AAC1D,gBAAI,EAAE,YAAY,MAAM,MAAM,KAAK,CAAC;AAAA,UACtC;AAAA,QACF;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA;AAAA,UAET,YAAY,CAAC,OAAO,EAAE,YAAY,EAAE,WAAW;AAAA,UAC/C,SAAS,CAAC,oBAAqD;AAAA,YAC7D,YAAY;AAAA,cACV;AAAA,YACF;AAAA,UACF;AAAA,UACA,oBAAoB,MAAM,CAAC,UAAU;AAEnC,gBAAI,MAAO,OAAM,OAAO,QAAQ,MAAM,UAAU;AAAA,UAClD;AAAA,QACF;AAAA,MACF;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,WAAS,gBAAsB;AAC7B,UAAM,OAAO,SAAS,CAAC,MAAM,EAAE,IAAI;AACnC,UAAM,aAAa,SAAS,CAAC,MAAM,EAAE,UAAU;AAC/C,cAAU,MAAM;AACd,sBAAgB,IAAI;AAAA,IACtB,GAAG,CAAC,IAAI,CAAC;AAET,cAAU,MAAM;AACd,UAAI,eAAe,SAAU;AAC7B,UAAI;AACJ,UAAI;AACF,aAAK,OAAO,WAAW,8BAA8B;AAAA,MACvD,QAAQ;AACN;AAAA,MACF;AACA,YAAM,WAAW,MAAM,SAAS,SAAS,EAAE,MAAM,GAAG,UAAU,SAAS,QAAQ,CAAC;AAChF,eAAS;AACT,SAAG,mBAAmB,UAAU,QAAQ;AACxC,aAAO,MAAM,GAAG,sBAAsB,UAAU,QAAQ;AAAA,IAC1D,GAAG,CAAC,UAAU,CAAC;AAAA,EACjB;AAEA,SAAO,EAAE,UAAU,cAAc;AACnC;","names":[]}
1
+ {"version":3,"sources":["../../src/theme/theme-store.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport { logger } from \"../lib/logger\";\nimport { registerDeviceSetting } from \"../lib/device-settings\";\n\nexport type ThemeMode = \"light\" | \"dark\";\n/** What the user chose; \"system\" tracks the OS `prefers-color-scheme`. */\nexport type ThemePreference = \"light\" | \"dark\" | \"system\";\n\nexport interface ThemeState {\n /** The user's choice (persisted). */\n preference: ThemePreference;\n /** The RESOLVED light/dark actually in effect (system → OS). Read this for\n * anything that needs the concrete theme (e.g. token selection, toasts). */\n mode: ThemeMode;\n setPreference: (preference: ThemePreference) => void;\n /** Set an explicit light/dark choice (preference follows). */\n setMode: (mode: ThemeMode) => void;\n /** Flip the effective theme to the opposite explicit choice. */\n toggle: () => void;\n}\n\nfunction systemPreference(): ThemeMode {\n try {\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n } catch {\n return \"light\";\n }\n}\n\nfunction resolve(preference: ThemePreference): ThemeMode {\n return preference === \"system\" ? systemPreference() : preference;\n}\n\n/** Normalize a persisted theme-state blob to a valid preference. Older blobs\n * persisted the concrete `mode` (light/dark); carry that over as an explicit\n * preference so the choice survives. Shared by the store's `migrate` and the\n * pre-hydration {@link applyPersistedTheme}. */\nfunction preferenceFromState(\n s: { mode?: string; preference?: string } | null | undefined,\n): ThemePreference {\n if (s?.preference === \"light\" || s?.preference === \"dark\" || s?.preference === \"system\") {\n return s.preference;\n }\n if (s?.mode === \"light\" || s?.mode === \"dark\") return s.mode;\n return \"system\";\n}\n\n/** Toggle the `.dark` class on <html> for a resolved light/dark mode. */\nfunction applyThemeClass(mode: ThemeMode): void {\n const root = document.documentElement;\n if (mode === \"dark\") root.classList.add(\"dark\");\n else root.classList.remove(\"dark\");\n}\n\n/**\n * Pre-hydration: read the theme PREFERENCE persisted under `storageKey` straight\n * from localStorage, resolve it to a concrete light/dark mode, and toggle the\n * `.dark` class on <html> — BEFORE React and the store mount, so the first paint\n * already has the right theme (no flash). Returns the resolved mode so the caller\n * can chain {@link applyPersistedPalette}. Encapsulates the zustand-persist\n * envelope + the legacy `mode`→`preference` fallback so apps never re-type the\n * storage shape in their entrypoint.\n */\nexport function applyPersistedTheme(storageKey: string): ThemeMode {\n let preference: ThemePreference = \"system\";\n try {\n const blob = JSON.parse(localStorage.getItem(storageKey) ?? \"{}\") as {\n state?: { mode?: string; preference?: string };\n };\n preference = preferenceFromState(blob.state);\n } catch {\n /* malformed storage → system */\n }\n const mode = resolve(preference);\n applyThemeClass(mode);\n return mode;\n}\n\n/**\n * Build a theme store bound to a consumer-supplied localStorage key. Tracks a\n * light/dark/system PREFERENCE (feedback #330) and exposes the resolved `mode`.\n * Returns the `useTheme` hook plus a `useApplyTheme` effect that toggles the\n * `.dark` class on <html> and, while on \"system\", follows OS theme changes live.\n */\nexport function createThemeStore(storageKey: string) {\n const useTheme = create<ThemeState>()(\n logger(\n persist(\n (set, get) => ({\n preference: \"system\" as ThemePreference,\n mode: systemPreference(),\n setPreference: (preference) => set({ preference, mode: resolve(preference) }),\n setMode: (mode) => set({ preference: mode, mode }),\n toggle: () => {\n const next: ThemeMode = get().mode === \"dark\" ? \"light\" : \"dark\";\n set({ preference: next, mode: next });\n },\n }),\n {\n name: storageKey,\n version: 2,\n // Only the preference is persisted; `mode` is derived on load.\n partialize: (s) => ({ preference: s.preference }),\n migrate: (persistedState): { preference: ThemePreference } => ({\n preference: preferenceFromState(\n persistedState as { mode?: string; preference?: string } | null,\n ),\n }),\n onRehydrateStorage: () => (state) => {\n // The persisted preference decides the concrete mode at load.\n if (state) state.mode = resolve(state.preference);\n },\n },\n ),\n \"theme\",\n ),\n );\n // Reset this device's settings (docs/settings-harmonization.md §11.4): back to\n // \"System\" through the setter, which re-resolves `mode`; `useApplyTheme` puts the\n // class back and follows the OS again, and the persisted blob is what the pre-paint\n // `applyPersistedTheme` reads next load.\n registerDeviceSetting({\n key: storageKey,\n kind: \"appearance\",\n reset: () => useTheme.getState().setPreference(\"system\"),\n });\n\n function useApplyTheme(): void {\n const mode = useTheme((s) => s.mode);\n const preference = useTheme((s) => s.preference);\n useEffect(() => {\n applyThemeClass(mode);\n }, [mode]);\n // While tracking the system, follow OS theme changes live.\n useEffect(() => {\n if (preference !== \"system\") return;\n let mq: MediaQueryList;\n try {\n mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n } catch {\n return;\n }\n const onChange = () => useTheme.setState({ mode: mq.matches ? \"dark\" : \"light\" });\n onChange();\n mq.addEventListener?.(\"change\", onChange);\n return () => mq.removeEventListener?.(\"change\", onChange);\n }, [preference]);\n }\n\n return { useTheme, useApplyTheme };\n}\n\nexport type ThemeStore = ReturnType<typeof createThemeStore>;\n"],"mappings":";AAAA,SAAS,iBAAiB;AAC1B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,6BAA6B;AAmBtC,SAAS,mBAA8B;AACrC,MAAI;AACF,WAAO,OAAO,WAAW,8BAA8B,EAAE,UAAU,SAAS;AAAA,EAC9E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,QAAQ,YAAwC;AACvD,SAAO,eAAe,WAAW,iBAAiB,IAAI;AACxD;AAMA,SAAS,oBACP,GACiB;AACjB,MAAI,GAAG,eAAe,WAAW,GAAG,eAAe,UAAU,GAAG,eAAe,UAAU;AACvF,WAAO,EAAE;AAAA,EACX;AACA,MAAI,GAAG,SAAS,WAAW,GAAG,SAAS,OAAQ,QAAO,EAAE;AACxD,SAAO;AACT;AAGA,SAAS,gBAAgB,MAAuB;AAC9C,QAAM,OAAO,SAAS;AACtB,MAAI,SAAS,OAAQ,MAAK,UAAU,IAAI,MAAM;AAAA,MACzC,MAAK,UAAU,OAAO,MAAM;AACnC;AAWO,SAAS,oBAAoB,YAA+B;AACjE,MAAI,aAA8B;AAClC,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,aAAa,QAAQ,UAAU,KAAK,IAAI;AAGhE,iBAAa,oBAAoB,KAAK,KAAK;AAAA,EAC7C,QAAQ;AAAA,EAER;AACA,QAAM,OAAO,QAAQ,UAAU;AAC/B,kBAAgB,IAAI;AACpB,SAAO;AACT;AAQO,SAAS,iBAAiB,YAAoB;AACnD,QAAM,WAAW,OAAmB;AAAA,IAClC;AAAA,MACE;AAAA,QACE,CAAC,KAAK,SAAS;AAAA,UACb,YAAY;AAAA,UACZ,MAAM,iBAAiB;AAAA,UACvB,eAAe,CAAC,eAAe,IAAI,EAAE,YAAY,MAAM,QAAQ,UAAU,EAAE,CAAC;AAAA,UAC5E,SAAS,CAAC,SAAS,IAAI,EAAE,YAAY,MAAM,KAAK,CAAC;AAAA,UACjD,QAAQ,MAAM;AACZ,kBAAM,OAAkB,IAAI,EAAE,SAAS,SAAS,UAAU;AAC1D,gBAAI,EAAE,YAAY,MAAM,MAAM,KAAK,CAAC;AAAA,UACtC;AAAA,QACF;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA;AAAA,UAET,YAAY,CAAC,OAAO,EAAE,YAAY,EAAE,WAAW;AAAA,UAC/C,SAAS,CAAC,oBAAqD;AAAA,YAC7D,YAAY;AAAA,cACV;AAAA,YACF;AAAA,UACF;AAAA,UACA,oBAAoB,MAAM,CAAC,UAAU;AAEnC,gBAAI,MAAO,OAAM,OAAO,QAAQ,MAAM,UAAU;AAAA,UAClD;AAAA,QACF;AAAA,MACF;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAKA,wBAAsB;AAAA,IACpB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,OAAO,MAAM,SAAS,SAAS,EAAE,cAAc,QAAQ;AAAA,EACzD,CAAC;AAED,WAAS,gBAAsB;AAC7B,UAAM,OAAO,SAAS,CAAC,MAAM,EAAE,IAAI;AACnC,UAAM,aAAa,SAAS,CAAC,MAAM,EAAE,UAAU;AAC/C,cAAU,MAAM;AACd,sBAAgB,IAAI;AAAA,IACtB,GAAG,CAAC,IAAI,CAAC;AAET,cAAU,MAAM;AACd,UAAI,eAAe,SAAU;AAC7B,UAAI;AACJ,UAAI;AACF,aAAK,OAAO,WAAW,8BAA8B;AAAA,MACvD,QAAQ;AACN;AAAA,MACF;AACA,YAAM,WAAW,MAAM,SAAS,SAAS,EAAE,MAAM,GAAG,UAAU,SAAS,QAAQ,CAAC;AAChF,eAAS;AACT,SAAG,mBAAmB,UAAU,QAAQ;AACxC,aAAO,MAAM,GAAG,sBAAsB,UAAU,QAAQ;AAAA,IAC1D,GAAG,CAAC,UAAU,CAAC;AAAA,EACjB;AAEA,SAAO,EAAE,UAAU,cAAc;AACnC;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eifi1/ui-kit",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "type": "module",
5
5
  "description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
6
6
  "_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
@@ -7,6 +7,7 @@ import type { CheckboxGroupOption } from "../components/checkbox-group";
7
7
  import { Select } from "../components/select";
8
8
  import type { SelectProps } from "../components/select";
9
9
  import type { CommitScope } from "../components/write-lock";
10
+ import type { FieldHelpProps } from "../components/field-parts";
10
11
  import { DEFAULT_ROLE_SELECT_LABELS } from "./role-select-labels";
11
12
 
12
13
  /**
@@ -185,7 +186,7 @@ export function RoleSelect<R extends string = string>({
185
186
 
186
187
  /* ── RolesEditor ────────────────────────────────────────────────────────────── */
187
188
 
188
- export interface RolesEditorProps<R extends string = string> {
189
+ export interface RolesEditorProps<R extends string = string> extends FieldHelpProps {
189
190
  /** The roles ticked — the draft the app holds while its dialog is open. */
190
191
  value: readonly R[];
191
192
  roles: RoleVocabulary<R>;
@@ -236,6 +237,7 @@ export function RolesEditor<R extends string = string>({
236
237
  legendVisibility,
237
238
  required,
238
239
  hint,
240
+ help,
239
241
  error,
240
242
  columns,
241
243
  commit,
@@ -267,6 +269,7 @@ export function RolesEditor<R extends string = string>({
267
269
  onChange={onChange}
268
270
  required={required}
269
271
  hint={hint}
272
+ help={help}
270
273
  error={error}
271
274
  columns={columns}
272
275
  commit={commit}
@@ -36,6 +36,7 @@ import {
36
36
  LockedReason,
37
37
  mergeRefs,
38
38
  type DisabledReasonDisplay,
39
+ type FieldHelpProps,
39
40
  type LockReason,
40
41
  } from "./field-parts";
41
42
  import { useEndWidth, useNumericField } from "./numeric-field";
@@ -43,7 +44,7 @@ import type { CommitScope } from "./write-lock";
43
44
 
44
45
  /** {@link AmountInput}'s props — exported since 0.34 (refactor audit 2026-10, API-9): a
45
46
  * wrapper typed itself off `ComponentProps<typeof AmountInput>` until then. */
46
- export interface AmountInputProps {
47
+ export interface AmountInputProps extends FieldHelpProps {
47
48
  value: string;
48
49
  onChange: (value: string) => void;
49
50
  currency?: string;
@@ -32,6 +32,7 @@ import {
32
32
  useFieldHint,
33
33
  useLockReason,
34
34
  type DisabledReasonDisplay,
35
+ type FieldHelpProps,
35
36
  } from "./field-parts";
36
37
  import type { CommitScope } from "./write-lock";
37
38
 
@@ -44,7 +45,8 @@ export interface AutocompleteProps<V extends string | number = string>
44
45
  extends Omit<
45
46
  ComponentPropsWithoutRef<"input">,
46
47
  "value" | "defaultValue" | "onChange" | "onSelect" | "children" | "type" | "size"
47
- > {
48
+ >,
49
+ FieldHelpProps {
48
50
  /** What is in the field. Controlled, and never reset by the component: opening,
49
51
  * closing and a failed lookup all leave it exactly as typed. */
50
52
  value: string;
@@ -220,6 +222,7 @@ export function Autocomplete<V extends string | number = string>(props: Autocomp
220
222
  invalid,
221
223
  error,
222
224
  hint,
225
+ help,
223
226
  status,
224
227
  emptyLabel,
225
228
  loadErrorLabel,
@@ -252,7 +255,7 @@ export function Autocomplete<V extends string | number = string>(props: Autocomp
252
255
  noResults: emptyLabel,
253
256
  loadError: loadErrorLabel,
254
257
  });
255
- const hintParts = useFieldHint(hint, ariaDescribedBy);
258
+ const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });
256
259
  // A form library's `aria-invalid` paints as well as announces (DUP-8: it only
257
260
  // announced, through the spread below).
258
261
  const field = useFieldError(error, invalid, hintParts.describedBy, rest["aria-invalid"]);
@@ -100,8 +100,13 @@ export const buttonVariantClasses: Record<ButtonVariant, string> = {
100
100
  // the base's focus ring stays: those copies had `outline-none` and no ring, so
101
101
  // a keyboard user tabbing onto them saw nothing at all. `rounded-sm` keeps that
102
102
  // ring hugging the word instead of drawing a pill round it.
103
+ //
104
+ // Under More contrast the word reads `--brand-muted`, the brand AS TEXT (0.36,
105
+ // colour-roles §13.3 decision 8): `--brand` is solved as a mark (3:1), and on More
106
+ // contrast's darker light well Moss's read 3.68:1; the derived dark brands sit at 3.0
107
+ // as text too. Both modes, since `contrast:` does not split them.
103
108
  link:
104
- "rounded-sm p-0 bg-transparent text-brand underline-offset-4 hover:underline focus-visible:ring-brand",
109
+ "rounded-sm p-0 bg-transparent text-brand underline-offset-4 hover:underline focus-visible:ring-brand contrast:text-brand-muted",
105
110
  };
106
111
 
107
112
  /**
@@ -132,11 +137,14 @@ export const buttonVariantClasses: Record<ButtonVariant, string> = {
132
137
  */
133
138
  export type ButtonTone = "default" | "muted" | "danger";
134
139
 
140
+ // `contrast:text-secondary` restates the tone's own colour under the `contrast:` variant,
141
+ // so tailwind-merge drops the link's `contrast:text-brand-muted` (0.36): a quiet link
142
+ // stays quiet under More contrast. On `ghost` it reads the same either way.
135
143
  const BUTTON_TONES: Record<Exclude<ButtonTone, "default">, string> = {
136
144
  muted:
137
- "text-secondary hover:text-primary disabled:hover:text-secondary",
145
+ "text-secondary hover:text-primary disabled:hover:text-secondary contrast:text-secondary",
138
146
  danger:
139
- "text-secondary hover:text-danger focus-visible:ring-danger-strong disabled:hover:text-secondary",
147
+ "text-secondary hover:text-danger focus-visible:ring-danger-strong disabled:hover:text-secondary contrast:text-secondary",
140
148
  };
141
149
 
142
150
  /** Only the two transparent variants take every tone; see {@link ButtonTone}. */
@@ -158,9 +166,11 @@ const BUTTON_BOXED_DANGER: Partial<Record<ButtonVariant, string>> = {
158
166
 
159
167
  // `pressed` on a `link`: the brand colour and a heavier weight, which is exactly what
160
168
  // lenkbank's "All speeds" toggle paints by hand (gear/hysteresis-charts.tsx:482). After
161
- // the tone, so a pressed muted link is brand, and an unpressed one is quiet grey.
169
+ // the tone, so a pressed muted link is brand, and an unpressed one is quiet grey. Under
170
+ // More contrast the brand as text, `--brand-muted`, at rest and hovered (0.36, as the
171
+ // `link` variant), whatever the tone.
162
172
  export const BUTTON_LINK_PRESSED =
163
- "font-medium text-brand hover:text-brand disabled:hover:text-brand";
173
+ "font-medium text-brand hover:text-brand disabled:hover:text-brand contrast:text-brand-muted contrast:hover:text-brand-muted contrast:disabled:hover:text-brand-muted";
164
174
 
165
175
  /** The second argument of {@link buttonClasses} in its options form. */
166
176
  export interface ButtonClassesOptions {
@@ -4,7 +4,14 @@ import { cn } from "../lib/cn";
4
4
  import { Checkbox } from "./checkbox";
5
5
  import { hasContent } from "../lib/presence";
6
6
  import { useCommitReason, type CommitScope } from "./write-lock";
7
- import { FIELD_ERROR_CLASS, mergeDescribedBy, RequiredStarOnLegend, useInlineHint } from "./field-parts";
7
+ import {
8
+ FIELD_ERROR_CLASS,
9
+ fieldHelpMark,
10
+ mergeDescribedBy,
11
+ RequiredStarOnLegend,
12
+ useInlineHint,
13
+ type FieldHelpProps,
14
+ } from "./field-parts";
8
15
 
9
16
  /** One box of a {@link CheckboxGroup}. */
10
17
  export interface CheckboxGroupOption<T extends string = string> {
@@ -22,7 +29,7 @@ export interface CheckboxGroupOption<T extends string = string> {
22
29
  * reader where the dialog's title already says it to the eye. */
23
30
  export type CheckboxGroupLegendVisibility = "visible" | "sr-only";
24
31
 
25
- export interface CheckboxGroupProps<T extends string = string> {
32
+ export interface CheckboxGroupProps<T extends string = string> extends FieldHelpProps {
26
33
  /** The question the boxes answer — the fieldset's `<legend>`, which names the group.
27
34
  * Without one, pass `aria-label`. */
28
35
  legend?: ReactNode;
@@ -64,6 +71,8 @@ export interface CheckboxGroupProps<T extends string = string> {
64
71
  /**
65
72
  * Standing advice for the group, under the boxes, attached to the fieldset with
66
73
  * `aria-describedby` before the error — the field anatomy every kit field keeps.
74
+ * A `help` beside it (0.36) is a "?" after the legend — with a visible legend only —
75
+ * and names nothing: the fieldset is then named by the legend's words alone.
67
76
  */
68
77
  hint?: ReactNode;
69
78
  /** What is wrong with the set, in the caller's words. Under the boxes, on the
@@ -128,6 +137,7 @@ export function CheckboxGroup<T extends string = string>({
128
137
  disabled,
129
138
  required,
130
139
  hint,
140
+ help,
131
141
  error,
132
142
  invalid,
133
143
  commit,
@@ -142,7 +152,11 @@ export function CheckboxGroup<T extends string = string>({
142
152
  const errorId = `${generated}-error`;
143
153
  // §4: a FieldHint under the group is its words at Large and on a touch screen, not a
144
154
  // lone "?" (DUP-2, refactor audit 2026-10). A HelpPopover stays a "?".
145
- const shownHint = useInlineHint(hint);
155
+ // A `help` "?" after a VISIBLE legend (0.36): the label line a group has. Its FieldHint
156
+ // hint is then its words at every size, as everywhere a help stands beside one.
157
+ const helpMark = fieldHelpMark(legend !== undefined && legendVisibility === "visible" ? help : undefined, legend);
158
+ const legendTextId = `${generated}-legend`;
159
+ const shownHint = useInlineHint(hint, helpMark !== undefined);
146
160
  const showHint = hasContent(shownHint);
147
161
  const showError = hasContent(error);
148
162
  const isInvalid = Boolean(invalid) || showError;
@@ -169,6 +183,9 @@ export function CheckboxGroup<T extends string = string>({
169
183
  <fieldset
170
184
  id={id}
171
185
  aria-label={ariaLabel}
186
+ // With a help "?" inside the legend, the legend's WORDS name the group: a legend
187
+ // names its fieldset by its whole content, the "?"'s "About …" included.
188
+ aria-labelledby={helpMark !== undefined && ariaLabel === undefined ? legendTextId : undefined}
172
189
  aria-describedby={mergeDescribedBy(ariaDescribedBy, showHint && hintId, showError && errorId)}
173
190
  disabled={disabled && !locked}
174
191
  // `relative` holds an `sr-only` legend (sr-only-containment.test); `min-w-0`
@@ -184,12 +201,13 @@ export function CheckboxGroup<T extends string = string>({
184
201
  : cn("mb-2 p-0 text-sm font-medium text-primary", (disabled || locked) && "opacity-60")
185
202
  }
186
203
  >
187
- {legend}
204
+ {helpMark === undefined ? legend : <span id={legendTextId}>{legend}</span>}
188
205
  {required && (
189
206
  <span aria-hidden className="ms-0.5 text-danger">
190
207
  *
191
208
  </span>
192
209
  )}
210
+ {helpMark !== undefined && <span className="ms-1 inline-flex align-middle font-normal">{helpMark}</span>}
193
211
  </legend>
194
212
  )}
195
213
  <RequiredStarOnLegend.Provider value={true}>
@@ -31,6 +31,7 @@ import {
31
31
  TableHeaderCell,
32
32
  TableRow,
33
33
  } from "./table";
34
+ import type { FieldHelpProps } from "./field-parts";
34
35
  import { Select } from "./select";
35
36
  import { Textarea } from "./textarea";
36
37
  import { DEFAULT_COLUMN_MAPPER_LABELS, markedShort } from "./column-mapper-labels";
@@ -425,7 +426,7 @@ export function ColumnRoleTable<
425
426
 
426
427
  // ── ColumnMapper ──────────────────────────────────────────────────────────────
427
428
 
428
- export interface ColumnMapperProps<R extends string> {
429
+ export interface ColumnMapperProps<R extends string> extends FieldHelpProps {
429
430
  /** The parts the columns play — translated labels, which are required, which must
430
431
  * be numbers. See {@link ColumnRole}. */
431
432
  roles: readonly ColumnRole<R>[];
@@ -440,7 +441,8 @@ export interface ColumnMapperProps<R extends string> {
440
441
  onChange: (result: ColumnMapperResult<R> | null) => void;
441
442
  /** The paste box's label. Default: `labels.paste`. */
442
443
  label?: ReactNode;
443
- /** The caption under the paste box. Default: `labels.pasteHint`. */
444
+ /** The caption under the paste box. Default: `labels.pasteHint`. The paste box's
445
+ * `help` (0.36) is `help`. */
444
446
  hint?: ReactNode;
445
447
  /** What the file dialog offers, and what a drop is checked against. Default:
446
448
  * `.csv,.tsv,.txt,.dat,.asc`. */
@@ -503,6 +505,7 @@ export function ColumnMapper<R extends string>({
503
505
  onChange,
504
506
  label,
505
507
  hint,
508
+ help,
506
509
  accept = DEFAULT_ACCEPT,
507
510
  maxSize,
508
511
  defaultText = "",
@@ -605,6 +608,7 @@ export function ColumnMapper<R extends string>({
605
608
  <Textarea
606
609
  label={label ?? labels.paste}
607
610
  hint={hint ?? labels.pasteHint}
611
+ help={help}
608
612
  rows={5}
609
613
  wrap="off"
610
614
  spellCheck={false}
@@ -22,6 +22,7 @@ import {
22
22
  useFieldHint,
23
23
  useLockReason,
24
24
  type DisabledReasonDisplay,
25
+ type FieldHelpProps,
25
26
  } from "./field-parts";
26
27
  import type { CommitScope } from "./write-lock";
27
28
  import { lockedKeyDown, rowClass, SuggestionList, usePrimaryPressOnly } from "./combobox-suggestions";
@@ -38,7 +39,8 @@ export type { InlineEntityComboboxProps } from "./inline-entity-combobox";
38
39
  * the DOM events of the wrapper. Everything else a wrapper needs reaches the root.
39
40
  */
40
41
  export interface ComboboxProps
41
- extends Omit<ComponentPropsWithoutRef<"div">, "id" | "onChange" | "onBlur" | "onSubmit"> {
42
+ extends Omit<ComponentPropsWithoutRef<"div">, "id" | "onChange" | "onBlur" | "onSubmit">,
43
+ FieldHelpProps {
42
44
  value: string;
43
45
  onChange: (v: string) => void;
44
46
  /** Suggestion pool (e.g. existing payee names). */
@@ -186,6 +188,7 @@ export function Combobox(props: ComboboxProps) {
186
188
  invalid,
187
189
  error,
188
190
  hint,
191
+ help,
189
192
  disabled,
190
193
  disabledReason,
191
194
  disabledReasonDisplay,
@@ -204,7 +207,7 @@ export function Combobox(props: ComboboxProps) {
204
207
  "aria-required": ariaRequired,
205
208
  ...rest
206
209
  } = props;
207
- const hintParts = useFieldHint(hint, ariaDescribedBy);
210
+ const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });
208
211
  const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);
209
212
  const generated = useId();
210
213
  const fieldId = id ?? generated;
@@ -19,6 +19,7 @@ import {
19
19
  useFieldMessages,
20
20
  useLockReason,
21
21
  type DisabledReasonDisplay,
22
+ type FieldHelpProps,
22
23
  } from "./field-parts";
23
24
  import { CurrencyFlag } from "./currency-select";
24
25
  import { ComboboxPanel, useComboboxCore, type ComboOption } from "./combobox-core";
@@ -66,7 +67,8 @@ export { DEFAULT_COUNTRY_SELECT_LABELS };
66
67
  * `null` of a clear cannot reach a handler that was never told about it.
67
68
  */
68
69
  export interface CountrySelectProps<Clearable extends boolean = false>
69
- extends Omit<ComponentPropsWithoutRef<"div">, "onChange" | "defaultValue"> {
70
+ extends Omit<ComponentPropsWithoutRef<"div">, "onChange" | "defaultValue">,
71
+ FieldHelpProps {
70
72
  /** The chosen country as an ISO 3166-1 alpha-2 code ("CH"), in either case;
71
73
  * `null`, `undefined` or `""` when none is. A code outside the list still shows
72
74
  * its name — the list restricts what can be PICKED, not what can be shown. */
@@ -249,6 +251,7 @@ export function CountrySelect<Clearable extends boolean = false>({
249
251
  flags = false,
250
252
  label,
251
253
  hint,
254
+ help,
252
255
  error,
253
256
  invalid,
254
257
  placeholder,
@@ -359,6 +362,8 @@ export function CountrySelect<Clearable extends boolean = false>({
359
362
  // the error — Select's order. Through useFieldMessages since 0.34 (DUP-2, DUP-8).
360
363
  const field = useFieldMessages({
361
364
  hint,
365
+ help,
366
+ name: label ?? ariaLabel,
362
367
  error,
363
368
  invalid,
364
369
  describedBy: ariaDescribedBy,
@@ -2,7 +2,7 @@ import { useCallback, useId } from "react";
2
2
  import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";
3
3
  import { FieldChevron, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from "./field-base";
4
4
  import { FieldLabel } from "./field-parts";
5
- import { assignRef, EndHintRow, useFieldMessages } from "./field-parts";
5
+ import { assignRef, EndHintRow, useFieldMessages, type FieldHelpProps } from "./field-parts";
6
6
  import { cn } from "../lib/cn";
7
7
  import { useComboboxCore } from "./combobox-core";
8
8
  import {
@@ -29,7 +29,7 @@ export type { CurrencyFlagProps, CurrencyOption };
29
29
 
30
30
  /** `onChange` is the kit's — "a currency was picked", carrying the code — rather
31
31
  * than the div's form event. */
32
- export interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
32
+ export interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange">, FieldHelpProps {
33
33
  value: string;
34
34
  onChange: (code: string) => void;
35
35
  placeholder?: string;
@@ -106,6 +106,7 @@ export function CurrencySelect({
106
106
  invalid,
107
107
  error,
108
108
  hint,
109
+ help,
109
110
  labels,
110
111
  currencyNames,
111
112
  "aria-label": ariaLabel,
@@ -143,7 +144,15 @@ export function CurrencySelect({
143
144
  // on the wrapper `<div>` through the spread, where nothing reads it. The caller's ids
144
145
  // first, then the caption, then the error — Select's order, through useFieldMessages
145
146
  // since 0.34 (DUP-2, DUP-8).
146
- const field = useFieldMessages({ hint, error, invalid, describedBy: ariaDescribedBy, ariaInvalid });
147
+ const field = useFieldMessages({
148
+ hint,
149
+ help,
150
+ name: label ?? ariaLabel,
151
+ error,
152
+ invalid,
153
+ describedBy: ariaDescribedBy,
154
+ ariaInvalid,
155
+ });
147
156
  const { labelHint, describedBy, isInvalid } = field;
148
157
 
149
158
  // The trigger's NAME. A combobox does not take its name from its content, and the
@@ -19,7 +19,7 @@ import {
19
19
  import { splitTriggerAria } from "./trigger-aria";
20
20
  import type { TriggerAria } from "./trigger-aria";
21
21
  import { FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from "./field-base";
22
- import { assignRef, FieldLabelLine, LABEL_ABOVE_BOX } from "./field-parts";
22
+ import { assignRef, FieldLabelLine, LABEL_ABOVE_BOX, type FieldHelpProps } from "./field-parts";
23
23
  import { FullBleedDialog } from "./full-bleed-dialog";
24
24
  import type { RemLength } from "../hooks/use-breakpoint";
25
25
  import type { MiniCalendarProps } from "./mini-calendar";
@@ -34,7 +34,7 @@ import { Popover } from "./popover";
34
34
  * `FormEvent`, while both pickers answer with ISO date strings. Each declares its own
35
35
  * (date-picker.tsx, date-range-picker.tsx).
36
36
  */
37
- export interface DatePickerBaseProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
37
+ export interface DatePickerBaseProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange">, FieldHelpProps {
38
38
  /** BCP 47 tag for the trigger's date and the calendar. Falls back to
39
39
  * `<UiKitProvider locale>`, then to the runtime's default. */
40
40
  locale?: string;
@@ -170,6 +170,7 @@ export function DatePicker(props: DatePickerProps) {
170
170
  disabled,
171
171
  invalid,
172
172
  hint,
173
+ help,
173
174
  error,
174
175
  // Named so it reaches the field (and so its trigger) when a step/today row is the
175
176
  // root and takes the rest of the caller's props.
@@ -185,6 +186,8 @@ export function DatePicker(props: DatePickerProps) {
185
186
  // (the element a screen reader is on) after any description of the caller's own.
186
187
  const messages = useFieldMessages({
187
188
  hint,
189
+ help,
190
+ name: label ?? rest["aria-label"],
188
191
  error,
189
192
  invalid,
190
193
  describedBy: rest["aria-describedby"],
@@ -394,6 +394,7 @@ export function DateRangePicker(props: DateRangePickerProps) {
394
394
  disabled,
395
395
  invalid,
396
396
  hint,
397
+ help,
397
398
  error,
398
399
  // The trigger's (API-3).
399
400
  ref,
@@ -404,6 +405,8 @@ export function DateRangePicker(props: DateRangePickerProps) {
404
405
  const render = useTriggerFormat("dateRangePicker", locale, formatOptions, formatValue);
405
406
  const messages = useFieldMessages({
406
407
  hint,
408
+ help,
409
+ name: label ?? rest["aria-label"],
407
410
  error,
408
411
  invalid,
409
412
  describedBy: rest["aria-describedby"],
@@ -25,6 +25,7 @@ import {
25
25
  useFieldHint,
26
26
  useLockReason,
27
27
  type DisabledReasonDisplay,
28
+ type FieldHelpProps,
28
29
  } from "./field-parts";
29
30
  import { useCommitReason, type CommitScope } from "./write-lock";
30
31
 
@@ -33,7 +34,8 @@ export type { ComboClearValue, ComboOption } from "./combobox-core";
33
34
  /** `onChange` is the kit's — "a selection was made", carrying values — rather
34
35
  * than the div's form event, so the DOM's spelling of it is omitted. */
35
36
  export interface EntityComboboxProps<V extends string | number, C extends ComboClearValue = null>
36
- extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
37
+ extends Omit<ComponentPropsWithoutRef<"div">, "onChange">,
38
+ FieldHelpProps {
37
39
  /** Selected id; `null`, `undefined` or `clearValue` when nothing is selected. */
38
40
  value: V | C | null | undefined;
39
41
  /** Selecting an option emits its value; the clear button emits `clearValue`. */
@@ -182,6 +184,7 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
182
184
  invalid,
183
185
  error,
184
186
  hint,
187
+ help,
185
188
  filter,
186
189
  minChars,
187
190
  debounceMs,
@@ -206,7 +209,7 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
206
209
  minChars,
207
210
  debounceMs,
208
211
  });
209
- const hintParts = useFieldHint(hint, ariaDescribedBy);
212
+ const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });
210
213
  const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);
211
214
  // The props are the per-instance overrides, the provider the app-wide ones; a
212
215
  // prop left `undefined` falls through to the provider rather than masking it.