@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/button-look.ts"],"sourcesContent":["import { cn } from \"../lib/cn\";\nimport { FOCUS_RING_WIDTH } from \"./focus-ring\";\nimport type { KitLinkComponent, KitLinkProps } from \"../i18n/kit-labels\";\n\n/**\n * The button look, which {@link Button} and {@link IconButton} share (0.34, MOD-2, refactor\n * audit 2026-10). In ui.tsx it was the one knot in an otherwise one-way graph: Button drew\n * IconButton's pressed look, and IconButton drew Button's variant colours, its hover ink\n * and its router link. Both now draw from this module, which renders nothing, so neither\n * imports the other.\n *\n * Public through ./ui by name: `buttonClasses`, `TOUCH_TARGET_LARGE` and the four types.\n * The other exports are the two buttons' shared parts (`@internal`).\n */\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\" | \"brand\" | \"link\";\n\n// Shared base ring for every button-styled element. Kept as a named const so the\n// <Button> component and the {@link buttonClasses} helper draw from one source and\n// can never drift apart.\n//\n// The ring is the kit's focus frame (§5): {@link FOCUS_RING_WIDTH} wide, on keyboard focus\n// (`focus-visible`) — a tap or a click no longer leaves a ring on the button — in each\n// variant's own colour below.\n//\n// `[&_svg]:shrink-0` (0.32.2, kastlan's 0.32.1 report): an icon beside a label keeps its\n// size when the label wraps at Large. Without it the svg was a flex item like the text,\n// and a two-line label squeezed it to a sliver (\"New invoice\" in a page header at 360 px\n// / Extra large).\nconst BUTTON_BASE = cn(\n \"inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:shrink-0\",\n FOCUS_RING_WIDTH,\n);\n\n/**\n * The touch target at Large and Extra large (docs/text-size-harmonization.md §4): at\n * least 48 px, the size a finger needs whatever the type. In px on purpose, like the\n * field floor ({@link FIELD_TOUCH_TEXT}): it is a physical threshold, not a size that\n * follows the text. Normal keeps the kit's own heights; a `large:` variant, so it costs\n * no render and is right on the first paint.\n */\nexport const TOUCH_TARGET_LARGE = \"large:min-h-[48px] large:min-w-[48px]\";\n\nexport type ButtonSize = \"sm\" | \"md\";\n\n// Box geometry per size, split out of the base so the two cannot be merged into one\n// string that a size then has to fight. `md` is the pre-0.8.0 look, unchanged. `sm` is\n// the compact secondary action keksdose repeats by hand as `px-2 py-1 text-xs` (and\n// `px-2 py-0.5 text-xs`) on the buttons in a toolbar or a table header; one rung is\n// enough, so the two spellings meet at `py-1`. There is no `lg`: no app has asked for\n// a bigger text button (IconButton's `lg` is a touch target, not a text size). The\n// variant map comes AFTER the size, so `link`'s `p-0` still wins at either size.\n//\n// At Large and above (§4) `sm` grows to `md`'s height — the padding that makes its\n// 1rem line as tall as `md`'s 1.25rem one — and both reach the 48 px touch target. The\n// type stays `sm`'s: the compact button is still the quieter one. Not on `link`, which\n// is a word in a sentence and has no box to grow (see `buttonLook`).\nconst BUTTON_SIZES: Record<ButtonSize, string> = {\n md: \"gap-2 px-3 py-2 text-sm\",\n sm: \"gap-1.5 px-2 py-1 text-xs\",\n};\nconst BUTTON_SIZES_LARGE: Record<ButtonSize, string> = {\n md: \"large:min-h-[48px]\",\n sm: \"large:min-h-[48px] large:py-2.5\",\n};\n\n/**\n * The hover of a control that may sit on the page, a card or a well (0.33, B′ —\n * docs/colour-roles-harmonization.md §12.1, §12.10): the body ink at 7 %, TRANSLUCENT,\n * so it darkens (in dark, lightens) whatever is under it by the same step — 1.11:1 or\n * more on all three surfaces, both themes, with the label at 7:1 and up. It was\n * `--bg-surface-2`, which since 0.33 is the page itself in light: a ghost button on the\n * page would not have moved at all. Not `--bg-hover` either, which is mixed against the\n * CARD and stands only 1.04:1 from the page. IconButton's `custom` tone uses the same\n * recipe at 12 %.\n */\nexport const HOVER_INK = \"hover:bg-[color-mix(in_srgb,var(--text-primary)_7%,transparent)]\";\n\n// Warm, palette-token-driven so buttons blend with the fields + cards in every theme.\n// Actions default to a warm bordered look (primary = filled warm chip, secondary =\n// outline); `brand` stays the solid accent for the rare strong CTA; `danger` takes the\n// semantic `--danger` family (destructive semantics), so a consumer can re-point the\n// destructive hue instead of inheriting a hard-coded red. Focus rings follow each\n// variant's own accent: brand for the four neutral ones, danger for `danger`.\nexport const buttonVariantClasses: Record<ButtonVariant, string> = {\n primary:\n // Hovers onto `--bg-active`, not `--border` (0.33, §12.9): More contrast steps the\n // border until it is a 3:1 LINE, and as a fill under the label that left 2.7:1.\n \"border border-subtle bg-surface-2 text-primary hover:bg-active focus-visible:ring-brand\",\n secondary: `border border-subtle bg-transparent text-primary ${HOVER_INK} focus-visible:ring-brand`,\n ghost: `bg-transparent text-primary ${HOVER_INK} focus-visible:ring-brand`,\n danger:\n \"bg-danger text-danger-contrast hover:bg-danger-hover focus-visible:ring-danger-strong\",\n brand:\n \"bg-brand text-brand-contrast hover:bg-brand-hover focus-visible:ring-brand\",\n // A text link that is still a `<button>` — keksdose's six hand-rolled\n // `<button className=\"text-brand underline\">` sites (\"Resend code\", \"Show all\",\n // \"Undo\" in a toast…), which act rather than navigate and so must not be anchors.\n // The box padding goes (`p-0`) so it sits in a sentence at the text's own size, but\n // the base's focus ring stays: those copies had `outline-none` and no ring, so\n // a keyboard user tabbing onto them saw nothing at all. `rounded-sm` keeps that\n // ring hugging the word instead of drawing a pill round it.\n link:\n \"rounded-sm p-0 bg-transparent text-brand underline-offset-4 hover:underline focus-visible:ring-brand\",\n};\n\n/**\n * The text colour of a `link` or `ghost` button, over the variant's own.\n *\n * `muted` is the QUIET link keksdose hand-rolls in six places (tours-page:321's \"Mark\n * undone\", transaction-fields:176/215's \"Add line\" / \"Fill total\",\n * invoice-lines-table:376, invoice-review:464): secondary text that darkens to the\n * body colour under the pointer, for an action that must be findable but not compete\n * with the brand-coloured one beside it. `danger` is the same quiet look turning\n * `--danger` on hover — transaction-editor:401's \"Remove split\", which is a text\n * action that destroys something.\n *\n * A `tone` rather than a `link-muted` variant, because it is the axis IconButton\n * already has (`tone=\"muted\"`, `tone=\"danger\"`, same quiet-until-hover meaning), and\n * because the same two looks are wanted on `ghost`.\n *\n * `danger` also reaches the two NEUTRAL boxed variants, where the box carries it:\n * `secondary` becomes an outlined destructive button (`--danger` text and a\n * `--danger-border` outline, the quiet danger fill on hover) and `primary` a soft one\n * (the quiet fill at rest, solid `--danger` on hover). kastlan's meeting-invitations-\n * tab.tsx \"Remove all\" is `variant=\"secondary\" className=\"text-destructive\"` — a\n * destructive action that must sit in a row of secondary buttons without shouting\n * like the solid `variant=\"danger\"` would, and whose red text alone left the border\n * and hover saying \"neutral\". `muted` stays a text look and does nothing on a box, and\n * `danger`, `brand` and `link`'s own colours are left alone: a tone that recoloured a\n * solid danger or brand button would be a second variant under another name.\n */\nexport type ButtonTone = \"default\" | \"muted\" | \"danger\";\n\nconst BUTTON_TONES: Record<Exclude<ButtonTone, \"default\">, string> = {\n muted:\n \"text-secondary hover:text-primary disabled:hover:text-secondary\",\n danger:\n \"text-secondary hover:text-danger focus-visible:ring-danger-strong disabled:hover:text-secondary\",\n};\n\n/** Only the two transparent variants take every tone; see {@link ButtonTone}. */\nconst TONED_VARIANTS = new Set<ButtonVariant>([\"link\", \"ghost\"]);\n\n// `tone=\"danger\"` on the neutral boxed variants — see {@link ButtonTone}. Each string\n// restates every colour the variant sets (border, fill, text, hover, ring) so none of\n// the neutral ones survives the merge.\nconst BUTTON_BOXED_DANGER: Partial<Record<ButtonVariant, string>> = {\n // Not boxed, but its hover is a fill: the danger wash under the danger text, as\n // IconButton's danger tone (0.33). On B′'s translucent ink `--danger` fell to 4.2:1\n // over a light page.\n ghost: \"hover:bg-danger-soft\",\n secondary:\n \"border-danger bg-transparent text-danger hover:bg-danger-soft focus-visible:ring-danger-strong\",\n primary:\n \"border-danger bg-danger-soft text-danger hover:border-[var(--danger)] hover:bg-danger hover:text-danger-contrast focus-visible:ring-danger-strong disabled:hover:border-danger disabled:hover:bg-danger-soft disabled:hover:text-danger\",\n};\n\n// `pressed` on a `link`: the brand colour and a heavier weight, which is exactly what\n// lenkbank's \"All speeds\" toggle paints by hand (gear/hysteresis-charts.tsx:482). After\n// the tone, so a pressed muted link is brand, and an unpressed one is quiet grey.\nexport const BUTTON_LINK_PRESSED =\n \"font-medium text-brand hover:text-brand disabled:hover:text-brand\";\n\n/** The second argument of {@link buttonClasses} in its options form. */\nexport interface ButtonClassesOptions {\n /** See {@link ButtonProps.size}. */\n size?: ButtonSize;\n className?: string;\n}\n\n/**\n * Button classes for the rare case where the styling must land on a non-`<button>`\n * element that {@link Button} can't render — e.g. a router `<Link>` or a Radix\n * AlertDialog Action/Cancel (which must stay the Radix element). Everywhere a real\n * button works, prefer `<Button>`. Draws from the same base, size and variant maps as\n * `<Button>`, so the two stay in lockstep.\n *\n * The second argument is either the extra classes (the pre-0.8.0 form) or\n * `{ size, className }` — `buttonClasses(\"secondary\", { size: \"sm\" })` for keksdose's\n * compact toolbar links.\n */\nexport function buttonClasses(\n variant: ButtonVariant = \"primary\",\n classNameOrOptions?: string | ButtonClassesOptions,\n): string {\n const { size = \"md\", className } =\n typeof classNameOrOptions === \"object\" ? classNameOrOptions : { className: classNameOrOptions };\n return cn(\n BUTTON_BASE,\n BUTTON_SIZES[size],\n variant !== \"link\" && BUTTON_SIZES_LARGE[size],\n buttonVariantClasses[variant],\n className,\n );\n}\n\n// `pressed`: a toggle that is on. The brand glyph on the quiet brand fill — the\n// \"selected\" look of a Chip or a SegmentedControl option, so an on toggle reads as on\n// beside them. After the tone, so a pressed `muted` button is brand, not grey. On hover\n// the glyph turns brand-muted: `--brand` on `--brand-bg-hover` is 2.69–2.80 in four dark\n// presets, under an icon's 3:1 (colour-roles §12.12). Button's `pressed` draws it on its\n// boxed variants too, which is why it lives here and not in ./icon-button.\nexport const ICON_BUTTON_PRESSED =\n \"bg-brand-soft text-brand hover:bg-brand-soft-hover hover:text-brand-muted disabled:hover:bg-brand-soft disabled:hover:text-brand\";\n\n/** @internal The button look shared by the `<button>` and the `<a>` form. */\nexport function buttonLook(variant: ButtonVariant, size: ButtonSize, stretch: boolean | undefined, tone: ButtonTone) {\n return cn(\n BUTTON_BASE,\n BUTTON_SIZES[size],\n // §4: `sm` at `md`'s height and the 48 px target at Large — not on a `link`. In the\n // order `buttonClasses` has it, so the two strings stay identical.\n variant !== \"link\" && BUTTON_SIZES_LARGE[size],\n // In a flex row next to a taller labelled field, `stretch` makes the button\n // fill the field's height so the two line up (self-stretch overrides the row's\n // align-items). No effect outside a flex row / when it's already the tallest.\n stretch && \"self-stretch\",\n buttonVariantClasses[variant],\n tone !== \"default\" && TONED_VARIANTS.has(variant) && BUTTON_TONES[tone],\n tone === \"danger\" && BUTTON_BOXED_DANGER[variant],\n );\n}\n\n/** @internal Calls a `renderLink` / provider link as a function, as ListItem and Chip\n * do — an inline `renderLink` arrow would otherwise be a new component type every\n * render. Both buttons' link forms render through it. */\nexport function RenderedButtonLink({ render, ...props }: KitLinkProps & { render: KitLinkComponent }) {\n return render(props);\n}\n"],"mappings":";AAAA,SAAS,UAAU;AACnB,SAAS,wBAAwB;AA4BjC,MAAM,cAAc;AAAA,EAClB;AAAA,EACA;AACF;AASO,MAAM,qBAAqB;AAgBlC,MAAM,eAA2C;AAAA,EAC/C,IAAI;AAAA,EACJ,IAAI;AACN;AACA,MAAM,qBAAiD;AAAA,EACrD,IAAI;AAAA,EACJ,IAAI;AACN;AAYO,MAAM,YAAY;AAQlB,MAAM,uBAAsD;AAAA,EACjE;AAAA;AAAA;AAAA,IAGE;AAAA;AAAA,EACF,WAAW,oDAAoD,SAAS;AAAA,EACxE,OAAO,+BAA+B,SAAS;AAAA,EAC/C,QACE;AAAA,EACF,OACE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQF,MACE;AACJ;AA8BA,MAAM,eAA+D;AAAA,EACnE,OACE;AAAA,EACF,QACE;AACJ;AAGA,MAAM,iBAAiB,oBAAI,IAAmB,CAAC,QAAQ,OAAO,CAAC;AAK/D,MAAM,sBAA8D;AAAA;AAAA;AAAA;AAAA,EAIlE,OAAO;AAAA,EACP,WACE;AAAA,EACF,SACE;AACJ;AAKO,MAAM,sBACX;AAoBK,SAAS,cACd,UAAyB,WACzB,oBACQ;AACR,QAAM,EAAE,OAAO,MAAM,UAAU,IAC7B,OAAO,uBAAuB,WAAW,qBAAqB,EAAE,WAAW,mBAAmB;AAChG,SAAO;AAAA,IACL;AAAA,IACA,aAAa,IAAI;AAAA,IACjB,YAAY,UAAU,mBAAmB,IAAI;AAAA,IAC7C,qBAAqB,OAAO;AAAA,IAC5B;AAAA,EACF;AACF;AAQO,MAAM,sBACX;AAGK,SAAS,WAAW,SAAwB,MAAkB,SAA8B,MAAkB;AACnH,SAAO;AAAA,IACL;AAAA,IACA,aAAa,IAAI;AAAA;AAAA;AAAA,IAGjB,YAAY,UAAU,mBAAmB,IAAI;AAAA;AAAA;AAAA;AAAA,IAI7C,WAAW;AAAA,IACX,qBAAqB,OAAO;AAAA,IAC5B,SAAS,aAAa,eAAe,IAAI,OAAO,KAAK,aAAa,IAAI;AAAA,IACtE,SAAS,YAAY,oBAAoB,OAAO;AAAA,EAClD;AACF;AAKO,SAAS,mBAAmB,EAAE,QAAQ,GAAG,MAAM,GAAgD;AACpG,SAAO,OAAO,KAAK;AACrB;","names":[]}
1
+ {"version":3,"sources":["../../src/components/button-look.ts"],"sourcesContent":["import { cn } from \"../lib/cn\";\nimport { FOCUS_RING_WIDTH } from \"./focus-ring\";\nimport type { KitLinkComponent, KitLinkProps } from \"../i18n/kit-labels\";\n\n/**\n * The button look, which {@link Button} and {@link IconButton} share (0.34, MOD-2, refactor\n * audit 2026-10). In ui.tsx it was the one knot in an otherwise one-way graph: Button drew\n * IconButton's pressed look, and IconButton drew Button's variant colours, its hover ink\n * and its router link. Both now draw from this module, which renders nothing, so neither\n * imports the other.\n *\n * Public through ./ui by name: `buttonClasses`, `TOUCH_TARGET_LARGE` and the four types.\n * The other exports are the two buttons' shared parts (`@internal`).\n */\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\" | \"brand\" | \"link\";\n\n// Shared base ring for every button-styled element. Kept as a named const so the\n// <Button> component and the {@link buttonClasses} helper draw from one source and\n// can never drift apart.\n//\n// The ring is the kit's focus frame (§5): {@link FOCUS_RING_WIDTH} wide, on keyboard focus\n// (`focus-visible`) — a tap or a click no longer leaves a ring on the button — in each\n// variant's own colour below.\n//\n// `[&_svg]:shrink-0` (0.32.2, kastlan's 0.32.1 report): an icon beside a label keeps its\n// size when the label wraps at Large. Without it the svg was a flex item like the text,\n// and a two-line label squeezed it to a sliver (\"New invoice\" in a page header at 360 px\n// / Extra large).\nconst BUTTON_BASE = cn(\n \"inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:shrink-0\",\n FOCUS_RING_WIDTH,\n);\n\n/**\n * The touch target at Large and Extra large (docs/text-size-harmonization.md §4): at\n * least 48 px, the size a finger needs whatever the type. In px on purpose, like the\n * field floor ({@link FIELD_TOUCH_TEXT}): it is a physical threshold, not a size that\n * follows the text. Normal keeps the kit's own heights; a `large:` variant, so it costs\n * no render and is right on the first paint.\n */\nexport const TOUCH_TARGET_LARGE = \"large:min-h-[48px] large:min-w-[48px]\";\n\nexport type ButtonSize = \"sm\" | \"md\";\n\n// Box geometry per size, split out of the base so the two cannot be merged into one\n// string that a size then has to fight. `md` is the pre-0.8.0 look, unchanged. `sm` is\n// the compact secondary action keksdose repeats by hand as `px-2 py-1 text-xs` (and\n// `px-2 py-0.5 text-xs`) on the buttons in a toolbar or a table header; one rung is\n// enough, so the two spellings meet at `py-1`. There is no `lg`: no app has asked for\n// a bigger text button (IconButton's `lg` is a touch target, not a text size). The\n// variant map comes AFTER the size, so `link`'s `p-0` still wins at either size.\n//\n// At Large and above (§4) `sm` grows to `md`'s height — the padding that makes its\n// 1rem line as tall as `md`'s 1.25rem one — and both reach the 48 px touch target. The\n// type stays `sm`'s: the compact button is still the quieter one. Not on `link`, which\n// is a word in a sentence and has no box to grow (see `buttonLook`).\nconst BUTTON_SIZES: Record<ButtonSize, string> = {\n md: \"gap-2 px-3 py-2 text-sm\",\n sm: \"gap-1.5 px-2 py-1 text-xs\",\n};\nconst BUTTON_SIZES_LARGE: Record<ButtonSize, string> = {\n md: \"large:min-h-[48px]\",\n sm: \"large:min-h-[48px] large:py-2.5\",\n};\n\n/**\n * The hover of a control that may sit on the page, a card or a well (0.33, B′ —\n * docs/colour-roles-harmonization.md §12.1, §12.10): the body ink at 7 %, TRANSLUCENT,\n * so it darkens (in dark, lightens) whatever is under it by the same step — 1.11:1 or\n * more on all three surfaces, both themes, with the label at 7:1 and up. It was\n * `--bg-surface-2`, which since 0.33 is the page itself in light: a ghost button on the\n * page would not have moved at all. Not `--bg-hover` either, which is mixed against the\n * CARD and stands only 1.04:1 from the page. IconButton's `custom` tone uses the same\n * recipe at 12 %.\n */\nexport const HOVER_INK = \"hover:bg-[color-mix(in_srgb,var(--text-primary)_7%,transparent)]\";\n\n// Warm, palette-token-driven so buttons blend with the fields + cards in every theme.\n// Actions default to a warm bordered look (primary = filled warm chip, secondary =\n// outline); `brand` stays the solid accent for the rare strong CTA; `danger` takes the\n// semantic `--danger` family (destructive semantics), so a consumer can re-point the\n// destructive hue instead of inheriting a hard-coded red. Focus rings follow each\n// variant's own accent: brand for the four neutral ones, danger for `danger`.\nexport const buttonVariantClasses: Record<ButtonVariant, string> = {\n primary:\n // Hovers onto `--bg-active`, not `--border` (0.33, §12.9): More contrast steps the\n // border until it is a 3:1 LINE, and as a fill under the label that left 2.7:1.\n \"border border-subtle bg-surface-2 text-primary hover:bg-active focus-visible:ring-brand\",\n secondary: `border border-subtle bg-transparent text-primary ${HOVER_INK} focus-visible:ring-brand`,\n ghost: `bg-transparent text-primary ${HOVER_INK} focus-visible:ring-brand`,\n danger:\n \"bg-danger text-danger-contrast hover:bg-danger-hover focus-visible:ring-danger-strong\",\n brand:\n \"bg-brand text-brand-contrast hover:bg-brand-hover focus-visible:ring-brand\",\n // A text link that is still a `<button>` — keksdose's six hand-rolled\n // `<button className=\"text-brand underline\">` sites (\"Resend code\", \"Show all\",\n // \"Undo\" in a toast…), which act rather than navigate and so must not be anchors.\n // The box padding goes (`p-0`) so it sits in a sentence at the text's own size, but\n // the base's focus ring stays: those copies had `outline-none` and no ring, so\n // a keyboard user tabbing onto them saw nothing at all. `rounded-sm` keeps that\n // ring hugging the word instead of drawing a pill round it.\n //\n // Under More contrast the word reads `--brand-muted`, the brand AS TEXT (0.36,\n // colour-roles §13.3 decision 8): `--brand` is solved as a mark (3:1), and on More\n // contrast's darker light well Moss's read 3.68:1; the derived dark brands sit at 3.0\n // as text too. Both modes, since `contrast:` does not split them.\n link:\n \"rounded-sm p-0 bg-transparent text-brand underline-offset-4 hover:underline focus-visible:ring-brand contrast:text-brand-muted\",\n};\n\n/**\n * The text colour of a `link` or `ghost` button, over the variant's own.\n *\n * `muted` is the QUIET link keksdose hand-rolls in six places (tours-page:321's \"Mark\n * undone\", transaction-fields:176/215's \"Add line\" / \"Fill total\",\n * invoice-lines-table:376, invoice-review:464): secondary text that darkens to the\n * body colour under the pointer, for an action that must be findable but not compete\n * with the brand-coloured one beside it. `danger` is the same quiet look turning\n * `--danger` on hover — transaction-editor:401's \"Remove split\", which is a text\n * action that destroys something.\n *\n * A `tone` rather than a `link-muted` variant, because it is the axis IconButton\n * already has (`tone=\"muted\"`, `tone=\"danger\"`, same quiet-until-hover meaning), and\n * because the same two looks are wanted on `ghost`.\n *\n * `danger` also reaches the two NEUTRAL boxed variants, where the box carries it:\n * `secondary` becomes an outlined destructive button (`--danger` text and a\n * `--danger-border` outline, the quiet danger fill on hover) and `primary` a soft one\n * (the quiet fill at rest, solid `--danger` on hover). kastlan's meeting-invitations-\n * tab.tsx \"Remove all\" is `variant=\"secondary\" className=\"text-destructive\"` — a\n * destructive action that must sit in a row of secondary buttons without shouting\n * like the solid `variant=\"danger\"` would, and whose red text alone left the border\n * and hover saying \"neutral\". `muted` stays a text look and does nothing on a box, and\n * `danger`, `brand` and `link`'s own colours are left alone: a tone that recoloured a\n * solid danger or brand button would be a second variant under another name.\n */\nexport type ButtonTone = \"default\" | \"muted\" | \"danger\";\n\n// `contrast:text-secondary` restates the tone's own colour under the `contrast:` variant,\n// so tailwind-merge drops the link's `contrast:text-brand-muted` (0.36): a quiet link\n// stays quiet under More contrast. On `ghost` it reads the same either way.\nconst BUTTON_TONES: Record<Exclude<ButtonTone, \"default\">, string> = {\n muted:\n \"text-secondary hover:text-primary disabled:hover:text-secondary contrast:text-secondary\",\n danger:\n \"text-secondary hover:text-danger focus-visible:ring-danger-strong disabled:hover:text-secondary contrast:text-secondary\",\n};\n\n/** Only the two transparent variants take every tone; see {@link ButtonTone}. */\nconst TONED_VARIANTS = new Set<ButtonVariant>([\"link\", \"ghost\"]);\n\n// `tone=\"danger\"` on the neutral boxed variants — see {@link ButtonTone}. Each string\n// restates every colour the variant sets (border, fill, text, hover, ring) so none of\n// the neutral ones survives the merge.\nconst BUTTON_BOXED_DANGER: Partial<Record<ButtonVariant, string>> = {\n // Not boxed, but its hover is a fill: the danger wash under the danger text, as\n // IconButton's danger tone (0.33). On B′'s translucent ink `--danger` fell to 4.2:1\n // over a light page.\n ghost: \"hover:bg-danger-soft\",\n secondary:\n \"border-danger bg-transparent text-danger hover:bg-danger-soft focus-visible:ring-danger-strong\",\n primary:\n \"border-danger bg-danger-soft text-danger hover:border-[var(--danger)] hover:bg-danger hover:text-danger-contrast focus-visible:ring-danger-strong disabled:hover:border-danger disabled:hover:bg-danger-soft disabled:hover:text-danger\",\n};\n\n// `pressed` on a `link`: the brand colour and a heavier weight, which is exactly what\n// lenkbank's \"All speeds\" toggle paints by hand (gear/hysteresis-charts.tsx:482). After\n// the tone, so a pressed muted link is brand, and an unpressed one is quiet grey. Under\n// More contrast the brand as text, `--brand-muted`, at rest and hovered (0.36, as the\n// `link` variant), whatever the tone.\nexport const BUTTON_LINK_PRESSED =\n \"font-medium text-brand hover:text-brand disabled:hover:text-brand contrast:text-brand-muted contrast:hover:text-brand-muted contrast:disabled:hover:text-brand-muted\";\n\n/** The second argument of {@link buttonClasses} in its options form. */\nexport interface ButtonClassesOptions {\n /** See {@link ButtonProps.size}. */\n size?: ButtonSize;\n className?: string;\n}\n\n/**\n * Button classes for the rare case where the styling must land on a non-`<button>`\n * element that {@link Button} can't render — e.g. a router `<Link>` or a Radix\n * AlertDialog Action/Cancel (which must stay the Radix element). Everywhere a real\n * button works, prefer `<Button>`. Draws from the same base, size and variant maps as\n * `<Button>`, so the two stay in lockstep.\n *\n * The second argument is either the extra classes (the pre-0.8.0 form) or\n * `{ size, className }` — `buttonClasses(\"secondary\", { size: \"sm\" })` for keksdose's\n * compact toolbar links.\n */\nexport function buttonClasses(\n variant: ButtonVariant = \"primary\",\n classNameOrOptions?: string | ButtonClassesOptions,\n): string {\n const { size = \"md\", className } =\n typeof classNameOrOptions === \"object\" ? classNameOrOptions : { className: classNameOrOptions };\n return cn(\n BUTTON_BASE,\n BUTTON_SIZES[size],\n variant !== \"link\" && BUTTON_SIZES_LARGE[size],\n buttonVariantClasses[variant],\n className,\n );\n}\n\n// `pressed`: a toggle that is on. The brand glyph on the quiet brand fill — the\n// \"selected\" look of a Chip or a SegmentedControl option, so an on toggle reads as on\n// beside them. After the tone, so a pressed `muted` button is brand, not grey. On hover\n// the glyph turns brand-muted: `--brand` on `--brand-bg-hover` is 2.69–2.80 in four dark\n// presets, under an icon's 3:1 (colour-roles §12.12). Button's `pressed` draws it on its\n// boxed variants too, which is why it lives here and not in ./icon-button.\nexport const ICON_BUTTON_PRESSED =\n \"bg-brand-soft text-brand hover:bg-brand-soft-hover hover:text-brand-muted disabled:hover:bg-brand-soft disabled:hover:text-brand\";\n\n/** @internal The button look shared by the `<button>` and the `<a>` form. */\nexport function buttonLook(variant: ButtonVariant, size: ButtonSize, stretch: boolean | undefined, tone: ButtonTone) {\n return cn(\n BUTTON_BASE,\n BUTTON_SIZES[size],\n // §4: `sm` at `md`'s height and the 48 px target at Large — not on a `link`. In the\n // order `buttonClasses` has it, so the two strings stay identical.\n variant !== \"link\" && BUTTON_SIZES_LARGE[size],\n // In a flex row next to a taller labelled field, `stretch` makes the button\n // fill the field's height so the two line up (self-stretch overrides the row's\n // align-items). No effect outside a flex row / when it's already the tallest.\n stretch && \"self-stretch\",\n buttonVariantClasses[variant],\n tone !== \"default\" && TONED_VARIANTS.has(variant) && BUTTON_TONES[tone],\n tone === \"danger\" && BUTTON_BOXED_DANGER[variant],\n );\n}\n\n/** @internal Calls a `renderLink` / provider link as a function, as ListItem and Chip\n * do — an inline `renderLink` arrow would otherwise be a new component type every\n * render. Both buttons' link forms render through it. */\nexport function RenderedButtonLink({ render, ...props }: KitLinkProps & { render: KitLinkComponent }) {\n return render(props);\n}\n"],"mappings":";AAAA,SAAS,UAAU;AACnB,SAAS,wBAAwB;AA4BjC,MAAM,cAAc;AAAA,EAClB;AAAA,EACA;AACF;AASO,MAAM,qBAAqB;AAgBlC,MAAM,eAA2C;AAAA,EAC/C,IAAI;AAAA,EACJ,IAAI;AACN;AACA,MAAM,qBAAiD;AAAA,EACrD,IAAI;AAAA,EACJ,IAAI;AACN;AAYO,MAAM,YAAY;AAQlB,MAAM,uBAAsD;AAAA,EACjE;AAAA;AAAA;AAAA,IAGE;AAAA;AAAA,EACF,WAAW,oDAAoD,SAAS;AAAA,EACxE,OAAO,+BAA+B,SAAS;AAAA,EAC/C,QACE;AAAA,EACF,OACE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaF,MACE;AACJ;AAiCA,MAAM,eAA+D;AAAA,EACnE,OACE;AAAA,EACF,QACE;AACJ;AAGA,MAAM,iBAAiB,oBAAI,IAAmB,CAAC,QAAQ,OAAO,CAAC;AAK/D,MAAM,sBAA8D;AAAA;AAAA;AAAA;AAAA,EAIlE,OAAO;AAAA,EACP,WACE;AAAA,EACF,SACE;AACJ;AAOO,MAAM,sBACX;AAoBK,SAAS,cACd,UAAyB,WACzB,oBACQ;AACR,QAAM,EAAE,OAAO,MAAM,UAAU,IAC7B,OAAO,uBAAuB,WAAW,qBAAqB,EAAE,WAAW,mBAAmB;AAChG,SAAO;AAAA,IACL;AAAA,IACA,aAAa,IAAI;AAAA,IACjB,YAAY,UAAU,mBAAmB,IAAI;AAAA,IAC7C,qBAAqB,OAAO;AAAA,IAC5B;AAAA,EACF;AACF;AAQO,MAAM,sBACX;AAGK,SAAS,WAAW,SAAwB,MAAkB,SAA8B,MAAkB;AACnH,SAAO;AAAA,IACL;AAAA,IACA,aAAa,IAAI;AAAA;AAAA;AAAA,IAGjB,YAAY,UAAU,mBAAmB,IAAI;AAAA;AAAA;AAAA;AAAA,IAI7C,WAAW;AAAA,IACX,qBAAqB,OAAO;AAAA,IAC5B,SAAS,aAAa,eAAe,IAAI,OAAO,KAAK,aAAa,IAAI;AAAA,IACtE,SAAS,YAAY,oBAAoB,OAAO;AAAA,EAClD;AACF;AAKO,SAAS,mBAAmB,EAAE,QAAQ,GAAG,MAAM,GAAgD;AACpG,SAAO,OAAO,KAAK;AACrB;","names":[]}
@@ -24,9 +24,6 @@ interface CalculatorButtonProps {
24
24
  className?: string;
25
25
  /** The trigger's accessible name; wins over `calculator.open` from the provider. */
26
26
  "aria-label"?: string;
27
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).
28
- * Names the trigger only when `aria-label` is absent. */
29
- ariaLabel?: string;
30
27
  /** Screen-reader names for the panel's own controls. `aria-label` above already
31
28
  * covers the trigger; these three were hardcoded English inside the popover,
32
29
  * which a translating host had no way to reach. */
@@ -38,5 +35,5 @@ interface CalculatorButtonProps {
38
35
  * `relative` wrapper, positioned absolutely. The keypad seeds from the current
39
36
  * field value and writes evaluated results straight back through `onChange`.
40
37
  */
41
- export declare function CalculatorButton({ value, onChange, className, "aria-label": ariaLabelAttr, ariaLabel: ariaLabelProp, labels: labelsProp, }: CalculatorButtonProps): import("react").JSX.Element;
38
+ export declare function CalculatorButton({ value, onChange, className, "aria-label": ariaLabel, labels: labelsProp, }: CalculatorButtonProps): import("react").JSX.Element;
42
39
  export {};
@@ -142,11 +142,9 @@ function CalculatorButton({
142
142
  value,
143
143
  onChange,
144
144
  className,
145
- "aria-label": ariaLabelAttr,
146
- ariaLabel: ariaLabelProp,
145
+ "aria-label": ariaLabel,
147
146
  labels: labelsProp
148
147
  }) {
149
- const ariaLabel = ariaLabelAttr ?? ariaLabelProp;
150
148
  const fromProps = useMemo(
151
149
  () => ariaLabel === void 0 ? labelsProp : { ...labelsProp, open: ariaLabel },
152
150
  [ariaLabel, labelsProp]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/calculator.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Calculator as CalculatorIcon, Delete } from \"lucide-react\";\nimport { Popover } from \"./popover\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_TOUCH_TEXT } from \"./field-base\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { evaluateExpression, formatResult, isBareAmount, splitLeadingSign } from \"../lib/calc\";\nimport {\n DEFAULT_CALCULATOR_LABELS,\n useKitLabels,\n type CalculatorLabels,\n} from \"../i18n/kit-labels\";\n\n/** Screen-reader names for the calculator panel's own controls — any part of the\n * kit's `calculator` namespace. Once three optional keys of its own; widened, not\n * replaced, so every object that type-checked before still does. */\nexport type CalculatorButtonLabels = Partial<CalculatorLabels>;\n\ninterface CalculatorButtonProps {\n /** Current field text — seeds the keypad so the user can keep calculating\n * from what's already entered. */\n value: string;\n /**\n * Called with the evaluated result whenever the running expression resolves\n * to a number, so the host field tracks the keypad live.\n *\n * `expression` is the TEXT that result came out of — `\"-42+50\"` for an 8 — and\n * is omitted when there was no calculation (a cleared pad). A host that keeps\n * the sign of its figure OUTSIDE the text (see `AmountInput`'s `negative` /\n * `onNegativeChange`, where a direction toggle owns it) cannot otherwise tell\n * that 8 from a freshly typed one: the first is a sum that came out positive\n * and has to take the direction with it, the second is a magnitude that must\n * leave the direction alone. Hosts whose text carries its own sign can take\n * the one argument and ignore this.\n */\n onChange: (value: string, expression?: string) => void;\n className?: string;\n /** The trigger's accessible name; wins over `calculator.open` from the provider. */\n \"aria-label\"?: string;\n /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).\n * Names the trigger only when `aria-label` is absent. */\n ariaLabel?: string;\n /** Screen-reader names for the panel's own controls. `aria-label` above already\n * covers the trigger; these three were hardcoded English inside the popover,\n * which a translating host had no way to reach. */\n labels?: CalculatorButtonLabels;\n}\n\n/** Keypad layout (4 columns). The final C/= row is rendered separately. `name` is\n * the key of the {@link CalculatorLabels} entry that names a non-digit key; a digit\n * is named by its own glyph. */\ntype Key =\n | { kind: \"ins\"; label: string; ins: string; accent?: boolean; name?: KeyName }\n | { kind: \"back\" };\n\ntype KeyName = \"plus\" | \"minus\" | \"times\" | \"divide\" | \"decimal\";\n\nconst KEYS: Key[] = [\n { kind: \"ins\", label: \"7\", ins: \"7\" },\n { kind: \"ins\", label: \"8\", ins: \"8\" },\n { kind: \"ins\", label: \"9\", ins: \"9\" },\n { kind: \"ins\", label: \"÷\", ins: \"÷\", accent: true, name: \"divide\" },\n { kind: \"ins\", label: \"4\", ins: \"4\" },\n { kind: \"ins\", label: \"5\", ins: \"5\" },\n { kind: \"ins\", label: \"6\", ins: \"6\" },\n { kind: \"ins\", label: \"×\", ins: \"×\", accent: true, name: \"times\" },\n { kind: \"ins\", label: \"1\", ins: \"1\" },\n { kind: \"ins\", label: \"2\", ins: \"2\" },\n { kind: \"ins\", label: \"3\", ins: \"3\" },\n { kind: \"ins\", label: \"−\", ins: \"−\", accent: true, name: \"minus\" },\n { kind: \"ins\", label: \"0\", ins: \"0\" },\n { kind: \"ins\", label: \".\", ins: \".\", name: \"decimal\" },\n { kind: \"back\" },\n { kind: \"ins\", label: \"+\", ins: \"+\", accent: true, name: \"plus\" },\n];\n\n// The kit's one focus frame (text-size §5). Until 0.34 the keys rang in `ring-strong` on\n// `focus:`, so a pointer click left a grey ring on the key it pressed (DOC-7).\nconst KEY_BASE = `flex h-9 items-center justify-center rounded-md text-sm font-medium transition-colors ${FOCUS_RING}`;\n// Two key families, two fills: digits sit on the inset surface and darken on\n// hover, operators sit a step down on `--bg-active` and lift toward that surface —\n// the same split `NumberPadSheet` uses for this keypad on a phone. The tokens\n// flip with the theme, so neither needs a `dark:` twin.\n//\n// 0.33 (docs/colour-roles-harmonization.md §12.2, §12.9): the digits hover onto\n// `--bg-active`, 1.11–1.18:1 from the key — `--bg-hover` is mixed against the card and\n// sat only 1.04:1 from the well, which in light is the page. The operators rest on\n// `--bg-active` rather than `--border`: More contrast steps the border until it is a\n// 3:1 LINE, and as a fill under the label that left 2.2–2.7:1.\nconst KEY_DIGIT = \"bg-surface-2 text-primary hover:bg-active\";\nconst KEY_ACCENT = \"bg-active text-secondary hover:bg-surface-2\";\n\nfunction seed(initial: string): string {\n const t = initial.trim();\n return t === \"-\" ? \"\" : t;\n}\n\nfunction CalculatorPanel({\n initial,\n onChange,\n labels,\n}: {\n initial: string;\n onChange: (value: string, expression?: string) => void;\n labels: CalculatorLabels;\n}) {\n const [expr, setExpr] = useState(() => seed(initial));\n const inputRef = useRef<HTMLInputElement>(null);\n // What the pad SHOWS. Normally the expression it holds — but when the host owns\n // the sign of the figure, the magnitude it hands back is not the whole number:\n // type 50 into a pad opened on an outflow and the host answers \"-50\". Derived\n // rather than stored, so there is no echo to guard against and a host that\n // ignores `onChange` entirely (a static `value`) still sees its own keystrokes:\n // the pad only defers when the host's text is character-for-character its own\n // last number with a minus in front.\n const host = splitLeadingSign(initial.trim());\n const text = host.sign === \"-\" && isBareAmount(expr) && host.rest === expr.trim() ? initial.trim() : expr;\n const result = evaluateExpression(text);\n\n // Focus the display on open so the user can type or hit Enter immediately.\n useEffect(() => {\n inputRef.current?.focus();\n }, []);\n\n // Update the host field whenever the expression resolves to a number; an\n // incomplete expression (e.g. \"12+\") simply leaves the field untouched. The\n // expression rides along with the result — see `onChange` above.\n const apply = (next: string) => {\n setExpr(next);\n const n = evaluateExpression(next);\n if (n !== null) onChange(formatResult(n), next);\n };\n\n const clearAll = () => {\n setExpr(\"\");\n onChange(\"\");\n };\n\n const equals = () => {\n if (result === null) return;\n const out = formatResult(result);\n setExpr(out);\n onChange(out, text);\n };\n\n return (\n <div className=\"space-y-2\">\n <input\n ref={inputRef}\n aria-label={labels.calculation}\n value={text}\n inputMode=\"decimal\"\n onChange={(e) => apply(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n equals();\n }\n }}\n // Arithmetic reads left to right in every script, and a calculator display\n // is right-aligned in every script too — so both stay physical, pinned\n // with `dir=\"ltr\"` rather than mirrored in a right-to-left form.\n dir=\"ltr\"\n className={cn(\"block w-full rounded-md border border-subtle bg-surface px-2 py-1.5 text-right font-mono text-sm text-primary focus:border-strong focus:ring-strong\", FIELD_TOUCH_TEXT)}\n />\n <div dir=\"ltr\" className=\"h-4 pr-1 text-right font-mono text-xs text-placeholder\">\n {result !== null && formatResult(result) !== text.trim() ? `= ${formatResult(result)}` : \"\"}\n </div>\n <div className=\"grid grid-cols-4 gap-1.5\">\n {KEYS.map((key, i) =>\n key.kind === \"back\" ? (\n <button\n key=\"back\"\n type=\"button\"\n aria-label={labels.backspace}\n onClick={() => apply(text.slice(0, -1))}\n className={cn(KEY_BASE, KEY_ACCENT)}\n >\n <Delete className=\"size-4\" />\n </button>\n ) : (\n <button\n key={`${key.ins}-${i}`}\n type=\"button\"\n aria-label={key.name ? labels[key.name] : undefined}\n onClick={() => apply(text + key.ins)}\n className={cn(KEY_BASE, key.accent ? KEY_ACCENT : KEY_DIGIT)}\n >\n {key.label}\n </button>\n ),\n )}\n <button\n type=\"button\"\n // \"C\" is a convention, not a name: read aloud it is the letter.\n aria-label={labels.clear}\n onClick={clearAll}\n className={cn(KEY_BASE, \"col-span-2\", KEY_ACCENT)}\n >\n C\n </button>\n <button\n type=\"button\"\n aria-label={labels.equals}\n onClick={equals}\n className={cn(\n KEY_BASE,\n // The one chip that inverts against the page. There is no hover token for\n // an inverted fill, so the hover is the chip mixed back toward the\n // surface — a step quieter in either theme, and still a colour, so\n // KEY_BASE's `transition-colors` carries it.\n \"col-span-2 bg-inverse text-inverse hover:bg-[color-mix(in_oklab,var(--bg-inverse)_88%,var(--bg-surface))]\",\n )}\n >\n =\n </button>\n </div>\n </div>\n );\n}\n\n/**\n * A calculator icon that opens a small keypad popover anchored to its host\n * field. Designed as a trailing-edge adornment: render it inside the field's\n * `relative` wrapper, positioned absolutely. The keypad seeds from the current\n * field value and writes evaluated results straight back through `onChange`.\n */\nexport function CalculatorButton({\n value,\n onChange,\n className,\n \"aria-label\": ariaLabelAttr,\n ariaLabel: ariaLabelProp,\n labels: labelsProp,\n}: CalculatorButtonProps) {\n // prop > provider > English, with the name prop folded in as the `open` key it has\n // always been. The DOM spelling wins over the deprecated `ariaLabel`.\n const ariaLabel = ariaLabelAttr ?? ariaLabelProp;\n const fromProps = useMemo(\n () => (ariaLabel === undefined ? labelsProp : { ...labelsProp, open: ariaLabel }),\n [ariaLabel, labelsProp],\n );\n const labels = useKitLabels(\"calculator\", DEFAULT_CALCULATOR_LABELS, fromProps);\n return (\n <Popover\n width=\"14rem\"\n labels={{ panel: labels.panel }}\n trigger={({ open, toggle, ref }) => (\n <button\n ref={ref}\n type=\"button\"\n tabIndex={-1}\n onClick={toggle}\n aria-label={labels.open}\n aria-expanded={open}\n className={cn(\n \"flex items-center justify-center text-placeholder transition-colors hover:text-secondary\",\n className,\n )}\n >\n <CalculatorIcon className=\"size-4\" />\n </button>\n )}\n >\n {() => <CalculatorPanel initial={value} onChange={onChange} labels={labels} />}\n </Popover>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/calculator.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Calculator as CalculatorIcon, Delete } from \"lucide-react\";\nimport { Popover } from \"./popover\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_TOUCH_TEXT } from \"./field-base\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { evaluateExpression, formatResult, isBareAmount, splitLeadingSign } from \"../lib/calc\";\nimport {\n DEFAULT_CALCULATOR_LABELS,\n useKitLabels,\n type CalculatorLabels,\n} from \"../i18n/kit-labels\";\n\n/** Screen-reader names for the calculator panel's own controls — any part of the\n * kit's `calculator` namespace. Once three optional keys of its own; widened, not\n * replaced, so every object that type-checked before still does. */\nexport type CalculatorButtonLabels = Partial<CalculatorLabels>;\n\ninterface CalculatorButtonProps {\n /** Current field text — seeds the keypad so the user can keep calculating\n * from what's already entered. */\n value: string;\n /**\n * Called with the evaluated result whenever the running expression resolves\n * to a number, so the host field tracks the keypad live.\n *\n * `expression` is the TEXT that result came out of — `\"-42+50\"` for an 8 — and\n * is omitted when there was no calculation (a cleared pad). A host that keeps\n * the sign of its figure OUTSIDE the text (see `AmountInput`'s `negative` /\n * `onNegativeChange`, where a direction toggle owns it) cannot otherwise tell\n * that 8 from a freshly typed one: the first is a sum that came out positive\n * and has to take the direction with it, the second is a magnitude that must\n * leave the direction alone. Hosts whose text carries its own sign can take\n * the one argument and ignore this.\n */\n onChange: (value: string, expression?: string) => void;\n className?: string;\n /** The trigger's accessible name; wins over `calculator.open` from the provider. */\n \"aria-label\"?: string;\n /** Screen-reader names for the panel's own controls. `aria-label` above already\n * covers the trigger; these three were hardcoded English inside the popover,\n * which a translating host had no way to reach. */\n labels?: CalculatorButtonLabels;\n}\n\n/** Keypad layout (4 columns). The final C/= row is rendered separately. `name` is\n * the key of the {@link CalculatorLabels} entry that names a non-digit key; a digit\n * is named by its own glyph. */\ntype Key =\n | { kind: \"ins\"; label: string; ins: string; accent?: boolean; name?: KeyName }\n | { kind: \"back\" };\n\ntype KeyName = \"plus\" | \"minus\" | \"times\" | \"divide\" | \"decimal\";\n\nconst KEYS: Key[] = [\n { kind: \"ins\", label: \"7\", ins: \"7\" },\n { kind: \"ins\", label: \"8\", ins: \"8\" },\n { kind: \"ins\", label: \"9\", ins: \"9\" },\n { kind: \"ins\", label: \"÷\", ins: \"÷\", accent: true, name: \"divide\" },\n { kind: \"ins\", label: \"4\", ins: \"4\" },\n { kind: \"ins\", label: \"5\", ins: \"5\" },\n { kind: \"ins\", label: \"6\", ins: \"6\" },\n { kind: \"ins\", label: \"×\", ins: \"×\", accent: true, name: \"times\" },\n { kind: \"ins\", label: \"1\", ins: \"1\" },\n { kind: \"ins\", label: \"2\", ins: \"2\" },\n { kind: \"ins\", label: \"3\", ins: \"3\" },\n { kind: \"ins\", label: \"−\", ins: \"−\", accent: true, name: \"minus\" },\n { kind: \"ins\", label: \"0\", ins: \"0\" },\n { kind: \"ins\", label: \".\", ins: \".\", name: \"decimal\" },\n { kind: \"back\" },\n { kind: \"ins\", label: \"+\", ins: \"+\", accent: true, name: \"plus\" },\n];\n\n// The kit's one focus frame (text-size §5). Until 0.34 the keys rang in `ring-strong` on\n// `focus:`, so a pointer click left a grey ring on the key it pressed (DOC-7).\nconst KEY_BASE = `flex h-9 items-center justify-center rounded-md text-sm font-medium transition-colors ${FOCUS_RING}`;\n// Two key families, two fills: digits sit on the inset surface and darken on\n// hover, operators sit a step down on `--bg-active` and lift toward that surface —\n// the same split `NumberPadSheet` uses for this keypad on a phone. The tokens\n// flip with the theme, so neither needs a `dark:` twin.\n//\n// 0.33 (docs/colour-roles-harmonization.md §12.2, §12.9): the digits hover onto\n// `--bg-active`, 1.11–1.18:1 from the key — `--bg-hover` is mixed against the card and\n// sat only 1.04:1 from the well, which in light is the page. The operators rest on\n// `--bg-active` rather than `--border`: More contrast steps the border until it is a\n// 3:1 LINE, and as a fill under the label that left 2.2–2.7:1.\nconst KEY_DIGIT = \"bg-surface-2 text-primary hover:bg-active\";\nconst KEY_ACCENT = \"bg-active text-secondary hover:bg-surface-2\";\n\nfunction seed(initial: string): string {\n const t = initial.trim();\n return t === \"-\" ? \"\" : t;\n}\n\nfunction CalculatorPanel({\n initial,\n onChange,\n labels,\n}: {\n initial: string;\n onChange: (value: string, expression?: string) => void;\n labels: CalculatorLabels;\n}) {\n const [expr, setExpr] = useState(() => seed(initial));\n const inputRef = useRef<HTMLInputElement>(null);\n // What the pad SHOWS. Normally the expression it holds — but when the host owns\n // the sign of the figure, the magnitude it hands back is not the whole number:\n // type 50 into a pad opened on an outflow and the host answers \"-50\". Derived\n // rather than stored, so there is no echo to guard against and a host that\n // ignores `onChange` entirely (a static `value`) still sees its own keystrokes:\n // the pad only defers when the host's text is character-for-character its own\n // last number with a minus in front.\n const host = splitLeadingSign(initial.trim());\n const text = host.sign === \"-\" && isBareAmount(expr) && host.rest === expr.trim() ? initial.trim() : expr;\n const result = evaluateExpression(text);\n\n // Focus the display on open so the user can type or hit Enter immediately.\n useEffect(() => {\n inputRef.current?.focus();\n }, []);\n\n // Update the host field whenever the expression resolves to a number; an\n // incomplete expression (e.g. \"12+\") simply leaves the field untouched. The\n // expression rides along with the result — see `onChange` above.\n const apply = (next: string) => {\n setExpr(next);\n const n = evaluateExpression(next);\n if (n !== null) onChange(formatResult(n), next);\n };\n\n const clearAll = () => {\n setExpr(\"\");\n onChange(\"\");\n };\n\n const equals = () => {\n if (result === null) return;\n const out = formatResult(result);\n setExpr(out);\n onChange(out, text);\n };\n\n return (\n <div className=\"space-y-2\">\n <input\n ref={inputRef}\n aria-label={labels.calculation}\n value={text}\n inputMode=\"decimal\"\n onChange={(e) => apply(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n equals();\n }\n }}\n // Arithmetic reads left to right in every script, and a calculator display\n // is right-aligned in every script too — so both stay physical, pinned\n // with `dir=\"ltr\"` rather than mirrored in a right-to-left form.\n dir=\"ltr\"\n className={cn(\"block w-full rounded-md border border-subtle bg-surface px-2 py-1.5 text-right font-mono text-sm text-primary focus:border-strong focus:ring-strong\", FIELD_TOUCH_TEXT)}\n />\n <div dir=\"ltr\" className=\"h-4 pr-1 text-right font-mono text-xs text-placeholder\">\n {result !== null && formatResult(result) !== text.trim() ? `= ${formatResult(result)}` : \"\"}\n </div>\n <div className=\"grid grid-cols-4 gap-1.5\">\n {KEYS.map((key, i) =>\n key.kind === \"back\" ? (\n <button\n key=\"back\"\n type=\"button\"\n aria-label={labels.backspace}\n onClick={() => apply(text.slice(0, -1))}\n className={cn(KEY_BASE, KEY_ACCENT)}\n >\n <Delete className=\"size-4\" />\n </button>\n ) : (\n <button\n key={`${key.ins}-${i}`}\n type=\"button\"\n aria-label={key.name ? labels[key.name] : undefined}\n onClick={() => apply(text + key.ins)}\n className={cn(KEY_BASE, key.accent ? KEY_ACCENT : KEY_DIGIT)}\n >\n {key.label}\n </button>\n ),\n )}\n <button\n type=\"button\"\n // \"C\" is a convention, not a name: read aloud it is the letter.\n aria-label={labels.clear}\n onClick={clearAll}\n className={cn(KEY_BASE, \"col-span-2\", KEY_ACCENT)}\n >\n C\n </button>\n <button\n type=\"button\"\n aria-label={labels.equals}\n onClick={equals}\n className={cn(\n KEY_BASE,\n // The one chip that inverts against the page. There is no hover token for\n // an inverted fill, so the hover is the chip mixed back toward the\n // surface — a step quieter in either theme, and still a colour, so\n // KEY_BASE's `transition-colors` carries it.\n \"col-span-2 bg-inverse text-inverse hover:bg-[color-mix(in_oklab,var(--bg-inverse)_88%,var(--bg-surface))]\",\n )}\n >\n =\n </button>\n </div>\n </div>\n );\n}\n\n/**\n * A calculator icon that opens a small keypad popover anchored to its host\n * field. Designed as a trailing-edge adornment: render it inside the field's\n * `relative` wrapper, positioned absolutely. The keypad seeds from the current\n * field value and writes evaluated results straight back through `onChange`.\n */\nexport function CalculatorButton({\n value,\n onChange,\n className,\n \"aria-label\": ariaLabel,\n labels: labelsProp,\n}: CalculatorButtonProps) {\n // prop > provider > English, with the name prop folded in as the `open` key it has\n // always been.\n const fromProps = useMemo(\n () => (ariaLabel === undefined ? labelsProp : { ...labelsProp, open: ariaLabel }),\n [ariaLabel, labelsProp],\n );\n const labels = useKitLabels(\"calculator\", DEFAULT_CALCULATOR_LABELS, fromProps);\n return (\n <Popover\n width=\"14rem\"\n labels={{ panel: labels.panel }}\n trigger={({ open, toggle, ref }) => (\n <button\n ref={ref}\n type=\"button\"\n tabIndex={-1}\n onClick={toggle}\n aria-label={labels.open}\n aria-expanded={open}\n className={cn(\n \"flex items-center justify-center text-placeholder transition-colors hover:text-secondary\",\n className,\n )}\n >\n <CalculatorIcon className=\"size-4\" />\n </button>\n )}\n >\n {() => <CalculatorPanel initial={value} onChange={onChange} labels={labels} />}\n </Popover>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,39 +1,2 @@
1
- import * as core from "./chart-zoom-core.js";
2
1
  export { SharedXZoom } from "./chart-zoom-core.js";
3
2
  export type { ZoomAxes, ZoomAxesSetting, ZoomBinding, ZoomDrag, ZoomFitSeries, ZoomFitSource, ZoomRow, ZoomSelection, ZoomState, ZoomTarget, } from "./chart-zoom-core.js";
4
- /** @deprecated 0.34 — a series without `axis` is on the chart's single y axis, `"y"`;
5
- * removed in 0.35 (API-6b). */
6
- export declare const DEFAULT_Y_AXIS = "y";
7
- /** @deprecated 0.34 — SeriesChart's zoom, which reads it; removed in 0.35 (API-6b). */
8
- export declare const ZOOM_SQUARE_ENOUGH = 0.5;
9
- /** @deprecated 0.34 — SeriesChart's zoom, which reads it; removed in 0.35 (API-6b). */
10
- export declare const ZOOM_MIN_DRAG = 0.02;
11
- /** @deprecated 0.34 — SeriesChart's zoom, which reads a drag this way; removed in 0.35
12
- * (API-6b). */
13
- export declare const zoomAxesFor: typeof core.zoomAxesFor;
14
- /** @deprecated 0.34 — SeriesChart's zoom, which reads a drag this way; removed in 0.35
15
- * (API-6b). */
16
- export declare const selectionFromDrag: typeof core.selectionFromDrag;
17
- /** @deprecated 0.34 — SeriesChart fits its axes with it; removed in 0.35 (API-6b). */
18
- export declare const axisExtent: typeof core.axisExtent;
19
- /** @deprecated 0.34 — SeriesChart's zoom, which refits the axes a drag did not name;
20
- * removed in 0.35 (API-6b). */
21
- export declare const fitYToX: typeof core.fitYToX;
22
- /** @deprecated 0.34 — SeriesChart's zoom, which refits the axes a drag did not name;
23
- * removed in 0.35 (API-6b). */
24
- export declare const fitXToY: typeof core.fitXToY;
25
- /** @deprecated 0.34 — SeriesChart's zoom, which keeps its own state; removed in 0.35
26
- * (API-6b). */
27
- export declare const NO_ZOOM: core.ZoomState;
28
- /** @deprecated 0.34 — SeriesChart's zoom, which keeps its own state; removed in 0.35
29
- * (API-6b). */
30
- export declare const zoomAfter: typeof core.zoomAfter;
31
- /** @deprecated 0.34 — SeriesChart's zoom, which keeps its own state; removed in 0.35
32
- * (API-6b). */
33
- export declare const zoomDomains: typeof core.zoomDomains;
34
- /** @deprecated 0.34 — SeriesChart's `zoomAxes` prop, which says what its default is;
35
- * removed in 0.35 (API-6b). */
36
- export declare const defaultZoomAxes: typeof core.defaultZoomAxes;
37
- /** @deprecated 0.34 — SeriesChart, which is a chart wrapped in it, and SharedXZoom;
38
- * removed in 0.35 (API-6b). */
39
- export declare const withChartZoom: typeof core.withChartZoom;
@@ -1,33 +1,6 @@
1
1
  "use client";
2
- import * as core from "./chart-zoom-core.js";
3
2
  import { SharedXZoom } from "./chart-zoom-core.js";
4
- const DEFAULT_Y_AXIS = core.DEFAULT_Y_AXIS;
5
- const ZOOM_SQUARE_ENOUGH = core.ZOOM_SQUARE_ENOUGH;
6
- const ZOOM_MIN_DRAG = core.ZOOM_MIN_DRAG;
7
- const zoomAxesFor = core.zoomAxesFor;
8
- const selectionFromDrag = core.selectionFromDrag;
9
- const axisExtent = core.axisExtent;
10
- const fitYToX = core.fitYToX;
11
- const fitXToY = core.fitXToY;
12
- const NO_ZOOM = core.NO_ZOOM;
13
- const zoomAfter = core.zoomAfter;
14
- const zoomDomains = core.zoomDomains;
15
- const defaultZoomAxes = core.defaultZoomAxes;
16
- const withChartZoom = core.withChartZoom;
17
3
  export {
18
- DEFAULT_Y_AXIS,
19
- NO_ZOOM,
20
- SharedXZoom,
21
- ZOOM_MIN_DRAG,
22
- ZOOM_SQUARE_ENOUGH,
23
- axisExtent,
24
- defaultZoomAxes,
25
- fitXToY,
26
- fitYToX,
27
- selectionFromDrag,
28
- withChartZoom,
29
- zoomAfter,
30
- zoomAxesFor,
31
- zoomDomains
4
+ SharedXZoom
32
5
  };
33
6
  //# sourceMappingURL=chart-zoom.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/chart-zoom.tsx"],"sourcesContent":["// Drag-to-zoom for cartesian charts — the public face of ./chart-zoom-core, which holds\n// the HOC, the shared x window and the arithmetic (its header says how a drag picks its\n// axes). `SeriesChart` is how an app zooms; `SharedXZoom` gives a column of charts one x\n// window.\n//\n// 0.34 (refactor audit 2026-10, API-6b = MOD-10): the arithmetic and the HOC leave the\n// public surface in 0.35. They were exported because lenkbank's, later Kurvenschmiede's,\n// own tests used them; no file in any of the three apps names one today. The kit's own\n// modules import them from the core, so their removal is the block below.\nimport * as core from \"./chart-zoom-core\";\n\nexport { SharedXZoom } from \"./chart-zoom-core\";\nexport type {\n ZoomAxes,\n ZoomAxesSetting,\n ZoomBinding,\n ZoomDrag,\n ZoomFitSeries,\n ZoomFitSource,\n ZoomRow,\n ZoomSelection,\n ZoomState,\n ZoomTarget,\n} from \"./chart-zoom-core\";\n\n/* ── Deprecated 0.34, removed in 0.35 ─────────────────────────────────────\n * Each is the core's own value under its old public name: SeriesChart draws the zoom\n * these compute, and a chart's `zoomAxes` prop is what an app sets. */\n\n/** @deprecated 0.34 — a series without `axis` is on the chart's single y axis, `\"y\"`;\n * removed in 0.35 (API-6b). */\nexport const DEFAULT_Y_AXIS = core.DEFAULT_Y_AXIS;\n/** @deprecated 0.34 — SeriesChart's zoom, which reads it; removed in 0.35 (API-6b). */\nexport const ZOOM_SQUARE_ENOUGH = core.ZOOM_SQUARE_ENOUGH;\n/** @deprecated 0.34 — SeriesChart's zoom, which reads it; removed in 0.35 (API-6b). */\nexport const ZOOM_MIN_DRAG = core.ZOOM_MIN_DRAG;\n/** @deprecated 0.34 — SeriesChart's zoom, which reads a drag this way; removed in 0.35\n * (API-6b). */\nexport const zoomAxesFor = core.zoomAxesFor;\n/** @deprecated 0.34 — SeriesChart's zoom, which reads a drag this way; removed in 0.35\n * (API-6b). */\nexport const selectionFromDrag = core.selectionFromDrag;\n/** @deprecated 0.34 — SeriesChart fits its axes with it; removed in 0.35 (API-6b). */\nexport const axisExtent = core.axisExtent;\n/** @deprecated 0.34 — SeriesChart's zoom, which refits the axes a drag did not name;\n * removed in 0.35 (API-6b). */\nexport const fitYToX = core.fitYToX;\n/** @deprecated 0.34 — SeriesChart's zoom, which refits the axes a drag did not name;\n * removed in 0.35 (API-6b). */\nexport const fitXToY = core.fitXToY;\n/** @deprecated 0.34 — SeriesChart's zoom, which keeps its own state; removed in 0.35\n * (API-6b). */\nexport const NO_ZOOM = core.NO_ZOOM;\n/** @deprecated 0.34 — SeriesChart's zoom, which keeps its own state; removed in 0.35\n * (API-6b). */\nexport const zoomAfter = core.zoomAfter;\n/** @deprecated 0.34 — SeriesChart's zoom, which keeps its own state; removed in 0.35\n * (API-6b). */\nexport const zoomDomains = core.zoomDomains;\n/** @deprecated 0.34 — SeriesChart's `zoomAxes` prop, which says what its default is;\n * removed in 0.35 (API-6b). */\nexport const defaultZoomAxes = core.defaultZoomAxes;\n/** @deprecated 0.34 — SeriesChart, which is a chart wrapped in it, and SharedXZoom;\n * removed in 0.35 (API-6b). */\nexport const withChartZoom = core.withChartZoom;\n"],"mappings":";AASA,YAAY,UAAU;AAEtB,SAAS,mBAAmB;AAoBrB,MAAM,iBAAiB,KAAK;AAE5B,MAAM,qBAAqB,KAAK;AAEhC,MAAM,gBAAgB,KAAK;AAG3B,MAAM,cAAc,KAAK;AAGzB,MAAM,oBAAoB,KAAK;AAE/B,MAAM,aAAa,KAAK;AAGxB,MAAM,UAAU,KAAK;AAGrB,MAAM,UAAU,KAAK;AAGrB,MAAM,UAAU,KAAK;AAGrB,MAAM,YAAY,KAAK;AAGvB,MAAM,cAAc,KAAK;AAGzB,MAAM,kBAAkB,KAAK;AAG7B,MAAM,gBAAgB,KAAK;","names":[]}
1
+ {"version":3,"sources":["../../src/components/chart-zoom.tsx"],"sourcesContent":["// Drag-to-zoom for cartesian charts — the public face of ./chart-zoom-core, which holds\n// the HOC, the shared x window and the arithmetic (its header says how a drag picks its\n// axes). `SeriesChart` is how an app zooms; `SharedXZoom` gives a column of charts one x\n// window.\n//\n// Those two and the zoom's types are all that is public. The arithmetic and the HOC were\n// too until 0.35 (refactor audit 2026-10, API-6b = MOD-10; deprecated in 0.34): exported\n// because lenkbank's, later Kurvenschmiede's, own tests used them, and named by no file in\n// any of the three apps since. The kit's own modules import them from the core.\nexport { SharedXZoom } from \"./chart-zoom-core\";\nexport type {\n ZoomAxes,\n ZoomAxesSetting,\n ZoomBinding,\n ZoomDrag,\n ZoomFitSeries,\n ZoomFitSource,\n ZoomRow,\n ZoomSelection,\n ZoomState,\n ZoomTarget,\n} from \"./chart-zoom-core\";\n"],"mappings":";AASA,SAAS,mBAAmB;","names":[]}
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { type CommitScope } from "./write-lock.js";
3
+ import { type FieldHelpProps } from "./field-parts.js";
3
4
  /** One box of a {@link CheckboxGroup}. */
4
5
  export interface CheckboxGroupOption<T extends string = string> {
5
6
  value: T;
@@ -14,7 +15,7 @@ export interface CheckboxGroupOption<T extends string = string> {
14
15
  /** Whether the legend is drawn. `"sr-only"` keeps it as the group's name for a screen
15
16
  * reader where the dialog's title already says it to the eye. */
16
17
  export type CheckboxGroupLegendVisibility = "visible" | "sr-only";
17
- export interface CheckboxGroupProps<T extends string = string> {
18
+ export interface CheckboxGroupProps<T extends string = string> extends FieldHelpProps {
18
19
  /** The question the boxes answer — the fieldset's `<legend>`, which names the group.
19
20
  * Without one, pass `aria-label`. */
20
21
  legend?: ReactNode;
@@ -56,6 +57,8 @@ export interface CheckboxGroupProps<T extends string = string> {
56
57
  /**
57
58
  * Standing advice for the group, under the boxes, attached to the fieldset with
58
59
  * `aria-describedby` before the error — the field anatomy every kit field keeps.
60
+ * A `help` beside it (0.36) is a "?" after the legend — with a visible legend only —
61
+ * and names nothing: the fieldset is then named by the legend's words alone.
59
62
  */
60
63
  hint?: ReactNode;
61
64
  /** What is wrong with the set, in the caller's words. Under the boxes, on the
@@ -98,4 +101,4 @@ export interface CheckboxGroupProps<T extends string = string> {
98
101
  * error describe the fieldset. For a choice of ONE, use radios ({@link ChoiceCardGroup},
99
102
  * {@link ToggleGroup}).
100
103
  */
101
- export declare function CheckboxGroup<T extends string = string>({ legend, legendVisibility, "aria-label": ariaLabel, options, value, onChange, layout, columns, name, disabled, required, hint, error, invalid, commit, disabledReason, id, className, listClassName, "aria-describedby": ariaDescribedBy, }: CheckboxGroupProps<T>): import("react").JSX.Element;
104
+ export declare function CheckboxGroup<T extends string = string>({ legend, legendVisibility, "aria-label": ariaLabel, options, value, onChange, layout, columns, name, disabled, required, hint, help, error, invalid, commit, disabledReason, id, className, listClassName, "aria-describedby": ariaDescribedBy, }: CheckboxGroupProps<T>): import("react").JSX.Element;
@@ -5,7 +5,13 @@ import { cn } from "../lib/cn.js";
5
5
  import { Checkbox } from "./checkbox.js";
6
6
  import { hasContent } from "../lib/presence.js";
7
7
  import { useCommitReason } from "./write-lock.js";
8
- import { FIELD_ERROR_CLASS, mergeDescribedBy, RequiredStarOnLegend, useInlineHint } from "./field-parts.js";
8
+ import {
9
+ FIELD_ERROR_CLASS,
10
+ fieldHelpMark,
11
+ mergeDescribedBy,
12
+ RequiredStarOnLegend,
13
+ useInlineHint
14
+ } from "./field-parts.js";
9
15
  const COLUMNS = {
10
16
  1: "flex flex-col gap-2",
11
17
  2: "grid grid-cols-1 gap-2 sm:grid-cols-2",
@@ -25,6 +31,7 @@ function CheckboxGroup({
25
31
  disabled,
26
32
  required,
27
33
  hint,
34
+ help,
28
35
  error,
29
36
  invalid,
30
37
  commit,
@@ -37,7 +44,9 @@ function CheckboxGroup({
37
44
  const generated = useId();
38
45
  const hintId = `${generated}-hint`;
39
46
  const errorId = `${generated}-error`;
40
- const shownHint = useInlineHint(hint);
47
+ const helpMark = fieldHelpMark(legend !== void 0 && legendVisibility === "visible" ? help : void 0, legend);
48
+ const legendTextId = `${generated}-legend`;
49
+ const shownHint = useInlineHint(hint, helpMark !== void 0);
41
50
  const showHint = hasContent(shownHint);
42
51
  const showError = hasContent(error);
43
52
  const isInvalid = Boolean(invalid) || showError;
@@ -60,6 +69,7 @@ function CheckboxGroup({
60
69
  {
61
70
  id,
62
71
  "aria-label": ariaLabel,
72
+ "aria-labelledby": helpMark !== void 0 && ariaLabel === void 0 ? legendTextId : void 0,
63
73
  "aria-describedby": mergeDescribedBy(ariaDescribedBy, showHint && hintId, showError && errorId),
64
74
  disabled: disabled && !locked,
65
75
  className: cn("relative m-0 min-w-0 border-0 p-0", className),
@@ -69,8 +79,9 @@ function CheckboxGroup({
69
79
  {
70
80
  className: legendVisibility === "sr-only" ? "sr-only" : cn("mb-2 p-0 text-sm font-medium text-primary", (disabled || locked) && "opacity-60"),
71
81
  children: [
72
- legend,
73
- required && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ms-0.5 text-danger", children: "*" })
82
+ helpMark === void 0 ? legend : /* @__PURE__ */ jsx("span", { id: legendTextId, children: legend }),
83
+ required && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ms-0.5 text-danger", children: "*" }),
84
+ helpMark !== void 0 && /* @__PURE__ */ jsx("span", { className: "ms-1 inline-flex align-middle font-normal", children: helpMark })
74
85
  ]
75
86
  }
76
87
  ),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/checkbox-group.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Checkbox } from \"./checkbox\";\nimport { hasContent } from \"../lib/presence\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport { FIELD_ERROR_CLASS, mergeDescribedBy, RequiredStarOnLegend, useInlineHint } from \"./field-parts\";\n\n/** One box of a {@link CheckboxGroup}. */\nexport interface CheckboxGroupOption<T extends string = string> {\n value: T;\n /** The words beside the box — its accessible name. */\n label: ReactNode;\n /** A line under the label (\"Imprint, Privacy Policy and Terms only\"), attached to the\n * box with `aria-describedby` — {@link Checkbox}'s `description`. */\n hint?: ReactNode;\n /** This box cannot be changed; the others can. */\n disabled?: boolean;\n}\n\n/** Whether the legend is drawn. `\"sr-only\"` keeps it as the group's name for a screen\n * reader where the dialog's title already says it to the eye. */\nexport type CheckboxGroupLegendVisibility = \"visible\" | \"sr-only\";\n\nexport interface CheckboxGroupProps<T extends string = string> {\n /** The question the boxes answer — the fieldset's `<legend>`, which names the group.\n * Without one, pass `aria-label`. */\n legend?: ReactNode;\n /** Default `\"visible\"`. */\n legendVisibility?: CheckboxGroupLegendVisibility;\n \"aria-label\"?: string;\n /** One box per option, in this order. */\n options: CheckboxGroupOption<T>[];\n /** The ticked values. Controlled. */\n value: T[];\n /**\n * The new set on every tick. In the ORDER OF `options`, not the order the boxes were\n * ticked, so a list saved from it does not change with the order someone clicked in\n * (ChoiceCardGroup's rule). A value in `value` that no option carries — a grant for a\n * locale the app no longer offers — is kept, after the known ones: a picker that\n * dropped what it could not show would delete it on the next unrelated tick.\n */\n onChange: (value: T[]) => void;\n /**\n * `\"vertical\"` (default): one box per line. `\"horizontal\"`: a wrapping row, for a few\n * short labels. With `columns`, vertical becomes a grid of that many columns from the\n * `sm` breakpoint up and stays one column on a phone — the reviewer dialog's shape.\n */\n layout?: \"vertical\" | \"horizontal\";\n /** Columns for a vertical group, from `sm` up (one on a phone). Default 1. */\n columns?: 1 | 2 | 3 | 4;\n /** Shared by every box — what a plain form submits the values under. */\n name?: string;\n /** Every box: the fieldset's own `disabled` (which disables them all natively), each\n * box faded as a disabled Checkbox is, and the legend with them. */\n disabled?: boolean;\n /**\n * At least one box must be ticked. Draws the required star after the legend, and puts\n * the native `required` on every box while NONE is ticked — so a form refuses an\n * empty set — and on none once one is (ChoiceCardGroup's rule: checkboxes cannot say\n * \"at least one\" natively).\n */\n required?: boolean;\n /**\n * Standing advice for the group, under the boxes, attached to the fieldset with\n * `aria-describedby` before the error — the field anatomy every kit field keeps.\n */\n hint?: ReactNode;\n /** What is wrong with the set, in the caller's words. Under the boxes, on the\n * fieldset's `aria-describedby`, and paints every box invalid. */\n error?: ReactNode;\n /** Paints every box invalid without a message (the message lives elsewhere). */\n invalid?: boolean;\n /**\n * The group SAVES on change — each tick is written straight away (a reviewer's\n * per-locale grants). Under a locked {@link WriteLockProvider} every box is locked\n * the focusable way with the lock's reason. See {@link Checkbox}'s `commit`.\n */\n commit?: CommitScope;\n /** Why no box can be changed — every box gets it, and it wins over `disabled` (the\n * boxes stay focusable to say it). See {@link Checkbox}'s `disabledReason`. */\n disabledReason?: ReactNode;\n /** The fieldset's id. */\n id?: string;\n /** Classes for the fieldset — the outermost element, where layout belongs. */\n className?: string;\n /** Classes for the box list (the grid or the row). */\n listClassName?: string;\n /** Ids of something else on the page that describes the group; merged before the\n * group's own hint and error. */\n \"aria-describedby\"?: string;\n}\n\n// Literal class strings so Tailwind's scanner sees each one.\nconst COLUMNS: Record<NonNullable<CheckboxGroupProps[\"columns\"]>, string> = {\n 1: \"flex flex-col gap-2\",\n 2: \"grid grid-cols-1 gap-2 sm:grid-cols-2\",\n 3: \"grid grid-cols-1 gap-2 sm:grid-cols-3\",\n 4: \"grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4\",\n};\n\n/**\n * A set of kit {@link Checkbox}es answering one question, in a `<fieldset>`.\n *\n * Kurvenschmiede's reviewer dialog (features/admin/reviewer-dialog.tsx) builds this by\n * hand — a fieldset, an `sr-only` legend, a grid of checkboxes, a `toggle(code, on)`\n * that adds or filters a `string[]` — and keksdose's and kastlan's reviewer grants build\n * the same picker for the same per-locale grant. The hand-built ones differ in the parts\n * that are easy to get wrong: whether the legend names the group, whether a hint or an\n * error is attached to anything, what order the saved list comes out in, and whether a\n * value the picker cannot show survives a tick.\n *\n * Every box is a kit {@link Checkbox}, so the native checkbox semantics stay (each box\n * submits under `name` in a plain form). The legend is the group's name; the hint and\n * error describe the fieldset. For a choice of ONE, use radios ({@link ChoiceCardGroup},\n * {@link ToggleGroup}).\n */\nexport function CheckboxGroup<T extends string = string>({\n legend,\n legendVisibility = \"visible\",\n \"aria-label\": ariaLabel,\n options,\n value,\n onChange,\n layout = \"vertical\",\n columns = 1,\n name,\n disabled,\n required,\n hint,\n error,\n invalid,\n commit,\n disabledReason,\n id,\n className,\n listClassName,\n \"aria-describedby\": ariaDescribedBy,\n}: CheckboxGroupProps<T>) {\n const generated = useId();\n const hintId = `${generated}-hint`;\n const errorId = `${generated}-error`;\n // §4: a FieldHint under the group is its words at Large and on a touch screen, not a\n // lone \"?\" (DUP-2, refactor audit 2026-10). A HelpPopover stays a \"?\".\n const shownHint = useInlineHint(hint);\n const showHint = hasContent(shownHint);\n const showError = hasContent(error);\n const isInvalid = Boolean(invalid) || showError;\n const ticked = new Set(value);\n // Resolved here rather than per box: a reason has to keep the fieldset from being\n // natively `disabled` too, or it would take every box out of the tab order — the\n // thing a reason exists to prevent.\n const reason = useCommitReason(commit, disabledReason);\n const locked = hasContent(reason);\n\n const toggle = (v: T, on: boolean) => {\n const next = new Set(value);\n if (on) next.add(v);\n else next.delete(v);\n const known = new Set(options.map((o) => o.value));\n onChange([\n ...options.map((o) => o.value).filter((x) => next.has(x)),\n // Values no option carries, kept in their own order — see `onChange`.\n ...value.filter((x) => !known.has(x)),\n ]);\n };\n\n return (\n <fieldset\n id={id}\n aria-label={ariaLabel}\n aria-describedby={mergeDescribedBy(ariaDescribedBy, showHint && hintId, showError && errorId)}\n disabled={disabled && !locked}\n // `relative` holds an `sr-only` legend (sr-only-containment.test); `min-w-0`\n // undoes a fieldset's `min-width: min-content`, which would otherwise push a long\n // label past a phone's edge instead of wrapping it.\n className={cn(\"relative m-0 min-w-0 border-0 p-0\", className)}\n >\n {legend !== undefined && (\n <legend\n className={\n legendVisibility === \"sr-only\"\n ? \"sr-only\"\n : cn(\"mb-2 p-0 text-sm font-medium text-primary\", (disabled || locked) && \"opacity-60\")\n }\n >\n {legend}\n {required && (\n <span aria-hidden className=\"ms-0.5 text-danger\">\n *\n </span>\n )}\n </legend>\n )}\n <RequiredStarOnLegend.Provider value={true}>\n <div\n className={cn(\n layout === \"horizontal\" ? \"flex flex-wrap gap-x-4 gap-y-2\" : COLUMNS[columns],\n listClassName,\n )}\n >\n {options.map((o) => (\n <Checkbox\n key={o.value}\n name={name}\n value={o.value}\n label={o.label}\n description={o.hint}\n checked={ticked.has(o.value)}\n onCheckedChange={(on) => toggle(o.value, on)}\n disabled={disabled || o.disabled}\n invalid={isInvalid || undefined}\n required={required && value.length === 0 ? true : undefined}\n disabledReason={reason}\n />\n ))}\n </div>\n </RequiredStarOnLegend.Provider>\n {showHint && (\n // A `div`, not a `p`: a hint may be a node with a link in it.\n <div id={hintId} className=\"mt-1.5 text-caption leading-tight text-muted\">\n {shownHint}\n </div>\n )}\n {showError && (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {error}\n </p>\n )}\n </fieldset>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/checkbox-group.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Checkbox } from \"./checkbox\";\nimport { hasContent } from \"../lib/presence\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport {\n FIELD_ERROR_CLASS,\n fieldHelpMark,\n mergeDescribedBy,\n RequiredStarOnLegend,\n useInlineHint,\n type FieldHelpProps,\n} from \"./field-parts\";\n\n/** One box of a {@link CheckboxGroup}. */\nexport interface CheckboxGroupOption<T extends string = string> {\n value: T;\n /** The words beside the box — its accessible name. */\n label: ReactNode;\n /** A line under the label (\"Imprint, Privacy Policy and Terms only\"), attached to the\n * box with `aria-describedby` — {@link Checkbox}'s `description`. */\n hint?: ReactNode;\n /** This box cannot be changed; the others can. */\n disabled?: boolean;\n}\n\n/** Whether the legend is drawn. `\"sr-only\"` keeps it as the group's name for a screen\n * reader where the dialog's title already says it to the eye. */\nexport type CheckboxGroupLegendVisibility = \"visible\" | \"sr-only\";\n\nexport interface CheckboxGroupProps<T extends string = string> extends FieldHelpProps {\n /** The question the boxes answer — the fieldset's `<legend>`, which names the group.\n * Without one, pass `aria-label`. */\n legend?: ReactNode;\n /** Default `\"visible\"`. */\n legendVisibility?: CheckboxGroupLegendVisibility;\n \"aria-label\"?: string;\n /** One box per option, in this order. */\n options: CheckboxGroupOption<T>[];\n /** The ticked values. Controlled. */\n value: T[];\n /**\n * The new set on every tick. In the ORDER OF `options`, not the order the boxes were\n * ticked, so a list saved from it does not change with the order someone clicked in\n * (ChoiceCardGroup's rule). A value in `value` that no option carries — a grant for a\n * locale the app no longer offers — is kept, after the known ones: a picker that\n * dropped what it could not show would delete it on the next unrelated tick.\n */\n onChange: (value: T[]) => void;\n /**\n * `\"vertical\"` (default): one box per line. `\"horizontal\"`: a wrapping row, for a few\n * short labels. With `columns`, vertical becomes a grid of that many columns from the\n * `sm` breakpoint up and stays one column on a phone — the reviewer dialog's shape.\n */\n layout?: \"vertical\" | \"horizontal\";\n /** Columns for a vertical group, from `sm` up (one on a phone). Default 1. */\n columns?: 1 | 2 | 3 | 4;\n /** Shared by every box — what a plain form submits the values under. */\n name?: string;\n /** Every box: the fieldset's own `disabled` (which disables them all natively), each\n * box faded as a disabled Checkbox is, and the legend with them. */\n disabled?: boolean;\n /**\n * At least one box must be ticked. Draws the required star after the legend, and puts\n * the native `required` on every box while NONE is ticked — so a form refuses an\n * empty set — and on none once one is (ChoiceCardGroup's rule: checkboxes cannot say\n * \"at least one\" natively).\n */\n required?: boolean;\n /**\n * Standing advice for the group, under the boxes, attached to the fieldset with\n * `aria-describedby` before the error — the field anatomy every kit field keeps.\n * A `help` beside it (0.36) is a \"?\" after the legend — with a visible legend only —\n * and names nothing: the fieldset is then named by the legend's words alone.\n */\n hint?: ReactNode;\n /** What is wrong with the set, in the caller's words. Under the boxes, on the\n * fieldset's `aria-describedby`, and paints every box invalid. */\n error?: ReactNode;\n /** Paints every box invalid without a message (the message lives elsewhere). */\n invalid?: boolean;\n /**\n * The group SAVES on change — each tick is written straight away (a reviewer's\n * per-locale grants). Under a locked {@link WriteLockProvider} every box is locked\n * the focusable way with the lock's reason. See {@link Checkbox}'s `commit`.\n */\n commit?: CommitScope;\n /** Why no box can be changed — every box gets it, and it wins over `disabled` (the\n * boxes stay focusable to say it). See {@link Checkbox}'s `disabledReason`. */\n disabledReason?: ReactNode;\n /** The fieldset's id. */\n id?: string;\n /** Classes for the fieldset — the outermost element, where layout belongs. */\n className?: string;\n /** Classes for the box list (the grid or the row). */\n listClassName?: string;\n /** Ids of something else on the page that describes the group; merged before the\n * group's own hint and error. */\n \"aria-describedby\"?: string;\n}\n\n// Literal class strings so Tailwind's scanner sees each one.\nconst COLUMNS: Record<NonNullable<CheckboxGroupProps[\"columns\"]>, string> = {\n 1: \"flex flex-col gap-2\",\n 2: \"grid grid-cols-1 gap-2 sm:grid-cols-2\",\n 3: \"grid grid-cols-1 gap-2 sm:grid-cols-3\",\n 4: \"grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4\",\n};\n\n/**\n * A set of kit {@link Checkbox}es answering one question, in a `<fieldset>`.\n *\n * Kurvenschmiede's reviewer dialog (features/admin/reviewer-dialog.tsx) builds this by\n * hand — a fieldset, an `sr-only` legend, a grid of checkboxes, a `toggle(code, on)`\n * that adds or filters a `string[]` — and keksdose's and kastlan's reviewer grants build\n * the same picker for the same per-locale grant. The hand-built ones differ in the parts\n * that are easy to get wrong: whether the legend names the group, whether a hint or an\n * error is attached to anything, what order the saved list comes out in, and whether a\n * value the picker cannot show survives a tick.\n *\n * Every box is a kit {@link Checkbox}, so the native checkbox semantics stay (each box\n * submits under `name` in a plain form). The legend is the group's name; the hint and\n * error describe the fieldset. For a choice of ONE, use radios ({@link ChoiceCardGroup},\n * {@link ToggleGroup}).\n */\nexport function CheckboxGroup<T extends string = string>({\n legend,\n legendVisibility = \"visible\",\n \"aria-label\": ariaLabel,\n options,\n value,\n onChange,\n layout = \"vertical\",\n columns = 1,\n name,\n disabled,\n required,\n hint,\n help,\n error,\n invalid,\n commit,\n disabledReason,\n id,\n className,\n listClassName,\n \"aria-describedby\": ariaDescribedBy,\n}: CheckboxGroupProps<T>) {\n const generated = useId();\n const hintId = `${generated}-hint`;\n const errorId = `${generated}-error`;\n // §4: a FieldHint under the group is its words at Large and on a touch screen, not a\n // lone \"?\" (DUP-2, refactor audit 2026-10). A HelpPopover stays a \"?\".\n // A `help` \"?\" after a VISIBLE legend (0.36): the label line a group has. Its FieldHint\n // hint is then its words at every size, as everywhere a help stands beside one.\n const helpMark = fieldHelpMark(legend !== undefined && legendVisibility === \"visible\" ? help : undefined, legend);\n const legendTextId = `${generated}-legend`;\n const shownHint = useInlineHint(hint, helpMark !== undefined);\n const showHint = hasContent(shownHint);\n const showError = hasContent(error);\n const isInvalid = Boolean(invalid) || showError;\n const ticked = new Set(value);\n // Resolved here rather than per box: a reason has to keep the fieldset from being\n // natively `disabled` too, or it would take every box out of the tab order — the\n // thing a reason exists to prevent.\n const reason = useCommitReason(commit, disabledReason);\n const locked = hasContent(reason);\n\n const toggle = (v: T, on: boolean) => {\n const next = new Set(value);\n if (on) next.add(v);\n else next.delete(v);\n const known = new Set(options.map((o) => o.value));\n onChange([\n ...options.map((o) => o.value).filter((x) => next.has(x)),\n // Values no option carries, kept in their own order — see `onChange`.\n ...value.filter((x) => !known.has(x)),\n ]);\n };\n\n return (\n <fieldset\n id={id}\n aria-label={ariaLabel}\n // With a help \"?\" inside the legend, the legend's WORDS name the group: a legend\n // names its fieldset by its whole content, the \"?\"'s \"About …\" included.\n aria-labelledby={helpMark !== undefined && ariaLabel === undefined ? legendTextId : undefined}\n aria-describedby={mergeDescribedBy(ariaDescribedBy, showHint && hintId, showError && errorId)}\n disabled={disabled && !locked}\n // `relative` holds an `sr-only` legend (sr-only-containment.test); `min-w-0`\n // undoes a fieldset's `min-width: min-content`, which would otherwise push a long\n // label past a phone's edge instead of wrapping it.\n className={cn(\"relative m-0 min-w-0 border-0 p-0\", className)}\n >\n {legend !== undefined && (\n <legend\n className={\n legendVisibility === \"sr-only\"\n ? \"sr-only\"\n : cn(\"mb-2 p-0 text-sm font-medium text-primary\", (disabled || locked) && \"opacity-60\")\n }\n >\n {helpMark === undefined ? legend : <span id={legendTextId}>{legend}</span>}\n {required && (\n <span aria-hidden className=\"ms-0.5 text-danger\">\n *\n </span>\n )}\n {helpMark !== undefined && <span className=\"ms-1 inline-flex align-middle font-normal\">{helpMark}</span>}\n </legend>\n )}\n <RequiredStarOnLegend.Provider value={true}>\n <div\n className={cn(\n layout === \"horizontal\" ? \"flex flex-wrap gap-x-4 gap-y-2\" : COLUMNS[columns],\n listClassName,\n )}\n >\n {options.map((o) => (\n <Checkbox\n key={o.value}\n name={name}\n value={o.value}\n label={o.label}\n description={o.hint}\n checked={ticked.has(o.value)}\n onCheckedChange={(on) => toggle(o.value, on)}\n disabled={disabled || o.disabled}\n invalid={isInvalid || undefined}\n required={required && value.length === 0 ? true : undefined}\n disabledReason={reason}\n />\n ))}\n </div>\n </RequiredStarOnLegend.Provider>\n {showHint && (\n // A `div`, not a `p`: a hint may be a node with a link in it.\n <div id={hintId} className=\"mt-1.5 text-caption leading-tight text-muted\">\n {shownHint}\n </div>\n )}\n {showError && (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {error}\n </p>\n )}\n </fieldset>\n );\n}\n"],"mappings":"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","names":[]}
@@ -15,10 +15,6 @@ export interface ChipInputLabels {
15
15
  duplicate: (value: string) => string;
16
16
  }
17
17
  export { DEFAULT_CHIP_INPUT_LABELS };
18
- /** @deprecated 0.34 — `useKitLabels("chipInput", DEFAULT_CHIP_INPUT_LABELS, labels)`, which
19
- * the provider reaches too, or ChipInput's own `labels`; removed in 0.35 (refactor audit
20
- * 2026-10, API-6a). A merge from before the provider, which no kit component calls. */
21
- export declare function resolveChipInputLabels(labels?: Partial<ChipInputLabels>): ChipInputLabels;
22
18
  /**
23
19
  * `onChange` is the field's own — the whole LIST, not a DOM event — so the wrapper
24
20
  * div's `onChange` is omitted rather than shadowed. `placeholder` is likewise this
@@ -10,9 +10,6 @@ import { FIELD_ERROR_CLASS, mergeRefs } from "./field-parts.js";
10
10
  import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
11
11
  import { DEFAULT_CHIP_INPUT_LABELS } from "./chip-labels.js";
12
12
  import { Chip } from "./chip.js";
13
- function resolveChipInputLabels(labels) {
14
- return labels ? { ...DEFAULT_CHIP_INPUT_LABELS, ...labels } : DEFAULT_CHIP_INPUT_LABELS;
15
- }
16
13
  function ChipInput({
17
14
  value,
18
15
  onChange,
@@ -218,7 +215,6 @@ function ChipInput({
218
215
  }
219
216
  export {
220
217
  ChipInput,
221
- DEFAULT_CHIP_INPUT_LABELS,
222
- resolveChipInputLabels
218
+ DEFAULT_CHIP_INPUT_LABELS
223
219
  };
224
220
  //# sourceMappingURL=chip-input.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/chip-input.tsx"],"sourcesContent":["// A field whose value is a list, entered as chips. Moved out of chip.tsx as it was\n// (MOD-9, refactor audit 2026-10). It renders `Chip`, so chip.tsx re-exports only its\n// TYPES: re-exporting `ChipInput` itself would make chip ⇄ chip-input a runtime cycle\n// (check:graph, import-x/no-cycle). The barrel exports this module.\nimport { useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { claimEscape } from \"../hooks/use-dismiss\";\nimport { FIELD_INVALID, FIELD_TOUCH_TEXT } from \"./field-base\";\nimport { FIELD_ERROR_CLASS, mergeRefs } from \"./field-parts\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_CHIP_INPUT_LABELS } from \"./chip-labels\";\nimport { Chip, type ChipSize, type ChipTone } from \"./chip\";\n\n/* ── ChipInput ────────────────────────────────────────────────────────────── */\n\nexport interface ChipInputLabels {\n /** Announced after a value is added. Receives the value. */\n added: (value: string) => string;\n /** Announced after a value is removed. Receives the value. */\n removed: (value: string) => string;\n /** Accessible name for each chip's dismiss button. */\n remove: string;\n /** Shown when `max` is reached. Receives the limit. */\n atLimit: (max: number) => string;\n /** Shown when a duplicate is rejected. Receives the value. */\n duplicate: (value: string) => string;\n}\n\n// Defined in ./chip-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_CHIP_INPUT_LABELS };\n\n/** @deprecated 0.34 — `useKitLabels(\"chipInput\", DEFAULT_CHIP_INPUT_LABELS, labels)`, which\n * the provider reaches too, or ChipInput's own `labels`; removed in 0.35 (refactor audit\n * 2026-10, API-6a). A merge from before the provider, which no kit component calls. */\nexport function resolveChipInputLabels(labels?: Partial<ChipInputLabels>): ChipInputLabels {\n return labels ? { ...DEFAULT_CHIP_INPUT_LABELS, ...labels } : DEFAULT_CHIP_INPUT_LABELS;\n}\n\n/**\n * `onChange` is the field's own — the whole LIST, not a DOM event — so the wrapper\n * div's `onChange` is omitted rather than shadowed. `placeholder` is likewise this\n * component's, forwarded to the inner `<input>`; the div has none of its own.\n */\nexport interface ChipInputProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"placeholder\" | \"children\"> {\n value: string[];\n onChange: (next: string[]) => void;\n placeholder?: string;\n /** Visible label, rendered above the field: a ReactNode, as on every kit field (a\n * string until 0.34, refactor audit 2026-10, API-9). Without one, name the field with\n * `aria-label`. */\n label?: ReactNode;\n /** Reject or rewrite a value on commit. Return a string to reject WITH that message. */\n validate?: (raw: string) => string | null;\n /** Characters that commit the current text, besides Enter. Default: comma. */\n separators?: string[];\n allowDuplicates?: boolean;\n max?: number;\n tone?: ChipTone;\n size?: ChipSize;\n disabled?: boolean;\n invalid?: boolean;\n /** Error text. Wires `aria-describedby` and implies `invalid`. */\n error?: ReactNode;\n labels?: LabelOverride<ChipInputLabels>;\n className?: string;\n /** The `<input>` the next value is typed into — the element that takes focus, which\n * react-hook-form's focus-on-error calls `focus()` on (API-3, refactor audit 2026-10).\n * Not the wrapper, which takes the rest of the props. */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * A field whose value is a LIST, entered as chips.\n *\n * The keyboard model is the whole component; the pills are the easy part. Per the WAI\n * pattern for a \"tag\" input:\n *\n * Enter, or any separator commit the typed text\n * Backspace on empty text move focus to the last chip (does NOT delete it)\n * ← → while on a chip move between chips, in reading order (mirrored in RTL)\n * Backspace / Delete remove the focused chip, focus its neighbour\n * Escape clear what is typed but keep the committed chips\n * paste splits on the separators, so a pasted CSV becomes chips\n *\n * The Backspace-on-empty behaviour is the one worth arguing about. Deleting immediately\n * is more common and is a trap: it is a destructive action with no feedback on a key\n * people hit by reflex, and the value it destroys may have taken a while to type. Moving\n * focus first makes the second Backspace a deliberate one, and it is what lets a\n * keyboard user reach a chip at all.\n */\nexport function ChipInput({\n value,\n onChange,\n placeholder,\n label,\n validate,\n separators = [\",\"],\n allowDuplicates = false,\n max,\n tone = \"neutral\",\n size = \"md\",\n disabled = false,\n invalid = false,\n error,\n labels,\n className,\n \"aria-label\": ariaLabelAttr,\n ref,\n ...rest\n}: ChipInputProps) {\n // `labels` over the provider's `chipInput` over English.\n const text = useKitLabels(\"chipInput\", DEFAULT_CHIP_INPUT_LABELS, labels);\n // A batch of pasted values is announced as one list, joined the way the language\n // joins a list in a sentence (\"a, b and c added\") — hence the long conjunction,\n // where a trigger's summary would use the narrow one.\n const locale = useKitLocale();\n const [draft, setDraft] = useState(\"\");\n const [message, setMessage] = useState(\"\");\n // The last refusal, shown under the field until the next edit. The live region alone\n // told only a screen-reader user why Enter did nothing; everyone else saw the text\n // simply stay put.\n const [rejected, setRejected] = useState<string | null>(null);\n const [focusedChip, setFocusedChip] = useState<number | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n // The field's own handle on its input (focus after a chip goes) and the caller's `ref`.\n const setInput = useMemo(() => mergeRefs(inputRef, ref), [ref]);\n const chipRefs = useRef<Array<HTMLElement | null>>([]);\n const id = useId();\n const listId = `${id}-list`;\n const errorId = `${id}-error`;\n const rejectedId = `${id}-rejected`;\n const isInvalid = invalid || !!error;\n const describedBy = [error ? errorId : null, rejected ? rejectedId : null]\n .filter(Boolean)\n .join(\" \");\n\n const focusChip = (index: number | null) => {\n setFocusedChip(index);\n if (index === null) inputRef.current?.focus();\n else chipRefs.current[index]?.focus();\n };\n\n /**\n * Try to add every candidate, as ONE change.\n *\n * Batched rather than looped for a reason that bites immediately: `commit` closes over\n * `value`, so calling it once per pasted item makes every call compute its result from\n * the list as it was BEFORE the paste, and each `onChange` overwrites the last. A\n * pasted \"one,two,three\" left only \"three\". Accumulating locally and emitting once is\n * also the correct behaviour for an undo stack — a paste is one action.\n */\n const commitMany = (raws: string[]): { added: string[]; rejection: string | null } => {\n const next = [...value];\n const added: string[] = [];\n let rejection: string | null = null;\n\n for (const raw of raws) {\n const candidate = raw.trim();\n if (!candidate) continue;\n if (max !== undefined && next.length >= max) {\n rejection ??= text.atLimit(max);\n break;\n }\n if (!allowDuplicates && next.includes(candidate)) {\n rejection ??= text.duplicate(candidate);\n continue;\n }\n const invalidReason = validate?.(candidate);\n if (invalidReason) {\n rejection ??= invalidReason;\n continue;\n }\n next.push(candidate);\n added.push(candidate);\n }\n\n if (added.length) onChange(next);\n // A rejection is the more useful thing to say: \"three added\" is obvious from the\n // chips appearing, whereas \"already in the list\" is the only sign of what did not.\n const addedList = new Intl.ListFormat(locale, { type: \"conjunction\", style: \"long\" });\n setMessage(rejection ?? (added.length ? text.added(addedList.format(added)) : \"\"));\n setRejected(rejection);\n return { added, rejection };\n };\n\n const commit = (raw: string): boolean => commitMany([raw]).added.length > 0;\n\n const removeAt = (index: number) => {\n const removed = value[index];\n onChange(value.filter((_, i) => i !== index));\n setMessage(text.removed(removed));\n setRejected(null);\n // Focus the neighbour that takes its place, or the input when the list empties —\n // dropping focus to <body> here is how a keyboard user loses the field entirely.\n const next = index >= value.length - 1 ? null : index;\n requestAnimationFrame(() => focusChip(value.length - 1 === 0 ? null : next));\n };\n\n const onInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n // The Enter that confirms an IME composition is the IME's: it commits no chip\n // and submits no form (BUG-9).\n if (isImeEnter(e)) return;\n // Only when there is something to commit: an empty Enter belongs to the form.\n if (draft.trim()) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n }\n return;\n }\n if (separators.includes(e.key)) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n return;\n }\n // Clearing the draft is this field's Escape, not a dialog's around it: claimed, so\n // the dialog stays — the same in a Modal and a FullBleedDialog, which read it\n // differently until 0.34 (Wave 2, DUP-1). With nothing typed the key goes on.\n if (e.key === \"Escape\" && draft) {\n claimEscape(e);\n setDraft(\"\");\n return;\n }\n if (e.key === \"Backspace\" && !draft && value.length) {\n e.preventDefault();\n focusChip(value.length - 1);\n }\n };\n\n const onChipKeyDown = (e: KeyboardEvent<HTMLElement>, index: number) => {\n if (e.key === \"Backspace\" || e.key === \"Delete\") {\n e.preventDefault();\n removeAt(index);\n return;\n }\n // ←/→ follow the reading direction, not DOM order: in RTL the previous chip is to\n // the RIGHT, and ArrowLeft moves on towards the input.\n const step = horizontalStep(e.key, e.currentTarget);\n if (step === -1 && index > 0) {\n e.preventDefault();\n focusChip(index - 1);\n return;\n }\n if (step === 1) {\n e.preventDefault();\n focusChip(index === value.length - 1 ? null : index + 1);\n return;\n }\n // Back to the input, a step inside the field: claimed like the draft's (DUP-1).\n if (e.key === \"Escape\") {\n claimEscape(e);\n focusChip(null);\n }\n };\n\n return (\n // The rest lands on the OUTER box — the thing a `data-tour` step or a test id\n // wants to point at — while the accessible name below goes on the `<input>`,\n // because that is what a screen-reader user actually lands in. Spreading the two\n // to the same element would have named a decorative wrapper and left the field\n // anonymous.\n <div {...rest} className={cn(\"space-y-1\", className)}>\n {label && (\n <label htmlFor={`${id}-input`} className=\"block text-sm text-secondary\">\n {label}\n </label>\n )}\n {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- a\n pointer convenience that widens the input's hit area to the whole field; the\n keyboard path is the <input> itself, which is in the tab order. */}\n <div\n // `relative` so the live region below can be `sr-only` without escaping to the\n // initial containing block and inflating the document height.\n className={cn(\n \"relative flex flex-wrap items-center gap-1.5 rounded-md border border-subtle bg-surface px-2 py-1.5 shadow-sm\",\n \"focus-within:border-brand focus-within:ring-1 focus-within:ring-brand\",\n disabled && \"cursor-default bg-surface-2\",\n isInvalid && FIELD_INVALID,\n )}\n onClick={() => !disabled && inputRef.current?.focus()}\n >\n {/* `display: contents` on the wrappers so the chips participate in the field's\n own flex wrap rather than forming a nested row. The list semantics are\n restored with explicit roles, because `contents` removes the ul/li boxes from\n the accessibility tree in several engines. */}\n {/* eslint-disable-next-line jsx-a11y/no-redundant-roles -- NOT redundant here: the\n `contents` display below removes the ul and li boxes from the box tree, and\n several engines drop the list semantics with them. The explicit role is what\n puts \"list, 3 items\" back. */}\n <ul id={listId} role=\"list\" className=\"contents\">\n {value.map((v, i) => (\n <li key={`${v}-${i}`} role=\"presentation\" className=\"contents\">\n <Chip\n ref={(el: HTMLElement | null) => {\n chipRefs.current[i] = el;\n }}\n tone={tone}\n size={size}\n disabled={disabled}\n removeLabel={text.remove}\n onRemove={disabled ? undefined : () => removeAt(i)}\n // A ROVING tabIndex: only the focused chip is tabbable, so Tab leaves\n // the field instead of walking through every value in it — the reason a\n // list of twenty tags is otherwise a twenty-stop detour for a keyboard.\n tabIndex={focusedChip === i ? 0 : -1}\n role=\"listitem\"\n aria-label={v}\n onKeyDown={(e) => onChipKeyDown(e, i)}\n onFocus={() => setFocusedChip(i)}\n data-chip-index={i}\n >\n {v}\n </Chip>\n </li>\n ))}\n </ul>\n <input\n id={`${id}-input`}\n ref={setInput}\n value={draft}\n onChange={(e) => {\n setDraft(e.target.value);\n setMessage(\"\");\n setRejected(null);\n }}\n onKeyDown={onInputKeyDown}\n onBlur={() => {\n // Commit on blur: a value typed and then clicked away from is a value the\n // user meant to add, and losing it silently is the commonest complaint\n // about this pattern.\n if (draft.trim() && commit(draft)) setDraft(\"\");\n }}\n onPaste={(e) => {\n const pasted = e.clipboardData.getData(\"text\");\n const pattern = new RegExp(`[${separators.map((s) => `\\\\${s}`).join(\"\")}\\n\\r]`);\n if (!pattern.test(pasted)) return;\n e.preventDefault();\n if (commitMany(pasted.split(pattern)).added.length) setDraft(\"\");\n }}\n placeholder={value.length ? undefined : placeholder}\n disabled={disabled}\n // A visible `label` is already wired through `htmlFor`, so adding a name\n // here would give the field two.\n aria-label={label ? undefined : ariaLabelAttr}\n aria-describedby={describedBy || undefined}\n aria-invalid={isInvalid || !!rejected || undefined}\n // `w-0` with flex-1: without a width the browser's default input size (20\n // characters) became the field's minimum width and pushed it past a 240 px\n // phone at 150 % (0.32's screenshot run).\n className={cn(\"w-0 min-w-[6rem] flex-1 bg-transparent py-0.5 text-sm text-primary outline-none placeholder:text-placeholder disabled:cursor-default\", FIELD_TOUCH_TEXT)}\n />\n {/* Additions, removals and rejections move no focus, so nothing would announce\n them. `sr-only-fixed` rather than `sr-only`: this sits inside a consumer's\n markup and must not extend the document height. */}\n <span role=\"status\" aria-live=\"polite\" aria-atomic className=\"sr-only-fixed\">\n {message}\n </span>\n </div>\n {/* Every field's error type (DUP-8: these were `text-xs`); the root's `space-y-1`\n spaces them, so not the class's own `mt-1` as well. */}\n {error && (\n <p id={errorId} className={cn(FIELD_ERROR_CLASS, \"mt-0\")}>\n {error}\n </p>\n )}\n {rejected && (\n <p id={rejectedId} className={cn(FIELD_ERROR_CLASS, \"mt-0\")} data-chip-input-rejected=\"\">\n {rejected}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/chip-input.tsx"],"sourcesContent":["// A field whose value is a list, entered as chips. Moved out of chip.tsx as it was\n// (MOD-9, refactor audit 2026-10). It renders `Chip`, so chip.tsx re-exports only its\n// TYPES: re-exporting `ChipInput` itself would make chip ⇄ chip-input a runtime cycle\n// (check:graph, import-x/no-cycle). The barrel exports this module.\nimport { useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { claimEscape } from \"../hooks/use-dismiss\";\nimport { FIELD_INVALID, FIELD_TOUCH_TEXT } from \"./field-base\";\nimport { FIELD_ERROR_CLASS, mergeRefs } from \"./field-parts\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_CHIP_INPUT_LABELS } from \"./chip-labels\";\nimport { Chip, type ChipSize, type ChipTone } from \"./chip\";\n\n/* ── ChipInput ────────────────────────────────────────────────────────────── */\n\nexport interface ChipInputLabels {\n /** Announced after a value is added. Receives the value. */\n added: (value: string) => string;\n /** Announced after a value is removed. Receives the value. */\n removed: (value: string) => string;\n /** Accessible name for each chip's dismiss button. */\n remove: string;\n /** Shown when `max` is reached. Receives the limit. */\n atLimit: (max: number) => string;\n /** Shown when a duplicate is rejected. Receives the value. */\n duplicate: (value: string) => string;\n}\n\n// Defined in ./chip-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_CHIP_INPUT_LABELS };\n\n/**\n * `onChange` is the field's own — the whole LIST, not a DOM event — so the wrapper\n * div's `onChange` is omitted rather than shadowed. `placeholder` is likewise this\n * component's, forwarded to the inner `<input>`; the div has none of its own.\n */\nexport interface ChipInputProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"placeholder\" | \"children\"> {\n value: string[];\n onChange: (next: string[]) => void;\n placeholder?: string;\n /** Visible label, rendered above the field: a ReactNode, as on every kit field (a\n * string until 0.34, refactor audit 2026-10, API-9). Without one, name the field with\n * `aria-label`. */\n label?: ReactNode;\n /** Reject or rewrite a value on commit. Return a string to reject WITH that message. */\n validate?: (raw: string) => string | null;\n /** Characters that commit the current text, besides Enter. Default: comma. */\n separators?: string[];\n allowDuplicates?: boolean;\n max?: number;\n tone?: ChipTone;\n size?: ChipSize;\n disabled?: boolean;\n invalid?: boolean;\n /** Error text. Wires `aria-describedby` and implies `invalid`. */\n error?: ReactNode;\n labels?: LabelOverride<ChipInputLabels>;\n className?: string;\n /** The `<input>` the next value is typed into — the element that takes focus, which\n * react-hook-form's focus-on-error calls `focus()` on (API-3, refactor audit 2026-10).\n * Not the wrapper, which takes the rest of the props. */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * A field whose value is a LIST, entered as chips.\n *\n * The keyboard model is the whole component; the pills are the easy part. Per the WAI\n * pattern for a \"tag\" input:\n *\n * Enter, or any separator commit the typed text\n * Backspace on empty text move focus to the last chip (does NOT delete it)\n * ← → while on a chip move between chips, in reading order (mirrored in RTL)\n * Backspace / Delete remove the focused chip, focus its neighbour\n * Escape clear what is typed but keep the committed chips\n * paste splits on the separators, so a pasted CSV becomes chips\n *\n * The Backspace-on-empty behaviour is the one worth arguing about. Deleting immediately\n * is more common and is a trap: it is a destructive action with no feedback on a key\n * people hit by reflex, and the value it destroys may have taken a while to type. Moving\n * focus first makes the second Backspace a deliberate one, and it is what lets a\n * keyboard user reach a chip at all.\n */\nexport function ChipInput({\n value,\n onChange,\n placeholder,\n label,\n validate,\n separators = [\",\"],\n allowDuplicates = false,\n max,\n tone = \"neutral\",\n size = \"md\",\n disabled = false,\n invalid = false,\n error,\n labels,\n className,\n \"aria-label\": ariaLabelAttr,\n ref,\n ...rest\n}: ChipInputProps) {\n // `labels` over the provider's `chipInput` over English.\n const text = useKitLabels(\"chipInput\", DEFAULT_CHIP_INPUT_LABELS, labels);\n // A batch of pasted values is announced as one list, joined the way the language\n // joins a list in a sentence (\"a, b and c added\") — hence the long conjunction,\n // where a trigger's summary would use the narrow one.\n const locale = useKitLocale();\n const [draft, setDraft] = useState(\"\");\n const [message, setMessage] = useState(\"\");\n // The last refusal, shown under the field until the next edit. The live region alone\n // told only a screen-reader user why Enter did nothing; everyone else saw the text\n // simply stay put.\n const [rejected, setRejected] = useState<string | null>(null);\n const [focusedChip, setFocusedChip] = useState<number | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n // The field's own handle on its input (focus after a chip goes) and the caller's `ref`.\n const setInput = useMemo(() => mergeRefs(inputRef, ref), [ref]);\n const chipRefs = useRef<Array<HTMLElement | null>>([]);\n const id = useId();\n const listId = `${id}-list`;\n const errorId = `${id}-error`;\n const rejectedId = `${id}-rejected`;\n const isInvalid = invalid || !!error;\n const describedBy = [error ? errorId : null, rejected ? rejectedId : null]\n .filter(Boolean)\n .join(\" \");\n\n const focusChip = (index: number | null) => {\n setFocusedChip(index);\n if (index === null) inputRef.current?.focus();\n else chipRefs.current[index]?.focus();\n };\n\n /**\n * Try to add every candidate, as ONE change.\n *\n * Batched rather than looped for a reason that bites immediately: `commit` closes over\n * `value`, so calling it once per pasted item makes every call compute its result from\n * the list as it was BEFORE the paste, and each `onChange` overwrites the last. A\n * pasted \"one,two,three\" left only \"three\". Accumulating locally and emitting once is\n * also the correct behaviour for an undo stack — a paste is one action.\n */\n const commitMany = (raws: string[]): { added: string[]; rejection: string | null } => {\n const next = [...value];\n const added: string[] = [];\n let rejection: string | null = null;\n\n for (const raw of raws) {\n const candidate = raw.trim();\n if (!candidate) continue;\n if (max !== undefined && next.length >= max) {\n rejection ??= text.atLimit(max);\n break;\n }\n if (!allowDuplicates && next.includes(candidate)) {\n rejection ??= text.duplicate(candidate);\n continue;\n }\n const invalidReason = validate?.(candidate);\n if (invalidReason) {\n rejection ??= invalidReason;\n continue;\n }\n next.push(candidate);\n added.push(candidate);\n }\n\n if (added.length) onChange(next);\n // A rejection is the more useful thing to say: \"three added\" is obvious from the\n // chips appearing, whereas \"already in the list\" is the only sign of what did not.\n const addedList = new Intl.ListFormat(locale, { type: \"conjunction\", style: \"long\" });\n setMessage(rejection ?? (added.length ? text.added(addedList.format(added)) : \"\"));\n setRejected(rejection);\n return { added, rejection };\n };\n\n const commit = (raw: string): boolean => commitMany([raw]).added.length > 0;\n\n const removeAt = (index: number) => {\n const removed = value[index];\n onChange(value.filter((_, i) => i !== index));\n setMessage(text.removed(removed));\n setRejected(null);\n // Focus the neighbour that takes its place, or the input when the list empties —\n // dropping focus to <body> here is how a keyboard user loses the field entirely.\n const next = index >= value.length - 1 ? null : index;\n requestAnimationFrame(() => focusChip(value.length - 1 === 0 ? null : next));\n };\n\n const onInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n // The Enter that confirms an IME composition is the IME's: it commits no chip\n // and submits no form (BUG-9).\n if (isImeEnter(e)) return;\n // Only when there is something to commit: an empty Enter belongs to the form.\n if (draft.trim()) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n }\n return;\n }\n if (separators.includes(e.key)) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n return;\n }\n // Clearing the draft is this field's Escape, not a dialog's around it: claimed, so\n // the dialog stays — the same in a Modal and a FullBleedDialog, which read it\n // differently until 0.34 (Wave 2, DUP-1). With nothing typed the key goes on.\n if (e.key === \"Escape\" && draft) {\n claimEscape(e);\n setDraft(\"\");\n return;\n }\n if (e.key === \"Backspace\" && !draft && value.length) {\n e.preventDefault();\n focusChip(value.length - 1);\n }\n };\n\n const onChipKeyDown = (e: KeyboardEvent<HTMLElement>, index: number) => {\n if (e.key === \"Backspace\" || e.key === \"Delete\") {\n e.preventDefault();\n removeAt(index);\n return;\n }\n // ←/→ follow the reading direction, not DOM order: in RTL the previous chip is to\n // the RIGHT, and ArrowLeft moves on towards the input.\n const step = horizontalStep(e.key, e.currentTarget);\n if (step === -1 && index > 0) {\n e.preventDefault();\n focusChip(index - 1);\n return;\n }\n if (step === 1) {\n e.preventDefault();\n focusChip(index === value.length - 1 ? null : index + 1);\n return;\n }\n // Back to the input, a step inside the field: claimed like the draft's (DUP-1).\n if (e.key === \"Escape\") {\n claimEscape(e);\n focusChip(null);\n }\n };\n\n return (\n // The rest lands on the OUTER box — the thing a `data-tour` step or a test id\n // wants to point at — while the accessible name below goes on the `<input>`,\n // because that is what a screen-reader user actually lands in. Spreading the two\n // to the same element would have named a decorative wrapper and left the field\n // anonymous.\n <div {...rest} className={cn(\"space-y-1\", className)}>\n {label && (\n <label htmlFor={`${id}-input`} className=\"block text-sm text-secondary\">\n {label}\n </label>\n )}\n {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- a\n pointer convenience that widens the input's hit area to the whole field; the\n keyboard path is the <input> itself, which is in the tab order. */}\n <div\n // `relative` so the live region below can be `sr-only` without escaping to the\n // initial containing block and inflating the document height.\n className={cn(\n \"relative flex flex-wrap items-center gap-1.5 rounded-md border border-subtle bg-surface px-2 py-1.5 shadow-sm\",\n \"focus-within:border-brand focus-within:ring-1 focus-within:ring-brand\",\n disabled && \"cursor-default bg-surface-2\",\n isInvalid && FIELD_INVALID,\n )}\n onClick={() => !disabled && inputRef.current?.focus()}\n >\n {/* `display: contents` on the wrappers so the chips participate in the field's\n own flex wrap rather than forming a nested row. The list semantics are\n restored with explicit roles, because `contents` removes the ul/li boxes from\n the accessibility tree in several engines. */}\n {/* eslint-disable-next-line jsx-a11y/no-redundant-roles -- NOT redundant here: the\n `contents` display below removes the ul and li boxes from the box tree, and\n several engines drop the list semantics with them. The explicit role is what\n puts \"list, 3 items\" back. */}\n <ul id={listId} role=\"list\" className=\"contents\">\n {value.map((v, i) => (\n <li key={`${v}-${i}`} role=\"presentation\" className=\"contents\">\n <Chip\n ref={(el: HTMLElement | null) => {\n chipRefs.current[i] = el;\n }}\n tone={tone}\n size={size}\n disabled={disabled}\n removeLabel={text.remove}\n onRemove={disabled ? undefined : () => removeAt(i)}\n // A ROVING tabIndex: only the focused chip is tabbable, so Tab leaves\n // the field instead of walking through every value in it — the reason a\n // list of twenty tags is otherwise a twenty-stop detour for a keyboard.\n tabIndex={focusedChip === i ? 0 : -1}\n role=\"listitem\"\n aria-label={v}\n onKeyDown={(e) => onChipKeyDown(e, i)}\n onFocus={() => setFocusedChip(i)}\n data-chip-index={i}\n >\n {v}\n </Chip>\n </li>\n ))}\n </ul>\n <input\n id={`${id}-input`}\n ref={setInput}\n value={draft}\n onChange={(e) => {\n setDraft(e.target.value);\n setMessage(\"\");\n setRejected(null);\n }}\n onKeyDown={onInputKeyDown}\n onBlur={() => {\n // Commit on blur: a value typed and then clicked away from is a value the\n // user meant to add, and losing it silently is the commonest complaint\n // about this pattern.\n if (draft.trim() && commit(draft)) setDraft(\"\");\n }}\n onPaste={(e) => {\n const pasted = e.clipboardData.getData(\"text\");\n const pattern = new RegExp(`[${separators.map((s) => `\\\\${s}`).join(\"\")}\\n\\r]`);\n if (!pattern.test(pasted)) return;\n e.preventDefault();\n if (commitMany(pasted.split(pattern)).added.length) setDraft(\"\");\n }}\n placeholder={value.length ? undefined : placeholder}\n disabled={disabled}\n // A visible `label` is already wired through `htmlFor`, so adding a name\n // here would give the field two.\n aria-label={label ? undefined : ariaLabelAttr}\n aria-describedby={describedBy || undefined}\n aria-invalid={isInvalid || !!rejected || undefined}\n // `w-0` with flex-1: without a width the browser's default input size (20\n // characters) became the field's minimum width and pushed it past a 240 px\n // phone at 150 % (0.32's screenshot run).\n className={cn(\"w-0 min-w-[6rem] flex-1 bg-transparent py-0.5 text-sm text-primary outline-none placeholder:text-placeholder disabled:cursor-default\", FIELD_TOUCH_TEXT)}\n />\n {/* Additions, removals and rejections move no focus, so nothing would announce\n them. `sr-only-fixed` rather than `sr-only`: this sits inside a consumer's\n markup and must not extend the document height. */}\n <span role=\"status\" aria-live=\"polite\" aria-atomic className=\"sr-only-fixed\">\n {message}\n </span>\n </div>\n {/* Every field's error type (DUP-8: these were `text-xs`); the root's `space-y-1`\n spaces them, so not the class's own `mt-1` as well. */}\n {error && (\n <p id={errorId} className={cn(FIELD_ERROR_CLASS, \"mt-0\")}>\n {error}\n </p>\n )}\n {rejected && (\n <p id={rejectedId} className={cn(FIELD_ERROR_CLASS, \"mt-0\")} data-chip-input-rejected=\"\">\n {rejected}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";AAoQQ,cAOF,YAPE;AAhQR,SAAS,OAAO,SAAS,QAAQ,gBAAgB;AAEjD,SAAS,UAAU;AACnB,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAC5B,SAAS,eAAe,wBAAwB;AAChD,SAAS,mBAAmB,iBAAiB;AAC7C,SAAS,cAAc,oBAAwC;AAC/D,SAAS,iCAAiC;AAC1C,SAAS,YAA0C;AAyE5C,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa,CAAC,GAAG;AAAA,EACjB,kBAAkB;AAAA,EAClB;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP,WAAW;AAAA,EACX,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,GAAG;AACL,GAAmB;AAEjB,QAAM,OAAO,aAAa,aAAa,2BAA2B,MAAM;AAIxE,QAAM,SAAS,aAAa;AAC5B,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,EAAE;AAIzC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,IAAI;AAC5D,QAAM,CAAC,aAAa,cAAc,IAAI,SAAwB,IAAI;AAClE,QAAM,WAAW,OAAyB,IAAI;AAE9C,QAAM,WAAW,QAAQ,MAAM,UAAU,UAAU,GAAG,GAAG,CAAC,GAAG,CAAC;AAC9D,QAAM,WAAW,OAAkC,CAAC,CAAC;AACrD,QAAM,KAAK,MAAM;AACjB,QAAM,SAAS,GAAG,EAAE;AACpB,QAAM,UAAU,GAAG,EAAE;AACrB,QAAM,aAAa,GAAG,EAAE;AACxB,QAAM,YAAY,WAAW,CAAC,CAAC;AAC/B,QAAM,cAAc,CAAC,QAAQ,UAAU,MAAM,WAAW,aAAa,IAAI,EACtE,OAAO,OAAO,EACd,KAAK,GAAG;AAEX,QAAM,YAAY,CAAC,UAAyB;AAC1C,mBAAe,KAAK;AACpB,QAAI,UAAU,KAAM,UAAS,SAAS,MAAM;AAAA,QACvC,UAAS,QAAQ,KAAK,GAAG,MAAM;AAAA,EACtC;AAWA,QAAM,aAAa,CAAC,SAAkE;AACpF,UAAM,OAAO,CAAC,GAAG,KAAK;AACtB,UAAM,QAAkB,CAAC;AACzB,QAAI,YAA2B;AAE/B,eAAW,OAAO,MAAM;AACtB,YAAM,YAAY,IAAI,KAAK;AAC3B,UAAI,CAAC,UAAW;AAChB,UAAI,QAAQ,UAAa,KAAK,UAAU,KAAK;AAC3C,sBAAc,KAAK,QAAQ,GAAG;AAC9B;AAAA,MACF;AACA,UAAI,CAAC,mBAAmB,KAAK,SAAS,SAAS,GAAG;AAChD,sBAAc,KAAK,UAAU,SAAS;AACtC;AAAA,MACF;AACA,YAAM,gBAAgB,WAAW,SAAS;AAC1C,UAAI,eAAe;AACjB,sBAAc;AACd;AAAA,MACF;AACA,WAAK,KAAK,SAAS;AACnB,YAAM,KAAK,SAAS;AAAA,IACtB;AAEA,QAAI,MAAM,OAAQ,UAAS,IAAI;AAG/B,UAAM,YAAY,IAAI,KAAK,WAAW,QAAQ,EAAE,MAAM,eAAe,OAAO,OAAO,CAAC;AACpF,eAAW,cAAc,MAAM,SAAS,KAAK,MAAM,UAAU,OAAO,KAAK,CAAC,IAAI,GAAG;AACjF,gBAAY,SAAS;AACrB,WAAO,EAAE,OAAO,UAAU;AAAA,EAC5B;AAEA,QAAM,SAAS,CAAC,QAAyB,WAAW,CAAC,GAAG,CAAC,EAAE,MAAM,SAAS;AAE1E,QAAM,WAAW,CAAC,UAAkB;AAClC,UAAM,UAAU,MAAM,KAAK;AAC3B,aAAS,MAAM,OAAO,CAAC,GAAG,MAAM,MAAM,KAAK,CAAC;AAC5C,eAAW,KAAK,QAAQ,OAAO,CAAC;AAChC,gBAAY,IAAI;AAGhB,UAAM,OAAO,SAAS,MAAM,SAAS,IAAI,OAAO;AAChD,0BAAsB,MAAM,UAAU,MAAM,SAAS,MAAM,IAAI,OAAO,IAAI,CAAC;AAAA,EAC7E;AAEA,QAAM,iBAAiB,CAAC,MAAuC;AAC7D,QAAI,EAAE,QAAQ,SAAS;AAGrB,UAAI,WAAW,CAAC,EAAG;AAEnB,UAAI,MAAM,KAAK,GAAG;AAChB,UAAE,eAAe;AACjB,YAAI,OAAO,KAAK,EAAG,UAAS,EAAE;AAAA,MAChC;AACA;AAAA,IACF;AACA,QAAI,WAAW,SAAS,EAAE,GAAG,GAAG;AAC9B,QAAE,eAAe;AACjB,UAAI,OAAO,KAAK,EAAG,UAAS,EAAE;AAC9B;AAAA,IACF;AAIA,QAAI,EAAE,QAAQ,YAAY,OAAO;AAC/B,kBAAY,CAAC;AACb,eAAS,EAAE;AACX;AAAA,IACF;AACA,QAAI,EAAE,QAAQ,eAAe,CAAC,SAAS,MAAM,QAAQ;AACnD,QAAE,eAAe;AACjB,gBAAU,MAAM,SAAS,CAAC;AAAA,IAC5B;AAAA,EACF;AAEA,QAAM,gBAAgB,CAAC,GAA+B,UAAkB;AACtE,QAAI,EAAE,QAAQ,eAAe,EAAE,QAAQ,UAAU;AAC/C,QAAE,eAAe;AACjB,eAAS,KAAK;AACd;AAAA,IACF;AAGA,UAAM,OAAO,eAAe,EAAE,KAAK,EAAE,aAAa;AAClD,QAAI,SAAS,MAAM,QAAQ,GAAG;AAC5B,QAAE,eAAe;AACjB,gBAAU,QAAQ,CAAC;AACnB;AAAA,IACF;AACA,QAAI,SAAS,GAAG;AACd,QAAE,eAAe;AACjB,gBAAU,UAAU,MAAM,SAAS,IAAI,OAAO,QAAQ,CAAC;AACvD;AAAA,IACF;AAEA,QAAI,EAAE,QAAQ,UAAU;AACtB,kBAAY,CAAC;AACb,gBAAU,IAAI;AAAA,IAChB;AAAA,EACF;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAME,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,aAAa,SAAS,GAChD;AAAA,eACC,oBAAC,WAAM,SAAS,GAAG,EAAE,UAAU,WAAU,gCACtC,iBACH;AAAA,MAKF;AAAA,QAAC;AAAA;AAAA,UAGC,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA,YAAY;AAAA,YACZ,aAAa;AAAA,UACf;AAAA,UACA,SAAS,MAAM,CAAC,YAAY,SAAS,SAAS,MAAM;AAAA,UAUpD;AAAA,gCAAC,QAAG,IAAI,QAAQ,MAAK,QAAO,WAAU,YACnC,gBAAM,IAAI,CAAC,GAAG,MACb,oBAAC,QAAqB,MAAK,gBAAe,WAAU,YAClD;AAAA,cAAC;AAAA;AAAA,gBACC,KAAK,CAAC,OAA2B;AAC/B,2BAAS,QAAQ,CAAC,IAAI;AAAA,gBACxB;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA,aAAa,KAAK;AAAA,gBAClB,UAAU,WAAW,SAAY,MAAM,SAAS,CAAC;AAAA,gBAIjD,UAAU,gBAAgB,IAAI,IAAI;AAAA,gBAClC,MAAK;AAAA,gBACL,cAAY;AAAA,gBACZ,WAAW,CAAC,MAAM,cAAc,GAAG,CAAC;AAAA,gBACpC,SAAS,MAAM,eAAe,CAAC;AAAA,gBAC/B,mBAAiB;AAAA,gBAEhB;AAAA;AAAA,YACH,KArBO,GAAG,CAAC,IAAI,CAAC,EAsBlB,CACD,GACH;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,IAAI,GAAG,EAAE;AAAA,gBACT,KAAK;AAAA,gBACL,OAAO;AAAA,gBACP,UAAU,CAAC,MAAM;AACf,2BAAS,EAAE,OAAO,KAAK;AACvB,6BAAW,EAAE;AACb,8BAAY,IAAI;AAAA,gBAClB;AAAA,gBACA,WAAW;AAAA,gBACX,QAAQ,MAAM;AAIZ,sBAAI,MAAM,KAAK,KAAK,OAAO,KAAK,EAAG,UAAS,EAAE;AAAA,gBAChD;AAAA,gBACA,SAAS,CAAC,MAAM;AACd,wBAAM,SAAS,EAAE,cAAc,QAAQ,MAAM;AAC7C,wBAAM,UAAU,IAAI,OAAO,IAAI,WAAW,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC;AAAA,IAAO;AAC9E,sBAAI,CAAC,QAAQ,KAAK,MAAM,EAAG;AAC3B,oBAAE,eAAe;AACjB,sBAAI,WAAW,OAAO,MAAM,OAAO,CAAC,EAAE,MAAM,OAAQ,UAAS,EAAE;AAAA,gBACjE;AAAA,gBACA,aAAa,MAAM,SAAS,SAAY;AAAA,gBACxC;AAAA,gBAGA,cAAY,QAAQ,SAAY;AAAA,gBAChC,oBAAkB,eAAe;AAAA,gBACjC,gBAAc,aAAa,CAAC,CAAC,YAAY;AAAA,gBAIzC,WAAW,GAAG,wIAAwI,gBAAgB;AAAA;AAAA,YACxK;AAAA,YAIA,oBAAC,UAAK,MAAK,UAAS,aAAU,UAAS,eAAW,MAAC,WAAU,iBAC1D,mBACH;AAAA;AAAA;AAAA,MACF;AAAA,MAGC,SACC,oBAAC,OAAE,IAAI,SAAS,WAAW,GAAG,mBAAmB,MAAM,GACpD,iBACH;AAAA,MAED,YACC,oBAAC,OAAE,IAAI,YAAY,WAAW,GAAG,mBAAmB,MAAM,GAAG,4BAAyB,IACnF,oBACH;AAAA,OAEJ;AAAA;AAEJ;","names":[]}