@eifi1/ui-kit 0.34.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 (446) hide show
  1. package/README.md +115 -41
  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/billing/billing-banner.d.ts +5 -2
  6. package/dist/billing/billing-banner.js.map +1 -1
  7. package/dist/billing/plan-limit-notice.d.ts +5 -2
  8. package/dist/billing/plan-limit-notice.js.map +1 -1
  9. package/dist/chart.js.map +1 -1
  10. package/dist/components/amount-input.d.ts +2 -5
  11. package/dist/components/amount-input.js +2 -2
  12. package/dist/components/amount-input.js.map +1 -1
  13. package/dist/components/autocomplete.d.ts +2 -2
  14. package/dist/components/autocomplete.js +2 -1
  15. package/dist/components/autocomplete.js.map +1 -1
  16. package/dist/components/button-look.d.ts +1 -1
  17. package/dist/components/button-look.js +9 -4
  18. package/dist/components/button-look.js.map +1 -1
  19. package/dist/components/calculator.d.ts +1 -4
  20. package/dist/components/calculator.js +1 -3
  21. package/dist/components/calculator.js.map +1 -1
  22. package/dist/components/chart-zoom.d.ts +0 -37
  23. package/dist/components/chart-zoom.js +1 -28
  24. package/dist/components/chart-zoom.js.map +1 -1
  25. package/dist/components/checkbox-group.d.ts +5 -2
  26. package/dist/components/checkbox-group.js +15 -4
  27. package/dist/components/checkbox-group.js.map +1 -1
  28. package/dist/components/chip-input.d.ts +0 -4
  29. package/dist/components/chip-input.js +1 -5
  30. package/dist/components/chip-input.js.map +1 -1
  31. package/dist/components/column-mapper.d.ts +5 -3
  32. package/dist/components/column-mapper.js +2 -0
  33. package/dist/components/column-mapper.js.map +1 -1
  34. package/dist/components/combobox.d.ts +3 -3
  35. package/dist/components/combobox.js +2 -1
  36. package/dist/components/combobox.js.map +1 -1
  37. package/dist/components/copy-button.d.ts +5 -1
  38. package/dist/components/copy-button.js.map +1 -1
  39. package/dist/components/country-select.d.ts +3 -3
  40. package/dist/components/country-select.js +3 -0
  41. package/dist/components/country-select.js.map +1 -1
  42. package/dist/components/currency-option-list.d.ts +3 -8
  43. package/dist/components/currency-option-list.js +1 -29
  44. package/dist/components/currency-option-list.js.map +1 -1
  45. package/dist/components/currency-select.d.ts +3 -2
  46. package/dist/components/currency-select.js +10 -1
  47. package/dist/components/currency-select.js.map +1 -1
  48. package/dist/components/danger-confirm.d.ts +3 -3
  49. package/dist/components/danger-confirm.js.map +1 -1
  50. package/dist/components/data-table-filter-popover.d.ts +1 -1
  51. package/dist/components/data-table-filter-popover.js +1 -1
  52. package/dist/components/data-table-filter-popover.js.map +1 -1
  53. package/dist/components/data-table-labels.d.ts +1 -42
  54. package/dist/components/data-table-labels.js +2 -31
  55. package/dist/components/data-table-labels.js.map +1 -1
  56. package/dist/components/data-table-shared.d.ts +17 -1
  57. package/dist/components/data-table-shared.js +22 -0
  58. package/dist/components/data-table-shared.js.map +1 -1
  59. package/dist/components/data-table.d.ts +12 -2
  60. package/dist/components/data-table.js +18 -14
  61. package/dist/components/data-table.js.map +1 -1
  62. package/dist/components/date-field.d.ts +2 -1
  63. package/dist/components/date-field.js.map +1 -1
  64. package/dist/components/date-picker.js +3 -0
  65. package/dist/components/date-picker.js.map +1 -1
  66. package/dist/components/date-range-picker.js +3 -0
  67. package/dist/components/date-range-picker.js.map +1 -1
  68. package/dist/components/dialog-frame.d.ts +3 -1
  69. package/dist/components/dialog-frame.js.map +1 -1
  70. package/dist/components/dropdown.d.ts +18 -15
  71. package/dist/components/dropdown.js +11 -3
  72. package/dist/components/dropdown.js.map +1 -1
  73. package/dist/components/entity-combobox.d.ts +3 -3
  74. package/dist/components/entity-combobox.js +2 -1
  75. package/dist/components/entity-combobox.js.map +1 -1
  76. package/dist/components/field-base.d.ts +0 -5
  77. package/dist/components/field-base.js +0 -5
  78. package/dist/components/field-base.js.map +1 -1
  79. package/dist/components/field-parts.d.ts +69 -22
  80. package/dist/components/field-parts.js +33 -19
  81. package/dist/components/field-parts.js.map +1 -1
  82. package/dist/components/field-strip.d.ts +9 -23
  83. package/dist/components/field-strip.js +26 -23
  84. package/dist/components/field-strip.js.map +1 -1
  85. package/dist/components/field-sync.d.ts +0 -4
  86. package/dist/components/field-sync.js +0 -4
  87. package/dist/components/field-sync.js.map +1 -1
  88. package/dist/components/field.d.ts +7 -6
  89. package/dist/components/field.js +24 -15
  90. package/dist/components/field.js.map +1 -1
  91. package/dist/components/full-bleed-dialog.js +2 -2
  92. package/dist/components/full-bleed-dialog.js.map +1 -1
  93. package/dist/components/help-popover.d.ts +9 -7
  94. package/dist/components/help-popover.js.map +1 -1
  95. package/dist/components/hover-menu.d.ts +6 -13
  96. package/dist/components/hover-menu.js +3 -3
  97. package/dist/components/hover-menu.js.map +1 -1
  98. package/dist/components/icon-picker.d.ts +3 -2
  99. package/dist/components/icon-picker.js +3 -0
  100. package/dist/components/icon-picker.js.map +1 -1
  101. package/dist/components/inline-entity-combobox.d.ts +3 -3
  102. package/dist/components/inline-entity-combobox.js +2 -1
  103. package/dist/components/inline-entity-combobox.js.map +1 -1
  104. package/dist/components/input.d.ts +2 -2
  105. package/dist/components/input.js +2 -1
  106. package/dist/components/input.js.map +1 -1
  107. package/dist/components/modal.d.ts +9 -1
  108. package/dist/components/modal.js +7 -4
  109. package/dist/components/modal.js.map +1 -1
  110. package/dist/components/month-picker.d.ts +2 -1
  111. package/dist/components/month-picker.js +8 -1
  112. package/dist/components/month-picker.js.map +1 -1
  113. package/dist/components/multi-entity-combobox.d.ts +2 -2
  114. package/dist/components/multi-entity-combobox.js +2 -1
  115. package/dist/components/multi-entity-combobox.js.map +1 -1
  116. package/dist/components/multi-select.d.ts +7 -18
  117. package/dist/components/multi-select.js +8 -10
  118. package/dist/components/multi-select.js.map +1 -1
  119. package/dist/components/number-input.d.ts +2 -5
  120. package/dist/components/number-input.js +2 -4
  121. package/dist/components/number-input.js.map +1 -1
  122. package/dist/components/numeric-field.d.ts +5 -2
  123. package/dist/components/numeric-field.js +4 -1
  124. package/dist/components/numeric-field.js.map +1 -1
  125. package/dist/components/one-time-code-input.d.ts +2 -1
  126. package/dist/components/one-time-code-input.js +2 -1
  127. package/dist/components/one-time-code-input.js.map +1 -1
  128. package/dist/components/phone-input.d.ts +1 -1
  129. package/dist/components/phone-input.js +4 -0
  130. package/dist/components/phone-input.js.map +1 -1
  131. package/dist/components/popover.js +3 -3
  132. package/dist/components/popover.js.map +1 -1
  133. package/dist/components/select.d.ts +4 -3
  134. package/dist/components/select.js +2 -1
  135. package/dist/components/select.js.map +1 -1
  136. package/dist/components/series-chart.js.map +1 -1
  137. package/dist/components/server-wake.d.ts +0 -6
  138. package/dist/components/server-wake.js.map +1 -1
  139. package/dist/components/settings-fields.d.ts +4 -2
  140. package/dist/components/settings-fields.js.map +1 -1
  141. package/dist/components/slider.d.ts +3 -2
  142. package/dist/components/slider.js +7 -1
  143. package/dist/components/slider.js.map +1 -1
  144. package/dist/components/stat-tile.d.ts +8 -2
  145. package/dist/components/stat-tile.js +2 -11
  146. package/dist/components/stat-tile.js.map +1 -1
  147. package/dist/components/swatch-picker.d.ts +3 -2
  148. package/dist/components/swatch-picker.js +3 -0
  149. package/dist/components/swatch-picker.js.map +1 -1
  150. package/dist/components/swipeable-row.d.ts +2 -17
  151. package/dist/components/swipeable-row.js +11 -16
  152. package/dist/components/swipeable-row.js.map +1 -1
  153. package/dist/components/text-link.js +3 -1
  154. package/dist/components/text-link.js.map +1 -1
  155. package/dist/components/textarea.d.ts +2 -1
  156. package/dist/components/textarea.js +9 -3
  157. package/dist/components/textarea.js.map +1 -1
  158. package/dist/components/toggle-group.d.ts +2 -2
  159. package/dist/components/toggle-group.js +7 -3
  160. package/dist/components/toggle-group.js.map +1 -1
  161. package/dist/components/tooltip.d.ts +9 -1
  162. package/dist/components/tooltip.js +20 -5
  163. package/dist/components/tooltip.js.map +1 -1
  164. package/dist/components/translation-review.d.ts +10 -2
  165. package/dist/components/translation-review.js +11 -3
  166. package/dist/components/translation-review.js.map +1 -1
  167. package/dist/components/use-table-state.d.ts +31 -3
  168. package/dist/components/use-table-state.js +53 -8
  169. package/dist/components/use-table-state.js.map +1 -1
  170. package/dist/components/user-avatar.d.ts +2 -2
  171. package/dist/components/user-avatar.js.map +1 -1
  172. package/dist/components/wizard-stepper.d.ts +1 -4
  173. package/dist/components/wizard-stepper.js +2 -3
  174. package/dist/components/wizard-stepper.js.map +1 -1
  175. package/dist/currencies.d.ts +23 -0
  176. package/dist/currencies.js +38 -0
  177. package/dist/currencies.js.map +1 -0
  178. package/dist/demo/demo-banner.d.ts +7 -2
  179. package/dist/demo/demo-banner.js.map +1 -1
  180. package/dist/eslint/index.d.ts +83 -0
  181. package/dist/eslint/index.js +69 -0
  182. package/dist/eslint/index.js.map +1 -0
  183. package/dist/eslint/role-utilities.d.ts +48 -0
  184. package/dist/eslint/role-utilities.generated.d.ts +2 -0
  185. package/dist/eslint/role-utilities.generated.js +143 -0
  186. package/dist/eslint/role-utilities.generated.js.map +1 -0
  187. package/dist/eslint/role-utilities.js +43 -0
  188. package/dist/eslint/role-utilities.js.map +1 -0
  189. package/dist/feedback/feedback-menu.d.ts +2 -2
  190. package/dist/feedback/feedback-menu.js.map +1 -1
  191. package/dist/feedback/feedback-thread.d.ts +3 -8
  192. package/dist/feedback/feedback-thread.js +0 -2
  193. package/dist/feedback/feedback-thread.js.map +1 -1
  194. package/dist/hooks/use-dismiss.d.ts +16 -3
  195. package/dist/hooks/use-dismiss.js +14 -2
  196. package/dist/hooks/use-dismiss.js.map +1 -1
  197. package/dist/i18n/defaults.js +5 -1
  198. package/dist/i18n/defaults.js.map +1 -1
  199. package/dist/i18n/german.js +97 -1
  200. package/dist/i18n/german.js.map +1 -1
  201. package/dist/i18n/kit-labels.d.ts +29 -4
  202. package/dist/i18n/kit-labels.js +21 -1
  203. package/dist/i18n/kit-labels.js.map +1 -1
  204. package/dist/i18n/locales/en.js +104 -3
  205. package/dist/i18n/locales/en.js.map +3 -3
  206. package/dist/i18n/locales/es.js +95 -1
  207. package/dist/i18n/locales/es.js.map +1 -1
  208. package/dist/i18n/locales/fr.js +99 -3
  209. package/dist/i18n/locales/fr.js.map +1 -1
  210. package/dist/i18n/locales/hu.js +95 -1
  211. package/dist/i18n/locales/hu.js.map +1 -1
  212. package/dist/i18n/locales/it.js +99 -3
  213. package/dist/i18n/locales/it.js.map +1 -1
  214. package/dist/i18n/locales/zh.js +95 -1
  215. package/dist/i18n/locales/zh.js.map +1 -1
  216. package/dist/i18n/review.js +13 -0
  217. package/dist/i18n/review.js.map +1 -1
  218. package/dist/i18n/shared-labels.js +3 -1
  219. package/dist/i18n/shared-labels.js.map +1 -1
  220. package/dist/index.d.ts +23 -1
  221. package/dist/index.js +40 -2
  222. package/dist/index.js.map +1 -1
  223. package/dist/lib/device-settings.d.ts +72 -0
  224. package/dist/lib/device-settings.js +64 -0
  225. package/dist/lib/device-settings.js.map +1 -0
  226. package/dist/lib/plain-text.d.ts +11 -0
  227. package/dist/lib/plain-text.js +16 -0
  228. package/dist/lib/plain-text.js.map +1 -0
  229. package/dist/lib/refs.d.ts +13 -0
  230. package/dist/lib/refs.js +15 -0
  231. package/dist/lib/refs.js.map +1 -0
  232. package/dist/lib/safe-storage.d.ts +6 -0
  233. package/dist/lib/safe-storage.js +11 -0
  234. package/dist/lib/safe-storage.js.map +1 -1
  235. package/dist/notifications/notification-parts.d.ts +39 -0
  236. package/dist/notifications/notification-parts.js +47 -0
  237. package/dist/notifications/notification-parts.js.map +1 -0
  238. package/dist/notifications/notification-types-setting.d.ts +62 -0
  239. package/dist/notifications/notification-types-setting.js +192 -0
  240. package/dist/notifications/notification-types-setting.js.map +1 -0
  241. package/dist/notifications/notification-types.d.ts +75 -0
  242. package/dist/notifications/notification-types.js +2 -0
  243. package/dist/notifications/notification-types.js.map +1 -0
  244. package/dist/notifications/notifications-labels.d.ts +131 -0
  245. package/dist/notifications/notifications-labels.js +83 -0
  246. package/dist/notifications/notifications-labels.js.map +1 -0
  247. package/dist/notifications/push-device-setting.d.ts +37 -0
  248. package/dist/notifications/push-device-setting.js +164 -0
  249. package/dist/notifications/push-device-setting.js.map +1 -0
  250. package/dist/notifications/push-devices-setting.d.ts +44 -0
  251. package/dist/notifications/push-devices-setting.js +203 -0
  252. package/dist/notifications/push-devices-setting.js.map +1 -0
  253. package/dist/notifications/quiet-hours-setting.d.ts +47 -0
  254. package/dist/notifications/quiet-hours-setting.js +131 -0
  255. package/dist/notifications/quiet-hours-setting.js.map +1 -0
  256. package/dist/notifications/time-zone-select.d.ts +50 -0
  257. package/dist/notifications/time-zone-select.js +76 -0
  258. package/dist/notifications/time-zone-select.js.map +1 -0
  259. package/dist/notifications/unsubscribe-panel.d.ts +53 -0
  260. package/dist/notifications/unsubscribe-panel.js +139 -0
  261. package/dist/notifications/unsubscribe-panel.js.map +1 -0
  262. package/dist/notifications/use-push-device.d.ts +122 -0
  263. package/dist/notifications/use-push-device.js +230 -0
  264. package/dist/notifications/use-push-device.js.map +1 -0
  265. package/dist/rhf/fields.d.ts +36 -29
  266. package/dist/rhf/fields.js +51 -22
  267. package/dist/rhf/fields.js.map +1 -1
  268. package/dist/rhf/form-core.d.ts +5 -4
  269. package/dist/rhf/form-core.js.map +1 -1
  270. package/dist/rhf/form.d.ts +7 -36
  271. package/dist/rhf/form.js +1 -16
  272. package/dist/rhf/form.js.map +1 -1
  273. package/dist/rhf/line-items.d.ts +1 -1
  274. package/dist/rhf/line-items.js.map +1 -1
  275. package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
  276. package/dist/rhf.js.map +1 -1
  277. package/dist/settings/device-reset-setting-labels.d.ts +3 -0
  278. package/dist/settings/device-reset-setting-labels.js +15 -0
  279. package/dist/settings/device-reset-setting-labels.js.map +1 -0
  280. package/dist/settings/device-reset-setting.d.ts +58 -0
  281. package/dist/settings/device-reset-setting.js +51 -0
  282. package/dist/settings/device-reset-setting.js.map +1 -0
  283. package/dist/shell/app-shell.js +5 -0
  284. package/dist/shell/app-shell.js.map +1 -1
  285. package/dist/shell/option-switcher-menu.d.ts +5 -17
  286. package/dist/shell/option-switcher-menu.js +1 -3
  287. package/dist/shell/option-switcher-menu.js.map +1 -1
  288. package/dist/shell/role-switcher.d.ts +1 -4
  289. package/dist/shell/role-switcher.js +2 -3
  290. package/dist/shell/role-switcher.js.map +1 -1
  291. package/dist/shell/topbar-action-menu.d.ts +7 -19
  292. package/dist/shell/topbar-action-menu.js +1 -3
  293. package/dist/shell/topbar-action-menu.js.map +1 -1
  294. package/dist/shell/topbar-controls.d.ts +3 -12
  295. package/dist/shell/topbar-controls.js +6 -9
  296. package/dist/shell/topbar-controls.js.map +1 -1
  297. package/dist/testing/index.d.ts +1 -1
  298. package/dist/testing/index.js +2 -0
  299. package/dist/testing/index.js.map +1 -1
  300. package/dist/testing/viewport.d.ts +25 -2
  301. package/dist/testing/viewport.js +61 -10
  302. package/dist/testing/viewport.js.map +1 -1
  303. package/dist/theme/contrast-tokens.d.ts +42 -4
  304. package/dist/theme/contrast-tokens.js +56 -9
  305. package/dist/theme/contrast-tokens.js.map +1 -1
  306. package/dist/theme/contrast.d.ts +4 -2
  307. package/dist/theme/contrast.js +13 -0
  308. package/dist/theme/contrast.js.map +1 -1
  309. package/dist/theme/palette-derive.d.ts +15 -1
  310. package/dist/theme/palette-derive.js +37 -8
  311. package/dist/theme/palette-derive.js.map +1 -1
  312. package/dist/theme/palette-presets.d.ts +2 -8
  313. package/dist/theme/palette-presets.js +5 -284
  314. package/dist/theme/palette-presets.js.map +1 -1
  315. package/dist/theme/palette-store.js +6 -0
  316. package/dist/theme/palette-store.js.map +1 -1
  317. package/dist/theme/text-size.js +6 -0
  318. package/dist/theme/text-size.js.map +1 -1
  319. package/dist/theme/theme-store.js +6 -0
  320. package/dist/theme/theme-store.js.map +1 -1
  321. package/dist/wizard/types.d.ts +0 -5
  322. package/dist/wizard/types.js +1 -5
  323. package/dist/wizard/types.js.map +1 -1
  324. package/package.json +9 -1
  325. package/src/admin/role-select.tsx +4 -1
  326. package/src/billing/billing-banner.tsx +5 -2
  327. package/src/billing/plan-limit-notice.tsx +5 -2
  328. package/src/chart.ts +5 -3
  329. package/src/components/amount-input.tsx +4 -7
  330. package/src/components/autocomplete.tsx +5 -2
  331. package/src/components/button-look.ts +15 -5
  332. package/src/components/calculator.tsx +2 -7
  333. package/src/components/chart-zoom.tsx +4 -47
  334. package/src/components/checkbox-group.tsx +22 -4
  335. package/src/components/chip-input.tsx +0 -7
  336. package/src/components/column-mapper.tsx +6 -2
  337. package/src/components/combobox.tsx +6 -3
  338. package/src/components/copy-button.tsx +5 -1
  339. package/src/components/country-select.tsx +6 -1
  340. package/src/components/currency-option-list.tsx +8 -42
  341. package/src/components/currency-select.tsx +12 -3
  342. package/src/components/danger-confirm.tsx +3 -3
  343. package/src/components/data-table-filter-popover.tsx +2 -1
  344. package/src/components/data-table-labels.ts +2 -84
  345. package/src/components/data-table-shared.tsx +55 -2
  346. package/src/components/data-table.tsx +29 -17
  347. package/src/components/date-field.tsx +2 -2
  348. package/src/components/date-picker.tsx +3 -0
  349. package/src/components/date-range-picker.tsx +3 -0
  350. package/src/components/dialog-frame.tsx +3 -1
  351. package/src/components/dropdown.tsx +42 -18
  352. package/src/components/entity-combobox.tsx +6 -3
  353. package/src/components/field-base.tsx +0 -11
  354. package/src/components/field-parts.tsx +112 -38
  355. package/src/components/field-strip.tsx +12 -27
  356. package/src/components/field-sync.tsx +0 -7
  357. package/src/components/field.tsx +32 -19
  358. package/src/components/full-bleed-dialog.tsx +4 -3
  359. package/src/components/help-popover.tsx +9 -7
  360. package/src/components/hover-menu.tsx +12 -16
  361. package/src/components/icon-picker.tsx +8 -3
  362. package/src/components/inline-entity-combobox.tsx +6 -3
  363. package/src/components/input.tsx +4 -2
  364. package/src/components/modal.tsx +20 -4
  365. package/src/components/month-picker.tsx +10 -3
  366. package/src/components/multi-entity-combobox.tsx +5 -2
  367. package/src/components/multi-select.tsx +19 -31
  368. package/src/components/number-input.tsx +5 -10
  369. package/src/components/numeric-field.ts +10 -2
  370. package/src/components/one-time-code-input.tsx +5 -3
  371. package/src/components/phone-input.tsx +4 -0
  372. package/src/components/popover.tsx +8 -4
  373. package/src/components/select.tsx +6 -3
  374. package/src/components/series-chart.tsx +1 -1
  375. package/src/components/server-wake.tsx +0 -6
  376. package/src/components/settings-fields.tsx +4 -2
  377. package/src/components/slider.tsx +10 -3
  378. package/src/components/stat-tile.tsx +11 -20
  379. package/src/components/swatch-picker.tsx +9 -4
  380. package/src/components/swipeable-row.tsx +15 -37
  381. package/src/components/text-link.tsx +3 -1
  382. package/src/components/textarea.tsx +11 -4
  383. package/src/components/toggle-group.tsx +21 -5
  384. package/src/components/tooltip.tsx +37 -6
  385. package/src/components/translation-review.tsx +25 -7
  386. package/src/components/use-table-state.ts +91 -10
  387. package/src/components/user-avatar.tsx +2 -2
  388. package/src/components/wizard-stepper.tsx +2 -6
  389. package/src/currencies.ts +70 -0
  390. package/src/demo/demo-banner.tsx +7 -2
  391. package/src/eslint/index.ts +161 -0
  392. package/src/eslint/role-utilities.generated.ts +146 -0
  393. package/src/eslint/role-utilities.ts +99 -0
  394. package/src/feedback/feedback-menu.tsx +2 -2
  395. package/src/feedback/feedback-thread.tsx +3 -8
  396. package/src/hooks/use-dismiss.ts +39 -4
  397. package/src/i18n/defaults.ts +4 -0
  398. package/src/i18n/german.ts +113 -0
  399. package/src/i18n/kit-labels.tsx +66 -6
  400. package/src/i18n/locales/en.ts +6 -0
  401. package/src/i18n/locales/es.ts +106 -0
  402. package/src/i18n/locales/fr.ts +109 -2
  403. package/src/i18n/locales/hu.ts +104 -0
  404. package/src/i18n/locales/it.ts +108 -2
  405. package/src/i18n/locales/zh.ts +96 -0
  406. package/src/i18n/review.ts +13 -0
  407. package/src/i18n/shared-labels.ts +2 -0
  408. package/src/index.ts +53 -1
  409. package/src/lib/device-settings.ts +146 -0
  410. package/src/lib/plain-text.ts +22 -0
  411. package/src/lib/refs.ts +22 -0
  412. package/src/lib/safe-storage.ts +16 -0
  413. package/src/notifications/notification-parts.tsx +83 -0
  414. package/src/notifications/notification-types-setting.tsx +309 -0
  415. package/src/notifications/notification-types.ts +80 -0
  416. package/src/notifications/notifications-labels.ts +238 -0
  417. package/src/notifications/push-device-setting.tsx +224 -0
  418. package/src/notifications/push-devices-setting.tsx +306 -0
  419. package/src/notifications/quiet-hours-setting.tsx +185 -0
  420. package/src/notifications/time-zone-select.tsx +126 -0
  421. package/src/notifications/unsubscribe-panel.tsx +203 -0
  422. package/src/notifications/use-push-device.ts +392 -0
  423. package/src/rhf/fields.tsx +87 -39
  424. package/src/rhf/form-core.tsx +5 -4
  425. package/src/rhf/form.tsx +7 -43
  426. package/src/rhf/line-items.tsx +1 -1
  427. package/src/rhf/use-rhf-wizard-step.ts +4 -4
  428. package/src/rhf.ts +3 -2
  429. package/src/settings/device-reset-setting-labels.ts +20 -0
  430. package/src/settings/device-reset-setting.tsx +119 -0
  431. package/src/shell/app-shell.tsx +8 -0
  432. package/src/shell/option-switcher-menu.tsx +5 -22
  433. package/src/shell/role-switcher.tsx +2 -6
  434. package/src/shell/topbar-action-menu.tsx +7 -24
  435. package/src/shell/topbar-controls.tsx +6 -18
  436. package/src/testing/index.ts +3 -1
  437. package/src/testing/viewport.ts +125 -19
  438. package/src/theme/contrast-tokens.ts +106 -12
  439. package/src/theme/contrast.ts +30 -4
  440. package/src/theme/palette-derive.ts +68 -7
  441. package/src/theme/palette-presets.ts +16 -299
  442. package/src/theme/palette-store.ts +12 -0
  443. package/src/theme/text-size.ts +8 -0
  444. package/src/theme/theme-store.ts +10 -0
  445. package/src/wizard/types.ts +0 -8
  446. package/tokens.css +38 -4
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/phone-input.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ChangeEvent, FocusEvent } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n PHONE_COUNTRIES,\n PHONE_DIAL_CODES,\n formatNationalPhone,\n formatPhone,\n isE164,\n parsePhone,\n} from \"../lib/phone\";\nimport type { ParsedPhone, PhoneCountry, PhoneCountryCode } from \"../lib/phone\";\nimport { useFieldMessages } from \"./field-parts\";\nimport { Input, type InputProps } from \"./input\";\nimport { Select } from \"./select\";\nimport { DEFAULT_PHONE_INPUT_LABELS } from \"./phone-input-labels\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words {@link PhoneInput} renders on its own behalf. */\nexport interface PhoneInputLabels {\n /** The country-code select's accessible name. */\n countryCode: string;\n /** The select's last option: a number from a country not listed, typed with its\n * own \"+\". */\n other: string;\n}\n\n// Defined in ./phone-input-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_PHONE_INPUT_LABELS };\n\n/**\n * The `phoneInput` namespace: prop > `<UiKitProvider labels>` > English (0.22.0).\n */\nfunction usePhoneInputLabels(prop: Partial<PhoneInputLabels> | undefined): PhoneInputLabels {\n const labels: PhoneInputLabels = useKitLabels(\"phoneInput\", DEFAULT_PHONE_INPUT_LABELS, prop);\n return labels;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\n/** A stored value read back: an E.164 number these rules can read, else `null`. */\nfunction storedNumber(value: string): ParsedPhone | null {\n return isE164(value) ? parsePhone(value, \"other\") : null;\n}\n\n/** The select's country for a stored E.164 value, or `null` for free text. */\nfunction countryOf(value: string): PhoneCountry | null {\n const parsed = storedNumber(value);\n return parsed ? (parsed.country ?? \"other\") : null;\n}\n\n/** What the field emits for typed text: E.164 when it reads, the text unchanged when\n * it does not. */\nfunction emitFor(raw: string, country: PhoneCountry): string {\n return parsePhone(raw, country)?.e164 ?? raw;\n}\n\n/**\n * The text a stored value shows, outside of typing. Under its own country: the national\n * number grouped (\"21 000 00 01\" beside \"CH +41\"). Under another (a \"+33\" number while\n * the select says \"Other\"): the whole international number. Free text: as it is.\n */\nfunction displayOf(value: string, country: PhoneCountry): string {\n const parsed = storedNumber(value);\n if (!parsed) return value;\n if (parsed.country !== null && parsed.country === country) {\n return formatNationalPhone(parsed.national, parsed.country);\n }\n return formatPhone(parsed.e164);\n}\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport interface PhoneInputProps extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\"> {\n /** The number: E.164 (\"+41210000001\") for one the field could read, or free text —\n * a value saved before the field existed is shown exactly as it is. `\"\"` is no\n * number. */\n value: string;\n /** On every change: E.164 when the typed text reads as a number of the selected\n * country (or carries its own \"+\"), otherwise the typed text UNCHANGED. Nothing is\n * refused — see {@link PhoneInput}. Plain `onChange` still fires with the raw event;\n * bind this one to a form. */\n onValueChange?: (value: string) => void;\n /** The country a number typed without \"+\" belongs to, until the value says\n * otherwise. Default `\"CH\"`. */\n defaultCountry?: PhoneCountry;\n /** The countries the select offers, in order; \"Other\" always follows. Default\n * {@link PHONE_COUNTRIES} (CH, LI, DE, AT, FR, IT). A stored number from a country\n * left out still shows under its own code. */\n countries?: readonly PhoneCountryCode[];\n /** See {@link PhoneInputLabels}. */\n labels?: LabelOverride<PhoneInputLabels>;\n /** Classes for the whole field — the select, the input and the lines under them.\n * `inputClassName` reaches the `<input>`. */\n className?: string;\n}\n\n/**\n * A phone number field (kastlan): a country-code select and the number, stored as\n * E.164 and shown grouped.\n *\n * kastlan's contacts kept whatever was typed — \"079 …\", \"+41 (0)79 …\", \"0041 79 …\" —\n * which nobody could dial from the app or match against the next import. This field\n * turns all of those into one stored form, \"+41210000001\", without taking anything\n * away from the person typing:\n *\n * - **A country to type against.** The select (default CH, then LI, DE, AT, FR, IT\n * and \"Other\") says which country a number without \"+\" belongs to; the national\n * trunk 0 may be typed or left out. Typing a \"+\" or \"00\" overrides it, and the\n * select follows the number (\"+49 …\" switches it to DE). Changing the select moves\n * the national number to the new country code.\n * - **E.164 when it reads, the text when it does not.** A number the rules read is\n * emitted as E.164; anything else (\"ask reception\", an extension) is emitted exactly\n * as typed. Never refused, never an error of its own: a contact's number is often\n * known only roughly, and a field that will not save \"079 … (mornings)\" makes\n * people type it into the notes instead. An app that needs a dialable number\n * checks {@link isE164} on submit.\n * - **Existing values untouched.** A free-text value from before is shown exactly as\n * it is, and normalised only when someone edits it — the field never rewrites a\n * record just by being opened.\n * - **Grouped when settled.** While typing the field shows what was typed; on blur a\n * number it read is regrouped by its country (\"21 000 00 01\").\n *\n * **The limitation** — there is no libphonenumber behind this (≈150 kB of metadata in\n * every bundle): the six countries have light rules (the trunk 0, a plausible length)\n * and every other country is any \"+\" with 8–15 digits. A well-formed number that does\n * not exist passes, and German and Austrian landlines, whose area codes vary in length,\n * are grouped only where the split is known (mobile prefixes, the largest cities) and\n * otherwise shown as one block. See `lib/phone.ts`.\n *\n * `label`, `hint`, `error` and `invalid` read as on {@link Input}: the label floats in\n * the number's box, and a text hint and the error go under the whole row, attached to\n * the number with `aria-describedby`. The select is named by `labels.countryCode`, and\n * the pair is a group named by a string `label`.\n */\nexport function PhoneInput({\n ref,\n value,\n onValueChange,\n onChange,\n onBlur,\n defaultCountry = \"CH\",\n countries = PHONE_COUNTRIES,\n labels,\n className,\n inputClassName,\n label,\n hint,\n error,\n invalid,\n disabled,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: PhoneInputProps) {\n const text = usePhoneInputLabels(labels);\n const [country, setCountry] = useState<PhoneCountry>(() => countryOf(value) ?? defaultCountry);\n // What was typed, shown for as long as it still reads as the value (AmountInput's\n // draft): the person's own spacing stays under their fingers, and is regrouped on\n // blur.\n const [draft, setDraft] = useState<string | null>(null);\n // The last value this field emitted, to tell its own echo from a value set outside.\n const [emitted, setEmitted] = useState<string | null>(null);\n const [seen, setSeen] = useState(value);\n if (seen !== value) {\n setSeen(value);\n if (value !== emitted) {\n // Set from outside (a reset, another record): its own country, none of our text.\n const own = countryOf(value);\n if (own) setCountry(own);\n setDraft(null);\n }\n }\n\n const emit = (next: string) => {\n setEmitted(next);\n onValueChange?.(next);\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n const raw = e.target.value;\n setDraft(raw);\n const parsed = parsePhone(raw, country);\n // A number typed with its own \"+\" takes the select with it.\n if (parsed && parsed.country !== country) setCountry(parsed.country ?? \"other\");\n emit(parsed?.e164 ?? raw);\n };\n\n const handleCountry = (next: PhoneCountry) => {\n setCountry(next);\n const stored = storedNumber(value);\n if (stored) {\n // \"Other\", the same country, or a generic number with its own \"+\": the number\n // stays, only the way it is shown changes.\n if (next === \"other\" || stored.country === null || stored.country === next) {\n setDraft(null);\n return;\n }\n // A different listed country: the national number moves to its code. If it does\n // not read there, the digits are kept as text for the person to finish.\n emit(parsePhone(stored.national, next)?.e164 ?? stored.national);\n setDraft(null);\n return;\n }\n // Free text (or a draft): read it again under the new country.\n const source = draft ?? value;\n const parsed = source.trim() === \"\" ? null : parsePhone(source, next);\n if (parsed && parsed.e164 !== value) {\n setDraft(source);\n emit(parsed.e164);\n }\n };\n\n const display = draft !== null && emitFor(draft, country) === value ? draft : displayOf(value, country);\n\n // The select offers the configured countries, plus the stored number's own if it\n // is not among them — a select cannot show a value it has no option for.\n const options: PhoneCountryCode[] = [...countries];\n if (country !== \"other\" && !options.includes(country)) options.push(country);\n\n // A FieldHint \"?\" rides the number's label line; text — and a FieldHint at Large and\n // on touch (§4) — goes under the whole row with the error. Through useFieldMessages\n // since 0.34, not a copy of its rule (DUP-2, DUP-8).\n const messages = useFieldMessages({\n hint,\n error,\n invalid,\n describedBy: ariaDescribedBy,\n ariaInvalid: rest[\"aria-invalid\"],\n });\n const named = typeof label === \"string\" && label !== \"\";\n\n return (\n <div\n className={className}\n role={named ? \"group\" : undefined}\n aria-label={named ? label : undefined}\n >\n {/* On a phone at Large the number takes a line of its own under the country (0.32.1,\n the 360 px Extra-large sweep; docs/text-size-harmonization.md §4 \"nothing\n truncates\"): the 7rem select is 168 px at 150 %, and on a 360 px phone it left\n the number 82 px — \"Ph…\" over \"021 (\". `large:max-sm:`, the scaled phone width,\n so a wider screen at Large and Normal keep the one row. */}\n <div className=\"flex items-stretch gap-2 large:max-sm:flex-wrap\">\n <Select\n aria-label={text.countryCode}\n value={country}\n disabled={disabled}\n onChange={(e) => handleCountry(e.target.value as PhoneCountry)}\n className=\"w-28 shrink-0\"\n // Stretched to the number's height: a labelled field is taller than an\n // unlabelled select, and the pair should read as one row.\n selectClassName=\"h-full tabular-nums\"\n >\n {options.map((code) => (\n <option key={code} value={code}>\n {`${code} +${PHONE_DIAL_CODES[code]}`}\n </option>\n ))}\n <option value=\"other\">{text.other}</option>\n </Select>\n {/* Its own flex item, so nothing Input wraps itself in can take the sizing. */}\n <div className=\"min-w-0 flex-1 large:max-sm:basis-full\">\n <Input\n ref={ref}\n {...rest}\n type=\"tel\"\n autoComplete={rest.autoComplete ?? \"tel\"}\n label={label}\n hint={messages.labelHint}\n invalid={messages.isInvalid}\n aria-describedby={messages.describedBy}\n disabled={disabled}\n value={display}\n onChange={handleChange}\n onBlur={(e: FocusEvent<HTMLInputElement>) => {\n setDraft(null);\n onBlur?.(e);\n }}\n inputClassName={cn(\"tabular-nums\", inputClassName)}\n />\n </div>\n </div>\n {messages.below}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/phone-input.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ChangeEvent, FocusEvent } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n PHONE_COUNTRIES,\n PHONE_DIAL_CODES,\n formatNationalPhone,\n formatPhone,\n isE164,\n parsePhone,\n} from \"../lib/phone\";\nimport type { ParsedPhone, PhoneCountry, PhoneCountryCode } from \"../lib/phone\";\nimport { useFieldMessages } from \"./field-parts\";\nimport { Input, type InputProps } from \"./input\";\nimport { Select } from \"./select\";\nimport { DEFAULT_PHONE_INPUT_LABELS } from \"./phone-input-labels\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words {@link PhoneInput} renders on its own behalf. */\nexport interface PhoneInputLabels {\n /** The country-code select's accessible name. */\n countryCode: string;\n /** The select's last option: a number from a country not listed, typed with its\n * own \"+\". */\n other: string;\n}\n\n// Defined in ./phone-input-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_PHONE_INPUT_LABELS };\n\n/**\n * The `phoneInput` namespace: prop > `<UiKitProvider labels>` > English (0.22.0).\n */\nfunction usePhoneInputLabels(prop: Partial<PhoneInputLabels> | undefined): PhoneInputLabels {\n const labels: PhoneInputLabels = useKitLabels(\"phoneInput\", DEFAULT_PHONE_INPUT_LABELS, prop);\n return labels;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\n/** A stored value read back: an E.164 number these rules can read, else `null`. */\nfunction storedNumber(value: string): ParsedPhone | null {\n return isE164(value) ? parsePhone(value, \"other\") : null;\n}\n\n/** The select's country for a stored E.164 value, or `null` for free text. */\nfunction countryOf(value: string): PhoneCountry | null {\n const parsed = storedNumber(value);\n return parsed ? (parsed.country ?? \"other\") : null;\n}\n\n/** What the field emits for typed text: E.164 when it reads, the text unchanged when\n * it does not. */\nfunction emitFor(raw: string, country: PhoneCountry): string {\n return parsePhone(raw, country)?.e164 ?? raw;\n}\n\n/**\n * The text a stored value shows, outside of typing. Under its own country: the national\n * number grouped (\"21 000 00 01\" beside \"CH +41\"). Under another (a \"+33\" number while\n * the select says \"Other\"): the whole international number. Free text: as it is.\n */\nfunction displayOf(value: string, country: PhoneCountry): string {\n const parsed = storedNumber(value);\n if (!parsed) return value;\n if (parsed.country !== null && parsed.country === country) {\n return formatNationalPhone(parsed.national, parsed.country);\n }\n return formatPhone(parsed.e164);\n}\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport interface PhoneInputProps extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\"> {\n /** The number: E.164 (\"+41210000001\") for one the field could read, or free text —\n * a value saved before the field existed is shown exactly as it is. `\"\"` is no\n * number. */\n value: string;\n /** On every change: E.164 when the typed text reads as a number of the selected\n * country (or carries its own \"+\"), otherwise the typed text UNCHANGED. Nothing is\n * refused — see {@link PhoneInput}. Plain `onChange` still fires with the raw event;\n * bind this one to a form. */\n onValueChange?: (value: string) => void;\n /** The country a number typed without \"+\" belongs to, until the value says\n * otherwise. Default `\"CH\"`. */\n defaultCountry?: PhoneCountry;\n /** The countries the select offers, in order; \"Other\" always follows. Default\n * {@link PHONE_COUNTRIES} (CH, LI, DE, AT, FR, IT). A stored number from a country\n * left out still shows under its own code. */\n countries?: readonly PhoneCountryCode[];\n /** See {@link PhoneInputLabels}. */\n labels?: LabelOverride<PhoneInputLabels>;\n /** Classes for the whole field — the select, the input and the lines under them.\n * `inputClassName` reaches the `<input>`. */\n className?: string;\n}\n\n/**\n * A phone number field (kastlan): a country-code select and the number, stored as\n * E.164 and shown grouped.\n *\n * kastlan's contacts kept whatever was typed — \"079 …\", \"+41 (0)79 …\", \"0041 79 …\" —\n * which nobody could dial from the app or match against the next import. This field\n * turns all of those into one stored form, \"+41210000001\", without taking anything\n * away from the person typing:\n *\n * - **A country to type against.** The select (default CH, then LI, DE, AT, FR, IT\n * and \"Other\") says which country a number without \"+\" belongs to; the national\n * trunk 0 may be typed or left out. Typing a \"+\" or \"00\" overrides it, and the\n * select follows the number (\"+49 …\" switches it to DE). Changing the select moves\n * the national number to the new country code.\n * - **E.164 when it reads, the text when it does not.** A number the rules read is\n * emitted as E.164; anything else (\"ask reception\", an extension) is emitted exactly\n * as typed. Never refused, never an error of its own: a contact's number is often\n * known only roughly, and a field that will not save \"079 … (mornings)\" makes\n * people type it into the notes instead. An app that needs a dialable number\n * checks {@link isE164} on submit.\n * - **Existing values untouched.** A free-text value from before is shown exactly as\n * it is, and normalised only when someone edits it — the field never rewrites a\n * record just by being opened.\n * - **Grouped when settled.** While typing the field shows what was typed; on blur a\n * number it read is regrouped by its country (\"21 000 00 01\").\n *\n * **The limitation** — there is no libphonenumber behind this (≈150 kB of metadata in\n * every bundle): the six countries have light rules (the trunk 0, a plausible length)\n * and every other country is any \"+\" with 8–15 digits. A well-formed number that does\n * not exist passes, and German and Austrian landlines, whose area codes vary in length,\n * are grouped only where the split is known (mobile prefixes, the largest cities) and\n * otherwise shown as one block. See `lib/phone.ts`.\n *\n * `label`, `hint`, `error` and `invalid` read as on {@link Input}: the label floats in\n * the number's box, and a text hint and the error go under the whole row, attached to\n * the number with `aria-describedby`. The select is named by `labels.countryCode`, and\n * the pair is a group named by a string `label`.\n */\nexport function PhoneInput({\n ref,\n value,\n onValueChange,\n onChange,\n onBlur,\n defaultCountry = \"CH\",\n countries = PHONE_COUNTRIES,\n labels,\n className,\n inputClassName,\n label,\n hint,\n help,\n error,\n invalid,\n disabled,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: PhoneInputProps) {\n const text = usePhoneInputLabels(labels);\n const [country, setCountry] = useState<PhoneCountry>(() => countryOf(value) ?? defaultCountry);\n // What was typed, shown for as long as it still reads as the value (AmountInput's\n // draft): the person's own spacing stays under their fingers, and is regrouped on\n // blur.\n const [draft, setDraft] = useState<string | null>(null);\n // The last value this field emitted, to tell its own echo from a value set outside.\n const [emitted, setEmitted] = useState<string | null>(null);\n const [seen, setSeen] = useState(value);\n if (seen !== value) {\n setSeen(value);\n if (value !== emitted) {\n // Set from outside (a reset, another record): its own country, none of our text.\n const own = countryOf(value);\n if (own) setCountry(own);\n setDraft(null);\n }\n }\n\n const emit = (next: string) => {\n setEmitted(next);\n onValueChange?.(next);\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n const raw = e.target.value;\n setDraft(raw);\n const parsed = parsePhone(raw, country);\n // A number typed with its own \"+\" takes the select with it.\n if (parsed && parsed.country !== country) setCountry(parsed.country ?? \"other\");\n emit(parsed?.e164 ?? raw);\n };\n\n const handleCountry = (next: PhoneCountry) => {\n setCountry(next);\n const stored = storedNumber(value);\n if (stored) {\n // \"Other\", the same country, or a generic number with its own \"+\": the number\n // stays, only the way it is shown changes.\n if (next === \"other\" || stored.country === null || stored.country === next) {\n setDraft(null);\n return;\n }\n // A different listed country: the national number moves to its code. If it does\n // not read there, the digits are kept as text for the person to finish.\n emit(parsePhone(stored.national, next)?.e164 ?? stored.national);\n setDraft(null);\n return;\n }\n // Free text (or a draft): read it again under the new country.\n const source = draft ?? value;\n const parsed = source.trim() === \"\" ? null : parsePhone(source, next);\n if (parsed && parsed.e164 !== value) {\n setDraft(source);\n emit(parsed.e164);\n }\n };\n\n const display = draft !== null && emitFor(draft, country) === value ? draft : displayOf(value, country);\n\n // The select offers the configured countries, plus the stored number's own if it\n // is not among them — a select cannot show a value it has no option for.\n const options: PhoneCountryCode[] = [...countries];\n if (country !== \"other\" && !options.includes(country)) options.push(country);\n\n // A FieldHint \"?\" rides the number's label line; text — and a FieldHint at Large and\n // on touch (§4) — goes under the whole row with the error. Through useFieldMessages\n // since 0.34, not a copy of its rule (DUP-2, DUP-8).\n const messages = useFieldMessages({\n hint,\n // The number's label line takes the help's \"?\" (0.36); the hint is then the caption.\n help,\n name: label ?? rest[\"aria-label\"],\n error,\n invalid,\n describedBy: ariaDescribedBy,\n ariaInvalid: rest[\"aria-invalid\"],\n });\n const named = typeof label === \"string\" && label !== \"\";\n\n return (\n <div\n className={className}\n role={named ? \"group\" : undefined}\n aria-label={named ? label : undefined}\n >\n {/* On a phone at Large the number takes a line of its own under the country (0.32.1,\n the 360 px Extra-large sweep; docs/text-size-harmonization.md §4 \"nothing\n truncates\"): the 7rem select is 168 px at 150 %, and on a 360 px phone it left\n the number 82 px — \"Ph…\" over \"021 (\". `large:max-sm:`, the scaled phone width,\n so a wider screen at Large and Normal keep the one row. */}\n <div className=\"flex items-stretch gap-2 large:max-sm:flex-wrap\">\n <Select\n aria-label={text.countryCode}\n value={country}\n disabled={disabled}\n onChange={(e) => handleCountry(e.target.value as PhoneCountry)}\n className=\"w-28 shrink-0\"\n // Stretched to the number's height: a labelled field is taller than an\n // unlabelled select, and the pair should read as one row.\n selectClassName=\"h-full tabular-nums\"\n >\n {options.map((code) => (\n <option key={code} value={code}>\n {`${code} +${PHONE_DIAL_CODES[code]}`}\n </option>\n ))}\n <option value=\"other\">{text.other}</option>\n </Select>\n {/* Its own flex item, so nothing Input wraps itself in can take the sizing. */}\n <div className=\"min-w-0 flex-1 large:max-sm:basis-full\">\n <Input\n ref={ref}\n {...rest}\n type=\"tel\"\n autoComplete={rest.autoComplete ?? \"tel\"}\n label={label}\n hint={messages.labelHint}\n invalid={messages.isInvalid}\n aria-describedby={messages.describedBy}\n disabled={disabled}\n value={display}\n onChange={handleChange}\n onBlur={(e: FocusEvent<HTMLInputElement>) => {\n setDraft(null);\n onBlur?.(e);\n }}\n inputClassName={cn(\"tabular-nums\", inputClassName)}\n />\n </div>\n </div>\n {messages.below}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -10,7 +10,7 @@ import { cn } from "../lib/cn.js";
10
10
  import { dirOf } from "../lib/direction.js";
11
11
  import { useKitLabels } from "../i18n/kit-labels.js";
12
12
  import { DEFAULT_POPOVER_LABELS } from "./popover-labels.js";
13
- import { mergeRefs } from "./field-parts.js";
13
+ import { mergeRefs } from "../lib/refs.js";
14
14
  const POPOVER_WIDTH = "18rem";
15
15
  const EDGE = 8;
16
16
  function Popover({
@@ -50,7 +50,7 @@ function Popover({
50
50
  left: anchoredInline(rect, panelWidth, { dir, align: "end", edge: EDGE, viewportWidth: glass }).left
51
51
  } : null;
52
52
  useOutsideClick([triggerRef, panelRef], close, open);
53
- useDismissLayer(open, close);
53
+ const layer = useDismissLayer(open, close);
54
54
  useFocusTrap(panelRef, {
55
55
  active: open && pos !== null,
56
56
  // The panel keeps a focus one of its own children has already taken. `FilterPopover`
@@ -65,7 +65,7 @@ function Popover({
65
65
  }
66
66
  });
67
67
  const handleKeyDown = (e) => {
68
- runOwnKeyDown(e, onKeyDownProp);
68
+ runOwnKeyDown(e, onKeyDownProp, layer);
69
69
  takeEscape(e);
70
70
  };
71
71
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/popover.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, Ref, RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { anchoredInline, useAnchoredPanel, viewportSize } from \"../hooks/use-anchored-panel\";\nimport { runOwnKeyDown, takeEscape, useDismissLayer, useOutsideClick } from \"../hooks/use-dismiss\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { lengthPx, useRemPx, type RemLength } from \"../hooks/use-breakpoint\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf } from \"../lib/direction\";\nimport type { Direction } from \"../lib/direction\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_POPOVER_LABELS } from \"./popover-labels\";\nimport { mergeRefs } from \"./field-parts\";\n\n/** The panel's accessible name. A `role=\"dialog\"` with no name announces as \"dialog\"\n * and nothing else, which is worse than the unlabelled div it replaced — so it is a\n * prop with an English default, like every other string this package renders. Callers\n * should pass the control's own name (\"Filter\", \"Choose a date\"); the default is only\n * there so an unmigrated call site is still named. */\nexport interface PopoverLabels {\n panel: string;\n}\n\n// Defined in ./popover-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_POPOVER_LABELS };\n\n/**\n * Exported, and extending the `<div>` attributes, so a consumer wrapping this — which is\n * what `DateField` and `FilterPopover` in this very package do — can hand anything\n * through to the PANEL: a `data-tour` anchor for the kit's own guided tour, a test id,\n * an `aria-describedby`, or an `aria-label` naming this particular popover.\n *\n * `children` is omitted from those attributes rather than inherited: here it is a render\n * prop taking the panel's `close`, not a `ReactNode`, so the two cannot be reconciled.\n */\nexport interface PopoverProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n trigger: (state: { open: boolean; toggle: () => void; ref: RefObject<HTMLButtonElement | null> }) => ReactNode;\n children: (close: () => void) => ReactNode;\n /**\n * The panel's width: `\"18rem\"` (the default) grows with the text size — the panel\n * holds text, and a 288 px calendar at 150 % cut its own month off (0.32,\n * docs/text-size-harmonization.md §3.2). A plain number is px, fixed, as before.\n * Either way the panel is never wider than the viewport less 8 px a side: 18rem is\n * 432 px at Extra large, more than a 360 px phone has.\n */\n width?: number | RemLength;\n labels?: LabelOverride<PopoverLabels>;\n /** Id for the panel, so a trigger wearing `role=\"combobox\"` can `aria-controls` it.\n * Required by ARIA on that role, and the panel is rendered here rather than by the\n * caller, so the id has to come in from outside. */\n panelId?: string;\n /** Extra classes for the PANEL. The trigger is the caller's own element. */\n className?: string;\n /** The PANEL, while it is open — where the rest of the props go (0.34, API-2, refactor\n * audit 2026-10). Merged with the panel's own ref (focus trap, outside click), so\n * neither loses it. The trigger is the caller's own element, ref and all. */\n ref?: Ref<HTMLDivElement>;\n}\n\n/** 288 px at Normal, in rem so it follows the text size (§3.2). */\nconst POPOVER_WIDTH: RemLength = \"18rem\";\n\n/** The gap the panel keeps from either edge of the viewport, in px. */\nconst EDGE = 8;\n\n/**\n * Portal-rendered popover anchored under its trigger button. The panel is fixed\n * to the viewport (so table/overflow ancestors can't clip it) and re-aligns on\n * scroll/resize; it closes on outside-click and Escape.\n *\n * ## Trapped, not merely focused — and why, because it is the closer call\n *\n * The audit's finding was that this panel *\"portals out of the tab order with no role\n * and no focus move\"*: Tab from the trigger went straight past the panel into the page\n * behind it, so the calendar, the calculator keypad and every column filter in the kit\n * were mouse-only. Two shapes fix that, and a popover is not automatically a dialog, so\n * this was chosen rather than inherited:\n *\n * - **The light one** — move focus in on open, hand it back on close, and let Tab walk\n * OUT of the panel into whatever follows the trigger. That is the right shape for a\n * non-modal popover sitting inline in the document.\n * - **The trap** — Tab cycles inside the panel; Escape and a click outside are the\n * ways out. What is here.\n *\n * The deciding fact is the portal. This panel is rendered to the END of `<body>`, not\n * beside its trigger, so \"let Tab walk out\" does not mean \"carry on from the trigger\" —\n * it means land at the end of the document, several hundred controls from where the\n * user was. Tabbing out of it would be a worse place to arrive than the one the finding\n * complains about, and fixing that properly needs a sentinel-and-return dance that\n * nothing in this package has. The trap gets the user back to the trigger every time,\n * because `useFocusTrap` restores focus on close.\n *\n * And the panels this kit actually opens are self-contained: pick a date, tap out a\n * sum, narrow a column. Each of them commits and closes, and each already has two\n * documented ways out wired above — Escape and an outside press — so containment costs\n * a keyboard user nothing they had.\n *\n * **It stays non-modal in every other respect**, deliberately: no backdrop, no body\n * scroll lock, and no `aria-modal`, so the page behind is still readable by a screen\n * reader, still scrollable, and still clickable — clicking it is what dismisses this.\n * Only Tab is contained. A popover that took the page away as well would be a\n * {@link Modal}, and there is one of those.\n */\nexport function Popover({\n trigger,\n children,\n width = POPOVER_WIDTH,\n labels,\n panelId,\n className,\n style,\n \"aria-label\": ariaLabel,\n onKeyDown: onKeyDownProp,\n ref,\n ...rest\n}: PopoverProps) {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const setPanel = useMemo(() => mergeRefs(panelRef, ref), [ref]);\n const [open, setOpen] = useState(false);\n // The trigger's reading direction, read when it is pressed. The panel is portalled to\n // <body>, out of any `dir=\"rtl\"` subtree the trigger sits in, so without carrying it\n // across a right-to-left calendar or filter rendered left-to-right.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const close = () => setOpen(false);\n const toggle = () => {\n setDir(dirOf(triggerRef.current));\n setOpen((v) => !v);\n };\n // prop > `<UiKitProvider labels={{ popover }}>` > English. The provider's\n // `popover.panel` is only the last-resort name for a panel no caller named.\n const text = useKitLabels(\"popover\", DEFAULT_POPOVER_LABELS, labels);\n\n // Align the panel's END edge to the trigger's — right in LTR, left in RTL; it was\n // always `rect.right`, so an RTL panel hung off the wrong side — and clamp it\n // horizontally; the\n // hook owns the vertical half — below by default, flipped above (and height-capped)\n // when that is where the room is, e.g. once a mobile keyboard has eaten the bottom\n // of the screen (feedback #135). It re-measures on scroll/resize.\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n // In px for the arithmetic below, at the text size in force; capped at the glass — the\n // root's client width, not `innerWidth`, which on a phone is the layout viewport grown\n // by whatever overflows (live #381, Wave 2 DUP-7).\n const remPx = useRemPx();\n const glass = rect ? viewportSize().width : 0;\n const panelWidth = rect ? Math.min(lengthPx(width, remPx), Math.max(0, glass - 2 * EDGE)) : 0;\n const pos = rect\n ? {\n // Flipped above, the panel hangs from its bottom edge, so one shorter than its\n // cap still meets the trigger (keksdose K1); the edge not in use is cleared.\n top: placement.above ? undefined : placement.top,\n bottom: placement.above ? placement.bottom : undefined,\n left: anchoredInline(rect, panelWidth, { dir, align: \"end\", edge: EDGE, viewportWidth: glass }).left,\n }\n : null;\n\n useOutsideClick([triggerRef, panelRef], close, open);\n // A layer on the Escape stack while open (Wave 2, DUP-1): a list opened inside closes\n // first, and the dialog this popover sits in only after it — the calculator on an\n // amount field in a Modal is exactly that.\n useDismissLayer(open, close);\n\n useFocusTrap(panelRef, {\n active: open && pos !== null,\n // The panel keeps a focus one of its own children has already taken. `FilterPopover`\n // autofocuses its text box on mount — that is what the press on \"Filter\" was FOR,\n // and it is a prop there precisely so the popover keeps it — and a trap that\n // unconditionally focused the container would take it straight back off again,\n // one effect later and invisibly.\n initialFocus: () => {\n const panel = panelRef.current;\n const active = document.activeElement;\n return panel?.contains(active) ? (active as HTMLElement) : panel;\n },\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // A caller's own handler first (0.32.2: RowActions stops Enter / Space at the panel);\n // it used to be dropped, overwritten by this one. A `preventDefault()` on Escape\n // there keeps the popover open — the owner's claim, as on the dialogs.\n runOwnKeyDown(e, onKeyDownProp);\n // Focus is in here, the normal case: the key is handed to the top layer and STOPPED,\n // so it does not travel on up the React tree to a dialog around the popover.\n takeEscape(e);\n };\n\n return (\n <>\n {trigger({ open, toggle, ref: triggerRef })}\n {open && pos &&\n createPortal(\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the\n dialog container, per the APG dialog pattern: `onKeyDown` is Escape-to-close for\n whatever inside it holds focus. */\n <div\n // `...rest` first: the role, the id the trigger's `aria-controls` points at,\n // the `tabIndex` the trap needs and the measured position are what make this\n // a popover, and a caller reaching for a `data-tour` anchor must not be able\n // to remove one of them by accident.\n {...rest}\n ref={setPanel}\n role=\"dialog\"\n id={panelId}\n // The caller's own `aria-label` wins over {@link PopoverLabels.panel}, which\n // is the package's English fallback for an unnamed panel. Both name the same\n // element; the DOM spelling is the one being standardised on, so where both\n // are present it is the one that survives.\n aria-label={ariaLabel ?? text.panel}\n dir={rest.dir ?? dir}\n // Without it the panel cannot take focus at all, and the trap above would\n // silently do nothing on a panel whose children are not yet tabbable.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n style={{\n // Merged over the caller's, never replaced by it: these are measured\n // against the trigger every scroll and resize, and a panel that dropped\n // them would paint at the top-left of the viewport.\n ...style,\n position: \"fixed\",\n top: pos.top,\n bottom: pos.bottom,\n left: pos.left,\n width: panelWidth,\n maxHeight: placement.maxHeight,\n }}\n className={cn(\n \"z-50 overflow-y-auto rounded-md border border-subtle bg-surface p-3 shadow-lg outline-none\",\n className,\n )}\n >\n {children(close)}\n </div>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/popover.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, Ref, RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { anchoredInline, useAnchoredPanel, viewportSize } from \"../hooks/use-anchored-panel\";\nimport { runOwnKeyDown, takeEscape, useDismissLayer, useOutsideClick } from \"../hooks/use-dismiss\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { lengthPx, useRemPx, type RemLength } from \"../hooks/use-breakpoint\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf } from \"../lib/direction\";\nimport type { Direction } from \"../lib/direction\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_POPOVER_LABELS } from \"./popover-labels\";\n// From ../lib/refs, not field-parts (round 0.36): field-parts renders a HelpPopover for a\n// field's `help`, so importing from there would close a cycle, as it did for Tooltip.\nimport { mergeRefs } from \"../lib/refs\";\n\n/** The panel's accessible name. A `role=\"dialog\"` with no name announces as \"dialog\"\n * and nothing else, which is worse than the unlabelled div it replaced — so it is a\n * prop with an English default, like every other string this package renders. Callers\n * should pass the control's own name (\"Filter\", \"Choose a date\"); the default is only\n * there so an unmigrated call site is still named. */\nexport interface PopoverLabels {\n panel: string;\n}\n\n// Defined in ./popover-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_POPOVER_LABELS };\n\n/**\n * Exported, and extending the `<div>` attributes, so a consumer wrapping this — which is\n * what `DateField` and `FilterPopover` in this very package do — can hand anything\n * through to the PANEL: a `data-tour` anchor for the kit's own guided tour, a test id,\n * an `aria-describedby`, or an `aria-label` naming this particular popover.\n *\n * `children` is omitted from those attributes rather than inherited: here it is a render\n * prop taking the panel's `close`, not a `ReactNode`, so the two cannot be reconciled.\n */\nexport interface PopoverProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n trigger: (state: { open: boolean; toggle: () => void; ref: RefObject<HTMLButtonElement | null> }) => ReactNode;\n children: (close: () => void) => ReactNode;\n /**\n * The panel's width: `\"18rem\"` (the default) grows with the text size — the panel\n * holds text, and a 288 px calendar at 150 % cut its own month off (0.32,\n * docs/text-size-harmonization.md §3.2). A plain number is px, fixed, as before.\n * Either way the panel is never wider than the viewport less 8 px a side: 18rem is\n * 432 px at Extra large, more than a 360 px phone has.\n */\n width?: number | RemLength;\n labels?: LabelOverride<PopoverLabels>;\n /** Id for the panel, so a trigger wearing `role=\"combobox\"` can `aria-controls` it.\n * Required by ARIA on that role, and the panel is rendered here rather than by the\n * caller, so the id has to come in from outside. */\n panelId?: string;\n /** Extra classes for the PANEL. The trigger is the caller's own element. */\n className?: string;\n /** The PANEL, while it is open — where the rest of the props go (0.34, API-2, refactor\n * audit 2026-10). Merged with the panel's own ref (focus trap, outside click), so\n * neither loses it. The trigger is the caller's own element, ref and all. */\n ref?: Ref<HTMLDivElement>;\n}\n\n/** 288 px at Normal, in rem so it follows the text size (§3.2). */\nconst POPOVER_WIDTH: RemLength = \"18rem\";\n\n/** The gap the panel keeps from either edge of the viewport, in px. */\nconst EDGE = 8;\n\n/**\n * Portal-rendered popover anchored under its trigger button. The panel is fixed\n * to the viewport (so table/overflow ancestors can't clip it) and re-aligns on\n * scroll/resize; it closes on outside-click and Escape.\n *\n * ## Trapped, not merely focused — and why, because it is the closer call\n *\n * The audit's finding was that this panel *\"portals out of the tab order with no role\n * and no focus move\"*: Tab from the trigger went straight past the panel into the page\n * behind it, so the calendar, the calculator keypad and every column filter in the kit\n * were mouse-only. Two shapes fix that, and a popover is not automatically a dialog, so\n * this was chosen rather than inherited:\n *\n * - **The light one** — move focus in on open, hand it back on close, and let Tab walk\n * OUT of the panel into whatever follows the trigger. That is the right shape for a\n * non-modal popover sitting inline in the document.\n * - **The trap** — Tab cycles inside the panel; Escape and a click outside are the\n * ways out. What is here.\n *\n * The deciding fact is the portal. This panel is rendered to the END of `<body>`, not\n * beside its trigger, so \"let Tab walk out\" does not mean \"carry on from the trigger\" —\n * it means land at the end of the document, several hundred controls from where the\n * user was. Tabbing out of it would be a worse place to arrive than the one the finding\n * complains about, and fixing that properly needs a sentinel-and-return dance that\n * nothing in this package has. The trap gets the user back to the trigger every time,\n * because `useFocusTrap` restores focus on close.\n *\n * And the panels this kit actually opens are self-contained: pick a date, tap out a\n * sum, narrow a column. Each of them commits and closes, and each already has two\n * documented ways out wired above — Escape and an outside press — so containment costs\n * a keyboard user nothing they had.\n *\n * **It stays non-modal in every other respect**, deliberately: no backdrop, no body\n * scroll lock, and no `aria-modal`, so the page behind is still readable by a screen\n * reader, still scrollable, and still clickable — clicking it is what dismisses this.\n * Only Tab is contained. A popover that took the page away as well would be a\n * {@link Modal}, and there is one of those.\n */\nexport function Popover({\n trigger,\n children,\n width = POPOVER_WIDTH,\n labels,\n panelId,\n className,\n style,\n \"aria-label\": ariaLabel,\n onKeyDown: onKeyDownProp,\n ref,\n ...rest\n}: PopoverProps) {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const setPanel = useMemo(() => mergeRefs(panelRef, ref), [ref]);\n const [open, setOpen] = useState(false);\n // The trigger's reading direction, read when it is pressed. The panel is portalled to\n // <body>, out of any `dir=\"rtl\"` subtree the trigger sits in, so without carrying it\n // across a right-to-left calendar or filter rendered left-to-right.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const close = () => setOpen(false);\n const toggle = () => {\n setDir(dirOf(triggerRef.current));\n setOpen((v) => !v);\n };\n // prop > `<UiKitProvider labels={{ popover }}>` > English. The provider's\n // `popover.panel` is only the last-resort name for a panel no caller named.\n const text = useKitLabels(\"popover\", DEFAULT_POPOVER_LABELS, labels);\n\n // Align the panel's END edge to the trigger's — right in LTR, left in RTL; it was\n // always `rect.right`, so an RTL panel hung off the wrong side — and clamp it\n // horizontally; the\n // hook owns the vertical half — below by default, flipped above (and height-capped)\n // when that is where the room is, e.g. once a mobile keyboard has eaten the bottom\n // of the screen (feedback #135). It re-measures on scroll/resize.\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n // In px for the arithmetic below, at the text size in force; capped at the glass — the\n // root's client width, not `innerWidth`, which on a phone is the layout viewport grown\n // by whatever overflows (live #381, Wave 2 DUP-7).\n const remPx = useRemPx();\n const glass = rect ? viewportSize().width : 0;\n const panelWidth = rect ? Math.min(lengthPx(width, remPx), Math.max(0, glass - 2 * EDGE)) : 0;\n const pos = rect\n ? {\n // Flipped above, the panel hangs from its bottom edge, so one shorter than its\n // cap still meets the trigger (keksdose K1); the edge not in use is cleared.\n top: placement.above ? undefined : placement.top,\n bottom: placement.above ? placement.bottom : undefined,\n left: anchoredInline(rect, panelWidth, { dir, align: \"end\", edge: EDGE, viewportWidth: glass }).left,\n }\n : null;\n\n useOutsideClick([triggerRef, panelRef], close, open);\n // A layer on the Escape stack while open (Wave 2, DUP-1): a list opened inside closes\n // first, and the dialog this popover sits in only after it — the calculator on an\n // amount field in a Modal is exactly that.\n const layer = useDismissLayer(open, close);\n\n useFocusTrap(panelRef, {\n active: open && pos !== null,\n // The panel keeps a focus one of its own children has already taken. `FilterPopover`\n // autofocuses its text box on mount — that is what the press on \"Filter\" was FOR,\n // and it is a prop there precisely so the popover keeps it — and a trap that\n // unconditionally focused the container would take it straight back off again,\n // one effect later and invisibly.\n initialFocus: () => {\n const panel = panelRef.current;\n const active = document.activeElement;\n return panel?.contains(active) ? (active as HTMLElement) : panel;\n },\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // A caller's own handler first (0.32.2: RowActions stops Enter / Space at the panel);\n // it used to be dropped, overwritten by this one. A `preventDefault()` on Escape\n // there keeps the popover open — the owner's claim, as on the dialogs. And as there,\n // the handler sees Escape only while this popover is the layer on top: a tooltip or a\n // menu open over it takes that press first (round 0.35 R2, the dialogs' rule).\n runOwnKeyDown(e, onKeyDownProp, layer);\n // Focus is in here, the normal case: the key is handed to the top layer and STOPPED,\n // so it does not travel on up the React tree to a dialog around the popover.\n takeEscape(e);\n };\n\n return (\n <>\n {trigger({ open, toggle, ref: triggerRef })}\n {open && pos &&\n createPortal(\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the\n dialog container, per the APG dialog pattern: `onKeyDown` is Escape-to-close for\n whatever inside it holds focus. */\n <div\n // `...rest` first: the role, the id the trigger's `aria-controls` points at,\n // the `tabIndex` the trap needs and the measured position are what make this\n // a popover, and a caller reaching for a `data-tour` anchor must not be able\n // to remove one of them by accident.\n {...rest}\n ref={setPanel}\n role=\"dialog\"\n id={panelId}\n // The caller's own `aria-label` wins over {@link PopoverLabels.panel}, which\n // is the package's English fallback for an unnamed panel. Both name the same\n // element; the DOM spelling is the one being standardised on, so where both\n // are present it is the one that survives.\n aria-label={ariaLabel ?? text.panel}\n dir={rest.dir ?? dir}\n // Without it the panel cannot take focus at all, and the trap above would\n // silently do nothing on a panel whose children are not yet tabbable.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n style={{\n // Merged over the caller's, never replaced by it: these are measured\n // against the trigger every scroll and resize, and a panel that dropped\n // them would paint at the top-left of the viewport.\n ...style,\n position: \"fixed\",\n top: pos.top,\n bottom: pos.bottom,\n left: pos.left,\n width: panelWidth,\n maxHeight: placement.maxHeight,\n }}\n className={cn(\n \"z-50 overflow-y-auto rounded-md border border-subtle bg-surface p-3 shadow-lg outline-none\",\n className,\n )}\n >\n {children(close)}\n </div>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode, Ref, SelectHTMLAttributes } from "react";
2
2
  import type { CommitScope } from "./write-lock.js";
3
- import { type DisabledReasonDisplay } from "./field-parts.js";
4
- export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "size"> {
3
+ import { type DisabledReasonDisplay, type FieldHelpProps } from "./field-parts.js";
4
+ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "size">, FieldHelpProps {
5
5
  label?: ReactNode;
6
6
  /** See {@link Input}'s `invalid`. */
7
7
  invalid?: boolean;
@@ -14,7 +14,8 @@ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement
14
14
  * set in the value's type on top of both the label and the value. On an
15
15
  * UNLABELLED Select (no label line) a FieldHint sits at the field's end edge,
16
16
  * outside the box. Since 0.34 a FieldHint is the caption at Large and on a touch
17
- * screen, as on Input (`useInlineHint`); a `HelpPopover` stays a "?". */
17
+ * screen, as on Input (`useInlineHint`); a `HelpPopover` stays a "?". Beside a
18
+ * `help` (0.36) the hint is the caption at every size: the help's "?" is the only one. */
18
19
  hint?: ReactNode;
19
20
  /**
20
21
  * `"sm"`: a 28px, 12px-type select for a toolbar or a table header, where the
@@ -31,6 +31,7 @@ function Select(props) {
31
31
  invalid,
32
32
  error,
33
33
  hint: hintProp,
34
+ help,
34
35
  size,
35
36
  selectClassName,
36
37
  disabledReason,
@@ -49,7 +50,7 @@ function Select(props) {
49
50
  const listBox = nativeSize !== void 0 && nativeSize > 1 || Boolean(rest.multiple);
50
51
  const small = size === "sm" && label === void 0;
51
52
  const fieldId = id ?? generated;
52
- const hintParts = useFieldHint(hintProp, rest["aria-describedby"]);
53
+ const hintParts = useFieldHint(hintProp, rest["aria-describedby"], { help, name: label ?? rest["aria-label"] });
53
54
  const hint = hintParts.labelHint;
54
55
  const { isInvalid, describedBy, errorEl } = useFieldError(
55
56
  error,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/select.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { KeyboardEvent, MouseEvent, ReactNode, Ref, SelectHTMLAttributes } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { CommitScope } from \"./write-lock\";\nimport {\n LockedReason,\n mergeDescribedBy,\n type DisabledReasonDisplay,\n useFieldError,\n useFieldHint,\n useLockReason,\n} from \"./field-parts\";\nimport {\n FIELD_BASE,\n FIELD_FLOATING_PAD,\n FIELD_INVALID,\n FieldChevron,\n FieldGroup,\n FloatingField,\n fieldBelow,\n} from \"./field-base\";\n\nexport interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, \"size\"> {\n label?: ReactNode;\n /** See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** See {@link Input}'s `error`. */\n error?: ReactNode;\n /** A {@link FieldHint} for the label line — see {@link FloatingField}. Plain\n * TEXT (a string or a number) is a caption instead, and goes UNDER the field\n * like every other field's hint, attached through `aria-describedby`: the label\n * line is 11px of strip shared with the label, and a sentence placed there was\n * set in the value's type on top of both the label and the value. On an\n * UNLABELLED Select (no label line) a FieldHint sits at the field's end edge,\n * outside the box. Since 0.34 a FieldHint is the caption at Large and on a touch\n * screen, as on Input (`useInlineHint`); a `HelpPopover` stays a \"?\". */\n hint?: ReactNode;\n /**\n * `\"sm\"`: a 28px, 12px-type select for a toolbar or a table header, where the\n * 36px field stands a head taller than the buttons beside it. `\"md\"` (default)\n * is the field every form uses. Unlabelled selects only — a floating label needs\n * the tall box to float in, so a labelled Select ignores `\"sm\"`.\n *\n * A NUMBER is still the native attribute (the rows of a list box) and is passed\n * straight through, so the one HTML meaning of `size` keeps working. Above 1 (as\n * with `multiple`) the browser draws a list box, and the Select drops its chevron\n * and the end padding reserved for it.\n */\n size?: \"sm\" | \"md\" | number;\n /** Classes for the `<select>` itself. `className` styles the WRAPPER — the box\n * the chevron is positioned against — so it could set a width and nothing\n * else; this is the way to the element. See {@link Input}'s `inputClassName`. */\n selectClassName?: string;\n /**\n * Why the choice cannot be changed — {@link Button}'s `disabledReason`, for a select\n * that SAVES on change (a role picker in a members table, a status in a row). keksdose\n * K3: those sat inside a hand-rolled `SaveGuard` that forced a native `disabled`, which\n * took the field out of the tab order so the reason never reached a keyboard.\n *\n * With a reason the select is `aria-disabled` instead — still focusable, still showing\n * its value — its list does not open (pointer and keys are swallowed, Tab and Escape\n * excepted), `onChange` is never called, and the reason is a line under the field at\n * Large and on a touch screen, else in the kit {@link Tooltip} (`disabledReasonDisplay`),\n * and on `aria-describedby`. It wears the settled look of a disabled field and drops\n * its chevron, as a disabled Select does (dev#474). It wins over `disabled`.\n *\n * Use it CONTROLLED (`value` + `onChange`): React puts a controlled value back after\n * the swallowed change. A touch platform's own picker may still open; nothing it picks\n * is kept or reported.\n */\n disabledReason?: ReactNode;\n /**\n * Where the reason shows — {@link Button}'s `disabledReasonDisplay`: by default a line\n * under the field at Large and on a touch screen, the tooltip otherwise (§4 \"No fact\n * only in a tooltip\"; DUP-3, refactor audit 2026-10).\n */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * This select COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is\n * locked the `disabledReason` way with the lock's reason (which wins over its own).\n * No provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3; a\n * {@link CommitScope} since 0.33.\n */\n commit?: CommitScope;\n /** The `<select>`. A React 19 ref prop since 0.34 — see {@link InputProps.ref}. */\n ref?: Ref<HTMLSelectElement>;\n}\n\n// The compact select: the field's colours, a toolbar button's height. `py-0` and a\n// fixed height rather than a smaller padding, so the box is 28px whatever line\n// height the caller's type brings with it.\nconst SELECT_SM = \"h-7 py-0 ps-2 pe-7 text-xs\";\n\n/** Keys a locked select still answers: leaving it, and dismissing its tooltip. Every\n * other key would open the list or step the value. */\nconst LOCKED_SELECT_KEYS = new Set([\"Tab\", \"Escape\", \"Shift\"]);\n\n/** FIELD_BASE's `disabled:` look, for a select that is locked without being `disabled`\n * (the `:disabled` variants cannot match it). */\nconst FIELD_LOCKED = \"cursor-not-allowed bg-surface-2 text-muted\";\n\nexport function Select(props: SelectProps) {\n const {\n ref,\n className,\n label,\n id,\n children,\n invalid,\n error,\n hint: hintProp,\n size,\n selectClassName,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onMouseDown,\n onKeyDown,\n onChange,\n ...rest\n } = props;\n // See Input's `reserve`.\n const reserve = \"error\" in props || \"hint\" in props || \"showCount\" in props;\n const generated = useId();\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n // Only a number reaches the DOM; the two words are this component's own.\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // A numeric size above 1 (or `multiple`) makes the browser draw a LIST BOX — all\n // the rows in the field, no menu to drop. The dropdown dress did not fit it: the\n // chevron was centred on a box several rows tall, sat on top of the rows' text\n // at the end edge, and `pe-9` clipped every option nine units early. A list box\n // gets the plain field instead — its own native scroll, no chevron, even padding.\n const listBox = (nativeSize !== undefined && nativeSize > 1) || Boolean(rest.multiple);\n const small = size === \"sm\" && label === undefined;\n const fieldId = id ?? generated;\n // Text is a caption under the field; a FieldHint rides the label line — and at Large\n // and on a touch screen is its words, a caption like any text (§4; a HelpPopover stays\n // on the label line, §10.23). See `hint` above. Through useFieldHint since 0.34, not a\n // copy of its rule (DUP-2, refactor audit 2026-10).\n const hintParts = useFieldHint(hintProp, rest[\"aria-describedby\"]);\n const hint = hintParts.labelHint;\n const { isInvalid, describedBy, errorEl } = useFieldError(\n error,\n invalid,\n // The caption goes in BEFORE the error — the standing advice first, the news\n // second, the order useFieldError keeps for a caller's own description. A lock's\n // reason sits between the two.\n mergeDescribedBy(hintParts.describedBy, locked && lock.reasonId),\n rest[\"aria-invalid\"],\n );\n const below = fieldBelow(hintParts, null, errorEl);\n // Custom chevron (native arrow hidden via appearance-none) so it sits a touch\n // in from the end border and matches both themes — feedback #223. A DISABLED\n // select has no menu to drop, so it drops the chevron too: the arrow is the one\n // thing on the control that promises a choice (Keksdose dev#474, where the\n // account type became read-only and still looked exactly like a picker). A locked\n // one has no menu either.\n const chevron =\n rest.disabled || listBox || locked ? null : (\n <FieldChevron className={small ? \"end-1.5 size-3.5\" : undefined} />\n );\n // `pe-9` is the room the chevron takes; a list box has none to make room for.\n //\n // A list box's height is `size` rows plus its padding, but its rows are clipped at\n // the PADDING edge, not the content edge — so FIELD_BASE's `pb-2` showed the next\n // row through the bottom padding: `size={4}` drew four and a half. The vertical room\n // moves onto the rows instead (option padding counts toward the row height the\n // browser sizes the box by), and the box keeps no bottom padding: exactly `size`\n // whole rows, at any width.\n const dress = listBox ? \"overflow-y-auto [&_option]:py-1\" : \"appearance-none pe-9\";\n // Locked: focusable, but nothing opens the list or moves the value, and nothing is\n // reported. The no-op `onChange` keeps a controlled select's value where React put\n // it (and keeps React from warning about a `value` with no handler).\n const interaction = locked\n ? {\n disabled: undefined,\n \"aria-disabled\": true as const,\n onMouseDown: (e: MouseEvent<HTMLSelectElement>) => e.preventDefault(),\n onKeyDown: (e: KeyboardEvent<HTMLSelectElement>) => {\n if (!LOCKED_SELECT_KEYS.has(e.key) && !e.ctrlKey && !e.metaKey) e.preventDefault();\n },\n onChange: () => {},\n }\n : { onMouseDown, onKeyDown, onChange };\n // While locked the field sits in the reason's frame (the line, or the Tooltip), which\n // becomes the outermost box — so `className` (a width, a grid cell) moves onto it, and\n // the form keeps its layout. `block` so a full-width field stays full width inside it.\n const boxClass = locked ? undefined : className;\n const withReason = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className={cn(\"block\", className)}>\n {box}\n </LockedReason>\n ) : (\n box\n );\n if (label === undefined) {\n const select = (\n <select\n ref={ref}\n // `id` is destructured out of the props to feed `fieldId`, and this branch\n // never put it back — so an UNLABELLED Select swallowed it and the\n // consumer's own `<label for=\"…\">` pointed at nothing. The control stayed\n // in the tab order with no accessible name at all: reachable, and silent\n // when it was reached. Input and Textarea both forward it here; this is\n // the third one doing the same thing.\n id={id}\n size={nativeSize}\n {...rest}\n {...interaction}\n // OR-ed with the spread for the reason spelled out on Input's copy: this\n // branch wrote `invalid || undefined`, so passing `aria-invalid` by hand\n // to a Select — which is what a caller does when the validity is\n // `\"grammar\"` or `\"spelling\"`, or when the paint is not wanted — had the\n // attribute silently dropped. Input has never done that.\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(\n FIELD_BASE,\n dress,\n listBox && \"py-0\",\n small && SELECT_SM,\n locked && FIELD_LOCKED,\n selectClassName,\n isInvalid && FIELD_INVALID,\n )}\n >\n {children}\n </select>\n );\n // A FieldHint with no label line to ride (0.15.5 P8): an unlabelled Select — named\n // by `aria-label` or a `<label>` of the caller's — dropped a non-text `hint`\n // without a trace, so the \"?\" a caller passed never rendered and a test looking\n // for its button found none. It goes at the end of the field instead, outside the\n // box, as the label line's own hint goes at the end of the label.\n if (hint !== undefined) {\n return (\n <FieldGroup errorEl={below} reserve={reserve}>\n {withReason(\n <div className={cn(\"flex items-center gap-1.5\", boxClass)}>\n <div className=\"relative min-w-0 flex-1\">\n {select}\n {chevron}\n </div>\n <span className=\"flex shrink-0 items-center\">{hint}</span>\n </div>,\n )}\n </FieldGroup>\n );\n }\n return (\n <FieldGroup errorEl={below} reserve={reserve}>\n {withReason(\n <div className={cn(\"relative\", boxClass)}>\n {select}\n {chevron}\n </div>,\n )}\n </FieldGroup>\n );\n }\n return (\n <FieldGroup errorEl={below} reserve={reserve}>\n {withReason(\n <FloatingField\n className={boxClass}\n htmlFor={fieldId}\n label={label}\n staticLabel\n hint={hint}\n >\n <select\n ref={ref}\n id={fieldId}\n size={nativeSize}\n {...rest}\n {...interaction}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(\n FIELD_BASE,\n // The floated label takes the top strip of a list box too, so its first\n // row starts under the label rather than behind it. No bottom padding — see\n // `dress`.\n // At Large the label stands above the field (§10.17): no strip.\n listBox ? \"pt-5 pb-0 large:pt-2\" : FIELD_FLOATING_PAD,\n \"peer\",\n dress,\n locked && FIELD_LOCKED,\n selectClassName,\n isInvalid && FIELD_INVALID,\n )}\n >\n {children}\n </select>\n {chevron}\n </FloatingField>,\n )}\n </FieldGroup>\n );\n}\n"],"mappings":";AAgKM,cAiFQ,YAjFR;AAhKN,SAAS,aAAa;AAEtB,SAAS,UAAU;AAEnB;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAuEP,MAAM,YAAY;AAIlB,MAAM,qBAAqB,oBAAI,IAAI,CAAC,OAAO,UAAU,OAAO,CAAC;AAI7D,MAAM,eAAe;AAEd,SAAS,OAAO,OAAoB;AACzC,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AAEJ,QAAM,UAAU,WAAW,SAAS,UAAU,SAAS,eAAe;AACtE,QAAM,YAAY,MAAM;AACxB,QAAM,OAAO,cAAc,QAAQ,cAAc;AACjD,QAAM,SAAS,KAAK;AAEpB,QAAM,aAAa,OAAO,SAAS,WAAW,OAAO;AAMrD,QAAM,UAAW,eAAe,UAAa,aAAa,KAAM,QAAQ,KAAK,QAAQ;AACrF,QAAM,QAAQ,SAAS,QAAQ,UAAU;AACzC,QAAM,UAAU,MAAM;AAKtB,QAAM,YAAY,aAAa,UAAU,KAAK,kBAAkB,CAAC;AACjE,QAAM,OAAO,UAAU;AACvB,QAAM,EAAE,WAAW,aAAa,QAAQ,IAAI;AAAA,IAC1C;AAAA,IACA;AAAA;AAAA;AAAA;AAAA,IAIA,iBAAiB,UAAU,aAAa,UAAU,KAAK,QAAQ;AAAA,IAC/D,KAAK,cAAc;AAAA,EACrB;AACA,QAAM,QAAQ,WAAW,WAAW,MAAM,OAAO;AAOjD,QAAM,UACJ,KAAK,YAAY,WAAW,SAAS,OACnC,oBAAC,gBAAa,WAAW,QAAQ,qBAAqB,QAAW;AAUrE,QAAM,QAAQ,UAAU,oCAAoC;AAI5D,QAAM,cAAc,SAChB;AAAA,IACE,UAAU;AAAA,IACV,iBAAiB;AAAA,IACjB,aAAa,CAAC,MAAqC,EAAE,eAAe;AAAA,IACpE,WAAW,CAAC,MAAwC;AAClD,UAAI,CAAC,mBAAmB,IAAI,EAAE,GAAG,KAAK,CAAC,EAAE,WAAW,CAAC,EAAE,QAAS,GAAE,eAAe;AAAA,IACnF;AAAA,IACA,UAAU,MAAM;AAAA,IAAC;AAAA,EACnB,IACA,EAAE,aAAa,WAAW,SAAS;AAIvC,QAAM,WAAW,SAAS,SAAY;AACtC,QAAM,aAAa,CAAC,QAClB,SACE,oBAAC,gBAAa,MAAY,SAAS,uBAAuB,WAAW,GAAG,SAAS,SAAS,GACvF,eACH,IAEA;AAEJ,MAAI,UAAU,QAAW;AACvB,UAAM,SACJ;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QAOA;AAAA,QACA,MAAM;AAAA,QACL,GAAG;AAAA,QACH,GAAG;AAAA,QAMJ,gBAAc,aAAa,KAAK,cAAc,KAAK;AAAA,QACnD,oBAAkB;AAAA,QAClB,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA,WAAW;AAAA,UACX,SAAS;AAAA,UACT,UAAU;AAAA,UACV;AAAA,UACA,aAAa;AAAA,QACf;AAAA,QAEC;AAAA;AAAA,IACH;AAOF,QAAI,SAAS,QAAW;AACtB,aACE,oBAAC,cAAW,SAAS,OAAO,SACzB;AAAA,QACC,qBAAC,SAAI,WAAW,GAAG,6BAA6B,QAAQ,GACtD;AAAA,+BAAC,SAAI,WAAU,2BACZ;AAAA;AAAA,YACA;AAAA,aACH;AAAA,UACA,oBAAC,UAAK,WAAU,8BAA8B,gBAAK;AAAA,WACrD;AAAA,MACF,GACF;AAAA,IAEJ;AACA,WACE,oBAAC,cAAW,SAAS,OAAO,SACzB;AAAA,MACC,qBAAC,SAAI,WAAW,GAAG,YAAY,QAAQ,GACpC;AAAA;AAAA,QACA;AAAA,SACH;AAAA,IACF,GACF;AAAA,EAEJ;AACA,SACE,oBAAC,cAAW,SAAS,OAAO,SACzB;AAAA,IACC;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,QACX,SAAS;AAAA,QACT;AAAA,QACA,aAAW;AAAA,QACX;AAAA,QAEA;AAAA;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA,IAAI;AAAA,cACJ,MAAM;AAAA,cACL,GAAG;AAAA,cACH,GAAG;AAAA,cACJ,gBAAc,aAAa,KAAK,cAAc,KAAK;AAAA,cACnD,oBAAkB;AAAA,cAClB,WAAW;AAAA,gBACT;AAAA;AAAA;AAAA;AAAA;AAAA,gBAKA,UAAU,yBAAyB;AAAA,gBACnC;AAAA,gBACA;AAAA,gBACA,UAAU;AAAA,gBACV;AAAA,gBACA,aAAa;AAAA,cACf;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,UACC;AAAA;AAAA;AAAA,IACH;AAAA,EACF,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/select.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { KeyboardEvent, MouseEvent, ReactNode, Ref, SelectHTMLAttributes } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { CommitScope } from \"./write-lock\";\nimport {\n LockedReason,\n mergeDescribedBy,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n useFieldError,\n useFieldHint,\n useLockReason,\n} from \"./field-parts\";\nimport {\n FIELD_BASE,\n FIELD_FLOATING_PAD,\n FIELD_INVALID,\n FieldChevron,\n FieldGroup,\n FloatingField,\n fieldBelow,\n} from \"./field-base\";\n\nexport interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, \"size\">, FieldHelpProps {\n label?: ReactNode;\n /** See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** See {@link Input}'s `error`. */\n error?: ReactNode;\n /** A {@link FieldHint} for the label line — see {@link FloatingField}. Plain\n * TEXT (a string or a number) is a caption instead, and goes UNDER the field\n * like every other field's hint, attached through `aria-describedby`: the label\n * line is 11px of strip shared with the label, and a sentence placed there was\n * set in the value's type on top of both the label and the value. On an\n * UNLABELLED Select (no label line) a FieldHint sits at the field's end edge,\n * outside the box. Since 0.34 a FieldHint is the caption at Large and on a touch\n * screen, as on Input (`useInlineHint`); a `HelpPopover` stays a \"?\". Beside a\n * `help` (0.36) the hint is the caption at every size: the help's \"?\" is the only one. */\n hint?: ReactNode;\n /**\n * `\"sm\"`: a 28px, 12px-type select for a toolbar or a table header, where the\n * 36px field stands a head taller than the buttons beside it. `\"md\"` (default)\n * is the field every form uses. Unlabelled selects only — a floating label needs\n * the tall box to float in, so a labelled Select ignores `\"sm\"`.\n *\n * A NUMBER is still the native attribute (the rows of a list box) and is passed\n * straight through, so the one HTML meaning of `size` keeps working. Above 1 (as\n * with `multiple`) the browser draws a list box, and the Select drops its chevron\n * and the end padding reserved for it.\n */\n size?: \"sm\" | \"md\" | number;\n /** Classes for the `<select>` itself. `className` styles the WRAPPER — the box\n * the chevron is positioned against — so it could set a width and nothing\n * else; this is the way to the element. See {@link Input}'s `inputClassName`. */\n selectClassName?: string;\n /**\n * Why the choice cannot be changed — {@link Button}'s `disabledReason`, for a select\n * that SAVES on change (a role picker in a members table, a status in a row). keksdose\n * K3: those sat inside a hand-rolled `SaveGuard` that forced a native `disabled`, which\n * took the field out of the tab order so the reason never reached a keyboard.\n *\n * With a reason the select is `aria-disabled` instead — still focusable, still showing\n * its value — its list does not open (pointer and keys are swallowed, Tab and Escape\n * excepted), `onChange` is never called, and the reason is a line under the field at\n * Large and on a touch screen, else in the kit {@link Tooltip} (`disabledReasonDisplay`),\n * and on `aria-describedby`. It wears the settled look of a disabled field and drops\n * its chevron, as a disabled Select does (dev#474). It wins over `disabled`.\n *\n * Use it CONTROLLED (`value` + `onChange`): React puts a controlled value back after\n * the swallowed change. A touch platform's own picker may still open; nothing it picks\n * is kept or reported.\n */\n disabledReason?: ReactNode;\n /**\n * Where the reason shows — {@link Button}'s `disabledReasonDisplay`: by default a line\n * under the field at Large and on a touch screen, the tooltip otherwise (§4 \"No fact\n * only in a tooltip\"; DUP-3, refactor audit 2026-10).\n */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * This select COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is\n * locked the `disabledReason` way with the lock's reason (which wins over its own).\n * No provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3; a\n * {@link CommitScope} since 0.33.\n */\n commit?: CommitScope;\n /** The `<select>`. A React 19 ref prop since 0.34 — see {@link InputProps.ref}. */\n ref?: Ref<HTMLSelectElement>;\n}\n\n// The compact select: the field's colours, a toolbar button's height. `py-0` and a\n// fixed height rather than a smaller padding, so the box is 28px whatever line\n// height the caller's type brings with it.\nconst SELECT_SM = \"h-7 py-0 ps-2 pe-7 text-xs\";\n\n/** Keys a locked select still answers: leaving it, and dismissing its tooltip. Every\n * other key would open the list or step the value. */\nconst LOCKED_SELECT_KEYS = new Set([\"Tab\", \"Escape\", \"Shift\"]);\n\n/** FIELD_BASE's `disabled:` look, for a select that is locked without being `disabled`\n * (the `:disabled` variants cannot match it). */\nconst FIELD_LOCKED = \"cursor-not-allowed bg-surface-2 text-muted\";\n\nexport function Select(props: SelectProps) {\n const {\n ref,\n className,\n label,\n id,\n children,\n invalid,\n error,\n hint: hintProp,\n help,\n size,\n selectClassName,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onMouseDown,\n onKeyDown,\n onChange,\n ...rest\n } = props;\n // See Input's `reserve`.\n const reserve = \"error\" in props || \"hint\" in props || \"showCount\" in props;\n const generated = useId();\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n // Only a number reaches the DOM; the two words are this component's own.\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // A numeric size above 1 (or `multiple`) makes the browser draw a LIST BOX — all\n // the rows in the field, no menu to drop. The dropdown dress did not fit it: the\n // chevron was centred on a box several rows tall, sat on top of the rows' text\n // at the end edge, and `pe-9` clipped every option nine units early. A list box\n // gets the plain field instead — its own native scroll, no chevron, even padding.\n const listBox = (nativeSize !== undefined && nativeSize > 1) || Boolean(rest.multiple);\n const small = size === \"sm\" && label === undefined;\n const fieldId = id ?? generated;\n // Text is a caption under the field; a FieldHint rides the label line — and at Large\n // and on a touch screen is its words, a caption like any text (§4; a HelpPopover stays\n // on the label line, §10.23). See `hint` above. Through useFieldHint since 0.34, not a\n // copy of its rule (DUP-2, refactor audit 2026-10).\n const hintParts = useFieldHint(hintProp, rest[\"aria-describedby\"], { help, name: label ?? rest[\"aria-label\"] });\n const hint = hintParts.labelHint;\n const { isInvalid, describedBy, errorEl } = useFieldError(\n error,\n invalid,\n // The caption goes in BEFORE the error — the standing advice first, the news\n // second, the order useFieldError keeps for a caller's own description. A lock's\n // reason sits between the two.\n mergeDescribedBy(hintParts.describedBy, locked && lock.reasonId),\n rest[\"aria-invalid\"],\n );\n const below = fieldBelow(hintParts, null, errorEl);\n // Custom chevron (native arrow hidden via appearance-none) so it sits a touch\n // in from the end border and matches both themes — feedback #223. A DISABLED\n // select has no menu to drop, so it drops the chevron too: the arrow is the one\n // thing on the control that promises a choice (Keksdose dev#474, where the\n // account type became read-only and still looked exactly like a picker). A locked\n // one has no menu either.\n const chevron =\n rest.disabled || listBox || locked ? null : (\n <FieldChevron className={small ? \"end-1.5 size-3.5\" : undefined} />\n );\n // `pe-9` is the room the chevron takes; a list box has none to make room for.\n //\n // A list box's height is `size` rows plus its padding, but its rows are clipped at\n // the PADDING edge, not the content edge — so FIELD_BASE's `pb-2` showed the next\n // row through the bottom padding: `size={4}` drew four and a half. The vertical room\n // moves onto the rows instead (option padding counts toward the row height the\n // browser sizes the box by), and the box keeps no bottom padding: exactly `size`\n // whole rows, at any width.\n const dress = listBox ? \"overflow-y-auto [&_option]:py-1\" : \"appearance-none pe-9\";\n // Locked: focusable, but nothing opens the list or moves the value, and nothing is\n // reported. The no-op `onChange` keeps a controlled select's value where React put\n // it (and keeps React from warning about a `value` with no handler).\n const interaction = locked\n ? {\n disabled: undefined,\n \"aria-disabled\": true as const,\n onMouseDown: (e: MouseEvent<HTMLSelectElement>) => e.preventDefault(),\n onKeyDown: (e: KeyboardEvent<HTMLSelectElement>) => {\n if (!LOCKED_SELECT_KEYS.has(e.key) && !e.ctrlKey && !e.metaKey) e.preventDefault();\n },\n onChange: () => {},\n }\n : { onMouseDown, onKeyDown, onChange };\n // While locked the field sits in the reason's frame (the line, or the Tooltip), which\n // becomes the outermost box — so `className` (a width, a grid cell) moves onto it, and\n // the form keeps its layout. `block` so a full-width field stays full width inside it.\n const boxClass = locked ? undefined : className;\n const withReason = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className={cn(\"block\", className)}>\n {box}\n </LockedReason>\n ) : (\n box\n );\n if (label === undefined) {\n const select = (\n <select\n ref={ref}\n // `id` is destructured out of the props to feed `fieldId`, and this branch\n // never put it back — so an UNLABELLED Select swallowed it and the\n // consumer's own `<label for=\"…\">` pointed at nothing. The control stayed\n // in the tab order with no accessible name at all: reachable, and silent\n // when it was reached. Input and Textarea both forward it here; this is\n // the third one doing the same thing.\n id={id}\n size={nativeSize}\n {...rest}\n {...interaction}\n // OR-ed with the spread for the reason spelled out on Input's copy: this\n // branch wrote `invalid || undefined`, so passing `aria-invalid` by hand\n // to a Select — which is what a caller does when the validity is\n // `\"grammar\"` or `\"spelling\"`, or when the paint is not wanted — had the\n // attribute silently dropped. Input has never done that.\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(\n FIELD_BASE,\n dress,\n listBox && \"py-0\",\n small && SELECT_SM,\n locked && FIELD_LOCKED,\n selectClassName,\n isInvalid && FIELD_INVALID,\n )}\n >\n {children}\n </select>\n );\n // A FieldHint with no label line to ride (0.15.5 P8): an unlabelled Select — named\n // by `aria-label` or a `<label>` of the caller's — dropped a non-text `hint`\n // without a trace, so the \"?\" a caller passed never rendered and a test looking\n // for its button found none. It goes at the end of the field instead, outside the\n // box, as the label line's own hint goes at the end of the label.\n if (hint !== undefined) {\n return (\n <FieldGroup errorEl={below} reserve={reserve}>\n {withReason(\n <div className={cn(\"flex items-center gap-1.5\", boxClass)}>\n <div className=\"relative min-w-0 flex-1\">\n {select}\n {chevron}\n </div>\n <span className=\"flex shrink-0 items-center\">{hint}</span>\n </div>,\n )}\n </FieldGroup>\n );\n }\n return (\n <FieldGroup errorEl={below} reserve={reserve}>\n {withReason(\n <div className={cn(\"relative\", boxClass)}>\n {select}\n {chevron}\n </div>,\n )}\n </FieldGroup>\n );\n }\n return (\n <FieldGroup errorEl={below} reserve={reserve}>\n {withReason(\n <FloatingField\n className={boxClass}\n htmlFor={fieldId}\n label={label}\n staticLabel\n hint={hint}\n >\n <select\n ref={ref}\n id={fieldId}\n size={nativeSize}\n {...rest}\n {...interaction}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(\n FIELD_BASE,\n // The floated label takes the top strip of a list box too, so its first\n // row starts under the label rather than behind it. No bottom padding — see\n // `dress`.\n // At Large the label stands above the field (§10.17): no strip.\n listBox ? \"pt-5 pb-0 large:pt-2\" : FIELD_FLOATING_PAD,\n \"peer\",\n dress,\n locked && FIELD_LOCKED,\n selectClassName,\n isInvalid && FIELD_INVALID,\n )}\n >\n {children}\n </select>\n {chevron}\n </FloatingField>,\n )}\n </FieldGroup>\n );\n}\n"],"mappings":"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","names":[]}