@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
@@ -0,0 +1,231 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { isValidElement } from "react";
4
+ import { MoreHorizontal } from "lucide-react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { useLargeText } from "../hooks/use-large-text.js";
7
+ import { useKitLabels } from "../i18n/kit-labels.js";
8
+ import { Popover } from "./popover.js";
9
+ import { Button, CompactControls, IconButton } from "./ui.js";
10
+ import { useCommitReason } from "./write-lock.js";
11
+ const DEFAULT_ROW_ACTIONS_LABELS = {
12
+ actions: "Actions",
13
+ actionsFor: (name) => `Actions for ${name}`
14
+ };
15
+ function visibleActions(actions) {
16
+ return actions.filter((a) => Boolean(a) && !a.hidden);
17
+ }
18
+ function glyphOf(icon) {
19
+ if (icon === void 0) return null;
20
+ if (isValidElement(icon)) return icon;
21
+ const Icon = icon;
22
+ return /* @__PURE__ */ jsx(Icon, {});
23
+ }
24
+ function RowActions({
25
+ actions,
26
+ name,
27
+ collapse = "auto",
28
+ size = "xs",
29
+ menuWidth = "15rem",
30
+ className,
31
+ labels: labelsProp
32
+ }) {
33
+ const labels = useKitLabels("rowActions", DEFAULT_ROW_ACTIONS_LABELS, labelsProp);
34
+ const large = useLargeText();
35
+ const entries = visibleActions(actions);
36
+ if (entries.length === 0) return null;
37
+ const asMenu = collapse === "menu" || collapse === "auto" && large && entries.length > 1;
38
+ if (!asMenu) {
39
+ const strip = /* @__PURE__ */ jsx("div", { "data-slot": "row-actions", className: cn("inline-flex items-center gap-0.5", className), children: entries.map((action) => /* @__PURE__ */ jsx(InlineAction, { action, size }, action.key ?? action.label)) });
40
+ return large ? strip : /* @__PURE__ */ jsx(CompactControls, { children: strip });
41
+ }
42
+ const title = name ? labels.actionsFor(name) : labels.actions;
43
+ const busy = entries.some((action) => action.pending);
44
+ return /* @__PURE__ */ jsx(
45
+ Popover,
46
+ {
47
+ width: menuWidth,
48
+ "aria-label": title,
49
+ className: "p-1",
50
+ trigger: ({ open, toggle, ref }) => /* @__PURE__ */ jsx(
51
+ IconButton,
52
+ {
53
+ ref,
54
+ size,
55
+ label: title,
56
+ labelVisible: false,
57
+ "aria-haspopup": "dialog",
58
+ "aria-expanded": open,
59
+ pending: busy,
60
+ stopPropagation: true,
61
+ onClick: toggle,
62
+ className,
63
+ children: /* @__PURE__ */ jsx(MoreHorizontal, {})
64
+ }
65
+ ),
66
+ children: (close) => (
67
+ // React carries an event out of a portal along the COMPONENT tree, so a click on
68
+ // an entry (or between two) reached a clickable row or a DataTable `onRowClick`
69
+ // the menu was opened from, though the panel is nowhere near it in the page —
70
+ // only the "⋯" trigger stopped its own (0.32.1, keksdose's 0.32 report). Stopped
71
+ // here for the whole panel: clicks, and the Enter / Space a row also listens for.
72
+ // Escape goes on, to the popover that closes on it.
73
+ /* @__PURE__ */ jsx("div", { role: "presentation", onClick: stopClick, onKeyDown: stopActivationKeys, children: /* @__PURE__ */ jsx("ul", { "data-slot": "row-actions-menu", className: "space-y-0.5", children: entries.map((action) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(MenuAction, { action, close }) }, action.key ?? action.label)) }) })
74
+ )
75
+ }
76
+ );
77
+ }
78
+ function stopClick(e) {
79
+ e.stopPropagation();
80
+ }
81
+ function stopActivationKeys(e) {
82
+ if (e.key === "Enter" || e.key === " ") e.stopPropagation();
83
+ }
84
+ function useActionHref(action) {
85
+ const reason = useCommitReason(action.commit, action.disabledReason);
86
+ const refused = reason !== void 0 && reason !== null && reason !== false && reason !== "";
87
+ return action.href !== void 0 && !action.pending && !refused ? action.href : void 0;
88
+ }
89
+ function MenuAction({ action, close }) {
90
+ const href = useActionHref(action);
91
+ const glyph = glyphOf(action.icon);
92
+ const words = /* @__PURE__ */ jsxs(Fragment, { children: [
93
+ glyph && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "flex shrink-0 [&_svg]:size-4", children: glyph }),
94
+ action.label
95
+ ] });
96
+ const tone = action.tone === "danger" ? "danger" : void 0;
97
+ const select = () => {
98
+ close();
99
+ action.onSelect?.();
100
+ };
101
+ if (href !== void 0) {
102
+ return /* @__PURE__ */ jsx(
103
+ Button,
104
+ {
105
+ href,
106
+ external: action.external,
107
+ variant: "ghost",
108
+ size: "sm",
109
+ stretch: true,
110
+ tone,
111
+ disabled: action.disabled,
112
+ className: "w-full justify-start text-start",
113
+ onClick: select,
114
+ children: words
115
+ }
116
+ );
117
+ }
118
+ return /* @__PURE__ */ jsx(
119
+ Button,
120
+ {
121
+ type: "button",
122
+ variant: "ghost",
123
+ size: "sm",
124
+ stretch: true,
125
+ tone,
126
+ disabledReason: action.disabledReason,
127
+ commit: action.commit,
128
+ pending: action.pending,
129
+ disabled: action.disabled,
130
+ className: "w-full justify-start text-start",
131
+ onClick: select,
132
+ children: words
133
+ }
134
+ );
135
+ }
136
+ function InlineAction({ action, size }) {
137
+ const href = useActionHref(action);
138
+ const glyph = glyphOf(action.icon);
139
+ const tone = action.tone === "danger" ? "danger" : void 0;
140
+ if (glyph) {
141
+ if (href !== void 0) {
142
+ return /* @__PURE__ */ jsx(
143
+ IconButton,
144
+ {
145
+ href,
146
+ external: action.external,
147
+ size,
148
+ label: action.label,
149
+ tone,
150
+ disabled: action.disabled,
151
+ stopPropagation: true,
152
+ onClick: () => action.onSelect?.(),
153
+ children: glyph
154
+ }
155
+ );
156
+ }
157
+ return /* @__PURE__ */ jsx(
158
+ IconButton,
159
+ {
160
+ size,
161
+ label: action.label,
162
+ tone,
163
+ disabledReason: action.disabledReason,
164
+ commit: action.commit,
165
+ pending: action.pending,
166
+ disabled: action.disabled,
167
+ stopPropagation: true,
168
+ onClick: () => action.onSelect?.(),
169
+ children: glyph
170
+ }
171
+ );
172
+ }
173
+ const stop = {
174
+ onClick: (e) => {
175
+ e.stopPropagation();
176
+ action.onSelect?.();
177
+ },
178
+ onKeyDown: (e) => {
179
+ if (e.key === "Enter" || e.key === " ") e.stopPropagation();
180
+ }
181
+ };
182
+ if (href !== void 0) {
183
+ return /* @__PURE__ */ jsx(Button, { href, external: action.external, variant: "ghost", size: "sm", tone, disabled: action.disabled, ...stop, children: action.label });
184
+ }
185
+ return /* @__PURE__ */ jsx(
186
+ Button,
187
+ {
188
+ type: "button",
189
+ variant: "ghost",
190
+ size: "sm",
191
+ tone,
192
+ disabledReason: action.disabledReason,
193
+ commit: action.commit,
194
+ pending: action.pending,
195
+ disabled: action.disabled,
196
+ ...stop,
197
+ children: action.label
198
+ }
199
+ );
200
+ }
201
+ function RowActionsHeader({ text, labels }) {
202
+ const resolved = useKitLabels("rowActions", DEFAULT_ROW_ACTIONS_LABELS, labels);
203
+ return /* @__PURE__ */ jsx("span", { className: "sr-only", children: text ?? resolved.actions });
204
+ }
205
+ function rowActionsColumn({
206
+ key = "actions",
207
+ actions,
208
+ name,
209
+ collapse,
210
+ size,
211
+ headerText,
212
+ labels,
213
+ column
214
+ }) {
215
+ return {
216
+ key,
217
+ header: /* @__PURE__ */ jsx(RowActionsHeader, { text: headerText, labels }),
218
+ headerText,
219
+ cell: (row) => /* @__PURE__ */ jsx(RowActions, { actions: actions(row), name: name?.(row), collapse, size, labels }),
220
+ className: "w-px whitespace-nowrap text-end",
221
+ headClassName: "w-px",
222
+ noRowLink: true,
223
+ ...column
224
+ };
225
+ }
226
+ export {
227
+ DEFAULT_ROW_ACTIONS_LABELS,
228
+ RowActions,
229
+ rowActionsColumn
230
+ };
231
+ //# sourceMappingURL=row-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/row-actions.tsx"],"sourcesContent":["import { isValidElement } from \"react\";\nimport type { KeyboardEvent, MouseEvent, ReactElement, ReactNode } from \"react\";\nimport { MoreHorizontal } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useLargeText } from \"../hooks/use-large-text\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Popover } from \"./popover\";\nimport { Button, CompactControls, IconButton } from \"./ui\";\nimport { useCommitReason } from \"./write-lock\";\nimport type { IconButtonSize } from \"./ui\";\nimport type { DataTableColumn } from \"./data-table\";\n\n/**\n * A row's actions — inline icons at Normal, one \"⋯\" menu at Large and above\n * (docs/text-size-harmonization.md §10.8).\n *\n * At Large an IconButton shows its label as text (§4). In a dense row — a register's\n * action column, a list row's end — every label at once does not fit: Kurvenschmiede\n * has 44 IconButtons in rows, kastlan 119. So a row's actions collapse instead, into\n * the menu the user roster has used since 0.31 (`UserRowActions`, now built on this):\n * one \"⋯\" button, and a menu with each action's icon AND words, where a locked one stays\n * listed and says why.\n *\n * The data is the roster's `UserRowActionList`, generalised: entries, and `false` /\n * `null` for the ones a condition left out (`row.locked || {…}`).\n */\n\n/** One action of a row. */\nexport interface RowAction {\n /** React key. Default: the label. */\n key?: string;\n /** The action's name: the inline icon's name and tooltip, the menu entry's words. */\n label: string;\n /**\n * Runs it. The kit sends nothing — the app opens its confirmation or calls its API.\n * Optional only beside `href`; an action has one or the other (or both: `onSelect`\n * then runs as the link is followed).\n */\n onSelect?: () => void;\n /**\n * The action NAVIGATES — an edit page, a mail to compose (0.32.1, kastlan's 0.32\n * report): a real link inline and in the menu, so open in a new tab, a middle click\n * and the URL on hover work, which `onSelect` + `navigate` lost. The kit's link rule\n * applies ({@link Button}'s `href`): in-app through the provider's `linkComponent`, an\n * external or `#anchor` one a plain `<a>`, `mailto:` included.\n *\n * A link cannot carry a reason or a spinner, so an action with a `disabledReason`\n * (or under a write lock, with `commit`) or `pending` stays a button and says why.\n */\n href?: string;\n /** With `href`: leaves the app — a new tab, and a screen reader is told so. */\n external?: boolean;\n /**\n * The glyph: a Lucide icon (`Pencil`), or an element (`<Pencil />`, an app's own\n * svg) — DataTable's `rowActions` hand their `icon` node through. Inline, an action\n * without one is a small text button; in the menu it is the words alone.\n */\n icon?: LucideIcon | ReactElement;\n /** `danger` for delete, revoke, erase: the quiet red of IconButton's `tone=\"danger\"`\n * inline, the danger text tone in the menu. */\n tone?: \"default\" | \"danger\";\n /** Why it is not available for this row. It stays listed, focusable, and says so — an\n * action that silently vanished would leave the reader looking for it. */\n disabledReason?: ReactNode;\n /**\n * This action COMMITS — it saves, sends, deletes: under a locked\n * {@link WriteLockProvider} it is refused with the lock's reason, as {@link Button}'s\n * `commit` is, inline and in the menu alike. No provider, or an unlocked one: no effect.\n */\n commit?: boolean;\n /**\n * In flight — the request it started has not answered. Inline, the icon turns into\n * IconButton's spinner (`pending`); collapsed, the \"⋯\" button does, since the menu\n * closed when the action was chosen. Either way nothing in the row can be pressed again\n * until it settles: the \"⋯\" opens no menu while busy.\n */\n pending?: boolean;\n /**\n * Not available for a moment, with nothing to explain — another action of the list\n * is in flight (an admin panel's `busy`). The plain `disabled`; a lasting refusal is a\n * `disabledReason`, which says why.\n */\n disabled?: boolean;\n /** Leave it out for this row. */\n hidden?: boolean;\n}\n\n/** What a row's actions may be: entries, and `false` / `null` / `undefined` for the ones\n * a condition left out. */\nexport type RowActionList = readonly (RowAction | false | null | undefined)[];\n\nexport interface RowActionsLabels {\n /** The menu button's name when the row has no name of its own; also the actions\n * column's (hidden) header in {@link rowActionsColumn}. */\n actions: string;\n /** The menu button's name, given the row's: \"Actions for Ada Example\". */\n actionsFor: (name: string) => string;\n}\n\nexport const DEFAULT_ROW_ACTIONS_LABELS: RowActionsLabels = {\n actions: \"Actions\",\n actionsFor: (name) => `Actions for ${name}`,\n};\n\n/**\n * - `\"auto\"` (default): the icons inline at Normal; the \"⋯\" menu at Large and Extra\n * large once there are two or more (§10.8). A single action stays inline at every\n * size, with its label as text at Large — one label fits a row, and a menu of one is\n * a detour.\n * - `\"menu\"`: the menu at every size — the roster's choice, whose actions grow (§2.4\n * there) and do not fit a phone card as a strip.\n * - `\"inline\"`: the icons at every size — for a row the app knows has room; at Large\n * each then shows its label (IconButton's `labelVisible`).\n */\nexport type RowActionsCollapse = \"auto\" | \"menu\" | \"inline\";\n\n/** The sizes a row action comes in: IconButton's row steps. `2xs` is DataTable's\n * compact density. At Large every one is at least a 48 px target (IconButton). */\nexport type RowActionsSize = Extract<IconButtonSize, \"md\" | \"sm\" | \"xs\" | \"2xs\">;\n\nexport interface RowActionsProps {\n actions: RowActionList;\n /** The row's name, for the menu button's name — \"Actions for Ada Example\". Without it\n * (or `null`, as API fields often are) the button is \"Actions\". */\n name?: string | null;\n /** When the actions collapse into the menu — see {@link RowActionsCollapse}. */\n collapse?: RowActionsCollapse;\n /** The inline icons' and the \"⋯\" button's size. Default `xs`, a list row's action. */\n size?: RowActionsSize;\n /** The menu's width. Default `\"15rem\"`, in rem so it grows with the words (§3.2). */\n menuWidth?: `${number}rem`;\n /** On the wrapper: the inline strip, or the \"⋯\" button's box. */\n className?: string;\n labels?: Partial<RowActionsLabels>;\n}\n\nfunction visibleActions(actions: RowActionList): RowAction[] {\n return actions.filter((a): a is RowAction => Boolean(a) && !(a as RowAction).hidden);\n}\n\n/** An action's glyph as a node: a component is rendered, an element is used as is. */\nfunction glyphOf(icon: RowAction[\"icon\"]): ReactNode {\n if (icon === undefined) return null;\n if (isValidElement(icon)) return icon;\n const Icon = icon as LucideIcon;\n return <Icon />;\n}\n\n/**\n * A row's actions, inline or as a \"⋯\" menu — see {@link RowActionsCollapse}. Every\n * control stops its click (and the Enter/Space it stands for) from reaching the row, so\n * a clickable row or a DataTable `rowLink` does not open as well; in a DataTable column\n * also set `noRowLink: true`, as {@link rowActionsColumn} does.\n *\n * Inline, the icons sit in a {@link CompactControls} region at Normal: their labels\n * stay the names and the tooltips, and a locked one keeps its reason in the tooltip — a\n * line under one icon of a row would break the row. In the menu a locked action's\n * reason is a line under its words at Large and on touch (Button's\n * `disabledReasonDisplay`).\n *\n * The menu is portalled, so a table's scroller cannot clip it; Escape and an outside\n * press close it, and focus goes back to the \"⋯\" button, which is where a confirmation\n * dialog then returns it.\n */\nexport function RowActions({\n actions,\n name,\n collapse = \"auto\",\n size = \"xs\",\n menuWidth = \"15rem\",\n className,\n labels: labelsProp,\n}: RowActionsProps) {\n const labels = useKitLabels(\"rowActions\", DEFAULT_ROW_ACTIONS_LABELS, labelsProp);\n const large = useLargeText();\n const entries = visibleActions(actions);\n if (entries.length === 0) return null;\n const asMenu = collapse === \"menu\" || (collapse === \"auto\" && large && entries.length > 1);\n if (!asMenu) {\n const strip = (\n <div data-slot=\"row-actions\" className={cn(\"inline-flex items-center gap-0.5\", className)}>\n {entries.map((action) => (\n <InlineAction key={action.key ?? action.label} action={action} size={size} />\n ))}\n </div>\n );\n // Compact at Normal (no room for a reason line in a row); at Large an inline action\n // is the app's choice (\"inline\") or the row's only one, and shows its words.\n return large ? strip : <CompactControls>{strip}</CompactControls>;\n }\n const title = name ? labels.actionsFor(name) : labels.actions;\n // The menu closed when the action was chosen, so what is in flight shows on \"⋯\".\n const busy = entries.some((action) => action.pending);\n return (\n <Popover\n width={menuWidth}\n aria-label={title}\n className=\"p-1\"\n trigger={({ open, toggle, ref }) => (\n <IconButton\n ref={ref}\n size={size}\n label={title}\n // \"⋯\" is read by everyone as \"more actions\"; the menu it opens is the words.\n labelVisible={false}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n pending={busy}\n stopPropagation\n onClick={toggle}\n className={className}\n >\n <MoreHorizontal />\n </IconButton>\n )}\n >\n {(close) => (\n // React carries an event out of a portal along the COMPONENT tree, so a click on\n // an entry (or between two) reached a clickable row or a DataTable `onRowClick`\n // the menu was opened from, though the panel is nowhere near it in the page —\n // only the \"⋯\" trigger stopped its own (0.32.1, keksdose's 0.32 report). Stopped\n // here for the whole panel: clicks, and the Enter / Space a row also listens for.\n // Escape goes on, to the popover that closes on it.\n <div role=\"presentation\" onClick={stopClick} onKeyDown={stopActivationKeys}>\n <ul data-slot=\"row-actions-menu\" className=\"space-y-0.5\">\n {entries.map((action) => (\n <li key={action.key ?? action.label}>\n <MenuAction action={action} close={close} />\n </li>\n ))}\n </ul>\n </div>\n )}\n </Popover>\n );\n}\n\nfunction stopClick(e: MouseEvent) {\n e.stopPropagation();\n}\n\nfunction stopActivationKeys(e: KeyboardEvent) {\n if (e.key === \"Enter\" || e.key === \" \") e.stopPropagation();\n}\n\n/**\n * The `href` an action renders as a link with, or `undefined` for a button: a link\n * cannot carry a reason or a spinner (Button's link form has neither), so a refused\n * action — its own `disabledReason`, or a write lock on a `commit` — and a pending one\n * stay buttons and say so.\n */\nfunction useActionHref(action: RowAction): string | undefined {\n const reason = useCommitReason(action.commit, action.disabledReason);\n const refused = reason !== undefined && reason !== null && reason !== false && reason !== \"\";\n return action.href !== undefined && !action.pending && !refused ? action.href : undefined;\n}\n\n/** One entry of the \"⋯\" menu: the words, as a link for an `href`, else a button. */\nfunction MenuAction({ action, close }: { action: RowAction; close: () => void }) {\n const href = useActionHref(action);\n const glyph = glyphOf(action.icon);\n const words = (\n <>\n {glyph && (\n <span aria-hidden className=\"flex shrink-0 [&_svg]:size-4\">\n {glyph}\n </span>\n )}\n {action.label}\n </>\n );\n const tone = action.tone === \"danger\" ? \"danger\" : undefined;\n const select = () => {\n close();\n action.onSelect?.();\n };\n if (href !== undefined) {\n return (\n <Button\n href={href}\n external={action.external}\n variant=\"ghost\"\n size=\"sm\"\n stretch\n tone={tone}\n disabled={action.disabled}\n className=\"w-full justify-start text-start\"\n onClick={select}\n >\n {words}\n </Button>\n );\n }\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n stretch\n tone={tone}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n className=\"w-full justify-start text-start\"\n onClick={select}\n >\n {words}\n </Button>\n );\n}\n\n/** One inline action: an IconButton with its icon, else a small text button — each a\n * link for an `href` (see {@link useActionHref}). */\nfunction InlineAction({ action, size }: { action: RowAction; size: RowActionsSize }) {\n const href = useActionHref(action);\n const glyph = glyphOf(action.icon);\n const tone = action.tone === \"danger\" ? \"danger\" : undefined;\n if (glyph) {\n if (href !== undefined) {\n return (\n <IconButton\n href={href}\n external={action.external}\n size={size}\n label={action.label}\n tone={tone}\n disabled={action.disabled}\n stopPropagation\n onClick={() => action.onSelect?.()}\n >\n {glyph}\n </IconButton>\n );\n }\n return (\n <IconButton\n size={size}\n label={action.label}\n tone={tone}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n stopPropagation\n onClick={() => action.onSelect?.()}\n >\n {glyph}\n </IconButton>\n );\n }\n // A row listening for clicks must not open as well (IconButton's stopPropagation).\n const stop = {\n onClick: (e: MouseEvent) => {\n e.stopPropagation();\n action.onSelect?.();\n },\n onKeyDown: (e: KeyboardEvent) => {\n if (e.key === \"Enter\" || e.key === \" \") e.stopPropagation();\n },\n };\n if (href !== undefined) {\n return (\n <Button href={href} external={action.external} variant=\"ghost\" size=\"sm\" tone={tone} disabled={action.disabled} {...stop}>\n {action.label}\n </Button>\n );\n }\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n tone={tone}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n {...stop}\n >\n {action.label}\n </Button>\n );\n}\n\n/** The actions column's hidden header, in the provider's words. */\nfunction RowActionsHeader({ text, labels }: { text?: string; labels?: Partial<RowActionsLabels> }) {\n const resolved = useKitLabels(\"rowActions\", DEFAULT_ROW_ACTIONS_LABELS, labels);\n return <span className=\"sr-only\">{text ?? resolved.actions}</span>;\n}\n\nexport interface RowActionsColumnOptions<T> {\n /** The column's key. Default `\"actions\"`. */\n key?: string;\n /** The row's actions — `false` / `null` for the ones a condition leaves out. */\n actions: (row: T) => RowActionList;\n /** The row's name, for the \"⋯\" button's name (\"Actions for Ada Example\"). `null` and\n * `undefined` both mean \"Actions\". */\n name?: (row: T) => string | null | undefined;\n collapse?: RowActionsCollapse;\n /** Default `xs`. */\n size?: RowActionsSize;\n /**\n * The column's name as plain text — the column-settings checklist and the phone\n * card's label read it (`DataTableColumn.headerText`). Pass the app's own word; left\n * out, the header shows the provider's `rowActions.actions` to a screen reader and\n * the plain-text places fall back to the column's `key`.\n */\n headerText?: string;\n labels?: Partial<RowActionsLabels>;\n /** Anything else for the column (`mobileHidden`, `cellProps`…), merged last. */\n column?: Partial<DataTableColumn<T>>;\n}\n\n/**\n * A DataTable column of {@link RowActions} — an app's action column in one line\n * (§10.8): inline icons at Normal, the \"⋯\" menu at Large with two or more actions.\n *\n * columns={[...columns, rowActionsColumn({ actions: (r) => [...], name: (r) => r.name })]}\n *\n * The header is visually hidden (the icons say what the column is), the column hugs its\n * content at the row's end, and `noRowLink` keeps a `rowHref` anchor off it, so the\n * buttons keep their own clicks. On a phone it is a line of the card like any column;\n * set `column: { mobileHidden: true }` to drop it there (with `mobileDetailsInRow` it\n * then shows in the opened row instead).\n */\nexport function rowActionsColumn<T>({\n key = \"actions\",\n actions,\n name,\n collapse,\n size,\n headerText,\n labels,\n column,\n}: RowActionsColumnOptions<T>): DataTableColumn<T> {\n return {\n key,\n header: <RowActionsHeader text={headerText} labels={labels} />,\n headerText,\n cell: (row) => (\n <RowActions actions={actions(row)} name={name?.(row)} collapse={collapse} size={size} labels={labels} />\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px\",\n noRowLink: true,\n ...column,\n };\n}\n"],"mappings":"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","names":[]}
@@ -72,7 +72,7 @@ function ScrollArea({
72
72
  // a row with its own background would cover exactly the part of the ring that
73
73
  // says where focus is. An outline paints above it, and the negative offset
74
74
  // keeps it inside a parent that clips.
75
- "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]",
75
+ "focus-visible:outline-[length:var(--focus-ring-width)] focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]",
76
76
  className
77
77
  )
78
78
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/scroll-area.tsx"],"sourcesContent":["import { useCallback, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, Ref, RefObject } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * Whether `ref`'s element currently overflows its box — the one fact a scroll\n * container needs to decide whether it is a keyboard stop.\n *\n * Measured on every commit (cheap: two reads, and state is set only on a change) and\n * whenever the element or one of its direct children resizes, which covers both\n * \"the panel got narrower\" and \"the rows arrived\". Without `ResizeObserver` (jsdom,\n * very old engines) the per-commit measurement is what remains.\n *\n * Shared with {@link Table}'s overflow wrapper, which has the same obligation.\n */\nexport function useScrollOverflow(ref: RefObject<HTMLElement | null>): boolean {\n const [overflowing, setOverflowing] = useState(false);\n\n const measure = useCallback(() => {\n const el = ref.current;\n if (!el) return;\n const next = el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1;\n setOverflowing((prev) => (prev === next ? prev : next));\n }, [ref]);\n\n // No dependency list on purpose: content changes arrive as re-renders.\n useLayoutEffect(() => {\n measure();\n });\n\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el || typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n for (const child of Array.from(el.children)) ro.observe(child);\n return () => ro.disconnect();\n }, [ref, measure]);\n\n return overflowing;\n}\n\n/** Thin, token-coloured scrollbars. `scrollbar-width` / `scrollbar-color` are the\n * standard (Firefox, Chrome 121+); the `::-webkit-scrollbar` rules are for Safari,\n * which has neither. Where the standard pair applies, Chrome ignores the rest. */\nexport const THIN_SCROLLBAR_CLASS = cn(\n \"[scrollbar-width:thin] [scrollbar-color:var(--border-strong)_transparent]\",\n \"[&::-webkit-scrollbar]:size-2 [&::-webkit-scrollbar-track]:bg-transparent\",\n \"[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-[var(--border-strong)]\",\n);\n\nconst AXIS = {\n vertical: \"overflow-y-auto overflow-x-hidden\",\n horizontal: \"overflow-x-auto overflow-y-hidden\",\n both: \"overflow-auto\",\n} as const;\n\nexport interface ScrollAreaProps extends ComponentPropsWithoutRef<\"div\"> {\n /** Which way the content may scroll. Default `vertical`. */\n orientation?: keyof typeof AXIS;\n /**\n * The region's accessible name. With it (or `aria-labelledby`) the container is a\n * `role=\"region\"` a reader can find and name; without it, it is a plain box.\n * Name every ScrollArea whose content can overflow: once it is a tab stop, a\n * reader announces it on focus, and an unnamed one is announced as nothing.\n */\n label?: string;\n ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * A styled overflow container: a thin scrollbar in the kit's colours, a focus ring,\n * and the keyboard access a plain `overflow-auto` div does not have.\n *\n * kastlan used Radix's ScrollArea for this and nothing else from it. Radix draws a\n * custom scrollbar in JavaScript; the platform's own, thinned and coloured, costs no\n * script and keeps native momentum, overscroll and scrollbar-dragging behaviour.\n *\n * A container that scrolls but holds nothing focusable cannot be scrolled from the\n * keyboard at all — Tab jumps straight past it (WCAG 2.1.1). So while the content\n * overflows, the container itself is a tab stop (`tabIndex=0`), and arrow keys and\n * Page Up/Down scroll it natively. While nothing overflows it is not, so a short list\n * adds no empty stop to the tab order.\n */\nexport function ScrollArea({\n orientation = \"vertical\",\n label,\n className,\n ref,\n tabIndex,\n \"aria-labelledby\": labelledBy,\n ...rest\n}: ScrollAreaProps) {\n const inner = useRef<HTMLDivElement | null>(null);\n const overflowing = useScrollOverflow(inner);\n const setRef = useCallback(\n (el: HTMLDivElement | null) => {\n inner.current = el;\n if (typeof ref === \"function\") ref(el);\n else if (ref) ref.current = el;\n },\n [ref],\n );\n const named = label !== undefined || labelledBy !== undefined;\n\n return (\n <div\n {...rest}\n ref={setRef}\n role={named ? \"region\" : undefined}\n aria-label={label}\n aria-labelledby={labelledBy}\n tabIndex={tabIndex ?? (overflowing ? 0 : undefined)}\n data-overflowing={overflowing || undefined}\n data-orientation={orientation}\n className={cn(\n \"relative min-h-0 min-w-0\",\n AXIS[orientation],\n THIN_SCROLLBAR_CLASS,\n // An OUTLINE, inset: a ring is a box-shadow, which paints beneath the content —\n // a row with its own background would cover exactly the part of the ring that\n // says where focus is. An outline paints above it, and the negative offset\n // keeps it inside a parent that clips.\n \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]\",\n className,\n )}\n />\n );\n}\n"],"mappings":";AA0GI;AA1GJ,SAAS,aAAa,iBAAiB,QAAQ,gBAAgB;AAE/D,SAAS,UAAU;AAaZ,SAAS,kBAAkB,KAA6C;AAC7E,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAEpD,QAAM,UAAU,YAAY,MAAM;AAChC,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AACT,UAAM,OAAO,GAAG,eAAe,GAAG,eAAe,KAAK,GAAG,cAAc,GAAG,cAAc;AACxF,mBAAe,CAAC,SAAU,SAAS,OAAO,OAAO,IAAK;AAAA,EACxD,GAAG,CAAC,GAAG,CAAC;AAGR,kBAAgB,MAAM;AACpB,YAAQ;AAAA,EACV,CAAC;AAED,kBAAgB,MAAM;AACpB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,MAAM,OAAO,mBAAmB,YAAa;AAClD,UAAM,KAAK,IAAI,eAAe,OAAO;AACrC,OAAG,QAAQ,EAAE;AACb,eAAW,SAAS,MAAM,KAAK,GAAG,QAAQ,EAAG,IAAG,QAAQ,KAAK;AAC7D,WAAO,MAAM,GAAG,WAAW;AAAA,EAC7B,GAAG,CAAC,KAAK,OAAO,CAAC;AAEjB,SAAO;AACT;AAKO,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,OAAO;AAAA,EACX,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AACR;AA6BO,SAAS,WAAW;AAAA,EACzB,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,mBAAmB;AAAA,EACnB,GAAG;AACL,GAAoB;AAClB,QAAM,QAAQ,OAA8B,IAAI;AAChD,QAAM,cAAc,kBAAkB,KAAK;AAC3C,QAAM,SAAS;AAAA,IACb,CAAC,OAA8B;AAC7B,YAAM,UAAU;AAChB,UAAI,OAAO,QAAQ,WAAY,KAAI,EAAE;AAAA,eAC5B,IAAK,KAAI,UAAU;AAAA,IAC9B;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AACA,QAAM,QAAQ,UAAU,UAAa,eAAe;AAEpD,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,KAAK;AAAA,MACL,MAAM,QAAQ,WAAW;AAAA,MACzB,cAAY;AAAA,MACZ,mBAAiB;AAAA,MACjB,UAAU,aAAa,cAAc,IAAI;AAAA,MACzC,oBAAkB,eAAe;AAAA,MACjC,oBAAkB;AAAA,MAClB,WAAW;AAAA,QACT;AAAA,QACA,KAAK,WAAW;AAAA,QAChB;AAAA;AAAA;AAAA;AAAA;AAAA,QAKA;AAAA,QACA;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/scroll-area.tsx"],"sourcesContent":["import { useCallback, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, Ref, RefObject } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * Whether `ref`'s element currently overflows its box — the one fact a scroll\n * container needs to decide whether it is a keyboard stop.\n *\n * Measured on every commit (cheap: two reads, and state is set only on a change) and\n * whenever the element or one of its direct children resizes, which covers both\n * \"the panel got narrower\" and \"the rows arrived\". Without `ResizeObserver` (jsdom,\n * very old engines) the per-commit measurement is what remains.\n *\n * Shared with {@link Table}'s overflow wrapper, which has the same obligation.\n */\nexport function useScrollOverflow(ref: RefObject<HTMLElement | null>): boolean {\n const [overflowing, setOverflowing] = useState(false);\n\n const measure = useCallback(() => {\n const el = ref.current;\n if (!el) return;\n const next = el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1;\n setOverflowing((prev) => (prev === next ? prev : next));\n }, [ref]);\n\n // No dependency list on purpose: content changes arrive as re-renders.\n useLayoutEffect(() => {\n measure();\n });\n\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el || typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n for (const child of Array.from(el.children)) ro.observe(child);\n return () => ro.disconnect();\n }, [ref, measure]);\n\n return overflowing;\n}\n\n/** Thin, token-coloured scrollbars. `scrollbar-width` / `scrollbar-color` are the\n * standard (Firefox, Chrome 121+); the `::-webkit-scrollbar` rules are for Safari,\n * which has neither. Where the standard pair applies, Chrome ignores the rest. */\nexport const THIN_SCROLLBAR_CLASS = cn(\n \"[scrollbar-width:thin] [scrollbar-color:var(--border-strong)_transparent]\",\n \"[&::-webkit-scrollbar]:size-2 [&::-webkit-scrollbar-track]:bg-transparent\",\n \"[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-[var(--border-strong)]\",\n);\n\nconst AXIS = {\n vertical: \"overflow-y-auto overflow-x-hidden\",\n horizontal: \"overflow-x-auto overflow-y-hidden\",\n both: \"overflow-auto\",\n} as const;\n\nexport interface ScrollAreaProps extends ComponentPropsWithoutRef<\"div\"> {\n /** Which way the content may scroll. Default `vertical`. */\n orientation?: keyof typeof AXIS;\n /**\n * The region's accessible name. With it (or `aria-labelledby`) the container is a\n * `role=\"region\"` a reader can find and name; without it, it is a plain box.\n * Name every ScrollArea whose content can overflow: once it is a tab stop, a\n * reader announces it on focus, and an unnamed one is announced as nothing.\n */\n label?: string;\n ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * A styled overflow container: a thin scrollbar in the kit's colours, a focus ring,\n * and the keyboard access a plain `overflow-auto` div does not have.\n *\n * kastlan used Radix's ScrollArea for this and nothing else from it. Radix draws a\n * custom scrollbar in JavaScript; the platform's own, thinned and coloured, costs no\n * script and keeps native momentum, overscroll and scrollbar-dragging behaviour.\n *\n * A container that scrolls but holds nothing focusable cannot be scrolled from the\n * keyboard at all — Tab jumps straight past it (WCAG 2.1.1). So while the content\n * overflows, the container itself is a tab stop (`tabIndex=0`), and arrow keys and\n * Page Up/Down scroll it natively. While nothing overflows it is not, so a short list\n * adds no empty stop to the tab order.\n */\nexport function ScrollArea({\n orientation = \"vertical\",\n label,\n className,\n ref,\n tabIndex,\n \"aria-labelledby\": labelledBy,\n ...rest\n}: ScrollAreaProps) {\n const inner = useRef<HTMLDivElement | null>(null);\n const overflowing = useScrollOverflow(inner);\n const setRef = useCallback(\n (el: HTMLDivElement | null) => {\n inner.current = el;\n if (typeof ref === \"function\") ref(el);\n else if (ref) ref.current = el;\n },\n [ref],\n );\n const named = label !== undefined || labelledBy !== undefined;\n\n return (\n <div\n {...rest}\n ref={setRef}\n role={named ? \"region\" : undefined}\n aria-label={label}\n aria-labelledby={labelledBy}\n tabIndex={tabIndex ?? (overflowing ? 0 : undefined)}\n data-overflowing={overflowing || undefined}\n data-orientation={orientation}\n className={cn(\n \"relative min-h-0 min-w-0\",\n AXIS[orientation],\n THIN_SCROLLBAR_CLASS,\n // An OUTLINE, inset: a ring is a box-shadow, which paints beneath the content —\n // a row with its own background would cover exactly the part of the ring that\n // says where focus is. An outline paints above it, and the negative offset\n // keeps it inside a parent that clips.\n \"focus-visible:outline-[length:var(--focus-ring-width)] focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]\",\n className,\n )}\n />\n );\n}\n"],"mappings":";AA0GI;AA1GJ,SAAS,aAAa,iBAAiB,QAAQ,gBAAgB;AAE/D,SAAS,UAAU;AAaZ,SAAS,kBAAkB,KAA6C;AAC7E,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAEpD,QAAM,UAAU,YAAY,MAAM;AAChC,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AACT,UAAM,OAAO,GAAG,eAAe,GAAG,eAAe,KAAK,GAAG,cAAc,GAAG,cAAc;AACxF,mBAAe,CAAC,SAAU,SAAS,OAAO,OAAO,IAAK;AAAA,EACxD,GAAG,CAAC,GAAG,CAAC;AAGR,kBAAgB,MAAM;AACpB,YAAQ;AAAA,EACV,CAAC;AAED,kBAAgB,MAAM;AACpB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,MAAM,OAAO,mBAAmB,YAAa;AAClD,UAAM,KAAK,IAAI,eAAe,OAAO;AACrC,OAAG,QAAQ,EAAE;AACb,eAAW,SAAS,MAAM,KAAK,GAAG,QAAQ,EAAG,IAAG,QAAQ,KAAK;AAC7D,WAAO,MAAM,GAAG,WAAW;AAAA,EAC7B,GAAG,CAAC,KAAK,OAAO,CAAC;AAEjB,SAAO;AACT;AAKO,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,OAAO;AAAA,EACX,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AACR;AA6BO,SAAS,WAAW;AAAA,EACzB,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,mBAAmB;AAAA,EACnB,GAAG;AACL,GAAoB;AAClB,QAAM,QAAQ,OAA8B,IAAI;AAChD,QAAM,cAAc,kBAAkB,KAAK;AAC3C,QAAM,SAAS;AAAA,IACb,CAAC,OAA8B;AAC7B,YAAM,UAAU;AAChB,UAAI,OAAO,QAAQ,WAAY,KAAI,EAAE;AAAA,eAC5B,IAAK,KAAI,UAAU;AAAA,IAC9B;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AACA,QAAM,QAAQ,UAAU,UAAa,eAAe;AAEpD,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,KAAK;AAAA,MACL,MAAM,QAAQ,WAAW;AAAA,MACzB,cAAY;AAAA,MACZ,mBAAiB;AAAA,MACjB,UAAU,aAAa,cAAc,IAAI;AAAA,MACzC,oBAAkB,eAAe;AAAA,MACjC,oBAAkB;AAAA,MAClB,WAAW;AAAA,QACT;AAAA,QACA,KAAK,WAAW;AAAA,QAChB;AAAA;AAAA;AAAA;AAAA;AAAA,QAKA;AAAA,QACA;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -3,9 +3,10 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { forwardRef, useRef } from "react";
4
4
  import { Search, X } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
- import { FIELD_BASE } from "./ui.js";
6
+ import { FIELD_BASE, FIELD_TOUCH_TEXT } from "./ui.js";
7
7
  import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels.js";
8
- const SEARCH_INLINE = "block w-full min-w-0 border-0 bg-transparent py-1 text-[var(--text-primary)] shadow-none placeholder:text-[var(--text-placeholder)] focus:outline-none focus:ring-0";
8
+ const SEARCH_INLINE = "block w-full min-w-0 border-0 bg-transparent py-1 text-[var(--text-primary)] shadow-none placeholder:text-[var(--text-placeholder)] focus:outline-none focus:ring-0 " + // FIELD_BASE carries the 16 px touch floor (§10.1); the inline look needs it too.
9
+ FIELD_TOUCH_TEXT;
9
10
  const SearchField = forwardRef(function SearchField2({
10
11
  value,
11
12
  onChange,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/search-field.tsx"],"sourcesContent":["import { forwardRef, useRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE } from \"./ui\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\ninterface SearchFieldOwnProps\n extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n \"value\" | \"onChange\" | \"type\" | \"aria-label\"\n > {\n value: string;\n /** The typed text. Emitted as a plain string, not an event: every caller of the\n * four this replaced wanted the string, and one of them held it in a URL param\n * rather than in state. */\n onChange: (value: string) => void;\n /**\n * The field's accessible name.\n *\n * @deprecated Pass `aria-label` instead — the DOM spelling this kit standardised on\n * when it found three names for one idea (`ariaLabel`, `aria-label`, and this).\n * Unchanged and still supported; it names the field only when `aria-label` is\n * absent, so a consumer can migrate one call site at a time.\n */\n label?: string;\n /** Names the clear button. Default: the catalogue's `common.clear`, in the\n * provider's language. */\n clearLabel?: string;\n /**\n * Draw the \"×\" that empties the field once there is something in it. Default\n * `true`.\n *\n * It used to appear only when a `clearLabel` was passed, and every kastlan call\n * site passed one — lease-list-page.tsx:91, trial-balance-page.tsx:~196,\n * account-ledger-page.tsx:~131 and data-table/resource-list-page.tsx:256 each\n * spelled `clearLabel={t(\"table.clear\")}` — so the opt-in was boilerplate: a filter\n * you can type into and not untype is the complaint that produced the button in\n * the first place, and the catalogue already names it. `false` is for a field\n * whose clearing lives elsewhere (a \"Reset filters\" beside it).\n */\n clearable?: boolean;\n /**\n * `\"field\"` (default): the bordered filter box that sits on a page.\n *\n * `\"inline\"`: no box at all — no border, no fill, no padding of its own — and no\n * type size of its own either, so it takes the size of the header it sits in.\n * For a search that IS the header: a command-palette sheet, a picker's top line\n * (Keksdose's transaction search, which lost its `text-base` headline to a\n * bordered `text-sm` box when it moved onto this component). The container is\n * what frames it, so give the container the focus cue if it needs one\n * (`focus-within:`); the caret is the field's own.\n */\n variant?: \"field\" | \"inline\";\n /** Classes for the `<input>` itself. `className` styles the wrapper — the box the\n * icon and the clear button are positioned against — so it could set a width\n * and nothing else. See {@link Input}'s `inputClassName`. */\n inputClassName?: string;\n}\n\n// The inline look: FIELD_DISPLAY's idea (the chrome gone, the value the thing\n// itself) without even its baseline, because the header it sits in already draws\n// the line. No `text-*` size, on purpose — the input inherits the caller's.\nconst SEARCH_INLINE =\n \"block w-full min-w-0 border-0 bg-transparent py-1 text-[var(--text-primary)] shadow-none placeholder:text-[var(--text-placeholder)] focus:outline-none focus:ring-0\";\n\n/**\n * One of the two spellings is REQUIRED, and the union is how that survives the\n * deprecation. Making `label` merely optional would have been the easy change and the\n * wrong one: the whole reason this component exists is that two of the four boxes it\n * replaced were named by nothing but their placeholder — which disappears the moment\n * someone types, i.e. exactly when a screen-reader user is working in the field. A\n * caller now has a choice of spelling; it still has no option to pass neither.\n */\nexport type SearchFieldProps = SearchFieldOwnProps &\n ({ \"aria-label\": string; label?: string } | { label: string; \"aria-label\"?: string });\n\n/**\n * The page-level filter box: a search icon, a `type=\"search\"` input, and a clear\n * \"×\" that appears once there is something to clear.\n *\n * ## Why it is a component\n *\n * Keksdose had four of these and no two agreed. Two were character-identical\n * twenty-line copies (the settings catalogue filter and the tour list filter) —\n * the same absolute icon at `left-3`, the same `pl-9 pr-9`, the same\n * `[&::-webkit-search-cancel-button]:appearance-none`, the same conditional clear\n * button at `right-2` — differing only in which state setter they called. The\n * third (the admin support queue) was the same idea at `left-2`/`size-3.5`/`pl-7`\n * with no clear button and no `type=\"search\"`, so it had neither the `searchbox`\n * role nor a way to empty itself. The fourth (the bank picker) was a bare `<Input\n * placeholder=\"Search\">`: no icon, no clear, and no accessible name at all once\n * the placeholder went away.\n *\n * None of that is a design decision anyone made; it is four people solving the\n * same problem on four days. A shared component is how the fourth screen gets the\n * clear button the first one earned.\n *\n * ## The two X's\n *\n * `type=\"search\"` is what gives the field its `searchbox` role — and, in Chrome, a\n * NATIVE clear cross that paints right next to ours, so a non-empty box shows two\n * X's an inch apart. Ours is the one that survives: it carries an accessible name\n * and matches the app's other icon buttons, and it returns focus to the input so\n * the next query can be typed straight away.\n */\nexport const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(function SearchField(\n {\n value,\n onChange,\n label,\n clearLabel: clearLabelProp,\n clearable = true,\n className,\n inputClassName,\n placeholder,\n variant = \"field\",\n \"aria-label\": ariaLabel,\n ...rest\n },\n ref,\n) {\n const innerRef = useRef<HTMLInputElement>(null);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const clearLabel = clearable ? (clearLabelProp ?? common.clear) : undefined;\n // Whichever spelling the caller used. It is the accessible name AND the placeholder\n // fallback, so a box named only by `aria-label` still says what it is before anyone\n // has typed in it — the same deal `label` has always had.\n const name = ariaLabel ?? label;\n const inline = variant === \"inline\";\n return (\n <div className={cn(\"relative\", className)}>\n <Search\n aria-hidden\n className={cn(\n \"pointer-events-none absolute top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\",\n // Logical sides throughout, so a right-to-left page gets the icon at the\n // start of the line and the clear button at its end.\n inline ? \"start-0\" : \"start-3\",\n )}\n />\n <input\n ref={(node) => {\n innerRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n type=\"search\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n aria-label={name}\n placeholder={placeholder ?? name}\n // Keksdose live #333: *\"Entering the search here — is that set as password?\n // … I got prompted for remembering my name or mail address\"*, typed into the\n // settings filter on a phone.\n //\n // A `type=\"search\"` input with no `name` and no `autocomplete` is a field\n // Chrome's autofill classifies by GUESSWORK, from the labels around it and\n // the other fields on the page — and a settings page has an email and a\n // password on it. Naming the field and declaring the intent is what takes it\n // out of that guess: `autocomplete=\"off\"` says it is not part of any profile,\n // and a `name` of \"search\" is the strongest hint the heuristics take.\n //\n // The three that follow are not about autofill but about the same keyboard:\n // a filter box that capitalises the first letter and autocorrects \"2fa\" to\n // \"2FA\" is fighting the person typing into it. Declared BEFORE `{...rest}` so\n // a caller with a reason can still override any of them.\n name=\"search\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n autoCorrect=\"off\"\n spellCheck={false}\n {...rest}\n className={cn(\n inline ? SEARCH_INLINE : FIELD_BASE,\n inline ? \"ps-7\" : \"ps-9\",\n // Room for our own \"×\" only when there is one; a field that can't be\n // cleared has no reason to reserve the space.\n clearLabel === undefined ? (inline ? \"pe-0\" : \"pe-3\") : inline ? \"pe-8\" : \"pe-9\",\n \"[&::-webkit-search-cancel-button]:appearance-none\",\n inputClassName,\n )}\n />\n {clearLabel !== undefined && value !== \"\" && (\n <button\n type=\"button\"\n onClick={() => {\n onChange(\"\");\n // Clearing is the start of the next query far more often than it is the\n // end of this one, so the caret stays where it can be typed into.\n innerRef.current?.focus();\n }}\n aria-label={clearLabel}\n className={cn(\n \"absolute top-1/2 -translate-y-1/2 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\",\n inline ? \"end-0\" : \"end-2\",\n )}\n >\n <X className=\"size-4\" />\n </button>\n )}\n </div>\n );\n});\nSearchField.displayName = \"SearchField\";\n"],"mappings":";AAmII,SACE,KADF;AAnIJ,SAAS,YAAY,cAAc;AAEnC,SAAS,QAAQ,SAAS;AAC1B,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB,oBAAoB;AA0DpD,MAAM,gBACJ;AA0CK,MAAM,cAAc,WAA+C,SAASA,aACjF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,cAAc;AAAA,EACd,GAAG;AACL,GACA,KACA;AACA,QAAM,WAAW,OAAyB,IAAI;AAC9C,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,aAAa,YAAa,kBAAkB,OAAO,QAAS;AAIlE,QAAM,OAAO,aAAa;AAC1B,QAAM,SAAS,YAAY;AAC3B,SACE,qBAAC,SAAI,WAAW,GAAG,YAAY,SAAS,GACtC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,WAAW;AAAA,UACT;AAAA;AAAA;AAAA,UAGA,SAAS,YAAY;AAAA,QACvB;AAAA;AAAA,IACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,CAAC,SAAS;AACb,mBAAS,UAAU;AACnB,cAAI,OAAO,QAAQ,WAAY,KAAI,IAAI;AAAA,mBAC9B,IAAK,KAAI,UAAU;AAAA,QAC9B;AAAA,QACA,MAAK;AAAA,QACL;AAAA,QACA,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,QACxC,cAAY;AAAA,QACZ,aAAa,eAAe;AAAA,QAgB5B,MAAK;AAAA,QACL,cAAa;AAAA,QACb,gBAAe;AAAA,QACf,aAAY;AAAA,QACZ,YAAY;AAAA,QACX,GAAG;AAAA,QACJ,WAAW;AAAA,UACT,SAAS,gBAAgB;AAAA,UACzB,SAAS,SAAS;AAAA;AAAA;AAAA,UAGlB,eAAe,SAAa,SAAS,SAAS,SAAU,SAAS,SAAS;AAAA,UAC1E;AAAA,UACA;AAAA,QACF;AAAA;AAAA,IACF;AAAA,IACC,eAAe,UAAa,UAAU,MACrC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM;AACb,mBAAS,EAAE;AAGX,mBAAS,SAAS,MAAM;AAAA,QAC1B;AAAA,QACA,cAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA,SAAS,UAAU;AAAA,QACrB;AAAA,QAEA,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,IACxB;AAAA,KAEJ;AAEJ,CAAC;AACD,YAAY,cAAc;","names":["SearchField"]}
1
+ {"version":3,"sources":["../../src/components/search-field.tsx"],"sourcesContent":["import { forwardRef, useRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE, FIELD_TOUCH_TEXT } from \"./ui\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\ninterface SearchFieldOwnProps\n extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n \"value\" | \"onChange\" | \"type\" | \"aria-label\"\n > {\n value: string;\n /** The typed text. Emitted as a plain string, not an event: every caller of the\n * four this replaced wanted the string, and one of them held it in a URL param\n * rather than in state. */\n onChange: (value: string) => void;\n /**\n * The field's accessible name.\n *\n * @deprecated Pass `aria-label` instead — the DOM spelling this kit standardised on\n * when it found three names for one idea (`ariaLabel`, `aria-label`, and this).\n * Unchanged and still supported; it names the field only when `aria-label` is\n * absent, so a consumer can migrate one call site at a time.\n */\n label?: string;\n /** Names the clear button. Default: the catalogue's `common.clear`, in the\n * provider's language. */\n clearLabel?: string;\n /**\n * Draw the \"×\" that empties the field once there is something in it. Default\n * `true`.\n *\n * It used to appear only when a `clearLabel` was passed, and every kastlan call\n * site passed one — lease-list-page.tsx:91, trial-balance-page.tsx:~196,\n * account-ledger-page.tsx:~131 and data-table/resource-list-page.tsx:256 each\n * spelled `clearLabel={t(\"table.clear\")}` — so the opt-in was boilerplate: a filter\n * you can type into and not untype is the complaint that produced the button in\n * the first place, and the catalogue already names it. `false` is for a field\n * whose clearing lives elsewhere (a \"Reset filters\" beside it).\n */\n clearable?: boolean;\n /**\n * `\"field\"` (default): the bordered filter box that sits on a page.\n *\n * `\"inline\"`: no box at all — no border, no fill, no padding of its own — and no\n * type size of its own either, so it takes the size of the header it sits in.\n * For a search that IS the header: a command-palette sheet, a picker's top line\n * (Keksdose's transaction search, which lost its `text-base` headline to a\n * bordered `text-sm` box when it moved onto this component). The container is\n * what frames it, so give the container the focus cue if it needs one\n * (`focus-within:`); the caret is the field's own.\n */\n variant?: \"field\" | \"inline\";\n /** Classes for the `<input>` itself. `className` styles the wrapper — the box the\n * icon and the clear button are positioned against — so it could set a width\n * and nothing else. See {@link Input}'s `inputClassName`. */\n inputClassName?: string;\n}\n\n// The inline look: FIELD_DISPLAY's idea (the chrome gone, the value the thing\n// itself) without even its baseline, because the header it sits in already draws\n// the line. No `text-*` size, on purpose — the input inherits the caller's.\nconst SEARCH_INLINE =\n \"block w-full min-w-0 border-0 bg-transparent py-1 text-[var(--text-primary)] shadow-none placeholder:text-[var(--text-placeholder)] focus:outline-none focus:ring-0 \" +\n // FIELD_BASE carries the 16 px touch floor (§10.1); the inline look needs it too.\n FIELD_TOUCH_TEXT;\n\n/**\n * One of the two spellings is REQUIRED, and the union is how that survives the\n * deprecation. Making `label` merely optional would have been the easy change and the\n * wrong one: the whole reason this component exists is that two of the four boxes it\n * replaced were named by nothing but their placeholder — which disappears the moment\n * someone types, i.e. exactly when a screen-reader user is working in the field. A\n * caller now has a choice of spelling; it still has no option to pass neither.\n */\nexport type SearchFieldProps = SearchFieldOwnProps &\n ({ \"aria-label\": string; label?: string } | { label: string; \"aria-label\"?: string });\n\n/**\n * The page-level filter box: a search icon, a `type=\"search\"` input, and a clear\n * \"×\" that appears once there is something to clear.\n *\n * ## Why it is a component\n *\n * Keksdose had four of these and no two agreed. Two were character-identical\n * twenty-line copies (the settings catalogue filter and the tour list filter) —\n * the same absolute icon at `left-3`, the same `pl-9 pr-9`, the same\n * `[&::-webkit-search-cancel-button]:appearance-none`, the same conditional clear\n * button at `right-2` — differing only in which state setter they called. The\n * third (the admin support queue) was the same idea at `left-2`/`size-3.5`/`pl-7`\n * with no clear button and no `type=\"search\"`, so it had neither the `searchbox`\n * role nor a way to empty itself. The fourth (the bank picker) was a bare `<Input\n * placeholder=\"Search\">`: no icon, no clear, and no accessible name at all once\n * the placeholder went away.\n *\n * None of that is a design decision anyone made; it is four people solving the\n * same problem on four days. A shared component is how the fourth screen gets the\n * clear button the first one earned.\n *\n * ## The two X's\n *\n * `type=\"search\"` is what gives the field its `searchbox` role — and, in Chrome, a\n * NATIVE clear cross that paints right next to ours, so a non-empty box shows two\n * X's an inch apart. Ours is the one that survives: it carries an accessible name\n * and matches the app's other icon buttons, and it returns focus to the input so\n * the next query can be typed straight away.\n */\nexport const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(function SearchField(\n {\n value,\n onChange,\n label,\n clearLabel: clearLabelProp,\n clearable = true,\n className,\n inputClassName,\n placeholder,\n variant = \"field\",\n \"aria-label\": ariaLabel,\n ...rest\n },\n ref,\n) {\n const innerRef = useRef<HTMLInputElement>(null);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const clearLabel = clearable ? (clearLabelProp ?? common.clear) : undefined;\n // Whichever spelling the caller used. It is the accessible name AND the placeholder\n // fallback, so a box named only by `aria-label` still says what it is before anyone\n // has typed in it — the same deal `label` has always had.\n const name = ariaLabel ?? label;\n const inline = variant === \"inline\";\n return (\n <div className={cn(\"relative\", className)}>\n <Search\n aria-hidden\n className={cn(\n \"pointer-events-none absolute top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\",\n // Logical sides throughout, so a right-to-left page gets the icon at the\n // start of the line and the clear button at its end.\n inline ? \"start-0\" : \"start-3\",\n )}\n />\n <input\n ref={(node) => {\n innerRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n type=\"search\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n aria-label={name}\n placeholder={placeholder ?? name}\n // Keksdose live #333: *\"Entering the search here — is that set as password?\n // … I got prompted for remembering my name or mail address\"*, typed into the\n // settings filter on a phone.\n //\n // A `type=\"search\"` input with no `name` and no `autocomplete` is a field\n // Chrome's autofill classifies by GUESSWORK, from the labels around it and\n // the other fields on the page — and a settings page has an email and a\n // password on it. Naming the field and declaring the intent is what takes it\n // out of that guess: `autocomplete=\"off\"` says it is not part of any profile,\n // and a `name` of \"search\" is the strongest hint the heuristics take.\n //\n // The three that follow are not about autofill but about the same keyboard:\n // a filter box that capitalises the first letter and autocorrects \"2fa\" to\n // \"2FA\" is fighting the person typing into it. Declared BEFORE `{...rest}` so\n // a caller with a reason can still override any of them.\n name=\"search\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n autoCorrect=\"off\"\n spellCheck={false}\n {...rest}\n className={cn(\n inline ? SEARCH_INLINE : FIELD_BASE,\n inline ? \"ps-7\" : \"ps-9\",\n // Room for our own \"×\" only when there is one; a field that can't be\n // cleared has no reason to reserve the space.\n clearLabel === undefined ? (inline ? \"pe-0\" : \"pe-3\") : inline ? \"pe-8\" : \"pe-9\",\n \"[&::-webkit-search-cancel-button]:appearance-none\",\n inputClassName,\n )}\n />\n {clearLabel !== undefined && value !== \"\" && (\n <button\n type=\"button\"\n onClick={() => {\n onChange(\"\");\n // Clearing is the start of the next query far more often than it is the\n // end of this one, so the caret stays where it can be typed into.\n innerRef.current?.focus();\n }}\n aria-label={clearLabel}\n className={cn(\n \"absolute top-1/2 -translate-y-1/2 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\",\n inline ? \"end-0\" : \"end-2\",\n )}\n >\n <X className=\"size-4\" />\n </button>\n )}\n </div>\n );\n});\nSearchField.displayName = \"SearchField\";\n"],"mappings":";AAqII,SACE,KADF;AArIJ,SAAS,YAAY,cAAc;AAEnC,SAAS,QAAQ,SAAS;AAC1B,SAAS,UAAU;AACnB,SAAS,YAAY,wBAAwB;AAC7C,SAAS,uBAAuB,oBAAoB;AA0DpD,MAAM,gBACJ;AAEA;AA0CK,MAAM,cAAc,WAA+C,SAASA,aACjF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,cAAc;AAAA,EACd,GAAG;AACL,GACA,KACA;AACA,QAAM,WAAW,OAAyB,IAAI;AAC9C,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,aAAa,YAAa,kBAAkB,OAAO,QAAS;AAIlE,QAAM,OAAO,aAAa;AAC1B,QAAM,SAAS,YAAY;AAC3B,SACE,qBAAC,SAAI,WAAW,GAAG,YAAY,SAAS,GACtC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,WAAW;AAAA,UACT;AAAA;AAAA;AAAA,UAGA,SAAS,YAAY;AAAA,QACvB;AAAA;AAAA,IACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,CAAC,SAAS;AACb,mBAAS,UAAU;AACnB,cAAI,OAAO,QAAQ,WAAY,KAAI,IAAI;AAAA,mBAC9B,IAAK,KAAI,UAAU;AAAA,QAC9B;AAAA,QACA,MAAK;AAAA,QACL;AAAA,QACA,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,QACxC,cAAY;AAAA,QACZ,aAAa,eAAe;AAAA,QAgB5B,MAAK;AAAA,QACL,cAAa;AAAA,QACb,gBAAe;AAAA,QACf,aAAY;AAAA,QACZ,YAAY;AAAA,QACX,GAAG;AAAA,QACJ,WAAW;AAAA,UACT,SAAS,gBAAgB;AAAA,UACzB,SAAS,SAAS;AAAA;AAAA;AAAA,UAGlB,eAAe,SAAa,SAAS,SAAS,SAAU,SAAS,SAAS;AAAA,UAC1E;AAAA,UACA;AAAA,QACF;AAAA;AAAA,IACF;AAAA,IACC,eAAe,UAAa,UAAU,MACrC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM;AACb,mBAAS,EAAE;AAGX,mBAAS,SAAS,MAAM;AAAA,QAC1B;AAAA,QACA,cAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA,SAAS,UAAU;AAAA,QACrB;AAAA,QAEA,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,IACxB;AAAA,KAEJ;AAEJ,CAAC;AACD,YAAY,cAAc;","names":["SearchField"]}
@@ -16,7 +16,8 @@ interface BudgetAxis {
16
16
  width?: number;
17
17
  }
18
18
  /**
19
- * The narrowest the plot may get before the automatic budget steps in, in px.
19
+ * The narrowest the plot may get before the automatic budget steps in, in px at Normal —
20
+ * × the text size's scale since 0.32 (see {@link autoAxisBudget}).
20
21
  *
21
22
  * About a phone's plot with one axis a side (390 px viewport, card padding, two bands
22
23
  * of 48 + 16), where a curve's shape still reads. It is the point below which the chart
@@ -26,6 +27,11 @@ interface BudgetAxis {
26
27
  declare const MIN_PLOT_WIDTH = 160;
27
28
  /** The budget the automatic rule applies: one axis a side. */
28
29
  declare const NARROW_AXIS_BUDGET: SeriesChartAxisBudget;
30
+ /**
31
+ * The automatic rule's second step: the left axis alone — for a chart with an axis on
32
+ * each side that even one a side leaves too little plot (0.32, see {@link autoAxisBudget}).
33
+ */
34
+ declare const LEFT_AXIS_BUDGET: SeriesChartAxisBudget;
29
35
  /**
30
36
  * The ids of the VISIBLE axes a budget hides — axes already `hide: true` are neither
31
37
  * counted nor returned. Empty for no budget.
@@ -37,16 +43,33 @@ declare const NARROW_AXIS_BUDGET: SeriesChartAxisBudget;
37
43
  */
38
44
  declare function budgetedAxes(axes: readonly BudgetAxis[], budget: SeriesChartAxisBudget | undefined): string[];
39
45
  /**
40
- * The automatic budget for a chart `width` px wide: {@link NARROW_AXIS_BUDGET} when the
41
- * visible axes' bands would leave the plot under {@link MIN_PLOT_WIDTH} AND some side
42
- * draws more than one axis — the only case one-a-side gives anything back. `undefined`
43
- * otherwise, and for a width that is not known (no layout, no `ResizeObserver`).
46
+ * The automatic budget for a chart `width` px wide, in two steps — each taken only when
47
+ * the plot would otherwise be narrower than {@link MIN_PLOT_WIDTH} × `scale`:
48
+ *
49
+ * 1. {@link NARROW_AXIS_BUDGET}, one axis a side, when some side draws more than one
50
+ * (lenkbank's four-axis curve plot on a phone);
51
+ * 2. {@link LEFT_AXIS_BUDGET}, the left axis alone, when the chart has an axis on EACH
52
+ * side and one a side still leaves the plot too narrow (0.32,
53
+ * docs/text-size-harmonization.md §4, §10.10). At 150 % a 390 px phone's two bands
54
+ * are 96 px each and the plot kept about 80 px. The right-hand axis is the secondary
55
+ * one — the grid hangs its rules off the left — so it goes: its lines keep their own
56
+ * scale, and its unit moves into the tooltip, the readout and the legend
57
+ * ({@link axisUnit}, `seriesLegendEntries`), where its values stay readable.
58
+ *
59
+ * `scale` is the text size's (`useTextSize().scale`): the bands grow with the type
60
+ * (`bandWidth` is handed the scaled ones), and so does the floor (§3.2, "a chart's axis
61
+ * width and tick spacing are multiplied by `scale`") — the room a curve needs between
62
+ * labels grows with the labels.
63
+ *
64
+ * `undefined` when neither step is needed, for a single-axis chart (a facing pair's two
65
+ * charts each have one) and for a width that is not known (no layout, no
66
+ * `ResizeObserver`).
44
67
  *
45
68
  * Decided on the CHART's width and the axes as declared, never on the plot the budget
46
69
  * produced, so hiding an axis cannot widen the plot back over the line and bring it
47
70
  * back: the rule has no feedback to oscillate on.
48
71
  */
49
- declare function autoAxisBudget(axes: readonly BudgetAxis[], width: number | undefined, bandWidth: (axis: BudgetAxis) => number): SeriesChartAxisBudget | undefined;
72
+ declare function autoAxisBudget(axes: readonly BudgetAxis[], width: number | undefined, bandWidth: (axis: BudgetAxis) => number, scale?: number): SeriesChartAxisBudget | undefined;
50
73
  /**
51
74
  * The unit an axis' series say when the axis is not drawn: its `unit`, or else the
52
75
  * parenthesised tail of its title — "Load (N)" says "N", which is how the apps write
@@ -57,4 +80,4 @@ declare function axisUnit(axis: {
57
80
  title?: string;
58
81
  }): string | undefined;
59
82
 
60
- export { MIN_PLOT_WIDTH, NARROW_AXIS_BUDGET, type SeriesChartAxisBudget, autoAxisBudget, axisUnit, budgetedAxes };
83
+ export { LEFT_AXIS_BUDGET, MIN_PLOT_WIDTH, NARROW_AXIS_BUDGET, type SeriesChartAxisBudget, autoAxisBudget, axisUnit, budgetedAxes };
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  const MIN_PLOT_WIDTH = 160;
3
3
  const NARROW_AXIS_BUDGET = { left: 1, right: 1 };
4
+ const LEFT_AXIS_BUDGET = { left: 1, right: 0 };
4
5
  const BARE_SIDE_MARGIN = 10;
5
6
  const sideOf = (axis) => axis.orientation ?? "left";
6
7
  function budgetedAxes(axes, budget) {
@@ -31,15 +32,21 @@ function budgetedAxes(axes, budget) {
31
32
  }
32
33
  return out;
33
34
  }
34
- function autoAxisBudget(axes, width, bandWidth) {
35
+ function autoAxisBudget(axes, width, bandWidth, scale = 1) {
35
36
  if (width === void 0 || !(width > 0)) return void 0;
36
37
  const visible = axes.filter((axis) => !axis.hide);
37
38
  const left = visible.filter((axis) => sideOf(axis) === "left");
38
39
  const right = visible.filter((axis) => sideOf(axis) === "right");
40
+ const floor = MIN_PLOT_WIDTH * scale;
41
+ const plot = (drawn) => {
42
+ const onLeft = drawn.some((axis) => sideOf(axis) === "left");
43
+ const onRight = drawn.some((axis) => sideOf(axis) === "right");
44
+ const bands = drawn.reduce((sum, axis) => sum + bandWidth(axis), 0);
45
+ return width - bands - (onLeft ? 0 : BARE_SIDE_MARGIN) - (onRight ? 0 : BARE_SIDE_MARGIN);
46
+ };
47
+ if (left.length && right.length && plot([left[0], right[0]]) < floor) return LEFT_AXIS_BUDGET;
39
48
  if (left.length <= 1 && right.length <= 1) return void 0;
40
- const bands = visible.reduce((sum, axis) => sum + bandWidth(axis), 0);
41
- const margins = (left.length ? 0 : BARE_SIDE_MARGIN) + (right.length ? 0 : BARE_SIDE_MARGIN);
42
- return width - bands - margins < MIN_PLOT_WIDTH ? NARROW_AXIS_BUDGET : void 0;
49
+ return plot(visible) < floor ? NARROW_AXIS_BUDGET : void 0;
43
50
  }
44
51
  function axisUnit(axis) {
45
52
  if (axis.unit !== void 0) return axis.unit || void 0;
@@ -47,6 +54,7 @@ function axisUnit(axis) {
47
54
  return tail?.[1].trim() || void 0;
48
55
  }
49
56
  export {
57
+ LEFT_AXIS_BUDGET,
50
58
  MIN_PLOT_WIDTH,
51
59
  NARROW_AXIS_BUDGET,
52
60
  autoAxisBudget,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/series-chart-budget.ts"],"sourcesContent":["// The axis budget of a series chart: which of its y axes are drawn, when there are more\n// than the width can carry. Pure, and apart from the chart, so the rule is tested on\n// numbers rather than on a jsdom that has no layout to measure.\n//\n// lenkbank's curve plot declares four visible axes — velocity, position and acceleration\n// on the left, loads on the right — and every one reserves its band of ticks and title.\n// At 1280 px that is a quarter of the card; at 390 px it is all of it, and the plot the\n// axes are for is ten pixels wide. An axis over budget is drawn the way a `hide: true`\n// one always was: it still scales its lines (and the zoom still refits it), but it draws\n// no ticks, no title, and reserves no width.\n\n/**\n * How many y axes a chart draws. A number caps the TOTAL; `{ left, right }` caps each\n * side (a side left out is not capped). Axes over the cap are hidden in declaration\n * order — the first of each side is the last to go, see {@link budgetedAxes}.\n */\nexport type SeriesChartAxisBudget = number | { left?: number; right?: number };\n\n/** The part of an axis the budget reads. */\ninterface BudgetAxis {\n id: string;\n orientation?: \"left\" | \"right\";\n hide?: boolean;\n title?: string;\n width?: number;\n}\n\n/**\n * The narrowest the plot may get before the automatic budget steps in, in px.\n *\n * About a phone's plot with one axis a side (390 px viewport, card padding, two bands\n * of 48 + 16), where a curve's shape still reads. It is the point below which the chart\n * is already broken today — a plot the width of a thumb between four columns of numbers\n * — so the rule changes nothing that was working.\n */\nexport const MIN_PLOT_WIDTH = 160;\n\n/** The budget the automatic rule applies: one axis a side. */\nexport const NARROW_AXIS_BUDGET: SeriesChartAxisBudget = { left: 1, right: 1 };\n\n/** The plot's margin on a side no axis band covers — `SeriesPlot`'s own. */\nconst BARE_SIDE_MARGIN = 10;\n\nconst sideOf = (axis: BudgetAxis) => axis.orientation ?? \"left\";\n\n/**\n * The ids of the VISIBLE axes a budget hides — axes already `hide: true` are neither\n * counted nor returned. Empty for no budget.\n *\n * Kept, in order of priority: the first visible axis of each side (in declaration order,\n * so the axis a caller lists first — the one the grid hangs its rules off — always\n * stays), then the rest in declaration order, until the cap. Per side, that is simply the\n * first `left` left-hand axes and the first `right` right-hand ones.\n */\nexport function budgetedAxes(\n axes: readonly BudgetAxis[],\n budget: SeriesChartAxisBudget | undefined,\n): string[] {\n if (budget === undefined) return [];\n const visible = axes.filter((axis) => !axis.hide);\n if (typeof budget === \"number\") {\n const cap = Math.max(0, Math.floor(budget));\n const firsts = new Set<string>();\n const seen = new Set<string>();\n for (const axis of visible) {\n if (!seen.has(sideOf(axis))) firsts.add(axis.id);\n seen.add(sideOf(axis));\n }\n const priority = [\n ...visible.filter((axis) => firsts.has(axis.id)),\n ...visible.filter((axis) => !firsts.has(axis.id)),\n ];\n const kept = new Set(priority.slice(0, cap).map((axis) => axis.id));\n return visible.filter((axis) => !kept.has(axis.id)).map((axis) => axis.id);\n }\n const used = { left: 0, right: 0 };\n const out: string[] = [];\n for (const axis of visible) {\n const side = sideOf(axis);\n const cap = budget[side];\n if (cap !== undefined && used[side] >= Math.max(0, Math.floor(cap))) out.push(axis.id);\n else used[side] += 1;\n }\n return out;\n}\n\n/**\n * The automatic budget for a chart `width` px wide: {@link NARROW_AXIS_BUDGET} when the\n * visible axes' bands would leave the plot under {@link MIN_PLOT_WIDTH} AND some side\n * draws more than one axis — the only case one-a-side gives anything back. `undefined`\n * otherwise, and for a width that is not known (no layout, no `ResizeObserver`).\n *\n * Decided on the CHART's width and the axes as declared, never on the plot the budget\n * produced, so hiding an axis cannot widen the plot back over the line and bring it\n * back: the rule has no feedback to oscillate on.\n */\nexport function autoAxisBudget(\n axes: readonly BudgetAxis[],\n width: number | undefined,\n bandWidth: (axis: BudgetAxis) => number,\n): SeriesChartAxisBudget | undefined {\n if (width === undefined || !(width > 0)) return undefined;\n const visible = axes.filter((axis) => !axis.hide);\n const left = visible.filter((axis) => sideOf(axis) === \"left\");\n const right = visible.filter((axis) => sideOf(axis) === \"right\");\n if (left.length <= 1 && right.length <= 1) return undefined;\n const bands = visible.reduce((sum, axis) => sum + bandWidth(axis), 0);\n const margins = (left.length ? 0 : BARE_SIDE_MARGIN) + (right.length ? 0 : BARE_SIDE_MARGIN);\n return width - bands - margins < MIN_PLOT_WIDTH ? NARROW_AXIS_BUDGET : undefined;\n}\n\n/**\n * The unit an axis' series say when the axis is not drawn: its `unit`, or else the\n * parenthesised tail of its title — \"Load (N)\" says \"N\", which is how the apps write\n * every axis title. `undefined` when neither says one.\n */\nexport function axisUnit(axis: { unit?: string; title?: string }): string | undefined {\n if (axis.unit !== undefined) return axis.unit || undefined;\n const tail = /\\(([^()]+)\\)\\s*$/.exec(axis.title ?? \"\");\n return tail?.[1].trim() || undefined;\n}\n"],"mappings":";AAmCO,MAAM,iBAAiB;AAGvB,MAAM,qBAA4C,EAAE,MAAM,GAAG,OAAO,EAAE;AAG7E,MAAM,mBAAmB;AAEzB,MAAM,SAAS,CAAC,SAAqB,KAAK,eAAe;AAWlD,SAAS,aACd,MACA,QACU;AACV,MAAI,WAAW,OAAW,QAAO,CAAC;AAClC,QAAM,UAAU,KAAK,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI;AAChD,MAAI,OAAO,WAAW,UAAU;AAC9B,UAAM,MAAM,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,CAAC;AAC1C,UAAM,SAAS,oBAAI,IAAY;AAC/B,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,QAAQ,SAAS;AAC1B,UAAI,CAAC,KAAK,IAAI,OAAO,IAAI,CAAC,EAAG,QAAO,IAAI,KAAK,EAAE;AAC/C,WAAK,IAAI,OAAO,IAAI,CAAC;AAAA,IACvB;AACA,UAAM,WAAW;AAAA,MACf,GAAG,QAAQ,OAAO,CAAC,SAAS,OAAO,IAAI,KAAK,EAAE,CAAC;AAAA,MAC/C,GAAG,QAAQ,OAAO,CAAC,SAAS,CAAC,OAAO,IAAI,KAAK,EAAE,CAAC;AAAA,IAClD;AACA,UAAM,OAAO,IAAI,IAAI,SAAS,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AAClE,WAAO,QAAQ,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,SAAS,KAAK,EAAE;AAAA,EAC3E;AACA,QAAM,OAAO,EAAE,MAAM,GAAG,OAAO,EAAE;AACjC,QAAM,MAAgB,CAAC;AACvB,aAAW,QAAQ,SAAS;AAC1B,UAAM,OAAO,OAAO,IAAI;AACxB,UAAM,MAAM,OAAO,IAAI;AACvB,QAAI,QAAQ,UAAa,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,MAAM,GAAG,CAAC,EAAG,KAAI,KAAK,KAAK,EAAE;AAAA,QAChF,MAAK,IAAI,KAAK;AAAA,EACrB;AACA,SAAO;AACT;AAYO,SAAS,eACd,MACA,OACA,WACmC;AACnC,MAAI,UAAU,UAAa,EAAE,QAAQ,GAAI,QAAO;AAChD,QAAM,UAAU,KAAK,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI;AAChD,QAAM,OAAO,QAAQ,OAAO,CAAC,SAAS,OAAO,IAAI,MAAM,MAAM;AAC7D,QAAM,QAAQ,QAAQ,OAAO,CAAC,SAAS,OAAO,IAAI,MAAM,OAAO;AAC/D,MAAI,KAAK,UAAU,KAAK,MAAM,UAAU,EAAG,QAAO;AAClD,QAAM,QAAQ,QAAQ,OAAO,CAAC,KAAK,SAAS,MAAM,UAAU,IAAI,GAAG,CAAC;AACpE,QAAM,WAAW,KAAK,SAAS,IAAI,qBAAqB,MAAM,SAAS,IAAI;AAC3E,SAAO,QAAQ,QAAQ,UAAU,iBAAiB,qBAAqB;AACzE;AAOO,SAAS,SAAS,MAA6D;AACpF,MAAI,KAAK,SAAS,OAAW,QAAO,KAAK,QAAQ;AACjD,QAAM,OAAO,mBAAmB,KAAK,KAAK,SAAS,EAAE;AACrD,SAAO,OAAO,CAAC,EAAE,KAAK,KAAK;AAC7B;","names":[]}
1
+ {"version":3,"sources":["../../src/components/series-chart-budget.ts"],"sourcesContent":["// The axis budget of a series chart: which of its y axes are drawn, when there are more\n// than the width can carry. Pure, and apart from the chart, so the rule is tested on\n// numbers rather than on a jsdom that has no layout to measure.\n//\n// lenkbank's curve plot declares four visible axes — velocity, position and acceleration\n// on the left, loads on the right — and every one reserves its band of ticks and title.\n// At 1280 px that is a quarter of the card; at 390 px it is all of it, and the plot the\n// axes are for is ten pixels wide. An axis over budget is drawn the way a `hide: true`\n// one always was: it still scales its lines (and the zoom still refits it), but it draws\n// no ticks, no title, and reserves no width.\n\n/**\n * How many y axes a chart draws. A number caps the TOTAL; `{ left, right }` caps each\n * side (a side left out is not capped). Axes over the cap are hidden in declaration\n * order — the first of each side is the last to go, see {@link budgetedAxes}.\n */\nexport type SeriesChartAxisBudget = number | { left?: number; right?: number };\n\n/** The part of an axis the budget reads. */\ninterface BudgetAxis {\n id: string;\n orientation?: \"left\" | \"right\";\n hide?: boolean;\n title?: string;\n width?: number;\n}\n\n/**\n * The narrowest the plot may get before the automatic budget steps in, in px at Normal —\n * × the text size's scale since 0.32 (see {@link autoAxisBudget}).\n *\n * About a phone's plot with one axis a side (390 px viewport, card padding, two bands\n * of 48 + 16), where a curve's shape still reads. It is the point below which the chart\n * is already broken today — a plot the width of a thumb between four columns of numbers\n * — so the rule changes nothing that was working.\n */\nexport const MIN_PLOT_WIDTH = 160;\n\n/** The budget the automatic rule applies: one axis a side. */\nexport const NARROW_AXIS_BUDGET: SeriesChartAxisBudget = { left: 1, right: 1 };\n\n/**\n * The automatic rule's second step: the left axis alone — for a chart with an axis on\n * each side that even one a side leaves too little plot (0.32, see {@link autoAxisBudget}).\n */\nexport const LEFT_AXIS_BUDGET: SeriesChartAxisBudget = { left: 1, right: 0 };\n\n/** The plot's margin on a side no axis band covers — `SeriesPlot`'s own. */\nconst BARE_SIDE_MARGIN = 10;\n\nconst sideOf = (axis: BudgetAxis) => axis.orientation ?? \"left\";\n\n/**\n * The ids of the VISIBLE axes a budget hides — axes already `hide: true` are neither\n * counted nor returned. Empty for no budget.\n *\n * Kept, in order of priority: the first visible axis of each side (in declaration order,\n * so the axis a caller lists first — the one the grid hangs its rules off — always\n * stays), then the rest in declaration order, until the cap. Per side, that is simply the\n * first `left` left-hand axes and the first `right` right-hand ones.\n */\nexport function budgetedAxes(\n axes: readonly BudgetAxis[],\n budget: SeriesChartAxisBudget | undefined,\n): string[] {\n if (budget === undefined) return [];\n const visible = axes.filter((axis) => !axis.hide);\n if (typeof budget === \"number\") {\n const cap = Math.max(0, Math.floor(budget));\n const firsts = new Set<string>();\n const seen = new Set<string>();\n for (const axis of visible) {\n if (!seen.has(sideOf(axis))) firsts.add(axis.id);\n seen.add(sideOf(axis));\n }\n const priority = [\n ...visible.filter((axis) => firsts.has(axis.id)),\n ...visible.filter((axis) => !firsts.has(axis.id)),\n ];\n const kept = new Set(priority.slice(0, cap).map((axis) => axis.id));\n return visible.filter((axis) => !kept.has(axis.id)).map((axis) => axis.id);\n }\n const used = { left: 0, right: 0 };\n const out: string[] = [];\n for (const axis of visible) {\n const side = sideOf(axis);\n const cap = budget[side];\n if (cap !== undefined && used[side] >= Math.max(0, Math.floor(cap))) out.push(axis.id);\n else used[side] += 1;\n }\n return out;\n}\n\n/**\n * The automatic budget for a chart `width` px wide, in two steps — each taken only when\n * the plot would otherwise be narrower than {@link MIN_PLOT_WIDTH} × `scale`:\n *\n * 1. {@link NARROW_AXIS_BUDGET}, one axis a side, when some side draws more than one\n * (lenkbank's four-axis curve plot on a phone);\n * 2. {@link LEFT_AXIS_BUDGET}, the left axis alone, when the chart has an axis on EACH\n * side and one a side still leaves the plot too narrow (0.32,\n * docs/text-size-harmonization.md §4, §10.10). At 150 % a 390 px phone's two bands\n * are 96 px each and the plot kept about 80 px. The right-hand axis is the secondary\n * one — the grid hangs its rules off the left — so it goes: its lines keep their own\n * scale, and its unit moves into the tooltip, the readout and the legend\n * ({@link axisUnit}, `seriesLegendEntries`), where its values stay readable.\n *\n * `scale` is the text size's (`useTextSize().scale`): the bands grow with the type\n * (`bandWidth` is handed the scaled ones), and so does the floor (§3.2, \"a chart's axis\n * width and tick spacing are multiplied by `scale`\") — the room a curve needs between\n * labels grows with the labels.\n *\n * `undefined` when neither step is needed, for a single-axis chart (a facing pair's two\n * charts each have one) and for a width that is not known (no layout, no\n * `ResizeObserver`).\n *\n * Decided on the CHART's width and the axes as declared, never on the plot the budget\n * produced, so hiding an axis cannot widen the plot back over the line and bring it\n * back: the rule has no feedback to oscillate on.\n */\nexport function autoAxisBudget(\n axes: readonly BudgetAxis[],\n width: number | undefined,\n bandWidth: (axis: BudgetAxis) => number,\n scale = 1,\n): SeriesChartAxisBudget | undefined {\n if (width === undefined || !(width > 0)) return undefined;\n const visible = axes.filter((axis) => !axis.hide);\n const left = visible.filter((axis) => sideOf(axis) === \"left\");\n const right = visible.filter((axis) => sideOf(axis) === \"right\");\n const floor = MIN_PLOT_WIDTH * scale;\n /** The plot's width with `drawn` axes drawn: what their bands and the bare sides leave. */\n const plot = (drawn: readonly BudgetAxis[]) => {\n const onLeft = drawn.some((axis) => sideOf(axis) === \"left\");\n const onRight = drawn.some((axis) => sideOf(axis) === \"right\");\n const bands = drawn.reduce((sum, axis) => sum + bandWidth(axis), 0);\n return width - bands - (onLeft ? 0 : BARE_SIDE_MARGIN) - (onRight ? 0 : BARE_SIDE_MARGIN);\n };\n if (left.length && right.length && plot([left[0], right[0]]) < floor) return LEFT_AXIS_BUDGET;\n if (left.length <= 1 && right.length <= 1) return undefined;\n return plot(visible) < floor ? NARROW_AXIS_BUDGET : undefined;\n}\n\n/**\n * The unit an axis' series say when the axis is not drawn: its `unit`, or else the\n * parenthesised tail of its title — \"Load (N)\" says \"N\", which is how the apps write\n * every axis title. `undefined` when neither says one.\n */\nexport function axisUnit(axis: { unit?: string; title?: string }): string | undefined {\n if (axis.unit !== undefined) return axis.unit || undefined;\n const tail = /\\(([^()]+)\\)\\s*$/.exec(axis.title ?? \"\");\n return tail?.[1].trim() || undefined;\n}\n"],"mappings":"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","names":[]}
@@ -1,3 +1,9 @@
1
+ /** The aim at Large and Extra large (docs/text-size-harmonization.md §4): the labels are
2
+ * 25–50 % wider and taller, so five of them crowd an axis that holds them at Normal. */
3
+ declare const LARGE_TEXT_TARGET_TICKS = 3;
4
+ /** How many ticks to aim for at a text `scale` (`useTextSize().scale`): five at Normal,
5
+ * three from Large up. */
6
+ declare function tickTarget(scale?: number): number;
1
7
  /** The round step closest to `span / target`: 1, 2 or 5 times a power of ten. */
2
8
  declare function niceStep(span: number, target?: number): number;
3
9
  /**
@@ -43,4 +49,4 @@ declare function timeTicksWithUnit(domain: readonly [number, number] | undefined
43
49
  /** {@link timeTicksWithUnit}, ticks only. */
44
50
  declare function timeTicks(domain: readonly [number, number] | undefined, target?: number): number[] | undefined;
45
51
 
46
- export { type TimeTickUnit, categoryTicks, integerTicks, niceStep, niceTicks, timeTicks, timeTicksWithUnit };
52
+ export { LARGE_TEXT_TARGET_TICKS, type TimeTickUnit, categoryTicks, integerTicks, niceStep, niceTicks, tickTarget, timeTicks, timeTicksWithUnit };
@@ -1,5 +1,9 @@
1
1
  "use client";
2
2
  const TARGET_TICKS = 5;
3
+ const LARGE_TEXT_TARGET_TICKS = 3;
4
+ function tickTarget(scale = 1) {
5
+ return scale > 1 ? LARGE_TEXT_TARGET_TICKS : TARGET_TICKS;
6
+ }
3
7
  function niceStep(span, target = TARGET_TICKS) {
4
8
  const rough = span / Math.max(1, target);
5
9
  const magnitude = 10 ** Math.floor(Math.log10(rough));
@@ -101,10 +105,12 @@ function timeTicks(domain, target = TARGET_TICKS) {
101
105
  return timeTicksWithUnit(domain, target)?.ticks;
102
106
  }
103
107
  export {
108
+ LARGE_TEXT_TARGET_TICKS,
104
109
  categoryTicks,
105
110
  integerTicks,
106
111
  niceStep,
107
112
  niceTicks,
113
+ tickTarget,
108
114
  timeTicks,
109
115
  timeTicksWithUnit
110
116
  };