@eifi1/ui-kit 0.31.1 → 0.32.1

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 (565) hide show
  1. package/README.md +51 -28
  2. package/dist/account/delete-account-setting.d.ts +8 -3
  3. package/dist/admin/admin-action-confirm.d.ts +8 -3
  4. package/dist/admin/admin-action-log.d.ts +1 -1
  5. package/dist/admin/admin-action-log.js +4 -2
  6. package/dist/admin/admin-action-log.js.map +1 -1
  7. package/dist/admin/invitations-panel.d.ts +8 -3
  8. package/dist/admin/invitations-panel.js +29 -26
  9. package/dist/admin/invitations-panel.js.map +1 -1
  10. package/dist/admin/reviewer-scope-editor.d.ts +8 -3
  11. package/dist/admin/role-select.d.ts +10 -5
  12. package/dist/admin/transfer-ownership-dialog.d.ts +8 -3
  13. package/dist/admin/user-roster.d.ts +10 -5
  14. package/dist/admin/user-roster.js +8 -47
  15. package/dist/admin/user-roster.js.map +1 -1
  16. package/dist/auth/auth-errors.d.ts +43 -8
  17. package/dist/auth/auth-errors.js +24 -1
  18. package/dist/auth/auth-errors.js.map +1 -1
  19. package/dist/auth/register-form.d.ts +8 -3
  20. package/dist/billing/billing-banner.d.ts +184 -0
  21. package/dist/billing/billing-banner.js +98 -0
  22. package/dist/billing/billing-banner.js.map +1 -0
  23. package/dist/billing/billing-labels.d.ts +100 -0
  24. package/dist/billing/billing-labels.js +76 -0
  25. package/dist/billing/billing-labels.js.map +1 -0
  26. package/dist/billing/billing-lock.d.ts +158 -0
  27. package/dist/billing/billing-lock.js +17 -0
  28. package/dist/billing/billing-lock.js.map +1 -0
  29. package/dist/billing/billing-standing.d.ts +149 -0
  30. package/dist/billing/billing-standing.js +28 -0
  31. package/dist/billing/billing-standing.js.map +1 -0
  32. package/dist/billing/plan-card.d.ts +248 -0
  33. package/dist/billing/plan-card.js +248 -0
  34. package/dist/billing/plan-card.js.map +1 -0
  35. package/dist/billing/plan-limit-notice.d.ts +156 -0
  36. package/dist/billing/plan-limit-notice.js +76 -0
  37. package/dist/billing/plan-limit-notice.js.map +1 -0
  38. package/dist/billing/plan-limit.d.ts +34 -0
  39. package/dist/billing/plan-limit.js +20 -0
  40. package/dist/billing/plan-limit.js.map +1 -0
  41. package/dist/billing/plan-price.d.ts +61 -0
  42. package/dist/billing/plan-price.js +45 -0
  43. package/dist/billing/plan-price.js.map +1 -0
  44. package/dist/billing/subscription-actions.d.ts +133 -0
  45. package/dist/billing/subscription-actions.js +28 -0
  46. package/dist/billing/subscription-actions.js.map +1 -0
  47. package/dist/billing/subscription-status-chip.d.ts +149 -0
  48. package/dist/billing/subscription-status-chip.js +48 -0
  49. package/dist/billing/subscription-status-chip.js.map +1 -0
  50. package/dist/chart.d.ts +9 -3
  51. package/dist/chart.js.map +1 -1
  52. package/dist/components/account-chips.d.ts +16 -4
  53. package/dist/components/account-chips.js +21 -3
  54. package/dist/components/account-chips.js.map +1 -1
  55. package/dist/components/alert-banner.d.ts +6 -1
  56. package/dist/components/alert-banner.js +5 -4
  57. package/dist/components/alert-banner.js.map +1 -1
  58. package/dist/components/amount-input.d.ts +8 -3
  59. package/dist/components/amount-input.js +5 -5
  60. package/dist/components/amount-input.js.map +1 -1
  61. package/dist/components/appearance-labels.d.ts +38 -0
  62. package/dist/components/appearance-labels.js +19 -0
  63. package/dist/components/appearance-labels.js.map +1 -0
  64. package/dist/components/autocomplete.js +2 -2
  65. package/dist/components/autocomplete.js.map +1 -1
  66. package/dist/components/breadcrumbs.js +3 -2
  67. package/dist/components/breadcrumbs.js.map +1 -1
  68. package/dist/components/bulk-action-bar.d.ts +2 -2
  69. package/dist/components/bulk-action-bar.js +7 -12
  70. package/dist/components/bulk-action-bar.js.map +1 -1
  71. package/dist/components/button-group.d.ts +8 -3
  72. package/dist/components/button-group.js +7 -1
  73. package/dist/components/button-group.js.map +1 -1
  74. package/dist/components/calculator.d.ts +8 -3
  75. package/dist/components/calculator.js +4 -3
  76. package/dist/components/calculator.js.map +1 -1
  77. package/dist/components/calendar-heatmap.js +5 -5
  78. package/dist/components/calendar-heatmap.js.map +1 -1
  79. package/dist/components/chart-height.d.ts +23 -0
  80. package/dist/components/chart-height.js +15 -0
  81. package/dist/components/chart-height.js.map +1 -0
  82. package/dist/components/chart-zoom.js +6 -1
  83. package/dist/components/chart-zoom.js.map +1 -1
  84. package/dist/components/checkbox-group.js +2 -2
  85. package/dist/components/checkbox-group.js.map +1 -1
  86. package/dist/components/checkbox.d.ts +10 -0
  87. package/dist/components/checkbox.js +25 -10
  88. package/dist/components/checkbox.js.map +1 -1
  89. package/dist/components/chip.d.ts +19 -3
  90. package/dist/components/chip.js +54 -23
  91. package/dist/components/chip.js.map +1 -1
  92. package/dist/components/choice-card.d.ts +1 -1
  93. package/dist/components/choice-card.js +9 -8
  94. package/dist/components/choice-card.js.map +1 -1
  95. package/dist/components/column-mapper.js +1 -1
  96. package/dist/components/column-mapper.js.map +1 -1
  97. package/dist/components/combobox-core.js +8 -7
  98. package/dist/components/combobox-core.js.map +1 -1
  99. package/dist/components/combobox.js +8 -8
  100. package/dist/components/combobox.js.map +1 -1
  101. package/dist/components/confirm-dialog.d.ts +8 -3
  102. package/dist/components/copy-button.d.ts +10 -5
  103. package/dist/components/country-select.js +1 -1
  104. package/dist/components/country-select.js.map +1 -1
  105. package/dist/components/currency-select.js +3 -3
  106. package/dist/components/currency-select.js.map +1 -1
  107. package/dist/components/danger-confirm.d.ts +8 -3
  108. package/dist/components/data-table-cells.d.ts +1 -1
  109. package/dist/components/data-table-filter-popover.d.ts +1 -1
  110. package/dist/components/data-table-filter-popover.js +4 -1
  111. package/dist/components/data-table-filter-popover.js.map +1 -1
  112. package/dist/components/data-table-filters.d.ts +1 -1
  113. package/dist/components/data-table-labels.d.ts +1 -1
  114. package/dist/components/data-table-pagination.d.ts +3 -2
  115. package/dist/components/data-table-pagination.js +15 -6
  116. package/dist/components/data-table-pagination.js.map +1 -1
  117. package/dist/components/data-table.d.ts +1 -1
  118. package/dist/components/data-table.js +123 -52
  119. package/dist/components/data-table.js.map +1 -1
  120. package/dist/components/date-picker.d.ts +1 -1
  121. package/dist/components/date-picker.js +18 -6
  122. package/dist/components/date-picker.js.map +1 -1
  123. package/dist/components/description-list.js +2 -2
  124. package/dist/components/description-list.js.map +1 -1
  125. package/dist/components/dialog-frame.js +2 -1
  126. package/dist/components/dialog-frame.js.map +1 -1
  127. package/dist/components/disclosure.js +3 -2
  128. package/dist/components/disclosure.js.map +1 -1
  129. package/dist/components/dropdown.d.ts +1 -1
  130. package/dist/components/dropdown.js +7 -1
  131. package/dist/components/dropdown.js.map +1 -1
  132. package/dist/components/facing-pair.d.ts +19 -6
  133. package/dist/components/facing-pair.js +3 -2
  134. package/dist/components/facing-pair.js.map +1 -1
  135. package/dist/components/field-parts.d.ts +102 -6
  136. package/dist/components/field-parts.js +81 -6
  137. package/dist/components/field-parts.js.map +1 -1
  138. package/dist/components/field-sync.d.ts +4 -1
  139. package/dist/components/field-sync.js +12 -2
  140. package/dist/components/field-sync.js.map +1 -1
  141. package/dist/components/field.js +2 -2
  142. package/dist/components/field.js.map +1 -1
  143. package/dist/components/file-button.d.ts +8 -3
  144. package/dist/components/file-dropzone.d.ts +8 -3
  145. package/dist/components/floating-panel.js +7 -4
  146. package/dist/components/floating-panel.js.map +1 -1
  147. package/dist/components/focus-ring.d.ts +12 -0
  148. package/dist/components/focus-ring.js +8 -0
  149. package/dist/components/focus-ring.js.map +1 -0
  150. package/dist/components/form-actions.d.ts +8 -3
  151. package/dist/components/form-actions.js +33 -19
  152. package/dist/components/form-actions.js.map +1 -1
  153. package/dist/components/full-bleed-dialog.d.ts +11 -1
  154. package/dist/components/full-bleed-dialog.js +3 -1
  155. package/dist/components/full-bleed-dialog.js.map +1 -1
  156. package/dist/components/iban-input.d.ts +8 -3
  157. package/dist/components/image-grid.js +1 -1
  158. package/dist/components/image-grid.js.map +1 -1
  159. package/dist/components/inline-edit-field.js +10 -4
  160. package/dist/components/inline-edit-field.js.map +1 -1
  161. package/dist/components/language-select.d.ts +8 -3
  162. package/dist/components/legal-page.d.ts +26 -5
  163. package/dist/components/legal-page.js +3 -1
  164. package/dist/components/legal-page.js.map +1 -1
  165. package/dist/components/legal.d.ts +8 -3
  166. package/dist/components/legal.js +2 -1
  167. package/dist/components/legal.js.map +1 -1
  168. package/dist/components/lightbox.js +24 -2
  169. package/dist/components/lightbox.js.map +1 -1
  170. package/dist/components/line-items.js +2 -1
  171. package/dist/components/line-items.js.map +1 -1
  172. package/dist/components/list.d.ts +5 -0
  173. package/dist/components/list.js +13 -9
  174. package/dist/components/list.js.map +1 -1
  175. package/dist/components/measured-grid.d.ts +3 -1
  176. package/dist/components/measured-grid.js +38 -20
  177. package/dist/components/measured-grid.js.map +1 -1
  178. package/dist/components/menu-item.js +2 -1
  179. package/dist/components/menu-item.js.map +1 -1
  180. package/dist/components/mini-calendar.js +8 -4
  181. package/dist/components/mini-calendar.js.map +1 -1
  182. package/dist/components/money-field.d.ts +8 -3
  183. package/dist/components/month-picker.js +50 -39
  184. package/dist/components/month-picker.js.map +1 -1
  185. package/dist/components/nav-pills.js +1 -1
  186. package/dist/components/nav-pills.js.map +1 -1
  187. package/dist/components/number-field.d.ts +8 -3
  188. package/dist/components/number-input.d.ts +11 -4
  189. package/dist/components/number-input.js +8 -6
  190. package/dist/components/number-input.js.map +1 -1
  191. package/dist/components/numpad-sheet.d.ts +8 -3
  192. package/dist/components/numpad-sheet.js +2 -1
  193. package/dist/components/numpad-sheet.js.map +1 -1
  194. package/dist/components/one-time-code-input.js +2 -2
  195. package/dist/components/one-time-code-input.js.map +1 -1
  196. package/dist/components/page-contents.js +2 -2
  197. package/dist/components/page-contents.js.map +1 -1
  198. package/dist/components/phone-input.d.ts +8 -3
  199. package/dist/components/phone-input.js +4 -4
  200. package/dist/components/phone-input.js.map +1 -1
  201. package/dist/components/pie-chart.d.ts +8 -2
  202. package/dist/components/pie-chart.js +11 -7
  203. package/dist/components/pie-chart.js.map +1 -1
  204. package/dist/components/popover.d.ts +9 -1
  205. package/dist/components/popover.js +8 -4
  206. package/dist/components/popover.js.map +1 -1
  207. package/dist/components/row-actions.d.ts +100 -0
  208. package/dist/components/row-actions.js +231 -0
  209. package/dist/components/row-actions.js.map +1 -0
  210. package/dist/components/scroll-area.js +1 -1
  211. package/dist/components/scroll-area.js.map +1 -1
  212. package/dist/components/search-field.js +3 -2
  213. package/dist/components/search-field.js.map +1 -1
  214. package/dist/components/series-chart-budget.d.ts +30 -7
  215. package/dist/components/series-chart-budget.js +12 -4
  216. package/dist/components/series-chart-budget.js.map +1 -1
  217. package/dist/components/series-chart-ticks.d.ts +7 -1
  218. package/dist/components/series-chart-ticks.js +6 -0
  219. package/dist/components/series-chart-ticks.js.map +1 -1
  220. package/dist/components/series-chart.d.ts +58 -28
  221. package/dist/components/series-chart.js +42 -32
  222. package/dist/components/series-chart.js.map +1 -1
  223. package/dist/components/settings-fields.d.ts +57 -8
  224. package/dist/components/settings-fields.js +39 -1
  225. package/dist/components/settings-fields.js.map +1 -1
  226. package/dist/components/share-card.d.ts +11 -6
  227. package/dist/components/share-card.js +8 -2
  228. package/dist/components/share-card.js.map +1 -1
  229. package/dist/components/sign-chip.d.ts +4 -0
  230. package/dist/components/sign-chip.js +17 -4
  231. package/dist/components/sign-chip.js.map +1 -1
  232. package/dist/components/signature-pad.d.ts +3 -1
  233. package/dist/components/signature-pad.js +8 -5
  234. package/dist/components/signature-pad.js.map +1 -1
  235. package/dist/components/slider.js +7 -6
  236. package/dist/components/slider.js.map +1 -1
  237. package/dist/components/sparkline.d.ts +5 -1
  238. package/dist/components/sparkline.js +4 -2
  239. package/dist/components/sparkline.js.map +1 -1
  240. package/dist/components/stat-tile.d.ts +17 -1
  241. package/dist/components/stat-tile.js +48 -20
  242. package/dist/components/stat-tile.js.map +1 -1
  243. package/dist/components/status-dot.d.ts +4 -0
  244. package/dist/components/status-dot.js +9 -2
  245. package/dist/components/status-dot.js.map +1 -1
  246. package/dist/components/swipeable-row.js +10 -2
  247. package/dist/components/swipeable-row.js.map +1 -1
  248. package/dist/components/switch.d.ts +21 -7
  249. package/dist/components/switch.js +24 -11
  250. package/dist/components/switch.js.map +1 -1
  251. package/dist/components/table.js +2 -2
  252. package/dist/components/table.js.map +1 -1
  253. package/dist/components/text-link.d.ts +8 -3
  254. package/dist/components/text-link.js +1 -1
  255. package/dist/components/text-link.js.map +1 -1
  256. package/dist/components/text.d.ts +7 -2
  257. package/dist/components/text.js +5 -5
  258. package/dist/components/text.js.map +1 -1
  259. package/dist/components/tile-radio.js +2 -1
  260. package/dist/components/tile-radio.js.map +1 -1
  261. package/dist/components/time-input.d.ts +8 -3
  262. package/dist/components/toast.d.ts +18 -2
  263. package/dist/components/toast.js +8 -5
  264. package/dist/components/toast.js.map +1 -1
  265. package/dist/components/toggle-group.d.ts +25 -3
  266. package/dist/components/toggle-group.js +40 -11
  267. package/dist/components/toggle-group.js.map +1 -1
  268. package/dist/components/toggle-legend.d.ts +10 -1
  269. package/dist/components/toggle-legend.js +21 -7
  270. package/dist/components/toggle-legend.js.map +1 -1
  271. package/dist/components/translation-review-editor.js +1 -1
  272. package/dist/components/translation-review-editor.js.map +1 -1
  273. package/dist/components/translation-review-parts.d.ts +4 -0
  274. package/dist/components/translation-review.d.ts +4 -0
  275. package/dist/components/translation-review.js +3 -3
  276. package/dist/components/translation-review.js.map +1 -1
  277. package/dist/components/tree-view.js +3 -2
  278. package/dist/components/tree-view.js.map +1 -1
  279. package/dist/components/treemap.d.ts +5 -2
  280. package/dist/components/treemap.js +33 -16
  281. package/dist/components/treemap.js.map +1 -1
  282. package/dist/components/ui.d.ts +8 -3
  283. package/dist/components/ui.js +207 -119
  284. package/dist/components/ui.js.map +1 -1
  285. package/dist/components/use-table-state.d.ts +1 -1
  286. package/dist/components/user-avatar.d.ts +5 -1
  287. package/dist/components/user-avatar.js +1 -1
  288. package/dist/components/user-avatar.js.map +1 -1
  289. package/dist/components/wizard-stepper.js +20 -14
  290. package/dist/components/wizard-stepper.js.map +1 -1
  291. package/dist/{data-table-labels-DUELeXpa.d.ts → data-table-labels-kpAAVi6g.d.ts} +26 -2
  292. package/dist/data-table.d.ts +1 -1
  293. package/dist/{delete-account-setting-CpIhzxxZ.d.ts → delete-account-setting-FLyJf-kZ.d.ts} +642 -60
  294. package/dist/demo/demo-banner.js +1 -0
  295. package/dist/demo/demo-banner.js.map +1 -1
  296. package/dist/feedback/feedback-attachment.d.ts +8 -3
  297. package/dist/feedback/feedback-context.d.ts +8 -3
  298. package/dist/feedback/feedback-crash.d.ts +8 -3
  299. package/dist/feedback/feedback-dialog.d.ts +8 -3
  300. package/dist/feedback/feedback-dialog.js +3 -3
  301. package/dist/feedback/feedback-dialog.js.map +1 -1
  302. package/dist/feedback/feedback-inbox.d.ts +8 -3
  303. package/dist/feedback/feedback-inbox.js +2 -2
  304. package/dist/feedback/feedback-inbox.js.map +1 -1
  305. package/dist/feedback/feedback-labels.d.ts +8 -3
  306. package/dist/feedback/feedback-menu.d.ts +9 -4
  307. package/dist/feedback/feedback-record.d.ts +8 -3
  308. package/dist/feedback/feedback-row-detail.d.ts +8 -3
  309. package/dist/feedback/feedback-status-undo.d.ts +8 -3
  310. package/dist/feedback/feedback-submit.d.ts +8 -3
  311. package/dist/feedback/feedback-swipe.d.ts +8 -3
  312. package/dist/feedback/feedback-table.d.ts +8 -3
  313. package/dist/feedback/feedback-thread.d.ts +8 -3
  314. package/dist/feedback/feedback-thread.js +3 -3
  315. package/dist/feedback/feedback-thread.js.map +1 -1
  316. package/dist/feedback.d.ts +8 -3
  317. package/dist/hooks/use-anchored-panel.d.ts +32 -3
  318. package/dist/hooks/use-anchored-panel.js +21 -3
  319. package/dist/hooks/use-anchored-panel.js.map +1 -1
  320. package/dist/hooks/use-breakpoint.d.ts +88 -0
  321. package/dist/hooks/use-breakpoint.js +50 -0
  322. package/dist/hooks/use-breakpoint.js.map +1 -0
  323. package/dist/hooks/use-file-drop.d.ts +8 -3
  324. package/dist/hooks/use-large-text.d.ts +29 -0
  325. package/dist/hooks/use-large-text.js +20 -0
  326. package/dist/hooks/use-large-text.js.map +1 -0
  327. package/dist/hooks/use-windowed-rows.d.ts +52 -8
  328. package/dist/hooks/use-windowed-rows.js +94 -5
  329. package/dist/hooks/use-windowed-rows.js.map +1 -1
  330. package/dist/i18n/defaults.d.ts +8 -3
  331. package/dist/i18n/defaults.js +8 -0
  332. package/dist/i18n/defaults.js.map +1 -1
  333. package/dist/i18n/german.d.ts +8 -3
  334. package/dist/i18n/german.js +94 -3
  335. package/dist/i18n/german.js.map +1 -1
  336. package/dist/i18n/kit-labels.d.ts +8 -3
  337. package/dist/i18n/kit-labels.js.map +1 -1
  338. package/dist/i18n/languages.d.ts +10 -5
  339. package/dist/i18n/locales/de-CH.d.ts +8 -3
  340. package/dist/i18n/locales/en.d.ts +8 -3
  341. package/dist/i18n/locales/es.d.ts +8 -3
  342. package/dist/i18n/locales/es.js +89 -3
  343. package/dist/i18n/locales/es.js.map +1 -1
  344. package/dist/i18n/locales/fr.d.ts +8 -3
  345. package/dist/i18n/locales/fr.js +91 -3
  346. package/dist/i18n/locales/fr.js.map +1 -1
  347. package/dist/i18n/locales/hu.d.ts +8 -3
  348. package/dist/i18n/locales/hu.js +91 -3
  349. package/dist/i18n/locales/hu.js.map +1 -1
  350. package/dist/i18n/locales/it.d.ts +8 -3
  351. package/dist/i18n/locales/it.js +92 -3
  352. package/dist/i18n/locales/it.js.map +1 -1
  353. package/dist/i18n/locales/zh.d.ts +8 -3
  354. package/dist/i18n/locales/zh.js +89 -3
  355. package/dist/i18n/locales/zh.js.map +1 -1
  356. package/dist/i18n/review.d.ts +8 -3
  357. package/dist/i18n/review.js +15 -1
  358. package/dist/i18n/review.js.map +1 -1
  359. package/dist/index.d.ts +27 -11
  360. package/dist/index.js +77 -4
  361. package/dist/index.js.map +1 -1
  362. package/dist/landing/landing-actions.d.ts +8 -3
  363. package/dist/landing/landing-sections.d.ts +8 -3
  364. package/dist/landing/landing-sections.js +7 -3
  365. package/dist/landing/landing-sections.js.map +1 -1
  366. package/dist/landing/public-header.d.ts +9 -4
  367. package/dist/landing/public-header.js +3 -3
  368. package/dist/landing/public-header.js.map +1 -1
  369. package/dist/landing/routing.d.ts +8 -3
  370. package/dist/lib/cn.js +3 -2
  371. package/dist/lib/cn.js.map +1 -1
  372. package/dist/rhf/fields.d.ts +8 -3
  373. package/dist/rhf/fields.js +1 -1
  374. package/dist/rhf/fields.js.map +1 -1
  375. package/dist/rhf/form.d.ts +8 -3
  376. package/dist/rhf/form.js +2 -2
  377. package/dist/rhf/form.js.map +1 -1
  378. package/dist/rhf.d.ts +8 -3
  379. package/dist/search/command-palette.d.ts +1 -1
  380. package/dist/search/command-palette.js +9 -8
  381. package/dist/search/command-palette.js.map +1 -1
  382. package/dist/settings/settings-layout.js +2 -1
  383. package/dist/settings/settings-layout.js.map +1 -1
  384. package/dist/settings/settings-section.d.ts +8 -3
  385. package/dist/settings/use-account-appearance.d.ts +92 -0
  386. package/dist/settings/use-account-appearance.js +63 -0
  387. package/dist/settings/use-account-appearance.js.map +1 -0
  388. package/dist/settings/use-settings-route.d.ts +1 -1
  389. package/dist/settings/use-settings-route.js +2 -3
  390. package/dist/settings/use-settings-route.js.map +1 -1
  391. package/dist/shell/app-shell.d.ts +12 -171
  392. package/dist/shell/app-shell.js +144 -28
  393. package/dist/shell/app-shell.js.map +1 -1
  394. package/dist/shell/auth-layout.js +1 -1
  395. package/dist/shell/auth-layout.js.map +1 -1
  396. package/dist/shell/option-switcher-menu.js +1 -1
  397. package/dist/shell/option-switcher-menu.js.map +1 -1
  398. package/dist/shell/top-bar-brand.d.ts +8 -3
  399. package/dist/shell/top-bar-brand.js +3 -1
  400. package/dist/shell/top-bar-brand.js.map +1 -1
  401. package/dist/shell/topbar-action-menu.d.ts +4 -0
  402. package/dist/shell/topbar-action-menu.js +3 -3
  403. package/dist/shell/topbar-action-menu.js.map +1 -1
  404. package/dist/shell/topbar-controls.js +4 -4
  405. package/dist/shell/topbar-controls.js.map +1 -1
  406. package/dist/shell.d.ts +9 -5
  407. package/dist/theme/contrast-tokens.d.ts +41 -0
  408. package/dist/theme/contrast-tokens.js +59 -0
  409. package/dist/theme/contrast-tokens.js.map +1 -0
  410. package/dist/theme/contrast.d.ts +110 -0
  411. package/dist/theme/contrast.js +128 -0
  412. package/dist/theme/contrast.js.map +1 -0
  413. package/dist/theme/palette-presets.d.ts +12 -1
  414. package/dist/theme/palette-presets.js +9 -4
  415. package/dist/theme/palette-presets.js.map +1 -1
  416. package/dist/theme/palette-store.js.map +1 -1
  417. package/dist/theme/text-size.d.ts +173 -0
  418. package/dist/theme/text-size.js +121 -0
  419. package/dist/theme/text-size.js.map +1 -0
  420. package/dist/wizard/stepper-nav.d.ts +8 -3
  421. package/dist/wizard/wizard-summary.js +1 -1
  422. package/dist/wizard/wizard-summary.js.map +1 -1
  423. package/dist/wizard.d.ts +8 -3
  424. package/package.json +1 -1
  425. package/src/admin/admin-action-log.tsx +6 -1
  426. package/src/admin/invitations-panel.tsx +36 -29
  427. package/src/admin/user-roster.tsx +17 -72
  428. package/src/auth/auth-errors.ts +68 -7
  429. package/src/billing/billing-banner.tsx +205 -0
  430. package/src/billing/billing-labels.ts +240 -0
  431. package/src/billing/billing-lock.ts +75 -0
  432. package/src/billing/billing-standing.ts +72 -0
  433. package/src/billing/plan-card.tsx +458 -0
  434. package/src/billing/plan-limit-notice.tsx +145 -0
  435. package/src/billing/plan-limit.ts +53 -0
  436. package/src/billing/plan-price.ts +101 -0
  437. package/src/billing/subscription-actions.tsx +76 -0
  438. package/src/billing/subscription-status-chip.tsx +95 -0
  439. package/src/chart.ts +2 -0
  440. package/src/components/account-chips.tsx +36 -10
  441. package/src/components/alert-banner.tsx +16 -7
  442. package/src/components/amount-input.tsx +5 -5
  443. package/src/components/appearance-labels.ts +47 -0
  444. package/src/components/autocomplete.tsx +2 -2
  445. package/src/components/breadcrumbs.tsx +3 -3
  446. package/src/components/bulk-action-bar.tsx +11 -14
  447. package/src/components/button-group.tsx +21 -0
  448. package/src/components/calculator.tsx +4 -3
  449. package/src/components/calendar-heatmap.tsx +5 -5
  450. package/src/components/chart-height.ts +30 -0
  451. package/src/components/chart-zoom.tsx +6 -1
  452. package/src/components/checkbox-group.tsx +2 -2
  453. package/src/components/checkbox.tsx +62 -20
  454. package/src/components/chip.tsx +139 -30
  455. package/src/components/choice-card.tsx +16 -11
  456. package/src/components/column-mapper.tsx +1 -1
  457. package/src/components/combobox-core.tsx +8 -7
  458. package/src/components/combobox.tsx +8 -8
  459. package/src/components/country-select.tsx +1 -1
  460. package/src/components/currency-select.tsx +3 -3
  461. package/src/components/data-table-filter-popover.tsx +7 -1
  462. package/src/components/data-table-pagination.tsx +60 -31
  463. package/src/components/data-table.tsx +164 -43
  464. package/src/components/date-picker.tsx +44 -10
  465. package/src/components/description-list.tsx +2 -2
  466. package/src/components/dialog-frame.tsx +2 -1
  467. package/src/components/disclosure.tsx +3 -2
  468. package/src/components/dropdown.tsx +10 -2
  469. package/src/components/facing-pair.ts +12 -5
  470. package/src/components/field-parts.tsx +190 -7
  471. package/src/components/field-sync.tsx +17 -3
  472. package/src/components/field.tsx +2 -2
  473. package/src/components/floating-panel.tsx +7 -4
  474. package/src/components/focus-ring.ts +12 -0
  475. package/src/components/form-actions.tsx +55 -18
  476. package/src/components/full-bleed-dialog.tsx +18 -4
  477. package/src/components/image-grid.tsx +1 -1
  478. package/src/components/inline-edit-field.tsx +26 -4
  479. package/src/components/legal-page.tsx +28 -2
  480. package/src/components/legal.tsx +19 -1
  481. package/src/components/lightbox.tsx +24 -2
  482. package/src/components/line-items.tsx +7 -1
  483. package/src/components/list.tsx +36 -10
  484. package/src/components/measured-grid.tsx +49 -24
  485. package/src/components/menu-item.tsx +2 -2
  486. package/src/components/mini-calendar.tsx +8 -4
  487. package/src/components/month-picker.tsx +15 -6
  488. package/src/components/nav-pills.tsx +1 -1
  489. package/src/components/number-input.tsx +14 -7
  490. package/src/components/numpad-sheet.tsx +2 -2
  491. package/src/components/one-time-code-input.tsx +2 -2
  492. package/src/components/page-contents.tsx +2 -2
  493. package/src/components/phone-input.tsx +9 -4
  494. package/src/components/pie-chart.tsx +19 -9
  495. package/src/components/popover.tsx +22 -5
  496. package/src/components/row-actions.tsx +450 -0
  497. package/src/components/scroll-area.tsx +1 -1
  498. package/src/components/search-field.tsx +4 -2
  499. package/src/components/series-chart-budget.ts +40 -8
  500. package/src/components/series-chart-ticks.ts +10 -0
  501. package/src/components/series-chart.tsx +123 -72
  502. package/src/components/settings-fields.tsx +89 -5
  503. package/src/components/share-card.tsx +31 -2
  504. package/src/components/sign-chip.tsx +28 -4
  505. package/src/components/signature-pad.tsx +13 -7
  506. package/src/components/slider.tsx +22 -9
  507. package/src/components/sparkline.tsx +11 -3
  508. package/src/components/stat-tile.tsx +70 -14
  509. package/src/components/status-dot.tsx +25 -2
  510. package/src/components/swipeable-row.tsx +10 -2
  511. package/src/components/switch.tsx +57 -23
  512. package/src/components/table.tsx +2 -2
  513. package/src/components/text-link.tsx +1 -1
  514. package/src/components/text.tsx +11 -6
  515. package/src/components/tile-radio.tsx +4 -1
  516. package/src/components/toast.tsx +27 -7
  517. package/src/components/toggle-group.tsx +115 -14
  518. package/src/components/toggle-legend.tsx +53 -8
  519. package/src/components/translation-review-editor.tsx +1 -1
  520. package/src/components/translation-review.tsx +11 -5
  521. package/src/components/tree-view.tsx +3 -2
  522. package/src/components/treemap.tsx +27 -10
  523. package/src/components/ui.tsx +353 -153
  524. package/src/components/user-avatar.tsx +1 -1
  525. package/src/components/wizard-stepper.tsx +5 -1
  526. package/src/demo/demo-banner.tsx +5 -0
  527. package/src/feedback/feedback-dialog.tsx +3 -3
  528. package/src/feedback/feedback-inbox.tsx +2 -2
  529. package/src/feedback/feedback-thread.tsx +3 -3
  530. package/src/hooks/use-anchored-panel.ts +67 -5
  531. package/src/hooks/use-breakpoint.ts +132 -0
  532. package/src/hooks/use-large-text.ts +41 -0
  533. package/src/hooks/use-windowed-rows.ts +168 -12
  534. package/src/i18n/defaults.ts +8 -0
  535. package/src/i18n/german.ts +107 -0
  536. package/src/i18n/kit-labels.tsx +12 -0
  537. package/src/i18n/locales/es.ts +98 -0
  538. package/src/i18n/locales/fr.ts +102 -0
  539. package/src/i18n/locales/hu.ts +100 -0
  540. package/src/i18n/locales/it.ts +103 -0
  541. package/src/i18n/locales/zh.ts +94 -0
  542. package/src/i18n/review.ts +14 -0
  543. package/src/index.ts +76 -6
  544. package/src/landing/landing-sections.tsx +7 -3
  545. package/src/landing/public-header.tsx +4 -4
  546. package/src/lib/cn.ts +13 -2
  547. package/src/rhf/fields.tsx +1 -1
  548. package/src/rhf/form.tsx +2 -2
  549. package/src/search/command-palette.tsx +10 -9
  550. package/src/settings/settings-layout.tsx +2 -1
  551. package/src/settings/use-account-appearance.ts +148 -0
  552. package/src/settings/use-settings-route.ts +3 -4
  553. package/src/shell/app-shell.tsx +278 -54
  554. package/src/shell/auth-layout.tsx +1 -1
  555. package/src/shell/option-switcher-menu.tsx +1 -1
  556. package/src/shell/top-bar-brand.tsx +3 -1
  557. package/src/shell/topbar-action-menu.tsx +3 -3
  558. package/src/shell/topbar-controls.tsx +4 -4
  559. package/src/theme/contrast-tokens.ts +112 -0
  560. package/src/theme/contrast.ts +225 -0
  561. package/src/theme/palette-presets.ts +24 -4
  562. package/src/theme/palette-store.ts +6 -1
  563. package/src/theme/text-size.ts +283 -0
  564. package/src/wizard/wizard-summary.tsx +4 -2
  565. package/tokens.css +158 -8
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/autocomplete.tsx"],"sourcesContent":["import { Fragment, forwardRef, useId, useRef, useState } from \"react\";\nimport type {\n ComponentPropsWithoutRef,\n ForwardedRef,\n ReactElement,\n ReactNode,\n RefAttributes,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, Spinner } from \"./ui\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n isOptionEnabled,\n stepEnabled,\n useActiveOptionScroll,\n useComboboxFieldError,\n useOptionSource,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n StaticLabelRow,\n useFieldHint,\n useLockReason,\n} from \"./field-parts\";\nimport { mergeDescribedBy } from \"./choice-parts\";\n\n/**\n * `value`/`onChange` are the TEXT's, and `onSelect` is \"a suggestion was taken\" —\n * not the DOM's text-selection event — so the input's own spellings of those three\n * are omitted. Everything else reaches the `<input>`, the way it does on {@link Input}.\n */\nexport interface AutocompleteProps<V extends string | number = string>\n extends Omit<\n ComponentPropsWithoutRef<\"input\">,\n \"value\" | \"defaultValue\" | \"onChange\" | \"onSelect\" | \"children\" | \"type\" | \"size\"\n > {\n /** What is in the field. Controlled, and never reset by the component: opening,\n * closing and a failed lookup all leave it exactly as typed. */\n value: string;\n /** Every keystroke — and, with `fillOnSelect`, the label of a taken suggestion. */\n onChange: (text: string) => void;\n /** Suggestions the caller already has (e.g. from its own query hook). Narrowed by\n * the text unless `filter={false}`. An option with `disabled: true` is listed but\n * cannot be taken — see {@link ComboOption.disabled}. */\n options?: ComboOption<V>[];\n /** Or: fetch them. Debounced (`debounceMs`), race-safe, only at `minChars` and up,\n * never while the field is idle; a rejection shows `loadErrorLabel` and empties\n * the list rather than leaving the previous text's rows under it. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** Narrow `options` by label/sublabel substring. Default `true`. Pass `false` for\n * a list a server has already searched and ranked — it is shown as given. */\n filter?: boolean;\n /** External loading flag (a caller-side fetch), OR-ed with `loadOptions`'. */\n loading?: boolean;\n /** Below this many characters nothing is offered or fetched. Default 1: an empty\n * field asks nothing — focusing it is not a search. */\n minChars?: number;\n /** Quiet time before `loadOptions` runs. Default 300 ms — an ordinary typist's\n * inter-key interval is 150–250 ms, so the pickers' 150 fires on nearly every\n * letter, which a rate-limited geocoder cannot afford. */\n debounceMs?: number;\n /**\n * Whether the list MAY show — controlled. Omit (or pass `undefined`) and the field\n * decides: open while focused, until Escape, Tab or a taken row. `false` keeps it\n * shut whatever the user does, and stops `loadOptions` asking — keksdose's address\n * search, with its confirm map up, where it used to pass `minChars={Infinity}`.\n * `true` shows it even unfocused.\n *\n * Even open, the list only APPEARS when it has something to show (rows, or a\n * status line); `aria-expanded` follows what appears, not this flag.\n */\n open?: boolean;\n /** The field wants the list open or shut — focus, typing, ↓/↑ open it; blur,\n * Escape, Tab and taking a row shut it. Called on every change, controlled or not. */\n onOpenChange?: (open: boolean) => void;\n /**\n * `\"sm\"`: the 28px, 12px-type field of {@link Select}'s `size=\"sm\"`, with the icon,\n * spinner and list rows scaled to match — for a card or toolbar whose buttons are\n * small. `\"md\"` (default) is the form field. Unlabelled only, as on `Select`: a\n * floating label needs the tall box, so a labelled field ignores `\"sm\"`.\n *\n * A NUMBER is still the native `size` attribute (the width in characters), passed\n * straight through.\n */\n size?: \"sm\" | \"md\" | number;\n /** A suggestion was taken (click, or Enter on the highlighted row). */\n onSelect?: (option: ComboOption<V>) => void;\n /**\n * Put the taken suggestion's label into the field (`onChange(label)`). Default\n * `true` — the free-text case, where the text IS the value and a suggestion only\n * completes it (an address field). Pass `false` where taking a row is an ACTION\n * and the field is only the search that found it; the text then stays as typed.\n */\n fillOnSelect?: boolean;\n /** A floating label, as on {@link Combobox} — a real `<label for>` on the input.\n * Names the field (`aria-labelledby`) unless an `aria-label` is given. */\n label?: ReactNode;\n /** Leading decoration inside the field (a pin, a magnifier). Decorative: the\n * label names the field. An `<svg>` child is sized by the field — 16px, or 14px\n * at `size=\"sm\"` — so a lucide icon needs no class of its own. */\n icon?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}. */\n invalid?: boolean;\n /** What is wrong with the VALUE, as {@link Input}'s `error`: under the field, on\n * its `aria-describedby`, implies `invalid`. Not a failed lookup — that is\n * `loadErrorLabel`, shown in the list. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so this field reads like every other\n * (keksdose K4): plain TEXT is a caption under the field on the input's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the field's end edge, outside the box. Not the list's\n * `status` line, which talks about the lookup, not the field.\n *\n * Pass it — even as `undefined` — on a field whose hint comes and goes (0.24): the\n * unlabelled field then keeps its end-hint row whether or not a \"?\" is in it, so the\n * `<input>` is never re-parented, and focus and caret survive the toggle. The rule\n * the rest of the family has had since 0.23 (and the fields since 0.22).\n */\n hint?: ReactNode;\n /**\n * The caller's own line in the list, replacing the automatic one (loading / failed\n * / no results). For what only the caller knows: \"offline\", \"the address service\n * is switched off\", a privacy note, the hint a caller-fetched list needs below its\n * own minimum. While set, the list opens even with no rows. Announced politely.\n */\n status?: ReactNode;\n /** Default: `combobox.noResults` from the {@link UiKitProvider}, else English. */\n emptyLabel?: string;\n /** Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n /** Classes for the `<input>` itself; `className` styles the wrapper. */\n inputClassName?: string;\n /**\n * Why the field cannot be changed — {@link Button}'s `disabledReason`, the Combobox\n * family's write lock (keksdose G2, 0.23), for a field whose pick SAVES: an address\n * search whose taken row writes the address, a lookup that links a record.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey FIELD_BASE gives a `[readonly]` field), opens no list and asks no\n * `loadOptions` — whatever `open` says — and nothing reaches `onChange`, `onSelect` or\n * `onOpenChange(true)`. Enter is swallowed, so a locked field does not submit the form\n * around it, and a caller's `onKeyDown` does not hear it; every other key still\n * reaches the caller (an Escape that closes its panel) and the caret. The reason is in\n * the kit {@link Tooltip} and on the input's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /**\n * This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the\n * `disabledReason` way with the lock's reason (which wins over its own). A search\n * whose result is only draft state — the form's Save writes it — leaves this off and\n * stays live under the lock. No provider, or an unlocked one: no effect.\n */\n commit?: boolean;\n}\n\nfunction AutocompleteInner<V extends string | number = string>(\n props: AutocompleteProps<V>,\n ref: ForwardedRef<HTMLInputElement>,\n) {\n const {\n value,\n onChange,\n options,\n loadOptions,\n filter,\n loading,\n minChars = 1,\n debounceMs = 300,\n onSelect,\n fillOnSelect = true,\n label,\n icon,\n invalid,\n error,\n hint,\n status,\n emptyLabel,\n loadErrorLabel,\n open: openProp,\n onOpenChange,\n size,\n className,\n inputClassName,\n id,\n disabled,\n disabledReason,\n commit,\n onKeyDown,\n onFocus,\n onBlur,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n } = props;\n const generated = useId();\n const fieldId = id ?? generated;\n // Off the GENERATED id: a caller's `id` is theirs to collide with.\n const listboxId = `${generated}-listbox`;\n const labelId = `${generated}-label`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const fieldRef = useRef<HTMLDivElement>(null);\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n noResults: emptyLabel,\n loadError: loadErrorLabel,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useComboboxFieldError(error, invalid, hintParts.describedBy);\n\n /** The user's intent: the field is focused and the list has not been dismissed.\n * Whether anything SHOWS is decided below, from what there is to show. */\n const [ownOpen, setOwnOpen] = useState(false);\n // Controlled when the caller passes a boolean. The own state is kept up to date\n // either way, so a caller that controls only SOMETIMES (`open={confirming ? false :\n // undefined}`) hands back a field that is exactly as open as focus says it is.\n const open = openProp ?? ownOpen;\n const setOpen = (next: boolean) => {\n setOwnOpen(next);\n if (next !== open) onOpenChange?.(next);\n };\n const [active, setActive] = useState(-1);\n // The lock: focusable, `readOnly`, no list — see `disabledReason`. Folded into `live`,\n // so a locked field asks nothing and shows nothing even with `open` held true.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const live = open && !disabled && !locked;\n const { results, busy, failed, tooShort } = useOptionSource<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n query: value,\n active: live,\n });\n\n const hasStatus = status !== undefined && status !== null && status !== false && status !== \"\";\n // The visible line under the rows. Nothing below `minChars` unless the caller has\n // something to say — an empty field opening a box that says \"type more\" is noise.\n const statusLine: ReactNode = hasStatus\n ? status\n : tooShort\n ? null\n : busy && results.length === 0\n ? labels.loading\n : failed\n ? labels.loadError\n : !busy && results.length === 0\n ? labels.noResults\n : null;\n const expanded = live && (results.length > 0 || statusLine !== null);\n // A disabled row is never the keyboard's — not even when a list that changed under\n // the highlight put one there.\n const activeId = expanded && isOptionEnabled(results[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n const { rect, top, maxHeight } = useAnchoredPanel(fieldRef, expanded, { preferredHeight: 256 });\n const dir = useAnchorDir(fieldRef, expanded);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n };\n const take = (o: ComboOption<V>) => {\n if (o.disabled || locked) return;\n if (fillOnSelect) onChange(o.label);\n onSelect?.(o);\n close();\n };\n\n const hasLabel = label !== undefined;\n // `size=\"sm\"` is for an UNLABELLED field, as on Select: the floating label needs the\n // tall box. A number is the native attribute.\n const small = size === \"sm\" && !hasLabel;\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // The caller's name wins; else the floating label, by reference so a non-string\n // label still names the field.\n const labelledBy =\n ariaLabel === undefined ? (ariaLabelledBy ?? (hasLabel ? labelId : undefined)) : undefined;\n // While locked the field sits in the reason's Tooltip — in a fragment with a hidden\n // copy of the reason, Button's anatomy; `block` so a full-width field stays full width.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `relative`: the floating label and the live region's `sr-only` both need a\n // local containing block.\n <div className={cn(\"relative\", className)}>\n {/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the\n name — and the listbox's, which borrows it — is exactly what it was. */}\n {hasLabel && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId}>\n {label}\n </ComboboxFieldLabel>\n )}\n {/* `\"hint\" in props`: the family's 0.22 rule (see `endHintRowProps`) — passed at\n all, even as `undefined`, a hint keeps the unlabelled field's end-hint row, so\n a \"?\" coming and going never re-parents the <input> under the caret (0.24, the\n last of the family to get it; the siblings had it in 0.23). */}\n <EndHintRow {...endHintRowProps(\"hint\" in props, hasLabel, hintParts.labelHint)}>\n {withLock(\n <div ref={fieldRef} className={cn(\"relative\", locked && \"cursor-not-allowed\")}>\n {icon && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none absolute flex text-[var(--text-muted)]\",\n small ? \"start-2 [&>svg]:size-3.5\" : \"start-2.5 [&>svg]:size-4\",\n // Labelled, the icon sits on the VALUE's line — the one line box below\n // FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of\n // the box. Centred on the box it rose into the top strip and sat on the\n // label's first letters, which float at the same start inset. On the\n // value line it is beside what it describes, and the label keeps the\n // start edge every other label in the form's column shares.\n hasLabel\n ? \"top-[calc(1rem+1px)] h-5 items-center\"\n : \"top-1/2 -translate-y-1/2\",\n )}\n >\n {icon}\n </span>\n )}\n <input\n {...rest}\n ref={ref}\n id={fieldId}\n size={nativeSize}\n type=\"text\"\n value={value}\n // Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || rest.readOnly}\n aria-disabled={locked || rest[\"aria-disabled\"]}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={expanded}\n // Required by the role, and set while closed too, as on the siblings.\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n autoComplete=\"off\"\n onFocus={(e) => {\n onFocus?.(e);\n // A locked field takes focus to say why, and opens nothing.\n if (locked) return;\n // A seeded field (keksdose's `initialQuery`) is looked up as it is\n // focused; an empty one asks nothing, because of `minChars`.\n setOpen(true);\n }}\n onBlur={(e) => {\n onBlur?.(e);\n close();\n }}\n onChange={(e) => {\n if (locked) return;\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n // Locked: Enter is swallowed (no form submit, no caller's \"search now\");\n // every other key is the caller's and the caret's. No list to drive.\n if (locked) {\n if (e.key === \"Enter\") e.preventDefault();\n else onKeyDown?.(e);\n return;\n }\n // An Escape that closes the OPEN list is the list's, and is consumed before\n // the caller sees it. A caller whose Escape means \"close the panel\" (keksdose's\n // address search) otherwise closed the whole panel when the user only meant\n // to dismiss the suggestions — the opposite of what the docs promised.\n if (e.key === \"Escape\" && expanded) {\n e.preventDefault();\n e.stopPropagation();\n close();\n return;\n }\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n // Disabled rows are passed over; at either end the highlight stays put.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) setOpen(true);\n else setActive((i) => stepEnabled(results, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) setOpen(true);\n // From \"nothing highlighted\", Up goes to the bottom, as the APG has it.\n else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));\n } else if (e.key === \"Enter\") {\n // No row highlighted: the text is the answer, and a form's own submit\n // is left alone.\n if (expanded && activeId) {\n e.preventDefault();\n take(results[active]);\n }\n } else if (e.key === \"Tab\") {\n close();\n }\n }}\n className={cn(\n FIELD_BASE,\n hasLabel && FIELD_FLOATING_PAD,\n // Select's `SELECT_SM` box: a fixed 28px, so the caller's line height\n // cannot grow it.\n small && \"h-7 py-0 pe-2 ps-2 text-xs\",\n icon ? (small ? \"ps-7\" : \"ps-8\") : undefined,\n busy && (small ? \"pe-7\" : \"pe-9\"),\n field.isInvalid && FIELD_INVALID,\n inputClassName,\n )}\n />\n {busy && live && (\n // Decorative: the live region below already says \"Loading…\".\n <span\n className={cn(\n \"pointer-events-none absolute top-1/2 flex -translate-y-1/2\",\n small ? \"end-2\" : \"end-2.5\",\n )}\n >\n <Spinner label={null} className={small ? \"h-3.5 w-3.5\" : \"h-4 w-4\"} />\n </span>\n )}\n </div>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it, after the field so the \"?\"\n follows the control in the tab order (as on Input and Select). */}\n {hasLabel && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId} className={LABEL_IN_ROW}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {/* The live region, kept mounted so a change IS an announcement: focus stays\n in the field, and without it a reader typed into a geocoder and heard only\n their own letters. The panel's visible line is aria-hidden in favour of\n this, which also carries the row count the eye reads off the list. */}\n <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {!live\n ? null\n : statusLine !== null\n ? statusLine\n : results.length > 0\n ? labels.resultCount(results.length)\n : null}\n </div>\n {expanded &&\n rect &&\n typeof document !== \"undefined\" &&\n createPortal(\n <div\n // Every press inside the list keeps focus in the field — rows, the status\n // line, the scrollbar — or the input's blur would close the list first.\n role=\"presentation\"\n // Portalled out of the form's `dir`; the field's is put back. The list is\n // exactly the field's width, so `left` places it in either direction.\n dir={dir}\n onMouseDown={(e) => e.preventDefault()}\n className=\"fixed z-50 flex flex-col overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg\"\n style={{ top, left: rect.left, width: rect.width, maxHeight }}\n >\n {results.length > 0 && (\n <ul\n id={listboxId}\n role=\"listbox\"\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n className=\"min-h-0 flex-1 overflow-y-auto py-1\"\n >\n {results.map((o, i) => {\n const startsGroup = o.group != null && o.group !== results[i - 1]?.group;\n return (\n // Keyed by group AND value, as in the core panel: an option may\n // appear once per group.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {startsGroup && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)] first:pt-1\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n // CHOSEN, never \"highlighted\" — the family's rule. This\n // field holds no value, so the only row that can be chosen\n // is the one whose label the text already is.\n aria-selected={fillOnSelect && o.label === value}\n // Listed, announced as unavailable, never taken: `take`\n // refuses it and the arrows pass it over.\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onClick={() => take(o)}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n \"flex w-full items-center gap-2 px-3 text-start\",\n small ? \"py-1 text-xs\" : \"py-1.5 text-sm\",\n o.disabled\n ? DISABLED_ROW_CLASS\n : i === active\n ? \"bg-[var(--bg-active)]\"\n : \"hover:bg-[var(--bg-hover)]\",\n o.group != null && \"ps-6\",\n )}\n >\n {o.icon && (\n <span aria-hidden className=\"shrink-0 text-[var(--text-muted)]\">\n {o.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-[var(--text-primary)]\">\n {o.label}\n </span>\n {o.sublabel && (\n <span\n className={cn(\n \"block truncate text-[var(--text-placeholder)]\",\n small ? \"text-[11px]\" : \"text-xs\",\n )}\n >\n {o.sublabel}\n </span>\n )}\n </span>\n </button>\n </li>\n </Fragment>\n );\n })}\n </ul>\n )}\n {statusLine !== null && (\n // Visible, not live — the region above speaks for it.\n <div\n aria-hidden\n className={cn(\n small ? \"px-3 py-1.5 text-xs\" : \"px-3 py-2 text-sm\",\n failed && !hasStatus ? \"text-[var(--danger)]\" : \"text-[var(--text-muted)]\",\n results.length > 0 && \"border-t border-[var(--border)] text-xs\",\n )}\n >\n {statusLine}\n </div>\n )}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n\nconst AutocompleteBase = forwardRef(AutocompleteInner);\nAutocompleteBase.displayName = \"Autocomplete\";\n\n/**\n * An inline, async-capable text field with a suggestion list under it — the\n * WAI-ARIA APG \"editable combobox with list autocomplete\".\n *\n * Two jobs, one control:\n * - **free text** (kastlan's address field): the text is the value, the list only\n * offers completions, and taking one fills the field (`fillOnSelect`, default);\n * - **search-then-act** (keksdose's address search): the text is a query, taking a\n * row calls `onSelect` and nothing is held (`fillOnSelect={false}`).\n *\n * How it differs from its siblings: {@link Combobox} is free text over a STATIC\n * string pool and opens on focus; {@link EntityCombobox} is a trigger holding an id.\n * This one keeps no value of its own, never resets the text, asks nothing below\n * `minChars`, and reports a failed lookup instead of showing stale rows.\n *\n * Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted\n * row, `aria-controls` the list, and a polite live region says what the list now\n * holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter\n * takes the highlighted row (with none,\n * Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay\n * with the caret. An Escape that closes an open list is consumed and never reaches the\n * caller; with the list closed it does. For every other key a caller's `onKeyDown`\n * runs first and may `preventDefault()` to\n * keep a key for itself.\n *\n * The list is portalled and anchored under the field on every screen size: the\n * field already carries the keyboard, and `useAnchoredPanel` keeps the list clear\n * of it on a phone. There is no full-screen sheet as {@link Combobox} has.\n\n * Generic over the option value (`ComboOption<V>`); the ref is the `<input>`'s.\n */\nexport const Autocomplete = AutocompleteBase as <V extends string | number = string>(\n props: AutocompleteProps<V> & RefAttributes<HTMLInputElement>,\n) => ReactElement | null;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/autocomplete.tsx"],"sourcesContent":["import { Fragment, forwardRef, useId, useRef, useState } from \"react\";\nimport type {\n ComponentPropsWithoutRef,\n ForwardedRef,\n ReactElement,\n ReactNode,\n RefAttributes,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, Spinner } from \"./ui\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n isOptionEnabled,\n stepEnabled,\n useActiveOptionScroll,\n useComboboxFieldError,\n useOptionSource,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n StaticLabelRow,\n useFieldHint,\n useLockReason,\n} from \"./field-parts\";\nimport { mergeDescribedBy } from \"./choice-parts\";\n\n/**\n * `value`/`onChange` are the TEXT's, and `onSelect` is \"a suggestion was taken\" —\n * not the DOM's text-selection event — so the input's own spellings of those three\n * are omitted. Everything else reaches the `<input>`, the way it does on {@link Input}.\n */\nexport interface AutocompleteProps<V extends string | number = string>\n extends Omit<\n ComponentPropsWithoutRef<\"input\">,\n \"value\" | \"defaultValue\" | \"onChange\" | \"onSelect\" | \"children\" | \"type\" | \"size\"\n > {\n /** What is in the field. Controlled, and never reset by the component: opening,\n * closing and a failed lookup all leave it exactly as typed. */\n value: string;\n /** Every keystroke — and, with `fillOnSelect`, the label of a taken suggestion. */\n onChange: (text: string) => void;\n /** Suggestions the caller already has (e.g. from its own query hook). Narrowed by\n * the text unless `filter={false}`. An option with `disabled: true` is listed but\n * cannot be taken — see {@link ComboOption.disabled}. */\n options?: ComboOption<V>[];\n /** Or: fetch them. Debounced (`debounceMs`), race-safe, only at `minChars` and up,\n * never while the field is idle; a rejection shows `loadErrorLabel` and empties\n * the list rather than leaving the previous text's rows under it. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** Narrow `options` by label/sublabel substring. Default `true`. Pass `false` for\n * a list a server has already searched and ranked — it is shown as given. */\n filter?: boolean;\n /** External loading flag (a caller-side fetch), OR-ed with `loadOptions`'. */\n loading?: boolean;\n /** Below this many characters nothing is offered or fetched. Default 1: an empty\n * field asks nothing — focusing it is not a search. */\n minChars?: number;\n /** Quiet time before `loadOptions` runs. Default 300 ms — an ordinary typist's\n * inter-key interval is 150–250 ms, so the pickers' 150 fires on nearly every\n * letter, which a rate-limited geocoder cannot afford. */\n debounceMs?: number;\n /**\n * Whether the list MAY show — controlled. Omit (or pass `undefined`) and the field\n * decides: open while focused, until Escape, Tab or a taken row. `false` keeps it\n * shut whatever the user does, and stops `loadOptions` asking — keksdose's address\n * search, with its confirm map up, where it used to pass `minChars={Infinity}`.\n * `true` shows it even unfocused.\n *\n * Even open, the list only APPEARS when it has something to show (rows, or a\n * status line); `aria-expanded` follows what appears, not this flag.\n */\n open?: boolean;\n /** The field wants the list open or shut — focus, typing, ↓/↑ open it; blur,\n * Escape, Tab and taking a row shut it. Called on every change, controlled or not. */\n onOpenChange?: (open: boolean) => void;\n /**\n * `\"sm\"`: the 28px, 12px-type field of {@link Select}'s `size=\"sm\"`, with the icon,\n * spinner and list rows scaled to match — for a card or toolbar whose buttons are\n * small. `\"md\"` (default) is the form field. Unlabelled only, as on `Select`: a\n * floating label needs the tall box, so a labelled field ignores `\"sm\"`.\n *\n * A NUMBER is still the native `size` attribute (the width in characters), passed\n * straight through.\n */\n size?: \"sm\" | \"md\" | number;\n /** A suggestion was taken (click, or Enter on the highlighted row). */\n onSelect?: (option: ComboOption<V>) => void;\n /**\n * Put the taken suggestion's label into the field (`onChange(label)`). Default\n * `true` — the free-text case, where the text IS the value and a suggestion only\n * completes it (an address field). Pass `false` where taking a row is an ACTION\n * and the field is only the search that found it; the text then stays as typed.\n */\n fillOnSelect?: boolean;\n /** A floating label, as on {@link Combobox} — a real `<label for>` on the input.\n * Names the field (`aria-labelledby`) unless an `aria-label` is given. */\n label?: ReactNode;\n /** Leading decoration inside the field (a pin, a magnifier). Decorative: the\n * label names the field. An `<svg>` child is sized by the field — 16px, or 14px\n * at `size=\"sm\"` — so a lucide icon needs no class of its own. */\n icon?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}. */\n invalid?: boolean;\n /** What is wrong with the VALUE, as {@link Input}'s `error`: under the field, on\n * its `aria-describedby`, implies `invalid`. Not a failed lookup — that is\n * `loadErrorLabel`, shown in the list. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so this field reads like every other\n * (keksdose K4): plain TEXT is a caption under the field on the input's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the field's end edge, outside the box. Not the list's\n * `status` line, which talks about the lookup, not the field.\n *\n * Pass it — even as `undefined` — on a field whose hint comes and goes (0.24): the\n * unlabelled field then keeps its end-hint row whether or not a \"?\" is in it, so the\n * `<input>` is never re-parented, and focus and caret survive the toggle. The rule\n * the rest of the family has had since 0.23 (and the fields since 0.22).\n */\n hint?: ReactNode;\n /**\n * The caller's own line in the list, replacing the automatic one (loading / failed\n * / no results). For what only the caller knows: \"offline\", \"the address service\n * is switched off\", a privacy note, the hint a caller-fetched list needs below its\n * own minimum. While set, the list opens even with no rows. Announced politely.\n */\n status?: ReactNode;\n /** Default: `combobox.noResults` from the {@link UiKitProvider}, else English. */\n emptyLabel?: string;\n /** Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n /** Classes for the `<input>` itself; `className` styles the wrapper. */\n inputClassName?: string;\n /**\n * Why the field cannot be changed — {@link Button}'s `disabledReason`, the Combobox\n * family's write lock (keksdose G2, 0.23), for a field whose pick SAVES: an address\n * search whose taken row writes the address, a lookup that links a record.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey FIELD_BASE gives a `[readonly]` field), opens no list and asks no\n * `loadOptions` — whatever `open` says — and nothing reaches `onChange`, `onSelect` or\n * `onOpenChange(true)`. Enter is swallowed, so a locked field does not submit the form\n * around it, and a caller's `onKeyDown` does not hear it; every other key still\n * reaches the caller (an Escape that closes its panel) and the caret. The reason is in\n * the kit {@link Tooltip} and on the input's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /**\n * This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the\n * `disabledReason` way with the lock's reason (which wins over its own). A search\n * whose result is only draft state — the form's Save writes it — leaves this off and\n * stays live under the lock. No provider, or an unlocked one: no effect.\n */\n commit?: boolean;\n}\n\nfunction AutocompleteInner<V extends string | number = string>(\n props: AutocompleteProps<V>,\n ref: ForwardedRef<HTMLInputElement>,\n) {\n const {\n value,\n onChange,\n options,\n loadOptions,\n filter,\n loading,\n minChars = 1,\n debounceMs = 300,\n onSelect,\n fillOnSelect = true,\n label,\n icon,\n invalid,\n error,\n hint,\n status,\n emptyLabel,\n loadErrorLabel,\n open: openProp,\n onOpenChange,\n size,\n className,\n inputClassName,\n id,\n disabled,\n disabledReason,\n commit,\n onKeyDown,\n onFocus,\n onBlur,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n } = props;\n const generated = useId();\n const fieldId = id ?? generated;\n // Off the GENERATED id: a caller's `id` is theirs to collide with.\n const listboxId = `${generated}-listbox`;\n const labelId = `${generated}-label`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const fieldRef = useRef<HTMLDivElement>(null);\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n noResults: emptyLabel,\n loadError: loadErrorLabel,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useComboboxFieldError(error, invalid, hintParts.describedBy);\n\n /** The user's intent: the field is focused and the list has not been dismissed.\n * Whether anything SHOWS is decided below, from what there is to show. */\n const [ownOpen, setOwnOpen] = useState(false);\n // Controlled when the caller passes a boolean. The own state is kept up to date\n // either way, so a caller that controls only SOMETIMES (`open={confirming ? false :\n // undefined}`) hands back a field that is exactly as open as focus says it is.\n const open = openProp ?? ownOpen;\n const setOpen = (next: boolean) => {\n setOwnOpen(next);\n if (next !== open) onOpenChange?.(next);\n };\n const [active, setActive] = useState(-1);\n // The lock: focusable, `readOnly`, no list — see `disabledReason`. Folded into `live`,\n // so a locked field asks nothing and shows nothing even with `open` held true.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const live = open && !disabled && !locked;\n const { results, busy, failed, tooShort } = useOptionSource<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n query: value,\n active: live,\n });\n\n const hasStatus = status !== undefined && status !== null && status !== false && status !== \"\";\n // The visible line under the rows. Nothing below `minChars` unless the caller has\n // something to say — an empty field opening a box that says \"type more\" is noise.\n const statusLine: ReactNode = hasStatus\n ? status\n : tooShort\n ? null\n : busy && results.length === 0\n ? labels.loading\n : failed\n ? labels.loadError\n : !busy && results.length === 0\n ? labels.noResults\n : null;\n const expanded = live && (results.length > 0 || statusLine !== null);\n // A disabled row is never the keyboard's — not even when a list that changed under\n // the highlight put one there.\n const activeId = expanded && isOptionEnabled(results[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n const { rect, top, maxHeight } = useAnchoredPanel(fieldRef, expanded, { preferredHeight: 256 });\n const dir = useAnchorDir(fieldRef, expanded);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n };\n const take = (o: ComboOption<V>) => {\n if (o.disabled || locked) return;\n if (fillOnSelect) onChange(o.label);\n onSelect?.(o);\n close();\n };\n\n const hasLabel = label !== undefined;\n // `size=\"sm\"` is for an UNLABELLED field, as on Select: the floating label needs the\n // tall box. A number is the native attribute.\n const small = size === \"sm\" && !hasLabel;\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // The caller's name wins; else the floating label, by reference so a non-string\n // label still names the field.\n const labelledBy =\n ariaLabel === undefined ? (ariaLabelledBy ?? (hasLabel ? labelId : undefined)) : undefined;\n // While locked the field sits in the reason's Tooltip — in a fragment with a hidden\n // copy of the reason, Button's anatomy; `block` so a full-width field stays full width.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `relative`: the floating label and the live region's `sr-only` both need a\n // local containing block.\n <div className={cn(\"relative\", className)}>\n {/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the\n name — and the listbox's, which borrows it — is exactly what it was. */}\n {hasLabel && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId}>\n {label}\n </ComboboxFieldLabel>\n )}\n {/* `\"hint\" in props`: the family's 0.22 rule (see `endHintRowProps`) — passed at\n all, even as `undefined`, a hint keeps the unlabelled field's end-hint row, so\n a \"?\" coming and going never re-parents the <input> under the caret (0.24, the\n last of the family to get it; the siblings had it in 0.23). */}\n <EndHintRow {...endHintRowProps(\"hint\" in props, hasLabel, hintParts.labelHint)}>\n {withLock(\n <div ref={fieldRef} className={cn(\"relative\", locked && \"cursor-not-allowed\")}>\n {icon && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none absolute flex text-[var(--text-muted)]\",\n small ? \"start-2 [&>svg]:size-3.5\" : \"start-2.5 [&>svg]:size-4\",\n // Labelled, the icon sits on the VALUE's line — the one line box below\n // FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of\n // the box. Centred on the box it rose into the top strip and sat on the\n // label's first letters, which float at the same start inset. On the\n // value line it is beside what it describes, and the label keeps the\n // start edge every other label in the form's column shares.\n hasLabel\n ? \"top-[calc(1rem+1px)] h-5 items-center\"\n : \"top-1/2 -translate-y-1/2\",\n )}\n >\n {icon}\n </span>\n )}\n <input\n {...rest}\n ref={ref}\n id={fieldId}\n size={nativeSize}\n type=\"text\"\n value={value}\n // Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || rest.readOnly}\n aria-disabled={locked || rest[\"aria-disabled\"]}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={expanded}\n // Required by the role, and set while closed too, as on the siblings.\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n autoComplete=\"off\"\n onFocus={(e) => {\n onFocus?.(e);\n // A locked field takes focus to say why, and opens nothing.\n if (locked) return;\n // A seeded field (keksdose's `initialQuery`) is looked up as it is\n // focused; an empty one asks nothing, because of `minChars`.\n setOpen(true);\n }}\n onBlur={(e) => {\n onBlur?.(e);\n close();\n }}\n onChange={(e) => {\n if (locked) return;\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n // Locked: Enter is swallowed (no form submit, no caller's \"search now\");\n // every other key is the caller's and the caret's. No list to drive.\n if (locked) {\n if (e.key === \"Enter\") e.preventDefault();\n else onKeyDown?.(e);\n return;\n }\n // An Escape that closes the OPEN list is the list's, and is consumed before\n // the caller sees it. A caller whose Escape means \"close the panel\" (keksdose's\n // address search) otherwise closed the whole panel when the user only meant\n // to dismiss the suggestions — the opposite of what the docs promised.\n if (e.key === \"Escape\" && expanded) {\n e.preventDefault();\n e.stopPropagation();\n close();\n return;\n }\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n // Disabled rows are passed over; at either end the highlight stays put.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) setOpen(true);\n else setActive((i) => stepEnabled(results, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) setOpen(true);\n // From \"nothing highlighted\", Up goes to the bottom, as the APG has it.\n else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));\n } else if (e.key === \"Enter\") {\n // No row highlighted: the text is the answer, and a form's own submit\n // is left alone.\n if (expanded && activeId) {\n e.preventDefault();\n take(results[active]);\n }\n } else if (e.key === \"Tab\") {\n close();\n }\n }}\n className={cn(\n FIELD_BASE,\n hasLabel && FIELD_FLOATING_PAD,\n // Select's `SELECT_SM` box: a fixed 28px, so the caller's line height\n // cannot grow it.\n small && \"h-7 py-0 pe-2 ps-2 text-xs\",\n icon ? (small ? \"ps-7\" : \"ps-8\") : undefined,\n busy && (small ? \"pe-7\" : \"pe-9\"),\n field.isInvalid && FIELD_INVALID,\n inputClassName,\n )}\n />\n {busy && live && (\n // Decorative: the live region below already says \"Loading…\".\n <span\n className={cn(\n \"pointer-events-none absolute top-1/2 flex -translate-y-1/2\",\n small ? \"end-2\" : \"end-2.5\",\n )}\n >\n <Spinner label={null} className={small ? \"h-3.5 w-3.5\" : \"h-4 w-4\"} />\n </span>\n )}\n </div>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it, after the field so the \"?\"\n follows the control in the tab order (as on Input and Select). */}\n {hasLabel && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId} className={LABEL_IN_ROW}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {/* The live region, kept mounted so a change IS an announcement: focus stays\n in the field, and without it a reader typed into a geocoder and heard only\n their own letters. The panel's visible line is aria-hidden in favour of\n this, which also carries the row count the eye reads off the list. */}\n <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {!live\n ? null\n : statusLine !== null\n ? statusLine\n : results.length > 0\n ? labels.resultCount(results.length)\n : null}\n </div>\n {expanded &&\n rect &&\n typeof document !== \"undefined\" &&\n createPortal(\n <div\n // Every press inside the list keeps focus in the field — rows, the status\n // line, the scrollbar — or the input's blur would close the list first.\n role=\"presentation\"\n // Portalled out of the form's `dir`; the field's is put back. The list is\n // exactly the field's width, so `left` places it in either direction.\n dir={dir}\n onMouseDown={(e) => e.preventDefault()}\n className=\"fixed z-50 flex flex-col overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg\"\n style={{ top, left: rect.left, width: rect.width, maxHeight }}\n >\n {results.length > 0 && (\n <ul\n id={listboxId}\n role=\"listbox\"\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n className=\"min-h-0 flex-1 overflow-y-auto py-1\"\n >\n {results.map((o, i) => {\n const startsGroup = o.group != null && o.group !== results[i - 1]?.group;\n return (\n // Keyed by group AND value, as in the core panel: an option may\n // appear once per group.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {startsGroup && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-[var(--text-muted)] first:pt-1\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n // CHOSEN, never \"highlighted\" — the family's rule. This\n // field holds no value, so the only row that can be chosen\n // is the one whose label the text already is.\n aria-selected={fillOnSelect && o.label === value}\n // Listed, announced as unavailable, never taken: `take`\n // refuses it and the arrows pass it over.\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onClick={() => take(o)}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n \"flex w-full items-center gap-2 px-3 text-start\",\n small ? \"py-1 text-xs\" : \"py-1.5 text-sm\",\n o.disabled\n ? DISABLED_ROW_CLASS\n : i === active\n ? \"bg-[var(--bg-active)]\"\n : \"hover:bg-[var(--bg-hover)]\",\n o.group != null && \"ps-6\",\n )}\n >\n {o.icon && (\n <span aria-hidden className=\"shrink-0 text-[var(--text-muted)]\">\n {o.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-[var(--text-primary)]\">\n {o.label}\n </span>\n {o.sublabel && (\n <span\n className={cn(\n \"block truncate text-[var(--text-placeholder)]\",\n small ? \"text-caption\" : \"text-xs\",\n )}\n >\n {o.sublabel}\n </span>\n )}\n </span>\n </button>\n </li>\n </Fragment>\n );\n })}\n </ul>\n )}\n {statusLine !== null && (\n // Visible, not live — the region above speaks for it.\n <div\n aria-hidden\n className={cn(\n small ? \"px-3 py-1.5 text-xs\" : \"px-3 py-2 text-sm\",\n failed && !hasStatus ? \"text-[var(--danger)]\" : \"text-[var(--text-muted)]\",\n results.length > 0 && \"border-t border-[var(--border)] text-xs\",\n )}\n >\n {statusLine}\n </div>\n )}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n\nconst AutocompleteBase = forwardRef(AutocompleteInner);\nAutocompleteBase.displayName = \"Autocomplete\";\n\n/**\n * An inline, async-capable text field with a suggestion list under it — the\n * WAI-ARIA APG \"editable combobox with list autocomplete\".\n *\n * Two jobs, one control:\n * - **free text** (kastlan's address field): the text is the value, the list only\n * offers completions, and taking one fills the field (`fillOnSelect`, default);\n * - **search-then-act** (keksdose's address search): the text is a query, taking a\n * row calls `onSelect` and nothing is held (`fillOnSelect={false}`).\n *\n * How it differs from its siblings: {@link Combobox} is free text over a STATIC\n * string pool and opens on focus; {@link EntityCombobox} is a trigger holding an id.\n * This one keeps no value of its own, never resets the text, asks nothing below\n * `minChars`, and reports a failed lookup instead of showing stale rows.\n *\n * Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted\n * row, `aria-controls` the list, and a polite live region says what the list now\n * holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter\n * takes the highlighted row (with none,\n * Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay\n * with the caret. An Escape that closes an open list is consumed and never reaches the\n * caller; with the list closed it does. For every other key a caller's `onKeyDown`\n * runs first and may `preventDefault()` to\n * keep a key for itself.\n *\n * The list is portalled and anchored under the field on every screen size: the\n * field already carries the keyboard, and `useAnchoredPanel` keeps the list clear\n * of it on a phone. There is no full-screen sheet as {@link Combobox} has.\n\n * Generic over the option value (`ComboOption<V>`); the ref is the `<input>`'s.\n */\nexport const Autocomplete = AutocompleteBase as <V extends string | number = string>(\n props: AutocompleteProps<V> & RefAttributes<HTMLInputElement>,\n) => ReactElement | null;\n"],"mappings":"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","names":[]}
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Fragment, useEffect, useRef, useState } from "react";
4
4
  import { ChevronRight, MoreHorizontal } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
+ import { FOCUS_RING } from "./focus-ring.js";
6
7
  import { useKitLabels, useKitLink } from "../i18n/kit-labels.js";
7
8
  import { pickLinkRenderer, RenderedKitLink } from "./text-link.js";
8
9
  const DEFAULT_BREADCRUMBS_LABELS = {
@@ -37,7 +38,7 @@ function Breadcrumbs({
37
38
  const collapsible = collapse && !expanded && firstKept - 1 >= 2;
38
39
  const isHidden = (i) => collapsible && i >= 1 && i < firstKept;
39
40
  const sep = /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "flex shrink-0 items-center text-[var(--text-placeholder)]", children: separator ?? /* @__PURE__ */ jsx(ChevronRight, { className: "size-3.5 rtl:-scale-x-100" }) });
40
- const linkClass = "block max-w-[14rem] truncate rounded-sm hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]";
41
+ const linkClass = cn("block max-w-[14rem] truncate rounded-sm hover:text-[var(--text-primary)]", FOCUS_RING);
41
42
  return /* @__PURE__ */ jsx("nav", { ...rest, "aria-label": ariaLabel ?? labels.label, className: cn("min-w-0 text-sm", className), children: /* @__PURE__ */ jsx("ol", { className: "m-0 flex min-w-0 list-none flex-wrap items-center gap-x-1.5 gap-y-1 p-0 text-[var(--text-muted)]", children: items.map((item, i) => {
42
43
  const last = i === items.length - 1;
43
44
  const key = item.key ?? (typeof item.label === "string" ? `${i}-${item.label}` : String(i));
@@ -64,7 +65,7 @@ function Breadcrumbs({
64
65
  },
65
66
  "aria-label": labels.showAll,
66
67
  "aria-expanded": false,
67
- className: "flex items-center rounded-sm px-0.5 hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
68
+ className: cn("flex items-center rounded-sm px-0.5 hover:text-[var(--text-primary)]", FOCUS_RING),
68
69
  children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4", "aria-hidden": true })
69
70
  }
70
71
  )
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/breadcrumbs.tsx"],"sourcesContent":["import { Fragment, useEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactElement, ReactNode } from \"react\";\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer, RenderedKitLink } from \"./text-link\";\n\nexport interface BreadcrumbsLabels {\n /** The `nav` landmark's name — what a screen reader lists it as. */\n label: string;\n /** The \"…\" button that reveals the levels collapsed away on a phone. */\n showAll: string;\n}\n\nexport const DEFAULT_BREADCRUMBS_LABELS: BreadcrumbsLabels = {\n label: \"Breadcrumb\",\n showAll: \"Show full path\",\n};\n\nexport interface BreadcrumbItem {\n label: ReactNode;\n /** Where the crumb leads. A crumb without one is text (a level with no page of its\n * own). The LAST crumb is the current page and is text whether or not it has one. */\n href?: string;\n /** A stable React key, when `label` is not a string. */\n key?: string;\n}\n\n/** What {@link BreadcrumbsProps.renderLink} is handed. Spread it onto your router's\n * link — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface BreadcrumbLinkProps {\n href: string;\n className: string;\n children: ReactNode;\n}\n\nexport interface BreadcrumbsProps extends Omit<ComponentPropsWithoutRef<\"nav\">, \"children\"> {\n items: BreadcrumbItem[];\n /** Your router's link. Default: the `<UiKitProvider linkComponent>` (for an in-app\n * `href`), then `<a>`. The API `Chip`, `StatTile` and `ListItem` share. */\n renderLink?: (props: BreadcrumbLinkProps) => ReactElement;\n /**\n * The mark between crumbs. Default a chevron, mirrored in RTL — a chevron pointing\n * right in a right-to-left trail points back toward where you came from. Supply your\n * own and it is drawn as given (a \"/\" needs no mirroring).\n */\n separator?: ReactNode;\n /**\n * Collapse the middle on phones (below the `md` breakpoint) into a \"…\" button, keeping\n * the first crumb and the last {@link keepEnd}. Default `true`. kastlan's trail\n * scrolled sideways on a phone instead, which hides the current page — the one crumb\n * that must never be off-screen.\n */\n collapse?: boolean;\n /** How many crumbs at the end stay visible when collapsed — the current page and its\n * parent by default (2), because \"up one level\" is what a phone user reaches for. */\n keepEnd?: number;\n /** Overrides the {@link UiKitProvider}'s `breadcrumbs` labels. */\n labels?: Partial<BreadcrumbsLabels>;\n}\n\n/**\n * The trail from the app's root to this page.\n *\n * `nav` + `ol` + `aria-current=\"page\"` on the last crumb: the WAI breadcrumb pattern,\n * which kastlan's (shared/components/layout/breadcrumbs.tsx) nearly was — a `nav` of\n * `span`s, so a screen reader heard no list, no count, and nothing marking where you\n * are. The collapse is CSS (`hidden md:flex`), so it costs no media-query listener and\n * the server's first paint is already right.\n */\nexport function Breadcrumbs({\n items,\n renderLink,\n separator,\n collapse = true,\n keepEnd = 2,\n labels: labelsProp,\n className,\n \"aria-label\": ariaLabel,\n ...rest\n}: BreadcrumbsProps) {\n const labels = useKitLabels(\"breadcrumbs\", DEFAULT_BREADCRUMBS_LABELS, labelsProp);\n const kitLink = useKitLink();\n const [expanded, setExpanded] = useState(false);\n // The \"…\" button vanishes when pressed; focus goes to the first crumb it revealed,\n // not back to the top of the document.\n const revealedRef = useRef<HTMLLIElement>(null);\n const focusRevealed = useRef(false);\n useEffect(() => {\n if (!expanded || !focusRevealed.current) return;\n focusRevealed.current = false;\n const li = revealedRef.current;\n (li?.querySelector<HTMLElement>(\"a[href]\") ?? li)?.focus();\n }, [expanded]);\n if (items.length === 0) return null;\n\n const end = Math.max(1, keepEnd);\n // Crumbs [1, firstKept) are the collapsible middle. Collapsing a single crumb into a\n // \"…\" saves nothing, so it takes two.\n const firstKept = Math.max(1, items.length - end);\n const collapsible = collapse && !expanded && firstKept - 1 >= 2;\n const isHidden = (i: number) => collapsible && i >= 1 && i < firstKept;\n\n const sep = (\n <span aria-hidden className=\"flex shrink-0 items-center text-[var(--text-placeholder)]\">\n {separator ?? <ChevronRight className=\"size-3.5 rtl:-scale-x-100\" />}\n </span>\n );\n const linkClass =\n \"block max-w-[14rem] truncate rounded-sm hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\";\n\n return (\n <nav {...rest} aria-label={ariaLabel ?? labels.label} className={cn(\"min-w-0 text-sm\", className)}>\n <ol className=\"m-0 flex min-w-0 list-none flex-wrap items-center gap-x-1.5 gap-y-1 p-0 text-[var(--text-muted)]\">\n {items.map((item, i) => {\n const last = i === items.length - 1;\n const key = item.key ?? (typeof item.label === \"string\" ? `${i}-${item.label}` : String(i));\n let content: ReactNode;\n if (last) {\n content = (\n <span aria-current=\"page\" className=\"block max-w-[16rem] truncate font-medium text-[var(--text-primary)]\">\n {item.label}\n </span>\n );\n } else if (item.href !== undefined) {\n const props: BreadcrumbLinkProps = { href: item.href, className: linkClass, children: item.label };\n const kit = pickLinkRenderer<BreadcrumbLinkProps>(undefined, kitLink, item.href);\n content = renderLink ? (\n renderLink(props)\n ) : kit ? (\n <RenderedKitLink render={kit} props={props} />\n ) : (\n <a href={props.href} className={props.className}>\n {props.children}\n </a>\n );\n } else {\n content = <span className=\"block max-w-[14rem] truncate\">{item.label}</span>;\n }\n return (\n <Fragment key={key}>\n {collapsible && i === 1 && (\n <li className=\"flex items-center gap-1.5 md:hidden\">\n {sep}\n <button\n type=\"button\"\n onClick={() => {\n focusRevealed.current = true;\n setExpanded(true);\n }}\n aria-label={labels.showAll}\n aria-expanded={false}\n className=\"flex items-center rounded-sm px-0.5 hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\"\n >\n <MoreHorizontal className=\"size-4\" aria-hidden />\n </button>\n </li>\n )}\n <li\n ref={i === 1 ? revealedRef : undefined}\n // Focusable only as the landing place for a revealed crumb with no link.\n tabIndex={i === 1 && expanded && item.href === undefined ? -1 : undefined}\n className={cn(\"flex min-w-0 items-center gap-1.5 outline-none\", isHidden(i) && \"hidden md:flex\")}\n >\n {i > 0 && sep}\n {content}\n </li>\n </Fragment>\n );\n })}\n </ol>\n </nav>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/breadcrumbs.tsx"],"sourcesContent":["import { Fragment, useEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactElement, ReactNode } from \"react\";\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer, RenderedKitLink } from \"./text-link\";\n\nexport interface BreadcrumbsLabels {\n /** The `nav` landmark's name — what a screen reader lists it as. */\n label: string;\n /** The \"…\" button that reveals the levels collapsed away on a phone. */\n showAll: string;\n}\n\nexport const DEFAULT_BREADCRUMBS_LABELS: BreadcrumbsLabels = {\n label: \"Breadcrumb\",\n showAll: \"Show full path\",\n};\n\nexport interface BreadcrumbItem {\n label: ReactNode;\n /** Where the crumb leads. A crumb without one is text (a level with no page of its\n * own). The LAST crumb is the current page and is text whether or not it has one. */\n href?: string;\n /** A stable React key, when `label` is not a string. */\n key?: string;\n}\n\n/** What {@link BreadcrumbsProps.renderLink} is handed. Spread it onto your router's\n * link — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface BreadcrumbLinkProps {\n href: string;\n className: string;\n children: ReactNode;\n}\n\nexport interface BreadcrumbsProps extends Omit<ComponentPropsWithoutRef<\"nav\">, \"children\"> {\n items: BreadcrumbItem[];\n /** Your router's link. Default: the `<UiKitProvider linkComponent>` (for an in-app\n * `href`), then `<a>`. The API `Chip`, `StatTile` and `ListItem` share. */\n renderLink?: (props: BreadcrumbLinkProps) => ReactElement;\n /**\n * The mark between crumbs. Default a chevron, mirrored in RTL — a chevron pointing\n * right in a right-to-left trail points back toward where you came from. Supply your\n * own and it is drawn as given (a \"/\" needs no mirroring).\n */\n separator?: ReactNode;\n /**\n * Collapse the middle on phones (below the `md` breakpoint) into a \"…\" button, keeping\n * the first crumb and the last {@link keepEnd}. Default `true`. kastlan's trail\n * scrolled sideways on a phone instead, which hides the current page — the one crumb\n * that must never be off-screen.\n */\n collapse?: boolean;\n /** How many crumbs at the end stay visible when collapsed — the current page and its\n * parent by default (2), because \"up one level\" is what a phone user reaches for. */\n keepEnd?: number;\n /** Overrides the {@link UiKitProvider}'s `breadcrumbs` labels. */\n labels?: Partial<BreadcrumbsLabels>;\n}\n\n/**\n * The trail from the app's root to this page.\n *\n * `nav` + `ol` + `aria-current=\"page\"` on the last crumb: the WAI breadcrumb pattern,\n * which kastlan's (shared/components/layout/breadcrumbs.tsx) nearly was — a `nav` of\n * `span`s, so a screen reader heard no list, no count, and nothing marking where you\n * are. The collapse is CSS (`hidden md:flex`), so it costs no media-query listener and\n * the server's first paint is already right.\n */\nexport function Breadcrumbs({\n items,\n renderLink,\n separator,\n collapse = true,\n keepEnd = 2,\n labels: labelsProp,\n className,\n \"aria-label\": ariaLabel,\n ...rest\n}: BreadcrumbsProps) {\n const labels = useKitLabels(\"breadcrumbs\", DEFAULT_BREADCRUMBS_LABELS, labelsProp);\n const kitLink = useKitLink();\n const [expanded, setExpanded] = useState(false);\n // The \"…\" button vanishes when pressed; focus goes to the first crumb it revealed,\n // not back to the top of the document.\n const revealedRef = useRef<HTMLLIElement>(null);\n const focusRevealed = useRef(false);\n useEffect(() => {\n if (!expanded || !focusRevealed.current) return;\n focusRevealed.current = false;\n const li = revealedRef.current;\n (li?.querySelector<HTMLElement>(\"a[href]\") ?? li)?.focus();\n }, [expanded]);\n if (items.length === 0) return null;\n\n const end = Math.max(1, keepEnd);\n // Crumbs [1, firstKept) are the collapsible middle. Collapsing a single crumb into a\n // \"…\" saves nothing, so it takes two.\n const firstKept = Math.max(1, items.length - end);\n const collapsible = collapse && !expanded && firstKept - 1 >= 2;\n const isHidden = (i: number) => collapsible && i >= 1 && i < firstKept;\n\n const sep = (\n <span aria-hidden className=\"flex shrink-0 items-center text-[var(--text-placeholder)]\">\n {separator ?? <ChevronRight className=\"size-3.5 rtl:-scale-x-100\" />}\n </span>\n );\n const linkClass = cn(\"block max-w-[14rem] truncate rounded-sm hover:text-[var(--text-primary)]\", FOCUS_RING);\n\n return (\n <nav {...rest} aria-label={ariaLabel ?? labels.label} className={cn(\"min-w-0 text-sm\", className)}>\n <ol className=\"m-0 flex min-w-0 list-none flex-wrap items-center gap-x-1.5 gap-y-1 p-0 text-[var(--text-muted)]\">\n {items.map((item, i) => {\n const last = i === items.length - 1;\n const key = item.key ?? (typeof item.label === \"string\" ? `${i}-${item.label}` : String(i));\n let content: ReactNode;\n if (last) {\n content = (\n <span aria-current=\"page\" className=\"block max-w-[16rem] truncate font-medium text-[var(--text-primary)]\">\n {item.label}\n </span>\n );\n } else if (item.href !== undefined) {\n const props: BreadcrumbLinkProps = { href: item.href, className: linkClass, children: item.label };\n const kit = pickLinkRenderer<BreadcrumbLinkProps>(undefined, kitLink, item.href);\n content = renderLink ? (\n renderLink(props)\n ) : kit ? (\n <RenderedKitLink render={kit} props={props} />\n ) : (\n <a href={props.href} className={props.className}>\n {props.children}\n </a>\n );\n } else {\n content = <span className=\"block max-w-[14rem] truncate\">{item.label}</span>;\n }\n return (\n <Fragment key={key}>\n {collapsible && i === 1 && (\n <li className=\"flex items-center gap-1.5 md:hidden\">\n {sep}\n <button\n type=\"button\"\n onClick={() => {\n focusRevealed.current = true;\n setExpanded(true);\n }}\n aria-label={labels.showAll}\n aria-expanded={false}\n className={cn(\"flex items-center rounded-sm px-0.5 hover:text-[var(--text-primary)]\", FOCUS_RING)}\n >\n <MoreHorizontal className=\"size-4\" aria-hidden />\n </button>\n </li>\n )}\n <li\n ref={i === 1 ? revealedRef : undefined}\n // Focusable only as the landing place for a revealed crumb with no link.\n tabIndex={i === 1 && expanded && item.href === undefined ? -1 : undefined}\n className={cn(\"flex min-w-0 items-center gap-1.5 outline-none\", isHidden(i) && \"hidden md:flex\")}\n >\n {i > 0 && sep}\n {content}\n </li>\n </Fragment>\n );\n })}\n </ol>\n </nav>\n );\n}\n"],"mappings":"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","names":[]}
@@ -22,8 +22,8 @@ type BulkActionBarVariant = "floating" | "sticky" | "inline";
22
22
  /**
23
23
  * A variant per breakpoint, mobile first — `{ base: "floating", md: "sticky" }` is the
24
24
  * phone's floating card below 768px and the desktop's sticky bar from there up. The
25
- * breakpoints are Tailwind's (sm 640, md 768, lg 1024, xl 1280px), the ones the kit's
26
- * classes already use. keksdose's payees selection bar (payees-selection-bar.tsx)
25
+ * breakpoints are the ones the kit's classes use (sm 640, md 768, lg 1024, xl 1280px
26
+ * at Normal text size; since 0.32 they move with the text size, as `md:` does). keksdose's payees selection bar (payees-selection-bar.tsx)
27
27
  * computes exactly this with its own `useMediaQuery("(min-width: 768px)")`; every bar
28
28
  * that is a card on a phone and a strip on a desktop would repeat it.
29
29
  *
@@ -6,7 +6,7 @@ import { cn } from "../lib/cn.js";
6
6
  import { horizontalStep } from "../lib/direction.js";
7
7
  import { useKitLabels } from "../i18n/kit-labels.js";
8
8
  import { useAnnounce } from "../hooks/use-announce.js";
9
- import { useMediaQuery } from "../hooks/use-media-query.js";
9
+ import { useBreakpoint } from "../hooks/use-breakpoint.js";
10
10
  import { OVERLAY_EXIT_MS, prefersReducedMotion } from "../hooks/use-close-transition.js";
11
11
  import { Button, IconButton } from "./ui.js";
12
12
  const DEFAULT_BULK_ACTION_BAR_LABELS = {
@@ -14,21 +14,16 @@ const DEFAULT_BULK_ACTION_BAR_LABELS = {
14
14
  clear: "Clear selection",
15
15
  cleared: "Selection cleared"
16
16
  };
17
- const BREAKPOINTS = [
18
- ["xl", "(min-width: 1280px)"],
19
- ["lg", "(min-width: 1024px)"],
20
- ["md", "(min-width: 768px)"],
21
- ["sm", "(min-width: 640px)"]
22
- ];
17
+ const BREAKPOINTS = ["xl", "lg", "md", "sm"];
23
18
  function useResolvedVariant(variant) {
24
19
  const matches = {
25
- xl: useMediaQuery(BREAKPOINTS[0][1], false),
26
- lg: useMediaQuery(BREAKPOINTS[1][1], false),
27
- md: useMediaQuery(BREAKPOINTS[2][1], false),
28
- sm: useMediaQuery(BREAKPOINTS[3][1], false)
20
+ xl: useBreakpoint("xl"),
21
+ lg: useBreakpoint("lg"),
22
+ md: useBreakpoint("md"),
23
+ sm: useBreakpoint("sm")
29
24
  };
30
25
  if (typeof variant === "string") return variant;
31
- for (const [key] of BREAKPOINTS) {
26
+ for (const key of BREAKPOINTS) {
32
27
  const v = variant[key];
33
28
  if (matches[key] && v !== void 0) return v;
34
29
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/bulk-action-bar.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { OVERLAY_EXIT_MS, prefersReducedMotion } from \"../hooks/use-close-transition\";\nimport { Button, IconButton } from \"./ui\";\n\nexport interface BulkActionBarLabels {\n /** The count, shown in the bar, spoken by its live region and used as the toolbar's\n * name: \"3 selected\". */\n selected: (count: number) => string;\n /** The clear/cancel action's name — the X of the floating bar, the text button of the\n * others. */\n clear: string;\n /** Spoken when the selection empties and the bar goes. */\n cleared: string;\n}\n\nexport const DEFAULT_BULK_ACTION_BAR_LABELS: BulkActionBarLabels = {\n selected: (count) => `${count} selected`,\n clear: \"Clear selection\",\n cleared: \"Selection cleared\",\n};\n\n/**\n * `floating` — the phone's: fixed above `AppShell`'s bottom nav (`--app-nav-h`) and the\n * home indicator, inset from both sides, a rounded card with a shadow. `sticky` — the\n * desktop's: pinned to the top of its scroll container, over the rows it acts on.\n * `inline` — in the flow, where the caller places it.\n */\nexport type BulkActionBarVariant = \"floating\" | \"sticky\" | \"inline\";\n\n/**\n * A variant per breakpoint, mobile first — `{ base: \"floating\", md: \"sticky\" }` is the\n * phone's floating card below 768px and the desktop's sticky bar from there up. The\n * breakpoints are Tailwind's (sm 640, md 768, lg 1024, xl 1280px), the ones the kit's\n * classes already use. keksdose's payees selection bar (payees-selection-bar.tsx)\n * computes exactly this with its own `useMediaQuery(\"(min-width: 768px)\")`; every bar\n * that is a card on a phone and a strip on a desktop would repeat it.\n *\n * Resolved in JS rather than with `md:` classes because the variants differ in more\n * than classes: the floating bar's clear is an icon at the start, the others' a text\n * button at the end, and the floating bar is positioned by inline style.\n */\nexport interface ResponsiveBulkActionBarVariant {\n base: BulkActionBarVariant;\n sm?: BulkActionBarVariant;\n md?: BulkActionBarVariant;\n lg?: BulkActionBarVariant;\n xl?: BulkActionBarVariant;\n}\n\nconst BREAKPOINTS = [\n [\"xl\", \"(min-width: 1280px)\"],\n [\"lg\", \"(min-width: 1024px)\"],\n [\"md\", \"(min-width: 768px)\"],\n [\"sm\", \"(min-width: 640px)\"],\n] as const;\n\n/** The variant in force: the widest breakpoint that matches AND names one, else\n * `base`. The four queries are subscribed unconditionally — hooks cannot be skipped —\n * and a plain string variant simply ignores them. Without `matchMedia` (SSR, tests)\n * every query is false, so the bar renders its `base`, the phone's, first. */\nfunction useResolvedVariant(variant: BulkActionBarVariant | ResponsiveBulkActionBarVariant): BulkActionBarVariant {\n const matches = {\n xl: useMediaQuery(BREAKPOINTS[0][1], false),\n lg: useMediaQuery(BREAKPOINTS[1][1], false),\n md: useMediaQuery(BREAKPOINTS[2][1], false),\n sm: useMediaQuery(BREAKPOINTS[3][1], false),\n };\n if (typeof variant === \"string\") return variant;\n for (const [key] of BREAKPOINTS) {\n const v = variant[key];\n if (matches[key] && v !== undefined) return v;\n }\n return variant.base;\n}\n\nexport interface BulkActionBarProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"role\" | \"children\"> {\n /** How many items are selected. The bar shows only while this is above zero —\n * unless {@link open} says otherwise. */\n count: number;\n /**\n * Show the bar regardless of `count` — `true` for SELECTION MODE with nothing picked\n * yet, `false` to hide it with rows still selected. Left out, `count > 0` decides, as\n * before.\n *\n * keksdose's invoice lines (invoice-lines-bulk-bar.tsx) enter selection with a\n * \"Select\" button, and the bar must be there at \"0 selected\" to hold the way out and\n * a \"Select all\" (pass it as a child); gated on the count, the bar appeared only at\n * the first tick, and the user who pressed \"Select\" saw nothing happen. The exit plays\n * when it closes either way, and the count is still announced on every change.\n */\n open?: boolean;\n /** Clear the selection / leave selection mode. */\n onClear: () => void;\n /**\n * The actions: buttons (`IconButton size=\"lg\"` on a phone, `Button` from md up),\n * in the caller's order. They are the toolbar's arrow-key stops, after the clear.\n *\n * LINKS rove too (keksdose F8): a `<Button href>` — \"View report\", which navigates\n * rather than acts — is an `<a href>`, and it is one more arrow-key stop in its place\n * among the buttons, not a second Tab stop beside the toolbar's one. A disabled\n * `<Button href disabled>` has no `href` and is skipped, as a disabled button is. Any\n * other focusable of your own joins with `data-toolbar-item`.\n */\n children?: ReactNode;\n /** Default `floating`. See {@link BulkActionBarVariant}; an object picks one per\n * breakpoint, see {@link ResponsiveBulkActionBarVariant}. */\n variant?: BulkActionBarVariant | ResponsiveBulkActionBarVariant;\n /**\n * Content BELOW the toolbar row, inside the same surface — the same floating card or\n * sticky tinted strip — but OUTSIDE the toolbar: its fields keep their own Tab stops\n * and arrow keys, and the toolbar's roving focus never walks into it. keksdose's\n * desktop receipt-line bar (invoice-lines-bulk-bar.tsx) shows its `BulkFields` form —\n * a category picker, an exclude checkbox, Apply — under the count and the clear; a\n * form is not a toolbar's content (`role=\"toolbar\"` promises one arrow-key widget),\n * and set beside the bar it lost the sticky tint and scrolled away from it.\n *\n * On the floating variant the panel scrolls within 60% of the viewport's height, so a\n * long form on a phone cannot push the bar off the top of the screen.\n */\n panel?: ReactNode;\n /** Default: `bulkActionBar` from the {@link UiKitProvider}, else English. */\n labels?: Partial<BulkActionBarLabels>;\n}\n\n/** Where the floating bar sits: clear of the phone nav and of the home indicator,\n * whichever reaches higher — `FloatingPanel`'s rule, plus the bar's own 0.5rem gap.\n * Set as inline style, so reposition it through `style` (which is merged over this,\n * e.g. `style={{ insetInline: \"auto 2rem\", width: \"24rem\" }}`), not `className`. */\nconst FLOATING_STYLE: CSSProperties = {\n bottom: \"calc(max(var(--app-nav-h, 0px), env(safe-area-inset-bottom, 0px)) + 0.5rem)\",\n // Both insets on both sides: a logical side has no one physical inset to name, and\n // the bar is symmetric, so RTL needs nothing more.\n insetInline: \"max(0.5rem, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))\",\n};\n\n/** What the arrow keys walk: the toolbar's enabled controls. Matched by element, not by\n * `tabindex`, because the roving below sets every one but the current to `-1` — a\n * custom control joins with `data-toolbar-item`.\n *\n * `a[href]` is here on purpose (keksdose F8, a \"View report\" `Button href` in the bar):\n * a link in a toolbar is a toolbar item like any other, and left out it would keep its\n * own Tab stop, so Tab would stop twice in a bar that promises one. `[href]` rather than `a`: the kit's disabled link is an `<a>`\n * WITHOUT one, and is skipped as a disabled button is. */\nconst ITEMS =\n \"button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[data-toolbar-item]\";\n\n/** Keys a text field needs for itself — the toolbar must not take its caret away. */\nfunction ownsArrows(el: Element): boolean {\n if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) return true;\n if (el instanceof HTMLInputElement) return ![\"button\", \"checkbox\", \"radio\", \"submit\", \"reset\"].includes(el.type);\n return (el as HTMLElement).isContentEditable;\n}\n\n/**\n * The bar that appears while rows are selected: the count, a way out, and what can be\n * done to all of them at once.\n *\n * keksdose had written it three times — the transactions phone bar\n * (mobile-bulk-bar.tsx), the invoice lines' phone and desktop bars\n * (invoice-lines-bulk-bar.tsx) and the payees selection bar — and the copies had\n * drifted where it matters: two parked themselves with a guessed `bottom-16` that the\n * third had already found leaves a 24px gap (live #314: the nav is content-sized and\n * `AppShell` publishes its height as `--app-nav-h`), none of them announced the count,\n * and all three were `role=\"toolbar\"` without the arrow keys that role promises.\n *\n * ## Keep it mounted\n *\n * Render it unconditionally and let `count` decide. At zero it renders only its live\n * region — which must already be in the DOM for the first \"3 selected\" to be heard —\n * and when the count drops to zero it plays its exit before it goes, which a\n * `{count > 0 && <Bar/>}` gate would cut off.\n *\n * ## The toolbar\n *\n * One Tab stop: ←/→ (mirrored in RTL), Home and End move between the controls, and Tab\n * leaves — the WAI-ARIA toolbar pattern, since that is what `role=\"toolbar\"` tells a\n * screen reader to expect. The stop remembers the control last used. Arrow keys inside\n * a text field stay the field's.\n *\n * It is NOT a dialog: no focus trap, no scroll lock — the list behind stays live,\n * because tapping more rows is how a selection grows.\n */\nexport function BulkActionBar({\n count,\n onClear,\n children,\n variant: variantProp = \"floating\",\n open,\n panel,\n labels: labelsProp,\n className,\n style,\n onKeyDown,\n onFocus,\n ...rest\n}: BulkActionBarProps) {\n const labels = useKitLabels(\"bulkActionBar\", DEFAULT_BULK_ACTION_BAR_LABELS, labelsProp);\n const { announce, regionProps } = useAnnounce();\n const barRef = useRef<HTMLDivElement>(null);\n const variant = useResolvedVariant(variantProp);\n const shown = open ?? count > 0;\n\n // The count the bar last SHOWED, so a bar lowering itself after \"clear\" still reads\n // \"3 selected\" on its way out rather than \"0 selected\". Adjusted during render\n // (`Collapse`'s pattern), as is the exit below.\n const [shownCount, setShownCount] = useState(count);\n const [leaving, setLeaving] = useState(false);\n const [prev, setPrev] = useState({ count, shown });\n if (count !== prev.count || shown !== prev.shown) {\n setPrev({ count, shown });\n if (shown) {\n setShownCount(count);\n setLeaving(false);\n } else if (prev.shown) {\n setLeaving(!prefersReducedMotion());\n }\n }\n useEffect(() => {\n if (!leaving) return;\n const timer = setTimeout(() => setLeaving(false), OVERLAY_EXIT_MS);\n return () => clearTimeout(timer);\n }, [leaving]);\n\n // Spoken on every CHANGE, not on mount: a bar mounted with rows already selected\n // was not the result of anything the user just did.\n const spoken = useRef(count);\n useEffect(() => {\n if (spoken.current === count) return;\n spoken.current = count;\n // In selection mode the bar stays at zero, so zero is a count like any other.\n announce(count > 0 || shown ? labels.selected(count) : labels.cleared);\n }, [count, shown, labels, announce]);\n\n // Roving tab stop. The controls are the caller's, so the stop is kept on the DOM\n // rather than through props: after every render the remembered control (or the\n // first) is the one with `tabindex=0`, every other one `-1`.\n const active = useRef<HTMLElement | null>(null);\n // Every render: the caller's controls come and go (a \"remove added\" that exists\n // only while some are removable), and a stop on a control that left must move on.\n useLayoutEffect(() => {\n rove(barRef.current, active);\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(e);\n if (e.defaultPrevented || ownsArrows(e.target as Element)) return;\n const list = itemsOf(barRef.current);\n const at = list.indexOf(document.activeElement as HTMLElement);\n if (at === -1 || list.length === 0) return;\n const step = horizontalStep(e.key, barRef.current);\n let next = -1;\n if (step !== 0) next = (at + step + list.length) % list.length;\n else if (e.key === \"Home\") next = 0;\n else if (e.key === \"End\") next = list.length - 1;\n if (next === -1) return;\n e.preventDefault();\n active.current = list[next];\n rove(barRef.current, active);\n list[next].focus();\n };\n\n if (!shown && !leaving) {\n return <span {...regionProps} />;\n }\n\n const floating = variant === \"floating\";\n const clear = floating ? (\n <IconButton size=\"lg\" onClick={onClear} aria-label={labels.clear}>\n <X />\n </IconButton>\n ) : (\n // `ms-auto`: when the actions wrap, the way out still sits at the bar's end.\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={onClear} className=\"ms-auto\">\n {labels.clear}\n </Button>\n );\n\n const hasPanel = panel !== undefined && panel !== null && panel !== false;\n // The surface — where the bar sits and what it looks like — and the toolbar row's\n // own layout. Without a panel they are one element, exactly as before 0.11.0; with\n // one the surface wraps the row and the panel, so both share its tint and position.\n const surfaceClass = floating\n ? cn(\n \"fixed z-30 rounded-2xl border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg\",\n shown ? \"animate-sheet\" : \"animate-sheet-out\",\n )\n : cn(\n variant === \"sticky\" &&\n \"sticky top-0 z-20 border-b border-[var(--border)] bg-[color-mix(in_srgb,var(--brand)_8%,var(--bg-surface))] backdrop-blur\",\n shown ? \"animate-overlay\" : \"animate-overlay-out\",\n );\n // `flex-wrap`: on a phone the count, three actions and a text \"Clear selection\" are\n // wider than the screen. Without wrapping the count (the one shrinkable item)\n // collapsed to a sliver and the rest ran out of the bar; wrapped, the actions that do\n // not fit move to a second line and every label stays whole.\n const rowClass = cn(\"flex flex-wrap items-center\", floating ? \"gap-1 px-2 py-1\" : \"gap-2 px-3 py-2\");\n const surfaceStyle = floating ? { ...FLOATING_STYLE, ...style } : style;\n\n const toolbar = (\n <div\n {...(hasPanel ? null : rest)}\n ref={barRef}\n role=\"toolbar\"\n aria-label={labels.selected(shownCount)}\n aria-orientation=\"horizontal\"\n data-bulk-action-bar={hasPanel ? undefined : variant}\n // Leaving: out of the tab order and the accessibility tree at once, while the\n // bar still animates. The selection it acted on is already gone.\n inert={(!hasPanel && !shown) || undefined}\n onKeyDown={handleKeyDown}\n onFocus={(e) => {\n onFocus?.(e);\n // Clicking a control is using it: the stop follows the pointer too.\n const hit = itemsOf(barRef.current).find((el) => el === e.target);\n if (hit) {\n active.current = hit;\n rove(barRef.current, active);\n }\n }}\n style={hasPanel ? undefined : surfaceStyle}\n className={hasPanel ? rowClass : cn(rowClass, surfaceClass, className)}\n >\n {floating && clear}\n {/* `flex-auto`, not `flex-1`: a zero basis told the wrapping that the count needs\n no room, so it was the item squeezed to nothing instead of a line breaking.\n Its own width is its basis now; `truncate` is left for a label too long for a\n line on its own. */}\n <span className=\"min-w-0 flex-auto truncate text-sm font-medium text-[var(--text-primary)]\">\n {labels.selected(shownCount)}\n </span>\n {/* `contents`, so the caller's controls are flex items of the bar itself. */}\n <div className=\"contents\">{children}</div>\n {!floating && clear}\n </div>\n );\n\n return (\n <>\n <span {...regionProps} />\n {hasPanel ? (\n <div\n {...rest}\n data-bulk-action-bar={variant}\n inert={!shown || undefined}\n style={surfaceStyle}\n className={cn(\"flex flex-col\", surfaceClass, className)}\n >\n {toolbar}\n <div\n data-bulk-action-bar-panel=\"\"\n className={cn(\n \"border-t border-[var(--border)] px-3 py-3\",\n floating && \"max-h-[60dvh] overflow-y-auto\",\n )}\n >\n {panel}\n </div>\n </div>\n ) : (\n toolbar\n )}\n </>\n );\n}\n\nfunction itemsOf(bar: HTMLElement | null): HTMLElement[] {\n return bar ? Array.from(bar.querySelectorAll<HTMLElement>(ITEMS)) : [];\n}\n\n/** Put the one Tab stop on the remembered control — or the first, when that one has\n * gone or was never set — and take it off every other. */\nfunction rove(bar: HTMLElement | null, active: { current: HTMLElement | null }): void {\n const list = itemsOf(bar);\n if (!active.current || !list.includes(active.current)) active.current = list[0] ?? null;\n for (const el of list) el.tabIndex = el === active.current ? 0 : -1;\n}\n"],"mappings":";AA8QW,SA2EP,UA3EO,KAqCP,YArCO;AA9QX,SAAS,WAAW,iBAAiB,QAAQ,gBAAgB;AAE7D,SAAS,SAAS;AAElB,SAAS,UAAU;AACnB,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB,4BAA4B;AACtD,SAAS,QAAQ,kBAAkB;AAa5B,MAAM,iCAAsD;AAAA,EACjE,UAAU,CAAC,UAAU,GAAG,KAAK;AAAA,EAC7B,OAAO;AAAA,EACP,SAAS;AACX;AA8BA,MAAM,cAAc;AAAA,EAClB,CAAC,MAAM,qBAAqB;AAAA,EAC5B,CAAC,MAAM,qBAAqB;AAAA,EAC5B,CAAC,MAAM,oBAAoB;AAAA,EAC3B,CAAC,MAAM,oBAAoB;AAC7B;AAMA,SAAS,mBAAmB,SAAsF;AAChH,QAAM,UAAU;AAAA,IACd,IAAI,cAAc,YAAY,CAAC,EAAE,CAAC,GAAG,KAAK;AAAA,IAC1C,IAAI,cAAc,YAAY,CAAC,EAAE,CAAC,GAAG,KAAK;AAAA,IAC1C,IAAI,cAAc,YAAY,CAAC,EAAE,CAAC,GAAG,KAAK;AAAA,IAC1C,IAAI,cAAc,YAAY,CAAC,EAAE,CAAC,GAAG,KAAK;AAAA,EAC5C;AACA,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,aAAW,CAAC,GAAG,KAAK,aAAa;AAC/B,UAAM,IAAI,QAAQ,GAAG;AACrB,QAAI,QAAQ,GAAG,KAAK,MAAM,OAAW,QAAO;AAAA,EAC9C;AACA,SAAO,QAAQ;AACjB;AAuDA,MAAM,iBAAgC;AAAA,EACpC,QAAQ;AAAA;AAAA;AAAA,EAGR,aAAa;AACf;AAUA,MAAM,QACJ;AAGF,SAAS,WAAW,IAAsB;AACxC,MAAI,cAAc,uBAAuB,cAAc,kBAAmB,QAAO;AACjF,MAAI,cAAc,iBAAkB,QAAO,CAAC,CAAC,UAAU,YAAY,SAAS,UAAU,OAAO,EAAE,SAAS,GAAG,IAAI;AAC/G,SAAQ,GAAmB;AAC7B;AA+BO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,cAAc;AAAA,EACvB;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAuB;AACrB,QAAM,SAAS,aAAa,iBAAiB,gCAAgC,UAAU;AACvF,QAAM,EAAE,UAAU,YAAY,IAAI,YAAY;AAC9C,QAAM,SAAS,OAAuB,IAAI;AAC1C,QAAM,UAAU,mBAAmB,WAAW;AAC9C,QAAM,QAAQ,QAAQ,QAAQ;AAK9B,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,EAAE,OAAO,MAAM,CAAC;AACjD,MAAI,UAAU,KAAK,SAAS,UAAU,KAAK,OAAO;AAChD,YAAQ,EAAE,OAAO,MAAM,CAAC;AACxB,QAAI,OAAO;AACT,oBAAc,KAAK;AACnB,iBAAW,KAAK;AAAA,IAClB,WAAW,KAAK,OAAO;AACrB,iBAAW,CAAC,qBAAqB,CAAC;AAAA,IACpC;AAAA,EACF;AACA,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,UAAM,QAAQ,WAAW,MAAM,WAAW,KAAK,GAAG,eAAe;AACjE,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,OAAO,CAAC;AAIZ,QAAM,SAAS,OAAO,KAAK;AAC3B,YAAU,MAAM;AACd,QAAI,OAAO,YAAY,MAAO;AAC9B,WAAO,UAAU;AAEjB,aAAS,QAAQ,KAAK,QAAQ,OAAO,SAAS,KAAK,IAAI,OAAO,OAAO;AAAA,EACvE,GAAG,CAAC,OAAO,OAAO,QAAQ,QAAQ,CAAC;AAKnC,QAAM,SAAS,OAA2B,IAAI;AAG9C,kBAAgB,MAAM;AACpB,SAAK,OAAO,SAAS,MAAM;AAAA,EAC7B,CAAC;AAED,QAAM,gBAAgB,CAAC,MAAqC;AAC1D,gBAAY,CAAC;AACb,QAAI,EAAE,oBAAoB,WAAW,EAAE,MAAiB,EAAG;AAC3D,UAAM,OAAO,QAAQ,OAAO,OAAO;AACnC,UAAM,KAAK,KAAK,QAAQ,SAAS,aAA4B;AAC7D,QAAI,OAAO,MAAM,KAAK,WAAW,EAAG;AACpC,UAAM,OAAO,eAAe,EAAE,KAAK,OAAO,OAAO;AACjD,QAAI,OAAO;AACX,QAAI,SAAS,EAAG,SAAQ,KAAK,OAAO,KAAK,UAAU,KAAK;AAAA,aAC/C,EAAE,QAAQ,OAAQ,QAAO;AAAA,aACzB,EAAE,QAAQ,MAAO,QAAO,KAAK,SAAS;AAC/C,QAAI,SAAS,GAAI;AACjB,MAAE,eAAe;AACjB,WAAO,UAAU,KAAK,IAAI;AAC1B,SAAK,OAAO,SAAS,MAAM;AAC3B,SAAK,IAAI,EAAE,MAAM;AAAA,EACnB;AAEA,MAAI,CAAC,SAAS,CAAC,SAAS;AACtB,WAAO,oBAAC,UAAM,GAAG,aAAa;AAAA,EAChC;AAEA,QAAM,WAAW,YAAY;AAC7B,QAAM,QAAQ,WACZ,oBAAC,cAAW,MAAK,MAAK,SAAS,SAAS,cAAY,OAAO,OACzD,8BAAC,KAAE,GACL;AAAA;AAAA,IAGA,oBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,MAAK,MAAK,SAAS,SAAS,WAAU,WAC7E,iBAAO,OACV;AAAA;AAGF,QAAM,WAAW,UAAU,UAAa,UAAU,QAAQ,UAAU;AAIpE,QAAM,eAAe,WACjB;AAAA,IACE;AAAA,IACA,QAAQ,kBAAkB;AAAA,EAC5B,IACA;AAAA,IACE,YAAY,YACV;AAAA,IACF,QAAQ,oBAAoB;AAAA,EAC9B;AAKJ,QAAM,WAAW,GAAG,+BAA+B,WAAW,oBAAoB,iBAAiB;AACnG,QAAM,eAAe,WAAW,EAAE,GAAG,gBAAgB,GAAG,MAAM,IAAI;AAElE,QAAM,UACJ;AAAA,IAAC;AAAA;AAAA,MACE,GAAI,WAAW,OAAO;AAAA,MACvB,KAAK;AAAA,MACL,MAAK;AAAA,MACL,cAAY,OAAO,SAAS,UAAU;AAAA,MACtC,oBAAiB;AAAA,MACjB,wBAAsB,WAAW,SAAY;AAAA,MAG7C,OAAQ,CAAC,YAAY,CAAC,SAAU;AAAA,MAChC,WAAW;AAAA,MACX,SAAS,CAAC,MAAM;AACd,kBAAU,CAAC;AAEX,cAAM,MAAM,QAAQ,OAAO,OAAO,EAAE,KAAK,CAAC,OAAO,OAAO,EAAE,MAAM;AAChE,YAAI,KAAK;AACP,iBAAO,UAAU;AACjB,eAAK,OAAO,SAAS,MAAM;AAAA,QAC7B;AAAA,MACF;AAAA,MACA,OAAO,WAAW,SAAY;AAAA,MAC9B,WAAW,WAAW,WAAW,GAAG,UAAU,cAAc,SAAS;AAAA,MAEpE;AAAA,oBAAY;AAAA,QAKb,oBAAC,UAAK,WAAU,6EACb,iBAAO,SAAS,UAAU,GAC7B;AAAA,QAEA,oBAAC,SAAI,WAAU,YAAY,UAAS;AAAA,QACnC,CAAC,YAAY;AAAA;AAAA;AAAA,EAChB;AAGF,SACE,iCACE;AAAA,wBAAC,UAAM,GAAG,aAAa;AAAA,IACtB,WACC;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ,wBAAsB;AAAA,QACtB,OAAO,CAAC,SAAS;AAAA,QACjB,OAAO;AAAA,QACP,WAAW,GAAG,iBAAiB,cAAc,SAAS;AAAA,QAErD;AAAA;AAAA,UACD;AAAA,YAAC;AAAA;AAAA,cACC,8BAA2B;AAAA,cAC3B,WAAW;AAAA,gBACT;AAAA,gBACA,YAAY;AAAA,cACd;AAAA,cAEC;AAAA;AAAA,UACH;AAAA;AAAA;AAAA,IACF,IAEA;AAAA,KAEJ;AAEJ;AAEA,SAAS,QAAQ,KAAwC;AACvD,SAAO,MAAM,MAAM,KAAK,IAAI,iBAA8B,KAAK,CAAC,IAAI,CAAC;AACvE;AAIA,SAAS,KAAK,KAAyB,QAA+C;AACpF,QAAM,OAAO,QAAQ,GAAG;AACxB,MAAI,CAAC,OAAO,WAAW,CAAC,KAAK,SAAS,OAAO,OAAO,EAAG,QAAO,UAAU,KAAK,CAAC,KAAK;AACnF,aAAW,MAAM,KAAM,IAAG,WAAW,OAAO,OAAO,UAAU,IAAI;AACnE;","names":[]}
1
+ {"version":3,"sources":["../../src/components/bulk-action-bar.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { useBreakpoint } from \"../hooks/use-breakpoint\";\nimport { OVERLAY_EXIT_MS, prefersReducedMotion } from \"../hooks/use-close-transition\";\nimport { Button, IconButton } from \"./ui\";\n\nexport interface BulkActionBarLabels {\n /** The count, shown in the bar, spoken by its live region and used as the toolbar's\n * name: \"3 selected\". */\n selected: (count: number) => string;\n /** The clear/cancel action's name — the X of the floating bar, the text button of the\n * others. */\n clear: string;\n /** Spoken when the selection empties and the bar goes. */\n cleared: string;\n}\n\nexport const DEFAULT_BULK_ACTION_BAR_LABELS: BulkActionBarLabels = {\n selected: (count) => `${count} selected`,\n clear: \"Clear selection\",\n cleared: \"Selection cleared\",\n};\n\n/**\n * `floating` — the phone's: fixed above `AppShell`'s bottom nav (`--app-nav-h`) and the\n * home indicator, inset from both sides, a rounded card with a shadow. `sticky` — the\n * desktop's: pinned to the top of its scroll container, over the rows it acts on.\n * `inline` — in the flow, where the caller places it.\n */\nexport type BulkActionBarVariant = \"floating\" | \"sticky\" | \"inline\";\n\n/**\n * A variant per breakpoint, mobile first — `{ base: \"floating\", md: \"sticky\" }` is the\n * phone's floating card below 768px and the desktop's sticky bar from there up. The\n * breakpoints are the ones the kit's classes use (sm 640, md 768, lg 1024, xl 1280px\n * at Normal text size; since 0.32 they move with the text size, as `md:` does). keksdose's payees selection bar (payees-selection-bar.tsx)\n * computes exactly this with its own `useMediaQuery(\"(min-width: 768px)\")`; every bar\n * that is a card on a phone and a strip on a desktop would repeat it.\n *\n * Resolved in JS rather than with `md:` classes because the variants differ in more\n * than classes: the floating bar's clear is an icon at the start, the others' a text\n * button at the end, and the floating bar is positioned by inline style.\n */\nexport interface ResponsiveBulkActionBarVariant {\n base: BulkActionBarVariant;\n sm?: BulkActionBarVariant;\n md?: BulkActionBarVariant;\n lg?: BulkActionBarVariant;\n xl?: BulkActionBarVariant;\n}\n\n/** Widest first: the first that matches and names a value wins. Each is the class's own\n * breakpoint at the text size in force (`useBreakpoint`), so `md` here is `md:` there. */\nconst BREAKPOINTS = [\"xl\", \"lg\", \"md\", \"sm\"] as const;\n\n/** The variant in force: the widest breakpoint that matches AND names one, else\n * `base`. The four queries are subscribed unconditionally — hooks cannot be skipped —\n * and a plain string variant simply ignores them. Without `matchMedia` (SSR, tests)\n * every query is false, so the bar renders its `base`, the phone's, first. */\nfunction useResolvedVariant(variant: BulkActionBarVariant | ResponsiveBulkActionBarVariant): BulkActionBarVariant {\n const matches = {\n xl: useBreakpoint(\"xl\"),\n lg: useBreakpoint(\"lg\"),\n md: useBreakpoint(\"md\"),\n sm: useBreakpoint(\"sm\"),\n };\n if (typeof variant === \"string\") return variant;\n for (const key of BREAKPOINTS) {\n const v = variant[key];\n if (matches[key] && v !== undefined) return v;\n }\n return variant.base;\n}\n\nexport interface BulkActionBarProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"role\" | \"children\"> {\n /** How many items are selected. The bar shows only while this is above zero —\n * unless {@link open} says otherwise. */\n count: number;\n /**\n * Show the bar regardless of `count` — `true` for SELECTION MODE with nothing picked\n * yet, `false` to hide it with rows still selected. Left out, `count > 0` decides, as\n * before.\n *\n * keksdose's invoice lines (invoice-lines-bulk-bar.tsx) enter selection with a\n * \"Select\" button, and the bar must be there at \"0 selected\" to hold the way out and\n * a \"Select all\" (pass it as a child); gated on the count, the bar appeared only at\n * the first tick, and the user who pressed \"Select\" saw nothing happen. The exit plays\n * when it closes either way, and the count is still announced on every change.\n */\n open?: boolean;\n /** Clear the selection / leave selection mode. */\n onClear: () => void;\n /**\n * The actions: buttons (`IconButton size=\"lg\"` on a phone, `Button` from md up),\n * in the caller's order. They are the toolbar's arrow-key stops, after the clear.\n *\n * LINKS rove too (keksdose F8): a `<Button href>` — \"View report\", which navigates\n * rather than acts — is an `<a href>`, and it is one more arrow-key stop in its place\n * among the buttons, not a second Tab stop beside the toolbar's one. A disabled\n * `<Button href disabled>` has no `href` and is skipped, as a disabled button is. Any\n * other focusable of your own joins with `data-toolbar-item`.\n */\n children?: ReactNode;\n /** Default `floating`. See {@link BulkActionBarVariant}; an object picks one per\n * breakpoint, see {@link ResponsiveBulkActionBarVariant}. */\n variant?: BulkActionBarVariant | ResponsiveBulkActionBarVariant;\n /**\n * Content BELOW the toolbar row, inside the same surface — the same floating card or\n * sticky tinted strip — but OUTSIDE the toolbar: its fields keep their own Tab stops\n * and arrow keys, and the toolbar's roving focus never walks into it. keksdose's\n * desktop receipt-line bar (invoice-lines-bulk-bar.tsx) shows its `BulkFields` form —\n * a category picker, an exclude checkbox, Apply — under the count and the clear; a\n * form is not a toolbar's content (`role=\"toolbar\"` promises one arrow-key widget),\n * and set beside the bar it lost the sticky tint and scrolled away from it.\n *\n * On the floating variant the panel scrolls within 60% of the viewport's height, so a\n * long form on a phone cannot push the bar off the top of the screen.\n */\n panel?: ReactNode;\n /** Default: `bulkActionBar` from the {@link UiKitProvider}, else English. */\n labels?: Partial<BulkActionBarLabels>;\n}\n\n/** Where the floating bar sits: clear of the phone nav and of the home indicator,\n * whichever reaches higher — `FloatingPanel`'s rule, plus the bar's own 0.5rem gap.\n * Set as inline style, so reposition it through `style` (which is merged over this,\n * e.g. `style={{ insetInline: \"auto 2rem\", width: \"24rem\" }}`), not `className`. */\nconst FLOATING_STYLE: CSSProperties = {\n bottom: \"calc(max(var(--app-nav-h, 0px), env(safe-area-inset-bottom, 0px)) + 0.5rem)\",\n // Both insets on both sides: a logical side has no one physical inset to name, and\n // the bar is symmetric, so RTL needs nothing more.\n insetInline: \"max(0.5rem, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))\",\n};\n\n/** What the arrow keys walk: the toolbar's enabled controls. Matched by element, not by\n * `tabindex`, because the roving below sets every one but the current to `-1` — a\n * custom control joins with `data-toolbar-item`.\n *\n * `a[href]` is here on purpose (keksdose F8, a \"View report\" `Button href` in the bar):\n * a link in a toolbar is a toolbar item like any other, and left out it would keep its\n * own Tab stop, so Tab would stop twice in a bar that promises one. `[href]` rather than `a`: the kit's disabled link is an `<a>`\n * WITHOUT one, and is skipped as a disabled button is. */\nconst ITEMS =\n \"button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[data-toolbar-item]\";\n\n/** Keys a text field needs for itself — the toolbar must not take its caret away. */\nfunction ownsArrows(el: Element): boolean {\n if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) return true;\n if (el instanceof HTMLInputElement) return ![\"button\", \"checkbox\", \"radio\", \"submit\", \"reset\"].includes(el.type);\n return (el as HTMLElement).isContentEditable;\n}\n\n/**\n * The bar that appears while rows are selected: the count, a way out, and what can be\n * done to all of them at once.\n *\n * keksdose had written it three times — the transactions phone bar\n * (mobile-bulk-bar.tsx), the invoice lines' phone and desktop bars\n * (invoice-lines-bulk-bar.tsx) and the payees selection bar — and the copies had\n * drifted where it matters: two parked themselves with a guessed `bottom-16` that the\n * third had already found leaves a 24px gap (live #314: the nav is content-sized and\n * `AppShell` publishes its height as `--app-nav-h`), none of them announced the count,\n * and all three were `role=\"toolbar\"` without the arrow keys that role promises.\n *\n * ## Keep it mounted\n *\n * Render it unconditionally and let `count` decide. At zero it renders only its live\n * region — which must already be in the DOM for the first \"3 selected\" to be heard —\n * and when the count drops to zero it plays its exit before it goes, which a\n * `{count > 0 && <Bar/>}` gate would cut off.\n *\n * ## The toolbar\n *\n * One Tab stop: ←/→ (mirrored in RTL), Home and End move between the controls, and Tab\n * leaves — the WAI-ARIA toolbar pattern, since that is what `role=\"toolbar\"` tells a\n * screen reader to expect. The stop remembers the control last used. Arrow keys inside\n * a text field stay the field's.\n *\n * It is NOT a dialog: no focus trap, no scroll lock — the list behind stays live,\n * because tapping more rows is how a selection grows.\n */\nexport function BulkActionBar({\n count,\n onClear,\n children,\n variant: variantProp = \"floating\",\n open,\n panel,\n labels: labelsProp,\n className,\n style,\n onKeyDown,\n onFocus,\n ...rest\n}: BulkActionBarProps) {\n const labels = useKitLabels(\"bulkActionBar\", DEFAULT_BULK_ACTION_BAR_LABELS, labelsProp);\n const { announce, regionProps } = useAnnounce();\n const barRef = useRef<HTMLDivElement>(null);\n const variant = useResolvedVariant(variantProp);\n const shown = open ?? count > 0;\n\n // The count the bar last SHOWED, so a bar lowering itself after \"clear\" still reads\n // \"3 selected\" on its way out rather than \"0 selected\". Adjusted during render\n // (`Collapse`'s pattern), as is the exit below.\n const [shownCount, setShownCount] = useState(count);\n const [leaving, setLeaving] = useState(false);\n const [prev, setPrev] = useState({ count, shown });\n if (count !== prev.count || shown !== prev.shown) {\n setPrev({ count, shown });\n if (shown) {\n setShownCount(count);\n setLeaving(false);\n } else if (prev.shown) {\n setLeaving(!prefersReducedMotion());\n }\n }\n useEffect(() => {\n if (!leaving) return;\n const timer = setTimeout(() => setLeaving(false), OVERLAY_EXIT_MS);\n return () => clearTimeout(timer);\n }, [leaving]);\n\n // Spoken on every CHANGE, not on mount: a bar mounted with rows already selected\n // was not the result of anything the user just did.\n const spoken = useRef(count);\n useEffect(() => {\n if (spoken.current === count) return;\n spoken.current = count;\n // In selection mode the bar stays at zero, so zero is a count like any other.\n announce(count > 0 || shown ? labels.selected(count) : labels.cleared);\n }, [count, shown, labels, announce]);\n\n // Roving tab stop. The controls are the caller's, so the stop is kept on the DOM\n // rather than through props: after every render the remembered control (or the\n // first) is the one with `tabindex=0`, every other one `-1`.\n const active = useRef<HTMLElement | null>(null);\n // Every render: the caller's controls come and go (a \"remove added\" that exists\n // only while some are removable), and a stop on a control that left must move on.\n useLayoutEffect(() => {\n rove(barRef.current, active);\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(e);\n if (e.defaultPrevented || ownsArrows(e.target as Element)) return;\n const list = itemsOf(barRef.current);\n const at = list.indexOf(document.activeElement as HTMLElement);\n if (at === -1 || list.length === 0) return;\n const step = horizontalStep(e.key, barRef.current);\n let next = -1;\n if (step !== 0) next = (at + step + list.length) % list.length;\n else if (e.key === \"Home\") next = 0;\n else if (e.key === \"End\") next = list.length - 1;\n if (next === -1) return;\n e.preventDefault();\n active.current = list[next];\n rove(barRef.current, active);\n list[next].focus();\n };\n\n if (!shown && !leaving) {\n return <span {...regionProps} />;\n }\n\n const floating = variant === \"floating\";\n const clear = floating ? (\n <IconButton size=\"lg\" onClick={onClear} aria-label={labels.clear}>\n <X />\n </IconButton>\n ) : (\n // `ms-auto`: when the actions wrap, the way out still sits at the bar's end.\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={onClear} className=\"ms-auto\">\n {labels.clear}\n </Button>\n );\n\n const hasPanel = panel !== undefined && panel !== null && panel !== false;\n // The surface — where the bar sits and what it looks like — and the toolbar row's\n // own layout. Without a panel they are one element, exactly as before 0.11.0; with\n // one the surface wraps the row and the panel, so both share its tint and position.\n const surfaceClass = floating\n ? cn(\n \"fixed z-30 rounded-2xl border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg\",\n shown ? \"animate-sheet\" : \"animate-sheet-out\",\n )\n : cn(\n variant === \"sticky\" &&\n \"sticky top-0 z-20 border-b border-[var(--border)] bg-[color-mix(in_srgb,var(--brand)_8%,var(--bg-surface))] backdrop-blur\",\n shown ? \"animate-overlay\" : \"animate-overlay-out\",\n );\n // `flex-wrap`: on a phone the count, three actions and a text \"Clear selection\" are\n // wider than the screen. Without wrapping the count (the one shrinkable item)\n // collapsed to a sliver and the rest ran out of the bar; wrapped, the actions that do\n // not fit move to a second line and every label stays whole.\n const rowClass = cn(\"flex flex-wrap items-center\", floating ? \"gap-1 px-2 py-1\" : \"gap-2 px-3 py-2\");\n const surfaceStyle = floating ? { ...FLOATING_STYLE, ...style } : style;\n\n const toolbar = (\n <div\n {...(hasPanel ? null : rest)}\n ref={barRef}\n role=\"toolbar\"\n aria-label={labels.selected(shownCount)}\n aria-orientation=\"horizontal\"\n data-bulk-action-bar={hasPanel ? undefined : variant}\n // Leaving: out of the tab order and the accessibility tree at once, while the\n // bar still animates. The selection it acted on is already gone.\n inert={(!hasPanel && !shown) || undefined}\n onKeyDown={handleKeyDown}\n onFocus={(e) => {\n onFocus?.(e);\n // Clicking a control is using it: the stop follows the pointer too.\n const hit = itemsOf(barRef.current).find((el) => el === e.target);\n if (hit) {\n active.current = hit;\n rove(barRef.current, active);\n }\n }}\n style={hasPanel ? undefined : surfaceStyle}\n className={hasPanel ? rowClass : cn(rowClass, surfaceClass, className)}\n >\n {floating && clear}\n {/* `flex-auto`, not `flex-1`: a zero basis told the wrapping that the count needs\n no room, so it was the item squeezed to nothing instead of a line breaking.\n Its own width is its basis now; `truncate` is left for a label too long for a\n line on its own. */}\n <span className=\"min-w-0 flex-auto truncate text-sm font-medium text-[var(--text-primary)]\">\n {labels.selected(shownCount)}\n </span>\n {/* `contents`, so the caller's controls are flex items of the bar itself. */}\n <div className=\"contents\">{children}</div>\n {!floating && clear}\n </div>\n );\n\n return (\n <>\n <span {...regionProps} />\n {hasPanel ? (\n <div\n {...rest}\n data-bulk-action-bar={variant}\n inert={!shown || undefined}\n style={surfaceStyle}\n className={cn(\"flex flex-col\", surfaceClass, className)}\n >\n {toolbar}\n <div\n data-bulk-action-bar-panel=\"\"\n className={cn(\n \"border-t border-[var(--border)] px-3 py-3\",\n floating && \"max-h-[60dvh] overflow-y-auto\",\n )}\n >\n {panel}\n </div>\n </div>\n ) : (\n toolbar\n )}\n </>\n );\n}\n\nfunction itemsOf(bar: HTMLElement | null): HTMLElement[] {\n return bar ? Array.from(bar.querySelectorAll<HTMLElement>(ITEMS)) : [];\n}\n\n/** Put the one Tab stop on the remembered control — or the first, when that one has\n * gone or was never set — and take it off every other. */\nfunction rove(bar: HTMLElement | null, active: { current: HTMLElement | null }): void {\n const list = itemsOf(bar);\n if (!active.current || !list.includes(active.current)) active.current = list[0] ?? null;\n for (const el of list) el.tabIndex = el === active.current ? 0 : -1;\n}\n"],"mappings":"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","names":[]}
@@ -1,17 +1,19 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentPropsWithoutRef, ReactNode, Ref, MouseEvent, ReactElement } from 'react';
3
- import { bH as ButtonVariant, bF as ButtonSize } from '../delete-account-setting-CpIhzxxZ.js';
3
+ import { bO as ButtonVariant, bM as ButtonSize } from '../delete-account-setting-FLyJf-kZ.js';
4
4
  import './tooltip.js';
5
5
  import '../hooks/use-anchored-rect.js';
6
6
  import '../lib/clipping.js';
7
- import '../data-table-labels-DUELeXpa.js';
7
+ import '../data-table-labels-kpAAVi6g.js';
8
8
  import './data-table-sort.js';
9
9
  import './swipeable-row.js';
10
10
  import './mini-calendar.js';
11
11
  import './calendar-heatmap.js';
12
12
  import './popover.js';
13
+ import '../hooks/use-breakpoint.js';
13
14
  import './chip.js';
14
15
  import 'lucide-react';
16
+ import './field-parts.js';
15
17
  import './field-sync.js';
16
18
  import '../wizard/types.js';
17
19
  import '../tour/tour.js';
@@ -80,6 +82,9 @@ import '../admin/admin-parts.js';
80
82
  import '../admin/admin-action-log.js';
81
83
  import '../settings/settings-labels.js';
82
84
  import '../demo/demo-labels.js';
85
+ import './appearance-labels.js';
86
+ import '../theme/contrast.js';
87
+ import '../theme/text-size.js';
83
88
  import './reauth-dialog.js';
84
89
  import './server-wake.js';
85
90
  import '../lib/server-wake.js';
@@ -93,7 +98,7 @@ import './sign-chip.js';
93
98
  import './column-mapper.js';
94
99
  import '../lib/column-mapping.js';
95
100
  import '../lib/table-text.js';
96
- import './field-parts.js';
101
+ import './focus-ring.js';
97
102
 
98
103
  interface ButtonGroupProps extends Omit<ComponentPropsWithoutRef<"div">, "role"> {
99
104
  /** `horizontal` (default): side by side, in reading order. `vertical`: stacked —
@@ -15,6 +15,11 @@ const JOINED_COMMON = cn(
15
15
  // the next one's background.
16
16
  "[&>*]:relative [&>*:focus-within]:z-10"
17
17
  );
18
+ const JOINED_WRAP_AT_LARGE = cn(
19
+ "large:max-w-full large:flex-wrap large:border-0 large:ps-px large:pt-px",
20
+ "large:[&>*]:border large:[&>*]:-ms-px large:[&>*]:-mt-px",
21
+ "large:[&>*]:grow large:[&>*>button]:grow large:[&>*>a]:grow"
22
+ );
18
23
  const JOINED = {
19
24
  // Logical edges and corners throughout (`border-s`, `rounded-s-*`), so in RTL the
20
25
  // first member sits on the right with the right-hand corners rounded — the physical
@@ -25,7 +30,8 @@ const JOINED = {
25
30
  "[&>*:not(:first-child)]:border-s",
26
31
  "[&>:first-child]:rounded-s-md [&>:last-child]:rounded-e-md",
27
32
  "[&>:first-child>button]:rounded-s-md [&>:last-child>button]:rounded-e-md",
28
- "[&>:first-child>a]:rounded-s-md [&>:last-child>a]:rounded-e-md"
33
+ "[&>:first-child>a]:rounded-s-md [&>:last-child>a]:rounded-e-md",
34
+ JOINED_WRAP_AT_LARGE
29
35
  ),
30
36
  // The block axis is top-to-bottom in both reading directions, so top/bottom are
31
37
  // already the logical edges here.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/button-group.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { buttonClasses, type ButtonSize, type ButtonVariant } from \"./ui\";\nimport { useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\n\n// Every class below is spelled out in full rather than assembled from parts: the\n// consumer's Tailwind build finds classes by scanning this package's source as text,\n// and a class that exists only after a template literal runs is never generated.\n//\n// Each rule is written twice, once for a direct child and once for a `<button>` one\n// level down. The second is for a Tooltip-wrapped IconButton — the map zoom's + and −\n// both carry a tooltip — where the direct child is the tooltip's wrapper span and the\n// button that draws the border and the corners sits inside it. The same again for an\n// `<a>` one level down: a router link member (see {@link ButtonGroupLink}) under a\n// tooltip. A bare link is a direct child and needs nothing extra.\nconst JOINED_COMMON = cn(\n \"inline-flex rounded-md border border-[var(--border)]\",\n // The group draws the frame; each member gives up its own border, radius and shadow.\n \"[&>*]:rounded-none [&>*]:border-0 [&>*]:shadow-none\",\n \"[&>*>button]:rounded-none [&>*>button]:border-0 [&>*>button]:shadow-none\",\n \"[&>*>a]:rounded-none [&>*>a]:border-0 [&>*>a]:shadow-none\",\n \"[&>*]:border-[var(--border)]\",\n // A focused member rises above its neighbours so its ring is not painted over by\n // the next one's background.\n \"[&>*]:relative [&>*:focus-within]:z-10\",\n);\n\nconst JOINED: Record<\"horizontal\" | \"vertical\", string> = {\n // Logical edges and corners throughout (`border-s`, `rounded-s-*`), so in RTL the\n // first member sits on the right with the right-hand corners rounded — the physical\n // pair would round the outside of the last button and square off the first.\n horizontal: cn(\n JOINED_COMMON,\n \"flex-row items-stretch\",\n \"[&>*:not(:first-child)]:border-s\",\n \"[&>:first-child]:rounded-s-md [&>:last-child]:rounded-e-md\",\n \"[&>:first-child>button]:rounded-s-md [&>:last-child>button]:rounded-e-md\",\n \"[&>:first-child>a]:rounded-s-md [&>:last-child>a]:rounded-e-md\",\n ),\n // The block axis is top-to-bottom in both reading directions, so top/bottom are\n // already the logical edges here.\n vertical: cn(\n JOINED_COMMON,\n \"flex-col items-stretch\",\n \"[&>*:not(:first-child)]:border-t\",\n \"[&>:first-child]:rounded-t-md [&>:last-child]:rounded-b-md\",\n \"[&>:first-child>button]:rounded-t-md [&>:last-child>button]:rounded-b-md\",\n \"[&>:first-child>a]:rounded-t-md [&>:last-child>a]:rounded-b-md\",\n ),\n};\n\n// `gapped`: the members keep their own shape — round overlay discs, each its own\n// border and radius — and sit apart. Nothing is stripped, so there is nothing to\n// restore per edge, and the gap is logical by nature.\nconst GAPPED: Record<\"horizontal\" | \"vertical\", string> = {\n horizontal: \"inline-flex flex-row items-center gap-1\",\n vertical: \"inline-flex flex-col items-center gap-1\",\n};\n\n// `elevated`, joined: the frame itself floats — a filled surface under the members (a\n// ghost member would otherwise show the content through it) and the shadow of a\n// control lifted over the page.\nconst ELEVATED_JOINED = \"bg-[var(--bg-surface)] shadow-lg\";\n// `elevated`, gapped: there is no frame, so each disc carries the shadow. Aimed at the\n// control, not the tooltip's square wrapper span, whose shadow would be a box round a\n// round disc.\nconst ELEVATED_GAPPED =\n \"[&>button]:shadow-md [&>a]:shadow-md [&>*>button]:shadow-md [&>*>a]:shadow-md\";\n\nexport interface ButtonGroupProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"role\"> {\n /** `horizontal` (default): side by side, in reading order. `vertical`: stacked —\n * the map's zoom + over −. */\n orientation?: \"horizontal\" | \"vertical\";\n /**\n * `joined` (default): one frame, hairline dividers, outer corners rounded. `gapped`:\n * separate controls with a small gap, each keeping its own shape — the overlay\n * discs of keksdose's receipt preview zoom (scan-file-preview.tsx:362, the − and +\n * `IconButton variant=\"overlay\"` in a hand-made `flex gap-1`), where a joined frame\n * would be a box drawn over the photo.\n */\n variant?: \"joined\" | \"gapped\";\n /**\n * Float it: a surface with a shadow, for a toolbar that sits OVER content (a map's\n * zoom, a photo's controls) rather than in the page's flow — keksdose adds\n * `shadow-sm` to each disc on the receipt preview by hand. On a joined group the\n * frame is filled and shadowed; on a gapped one, each member.\n */\n elevated?: boolean;\n}\n\n/**\n * Adjacent `Button`s or `IconButton`s drawn as one joined control: a single frame,\n * hairline dividers between the members, rounded only at the outer corners.\n *\n * keksdose asked for it twice — \"collapse all / expand all\" above its category\n * tree, and the + and − of its map zoom — both hand-built with negative margins and\n * physical `rounded-l` / `rounded-r`, which came out inside-out under `dir=\"rtl\"`.\n *\n * `role=\"group\"` with a name: a reader then introduces the pair as \"Zoom, group\"\n * before reading \"Zoom in\", which is what tells a listener that the next button\n * belongs with this one. Pass `aria-label` (or `aria-labelledby`); a group with no\n * name is one more unlabelled wrapper. It is NOT a toolbar — it adds no arrow-key\n * roving, and every member keeps its own tab stop, as plain adjacent buttons do.\n * For one-of-many selection use `ToggleGroup`, which has the radio semantics.\n */\nexport function ButtonGroup({\n orientation = \"horizontal\",\n variant = \"joined\",\n elevated = false,\n className,\n ...rest\n}: ButtonGroupProps) {\n const joined = variant === \"joined\";\n return (\n <div\n {...rest}\n role=\"group\"\n data-orientation={orientation}\n data-variant={variant}\n className={cn(\n joined ? JOINED[orientation] : GAPPED[orientation],\n elevated && (joined ? ELEVATED_JOINED : ELEVATED_GAPPED),\n className,\n )}\n />\n );\n}\n\n/** What {@link ButtonGroupLinkProps.renderLink} is handed. Spread it onto your router's\n * link, mapping `href` — `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface ButtonGroupLinkRenderProps {\n href: string;\n /** The button look — keep it, or the member is a bare link in the frame. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n \"aria-current\"?: \"page\";\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nexport interface ButtonGroupLinkProps extends Omit<ComponentPropsWithoutRef<\"a\">, \"href\" | \"className\" | \"children\"> {\n href: string;\n /** The {@link Button} variant it looks like. Default `secondary`, the usual member. */\n variant?: ButtonVariant;\n size?: ButtonSize;\n /** The member for the page you are on: `aria-current=\"page\"` and the \"on\" look of a\n * pressed member, so a link row reads as a segmented switch between pages. */\n current?: boolean;\n /** Render through your router's `Link` — see {@link ButtonGroupLinkRenderProps}.\n * Default: the `<UiKitProvider linkComponent>`, then `<a>`. */\n renderLink?: (props: ButtonGroupLinkRenderProps) => ReactElement;\n className?: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n}\n\nconst LINK_CURRENT = \"bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]\";\n\n/**\n * A {@link ButtonGroup} member that navigates: a link drawn as a {@link Button}, so it\n * joins the frame like one. A group mixing actions and pages — \"Export\" beside \"Open\n * report\" — had to either make the page a button with an `onClick` navigate (no\n * middle-click, no \"open in new tab\", no link semantics) or style an `<a>` whose\n * corners the group did not know about. Plain `<a>` by default; `renderLink` for the\n * router's `Link`, the kit's usual shape (`Chip`, `MenuItem`, `StatTile`).\n */\nexport function ButtonGroupLink({\n href,\n variant = \"secondary\",\n size,\n current = false,\n renderLink,\n className,\n children,\n ...rest\n}: ButtonGroupLinkProps) {\n const kitLink = useKitLink();\n const props: ButtonGroupLinkRenderProps = {\n ...rest,\n href,\n \"aria-current\": current ? \"page\" : undefined,\n className: buttonClasses(variant, { size, className: cn(current && LINK_CURRENT, className) }),\n children,\n };\n const render = pickLinkRenderer(renderLink, kitLink, href);\n if (render) return <RenderedGroupLink render={render} {...props} />;\n const { children: content, ...anchor } = props;\n return <a {...anchor}>{content}</a>;\n}\n\n/** Calls `renderLink` as a component, so the router link's hooks are its own. */\nfunction RenderedGroupLink({\n render,\n ...props\n}: ButtonGroupLinkRenderProps & { render: (props: ButtonGroupLinkRenderProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AAmHI;AAlHJ,SAAS,UAAU;AACnB,SAAS,qBAA0D;AACnE,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AAYjC,MAAM,gBAAgB;AAAA,EACpB;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AACF;AAEA,MAAM,SAAoD;AAAA;AAAA;AAAA;AAAA,EAIxD,YAAY;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA;AAAA;AAAA,EAGA,UAAU;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAKA,MAAM,SAAoD;AAAA,EACxD,YAAY;AAAA,EACZ,UAAU;AACZ;AAKA,MAAM,kBAAkB;AAIxB,MAAM,kBACJ;AAsCK,SAAS,YAAY;AAAA,EAC1B,cAAc;AAAA,EACd,UAAU;AAAA,EACV,WAAW;AAAA,EACX;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,SAAS,YAAY;AAC3B,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,oBAAkB;AAAA,MAClB,gBAAc;AAAA,MACd,WAAW;AAAA,QACT,SAAS,OAAO,WAAW,IAAI,OAAO,WAAW;AAAA,QACjD,aAAa,SAAS,kBAAkB;AAAA,QACxC;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;AAgCA,MAAM,eAAe;AAUd,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,QAAM,UAAU,WAAW;AAC3B,QAAM,QAAoC;AAAA,IACxC,GAAG;AAAA,IACH;AAAA,IACA,gBAAgB,UAAU,SAAS;AAAA,IACnC,WAAW,cAAc,SAAS,EAAE,MAAM,WAAW,GAAG,WAAW,cAAc,SAAS,EAAE,CAAC;AAAA,IAC7F;AAAA,EACF;AACA,QAAM,SAAS,iBAAiB,YAAY,SAAS,IAAI;AACzD,MAAI,OAAQ,QAAO,oBAAC,qBAAkB,QAAiB,GAAG,OAAO;AACjE,QAAM,EAAE,UAAU,SAAS,GAAG,OAAO,IAAI;AACzC,SAAO,oBAAC,OAAG,GAAG,QAAS,mBAAQ;AACjC;AAGA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA,GAAG;AACL,GAAiG;AAC/F,SAAO,OAAO,KAAK;AACrB;","names":[]}
1
+ {"version":3,"sources":["../../src/components/button-group.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { buttonClasses, type ButtonSize, type ButtonVariant } from \"./ui\";\nimport { useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\n\n// Every class below is spelled out in full rather than assembled from parts: the\n// consumer's Tailwind build finds classes by scanning this package's source as text,\n// and a class that exists only after a template literal runs is never generated.\n//\n// Each rule is written twice, once for a direct child and once for a `<button>` one\n// level down. The second is for a Tooltip-wrapped IconButton — the map zoom's + and −\n// both carry a tooltip — where the direct child is the tooltip's wrapper span and the\n// button that draws the border and the corners sits inside it. The same again for an\n// `<a>` one level down: a router link member (see {@link ButtonGroupLink}) under a\n// tooltip. A bare link is a direct child and needs nothing extra.\nconst JOINED_COMMON = cn(\n \"inline-flex rounded-md border border-[var(--border)]\",\n // The group draws the frame; each member gives up its own border, radius and shadow.\n \"[&>*]:rounded-none [&>*]:border-0 [&>*]:shadow-none\",\n \"[&>*>button]:rounded-none [&>*>button]:border-0 [&>*>button]:shadow-none\",\n \"[&>*>a]:rounded-none [&>*>a]:border-0 [&>*>a]:shadow-none\",\n \"[&>*]:border-[var(--border)]\",\n // A focused member rises above its neighbours so its ring is not painted over by\n // the next one's background.\n \"[&>*]:relative [&>*:focus-within]:z-10\",\n);\n\n/**\n * A joined row at Large and Extra large wraps rather than run past its container (0.32.1,\n * the 360 px Extra-large sweep; docs/text-size-harmonization.md §4 \"nothing\n * overflows\"): \"Buttons | Chips | Feedback\" was 329 px at 150 %, 67 px past a 360 px\n * phone's card. A wrapped row cannot keep one frame with dividers between members — the\n * second line's first member would carry a divider against the frame and no line would\n * part the rows — so at Large the frame is drawn by the members: each its own 1px\n * border, overlapping its neighbours by a pixel on both axes (the container's `ps-px\n * pt-px` takes the overlap back at the edges). On one row that draws the same frame and\n * dividers as Normal; wrapped, every line and every member is parted by one hairline,\n * and the members share out each line (`grow`), so the lines make one block. A group\n * that fits is only as wide as its members, so nothing grows there. A `large:` class,\n * so Normal keeps the frame it has, and it costs no render.\n */\nconst JOINED_WRAP_AT_LARGE = cn(\n \"large:max-w-full large:flex-wrap large:border-0 large:ps-px large:pt-px\",\n \"large:[&>*]:border large:[&>*]:-ms-px large:[&>*]:-mt-px\",\n \"large:[&>*]:grow large:[&>*>button]:grow large:[&>*>a]:grow\",\n);\n\nconst JOINED: Record<\"horizontal\" | \"vertical\", string> = {\n // Logical edges and corners throughout (`border-s`, `rounded-s-*`), so in RTL the\n // first member sits on the right with the right-hand corners rounded — the physical\n // pair would round the outside of the last button and square off the first.\n horizontal: cn(\n JOINED_COMMON,\n \"flex-row items-stretch\",\n \"[&>*:not(:first-child)]:border-s\",\n \"[&>:first-child]:rounded-s-md [&>:last-child]:rounded-e-md\",\n \"[&>:first-child>button]:rounded-s-md [&>:last-child>button]:rounded-e-md\",\n \"[&>:first-child>a]:rounded-s-md [&>:last-child>a]:rounded-e-md\",\n JOINED_WRAP_AT_LARGE,\n ),\n // The block axis is top-to-bottom in both reading directions, so top/bottom are\n // already the logical edges here.\n vertical: cn(\n JOINED_COMMON,\n \"flex-col items-stretch\",\n \"[&>*:not(:first-child)]:border-t\",\n \"[&>:first-child]:rounded-t-md [&>:last-child]:rounded-b-md\",\n \"[&>:first-child>button]:rounded-t-md [&>:last-child>button]:rounded-b-md\",\n \"[&>:first-child>a]:rounded-t-md [&>:last-child>a]:rounded-b-md\",\n ),\n};\n\n// `gapped`: the members keep their own shape — round overlay discs, each its own\n// border and radius — and sit apart. Nothing is stripped, so there is nothing to\n// restore per edge, and the gap is logical by nature.\nconst GAPPED: Record<\"horizontal\" | \"vertical\", string> = {\n horizontal: \"inline-flex flex-row items-center gap-1\",\n vertical: \"inline-flex flex-col items-center gap-1\",\n};\n\n// `elevated`, joined: the frame itself floats — a filled surface under the members (a\n// ghost member would otherwise show the content through it) and the shadow of a\n// control lifted over the page.\nconst ELEVATED_JOINED = \"bg-[var(--bg-surface)] shadow-lg\";\n// `elevated`, gapped: there is no frame, so each disc carries the shadow. Aimed at the\n// control, not the tooltip's square wrapper span, whose shadow would be a box round a\n// round disc.\nconst ELEVATED_GAPPED =\n \"[&>button]:shadow-md [&>a]:shadow-md [&>*>button]:shadow-md [&>*>a]:shadow-md\";\n\nexport interface ButtonGroupProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"role\"> {\n /** `horizontal` (default): side by side, in reading order. `vertical`: stacked —\n * the map's zoom + over −. */\n orientation?: \"horizontal\" | \"vertical\";\n /**\n * `joined` (default): one frame, hairline dividers, outer corners rounded. `gapped`:\n * separate controls with a small gap, each keeping its own shape — the overlay\n * discs of keksdose's receipt preview zoom (scan-file-preview.tsx:362, the − and +\n * `IconButton variant=\"overlay\"` in a hand-made `flex gap-1`), where a joined frame\n * would be a box drawn over the photo.\n */\n variant?: \"joined\" | \"gapped\";\n /**\n * Float it: a surface with a shadow, for a toolbar that sits OVER content (a map's\n * zoom, a photo's controls) rather than in the page's flow — keksdose adds\n * `shadow-sm` to each disc on the receipt preview by hand. On a joined group the\n * frame is filled and shadowed; on a gapped one, each member.\n */\n elevated?: boolean;\n}\n\n/**\n * Adjacent `Button`s or `IconButton`s drawn as one joined control: a single frame,\n * hairline dividers between the members, rounded only at the outer corners.\n *\n * keksdose asked for it twice — \"collapse all / expand all\" above its category\n * tree, and the + and − of its map zoom — both hand-built with negative margins and\n * physical `rounded-l` / `rounded-r`, which came out inside-out under `dir=\"rtl\"`.\n *\n * `role=\"group\"` with a name: a reader then introduces the pair as \"Zoom, group\"\n * before reading \"Zoom in\", which is what tells a listener that the next button\n * belongs with this one. Pass `aria-label` (or `aria-labelledby`); a group with no\n * name is one more unlabelled wrapper. It is NOT a toolbar — it adds no arrow-key\n * roving, and every member keeps its own tab stop, as plain adjacent buttons do.\n * For one-of-many selection use `ToggleGroup`, which has the radio semantics.\n */\nexport function ButtonGroup({\n orientation = \"horizontal\",\n variant = \"joined\",\n elevated = false,\n className,\n ...rest\n}: ButtonGroupProps) {\n const joined = variant === \"joined\";\n return (\n <div\n {...rest}\n role=\"group\"\n data-orientation={orientation}\n data-variant={variant}\n className={cn(\n joined ? JOINED[orientation] : GAPPED[orientation],\n elevated && (joined ? ELEVATED_JOINED : ELEVATED_GAPPED),\n className,\n )}\n />\n );\n}\n\n/** What {@link ButtonGroupLinkProps.renderLink} is handed. Spread it onto your router's\n * link, mapping `href` — `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface ButtonGroupLinkRenderProps {\n href: string;\n /** The button look — keep it, or the member is a bare link in the frame. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n \"aria-current\"?: \"page\";\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nexport interface ButtonGroupLinkProps extends Omit<ComponentPropsWithoutRef<\"a\">, \"href\" | \"className\" | \"children\"> {\n href: string;\n /** The {@link Button} variant it looks like. Default `secondary`, the usual member. */\n variant?: ButtonVariant;\n size?: ButtonSize;\n /** The member for the page you are on: `aria-current=\"page\"` and the \"on\" look of a\n * pressed member, so a link row reads as a segmented switch between pages. */\n current?: boolean;\n /** Render through your router's `Link` — see {@link ButtonGroupLinkRenderProps}.\n * Default: the `<UiKitProvider linkComponent>`, then `<a>`. */\n renderLink?: (props: ButtonGroupLinkRenderProps) => ReactElement;\n className?: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n}\n\nconst LINK_CURRENT = \"bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]\";\n\n/**\n * A {@link ButtonGroup} member that navigates: a link drawn as a {@link Button}, so it\n * joins the frame like one. A group mixing actions and pages — \"Export\" beside \"Open\n * report\" — had to either make the page a button with an `onClick` navigate (no\n * middle-click, no \"open in new tab\", no link semantics) or style an `<a>` whose\n * corners the group did not know about. Plain `<a>` by default; `renderLink` for the\n * router's `Link`, the kit's usual shape (`Chip`, `MenuItem`, `StatTile`).\n */\nexport function ButtonGroupLink({\n href,\n variant = \"secondary\",\n size,\n current = false,\n renderLink,\n className,\n children,\n ...rest\n}: ButtonGroupLinkProps) {\n const kitLink = useKitLink();\n const props: ButtonGroupLinkRenderProps = {\n ...rest,\n href,\n \"aria-current\": current ? \"page\" : undefined,\n className: buttonClasses(variant, { size, className: cn(current && LINK_CURRENT, className) }),\n children,\n };\n const render = pickLinkRenderer(renderLink, kitLink, href);\n if (render) return <RenderedGroupLink render={render} {...props} />;\n const { children: content, ...anchor } = props;\n return <a {...anchor}>{content}</a>;\n}\n\n/** Calls `renderLink` as a component, so the router link's hooks are its own. */\nfunction RenderedGroupLink({\n render,\n ...props\n}: ButtonGroupLinkRenderProps & { render: (props: ButtonGroupLinkRenderProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AAwII;AAvIJ,SAAS,UAAU;AACnB,SAAS,qBAA0D;AACnE,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AAYjC,MAAM,gBAAgB;AAAA,EACpB;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AACF;AAgBA,MAAM,uBAAuB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,SAAoD;AAAA;AAAA;AAAA;AAAA,EAIxD,YAAY;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA;AAAA;AAAA,EAGA,UAAU;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAKA,MAAM,SAAoD;AAAA,EACxD,YAAY;AAAA,EACZ,UAAU;AACZ;AAKA,MAAM,kBAAkB;AAIxB,MAAM,kBACJ;AAsCK,SAAS,YAAY;AAAA,EAC1B,cAAc;AAAA,EACd,UAAU;AAAA,EACV,WAAW;AAAA,EACX;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,SAAS,YAAY;AAC3B,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,oBAAkB;AAAA,MAClB,gBAAc;AAAA,MACd,WAAW;AAAA,QACT,SAAS,OAAO,WAAW,IAAI,OAAO,WAAW;AAAA,QACjD,aAAa,SAAS,kBAAkB;AAAA,QACxC;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;AAgCA,MAAM,eAAe;AAUd,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,QAAM,UAAU,WAAW;AAC3B,QAAM,QAAoC;AAAA,IACxC,GAAG;AAAA,IACH;AAAA,IACA,gBAAgB,UAAU,SAAS;AAAA,IACnC,WAAW,cAAc,SAAS,EAAE,MAAM,WAAW,GAAG,WAAW,cAAc,SAAS,EAAE,CAAC;AAAA,IAC7F;AAAA,EACF;AACA,QAAM,SAAS,iBAAiB,YAAY,SAAS,IAAI;AACzD,MAAI,OAAQ,QAAO,oBAAC,qBAAkB,QAAiB,GAAG,OAAO;AACjE,QAAM,EAAE,UAAU,SAAS,GAAG,OAAO,IAAI;AACzC,SAAO,oBAAC,OAAG,GAAG,QAAS,mBAAQ;AACjC;AAGA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA,GAAG;AACL,GAAiG;AAC/F,SAAO,OAAO,KAAK;AACrB;","names":[]}
@@ -1,16 +1,18 @@
1
1
  import * as react from 'react';
2
- import { bI as CalculatorLabels } from '../delete-account-setting-CpIhzxxZ.js';
2
+ import { bP as CalculatorLabels } from '../delete-account-setting-FLyJf-kZ.js';
3
3
  import './tooltip.js';
4
4
  import '../hooks/use-anchored-rect.js';
5
5
  import '../lib/clipping.js';
6
- import '../data-table-labels-DUELeXpa.js';
6
+ import '../data-table-labels-kpAAVi6g.js';
7
7
  import './data-table-sort.js';
8
8
  import './swipeable-row.js';
9
9
  import './mini-calendar.js';
10
10
  import './calendar-heatmap.js';
11
11
  import './popover.js';
12
+ import '../hooks/use-breakpoint.js';
12
13
  import './chip.js';
13
14
  import 'lucide-react';
15
+ import './field-parts.js';
14
16
  import './field-sync.js';
15
17
  import '../wizard/types.js';
16
18
  import '../tour/tour.js';
@@ -79,6 +81,9 @@ import '../admin/admin-parts.js';
79
81
  import '../admin/admin-action-log.js';
80
82
  import '../settings/settings-labels.js';
81
83
  import '../demo/demo-labels.js';
84
+ import './appearance-labels.js';
85
+ import '../theme/contrast.js';
86
+ import '../theme/text-size.js';
82
87
  import './reauth-dialog.js';
83
88
  import './server-wake.js';
84
89
  import '../lib/server-wake.js';
@@ -92,7 +97,7 @@ import './sign-chip.js';
92
97
  import './column-mapper.js';
93
98
  import '../lib/column-mapping.js';
94
99
  import '../lib/table-text.js';
95
- import './field-parts.js';
100
+ import './focus-ring.js';
96
101
 
97
102
  /** Screen-reader names for the calculator panel's own controls — any part of the
98
103
  * kit's `calculator` namespace. Once three optional keys of its own; widened, not