@eifi1/ui-kit 0.31.1 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (565) hide show
  1. package/README.md +51 -28
  2. package/dist/account/delete-account-setting.d.ts +8 -3
  3. package/dist/admin/admin-action-confirm.d.ts +8 -3
  4. package/dist/admin/admin-action-log.d.ts +1 -1
  5. package/dist/admin/admin-action-log.js +4 -2
  6. package/dist/admin/admin-action-log.js.map +1 -1
  7. package/dist/admin/invitations-panel.d.ts +8 -3
  8. package/dist/admin/invitations-panel.js +29 -26
  9. package/dist/admin/invitations-panel.js.map +1 -1
  10. package/dist/admin/reviewer-scope-editor.d.ts +8 -3
  11. package/dist/admin/role-select.d.ts +10 -5
  12. package/dist/admin/transfer-ownership-dialog.d.ts +8 -3
  13. package/dist/admin/user-roster.d.ts +10 -5
  14. package/dist/admin/user-roster.js +8 -47
  15. package/dist/admin/user-roster.js.map +1 -1
  16. package/dist/auth/auth-errors.d.ts +43 -8
  17. package/dist/auth/auth-errors.js +24 -1
  18. package/dist/auth/auth-errors.js.map +1 -1
  19. package/dist/auth/register-form.d.ts +8 -3
  20. package/dist/billing/billing-banner.d.ts +184 -0
  21. package/dist/billing/billing-banner.js +98 -0
  22. package/dist/billing/billing-banner.js.map +1 -0
  23. package/dist/billing/billing-labels.d.ts +100 -0
  24. package/dist/billing/billing-labels.js +76 -0
  25. package/dist/billing/billing-labels.js.map +1 -0
  26. package/dist/billing/billing-lock.d.ts +158 -0
  27. package/dist/billing/billing-lock.js +17 -0
  28. package/dist/billing/billing-lock.js.map +1 -0
  29. package/dist/billing/billing-standing.d.ts +149 -0
  30. package/dist/billing/billing-standing.js +28 -0
  31. package/dist/billing/billing-standing.js.map +1 -0
  32. package/dist/billing/plan-card.d.ts +248 -0
  33. package/dist/billing/plan-card.js +248 -0
  34. package/dist/billing/plan-card.js.map +1 -0
  35. package/dist/billing/plan-limit-notice.d.ts +156 -0
  36. package/dist/billing/plan-limit-notice.js +76 -0
  37. package/dist/billing/plan-limit-notice.js.map +1 -0
  38. package/dist/billing/plan-limit.d.ts +34 -0
  39. package/dist/billing/plan-limit.js +20 -0
  40. package/dist/billing/plan-limit.js.map +1 -0
  41. package/dist/billing/plan-price.d.ts +61 -0
  42. package/dist/billing/plan-price.js +45 -0
  43. package/dist/billing/plan-price.js.map +1 -0
  44. package/dist/billing/subscription-actions.d.ts +133 -0
  45. package/dist/billing/subscription-actions.js +28 -0
  46. package/dist/billing/subscription-actions.js.map +1 -0
  47. package/dist/billing/subscription-status-chip.d.ts +149 -0
  48. package/dist/billing/subscription-status-chip.js +48 -0
  49. package/dist/billing/subscription-status-chip.js.map +1 -0
  50. package/dist/chart.d.ts +9 -3
  51. package/dist/chart.js.map +1 -1
  52. package/dist/components/account-chips.d.ts +16 -4
  53. package/dist/components/account-chips.js +21 -3
  54. package/dist/components/account-chips.js.map +1 -1
  55. package/dist/components/alert-banner.d.ts +6 -1
  56. package/dist/components/alert-banner.js +5 -4
  57. package/dist/components/alert-banner.js.map +1 -1
  58. package/dist/components/amount-input.d.ts +8 -3
  59. package/dist/components/amount-input.js +5 -5
  60. package/dist/components/amount-input.js.map +1 -1
  61. package/dist/components/appearance-labels.d.ts +38 -0
  62. package/dist/components/appearance-labels.js +19 -0
  63. package/dist/components/appearance-labels.js.map +1 -0
  64. package/dist/components/autocomplete.js +2 -2
  65. package/dist/components/autocomplete.js.map +1 -1
  66. package/dist/components/breadcrumbs.js +3 -2
  67. package/dist/components/breadcrumbs.js.map +1 -1
  68. package/dist/components/bulk-action-bar.d.ts +2 -2
  69. package/dist/components/bulk-action-bar.js +7 -12
  70. package/dist/components/bulk-action-bar.js.map +1 -1
  71. package/dist/components/button-group.d.ts +8 -3
  72. package/dist/components/button-group.js +7 -1
  73. package/dist/components/button-group.js.map +1 -1
  74. package/dist/components/calculator.d.ts +8 -3
  75. package/dist/components/calculator.js +4 -3
  76. package/dist/components/calculator.js.map +1 -1
  77. package/dist/components/calendar-heatmap.js +5 -5
  78. package/dist/components/calendar-heatmap.js.map +1 -1
  79. package/dist/components/chart-height.d.ts +23 -0
  80. package/dist/components/chart-height.js +15 -0
  81. package/dist/components/chart-height.js.map +1 -0
  82. package/dist/components/chart-zoom.js +6 -1
  83. package/dist/components/chart-zoom.js.map +1 -1
  84. package/dist/components/checkbox-group.js +2 -2
  85. package/dist/components/checkbox-group.js.map +1 -1
  86. package/dist/components/checkbox.d.ts +10 -0
  87. package/dist/components/checkbox.js +25 -10
  88. package/dist/components/checkbox.js.map +1 -1
  89. package/dist/components/chip.d.ts +19 -3
  90. package/dist/components/chip.js +54 -23
  91. package/dist/components/chip.js.map +1 -1
  92. package/dist/components/choice-card.d.ts +1 -1
  93. package/dist/components/choice-card.js +9 -8
  94. package/dist/components/choice-card.js.map +1 -1
  95. package/dist/components/column-mapper.js +1 -1
  96. package/dist/components/column-mapper.js.map +1 -1
  97. package/dist/components/combobox-core.js +8 -7
  98. package/dist/components/combobox-core.js.map +1 -1
  99. package/dist/components/combobox.js +8 -8
  100. package/dist/components/combobox.js.map +1 -1
  101. package/dist/components/confirm-dialog.d.ts +8 -3
  102. package/dist/components/copy-button.d.ts +10 -5
  103. package/dist/components/country-select.js +1 -1
  104. package/dist/components/country-select.js.map +1 -1
  105. package/dist/components/currency-select.js +3 -3
  106. package/dist/components/currency-select.js.map +1 -1
  107. package/dist/components/danger-confirm.d.ts +8 -3
  108. package/dist/components/data-table-cells.d.ts +1 -1
  109. package/dist/components/data-table-filter-popover.d.ts +1 -1
  110. package/dist/components/data-table-filter-popover.js +4 -1
  111. package/dist/components/data-table-filter-popover.js.map +1 -1
  112. package/dist/components/data-table-filters.d.ts +1 -1
  113. package/dist/components/data-table-labels.d.ts +1 -1
  114. package/dist/components/data-table-pagination.d.ts +3 -2
  115. package/dist/components/data-table-pagination.js +15 -6
  116. package/dist/components/data-table-pagination.js.map +1 -1
  117. package/dist/components/data-table.d.ts +1 -1
  118. package/dist/components/data-table.js +123 -52
  119. package/dist/components/data-table.js.map +1 -1
  120. package/dist/components/date-picker.d.ts +1 -1
  121. package/dist/components/date-picker.js +18 -6
  122. package/dist/components/date-picker.js.map +1 -1
  123. package/dist/components/description-list.js +2 -2
  124. package/dist/components/description-list.js.map +1 -1
  125. package/dist/components/dialog-frame.js +2 -1
  126. package/dist/components/dialog-frame.js.map +1 -1
  127. package/dist/components/disclosure.js +3 -2
  128. package/dist/components/disclosure.js.map +1 -1
  129. package/dist/components/dropdown.d.ts +1 -1
  130. package/dist/components/dropdown.js +7 -1
  131. package/dist/components/dropdown.js.map +1 -1
  132. package/dist/components/facing-pair.d.ts +19 -6
  133. package/dist/components/facing-pair.js +3 -2
  134. package/dist/components/facing-pair.js.map +1 -1
  135. package/dist/components/field-parts.d.ts +102 -6
  136. package/dist/components/field-parts.js +81 -6
  137. package/dist/components/field-parts.js.map +1 -1
  138. package/dist/components/field-sync.d.ts +4 -1
  139. package/dist/components/field-sync.js +12 -2
  140. package/dist/components/field-sync.js.map +1 -1
  141. package/dist/components/field.js +2 -2
  142. package/dist/components/field.js.map +1 -1
  143. package/dist/components/file-button.d.ts +8 -3
  144. package/dist/components/file-dropzone.d.ts +8 -3
  145. package/dist/components/floating-panel.js +7 -4
  146. package/dist/components/floating-panel.js.map +1 -1
  147. package/dist/components/focus-ring.d.ts +12 -0
  148. package/dist/components/focus-ring.js +8 -0
  149. package/dist/components/focus-ring.js.map +1 -0
  150. package/dist/components/form-actions.d.ts +8 -3
  151. package/dist/components/form-actions.js +33 -19
  152. package/dist/components/form-actions.js.map +1 -1
  153. package/dist/components/full-bleed-dialog.d.ts +11 -1
  154. package/dist/components/full-bleed-dialog.js +3 -1
  155. package/dist/components/full-bleed-dialog.js.map +1 -1
  156. package/dist/components/iban-input.d.ts +8 -3
  157. package/dist/components/image-grid.js +1 -1
  158. package/dist/components/image-grid.js.map +1 -1
  159. package/dist/components/inline-edit-field.js +10 -4
  160. package/dist/components/inline-edit-field.js.map +1 -1
  161. package/dist/components/language-select.d.ts +8 -3
  162. package/dist/components/legal-page.d.ts +26 -5
  163. package/dist/components/legal-page.js +3 -1
  164. package/dist/components/legal-page.js.map +1 -1
  165. package/dist/components/legal.d.ts +8 -3
  166. package/dist/components/legal.js +2 -1
  167. package/dist/components/legal.js.map +1 -1
  168. package/dist/components/lightbox.js +24 -2
  169. package/dist/components/lightbox.js.map +1 -1
  170. package/dist/components/line-items.js +2 -1
  171. package/dist/components/line-items.js.map +1 -1
  172. package/dist/components/list.d.ts +5 -0
  173. package/dist/components/list.js +13 -9
  174. package/dist/components/list.js.map +1 -1
  175. package/dist/components/measured-grid.d.ts +3 -1
  176. package/dist/components/measured-grid.js +38 -20
  177. package/dist/components/measured-grid.js.map +1 -1
  178. package/dist/components/menu-item.js +2 -1
  179. package/dist/components/menu-item.js.map +1 -1
  180. package/dist/components/mini-calendar.js +8 -4
  181. package/dist/components/mini-calendar.js.map +1 -1
  182. package/dist/components/money-field.d.ts +8 -3
  183. package/dist/components/month-picker.js +50 -39
  184. package/dist/components/month-picker.js.map +1 -1
  185. package/dist/components/nav-pills.js +1 -1
  186. package/dist/components/nav-pills.js.map +1 -1
  187. package/dist/components/number-field.d.ts +8 -3
  188. package/dist/components/number-input.d.ts +11 -4
  189. package/dist/components/number-input.js +8 -6
  190. package/dist/components/number-input.js.map +1 -1
  191. package/dist/components/numpad-sheet.d.ts +8 -3
  192. package/dist/components/numpad-sheet.js +2 -1
  193. package/dist/components/numpad-sheet.js.map +1 -1
  194. package/dist/components/one-time-code-input.js +2 -2
  195. package/dist/components/one-time-code-input.js.map +1 -1
  196. package/dist/components/page-contents.js +2 -2
  197. package/dist/components/page-contents.js.map +1 -1
  198. package/dist/components/phone-input.d.ts +8 -3
  199. package/dist/components/phone-input.js +4 -4
  200. package/dist/components/phone-input.js.map +1 -1
  201. package/dist/components/pie-chart.d.ts +8 -2
  202. package/dist/components/pie-chart.js +11 -7
  203. package/dist/components/pie-chart.js.map +1 -1
  204. package/dist/components/popover.d.ts +9 -1
  205. package/dist/components/popover.js +8 -4
  206. package/dist/components/popover.js.map +1 -1
  207. package/dist/components/row-actions.d.ts +100 -0
  208. package/dist/components/row-actions.js +231 -0
  209. package/dist/components/row-actions.js.map +1 -0
  210. package/dist/components/scroll-area.js +1 -1
  211. package/dist/components/scroll-area.js.map +1 -1
  212. package/dist/components/search-field.js +3 -2
  213. package/dist/components/search-field.js.map +1 -1
  214. package/dist/components/series-chart-budget.d.ts +30 -7
  215. package/dist/components/series-chart-budget.js +12 -4
  216. package/dist/components/series-chart-budget.js.map +1 -1
  217. package/dist/components/series-chart-ticks.d.ts +7 -1
  218. package/dist/components/series-chart-ticks.js +6 -0
  219. package/dist/components/series-chart-ticks.js.map +1 -1
  220. package/dist/components/series-chart.d.ts +58 -28
  221. package/dist/components/series-chart.js +42 -32
  222. package/dist/components/series-chart.js.map +1 -1
  223. package/dist/components/settings-fields.d.ts +57 -8
  224. package/dist/components/settings-fields.js +39 -1
  225. package/dist/components/settings-fields.js.map +1 -1
  226. package/dist/components/share-card.d.ts +11 -6
  227. package/dist/components/share-card.js +8 -2
  228. package/dist/components/share-card.js.map +1 -1
  229. package/dist/components/sign-chip.d.ts +4 -0
  230. package/dist/components/sign-chip.js +17 -4
  231. package/dist/components/sign-chip.js.map +1 -1
  232. package/dist/components/signature-pad.d.ts +3 -1
  233. package/dist/components/signature-pad.js +8 -5
  234. package/dist/components/signature-pad.js.map +1 -1
  235. package/dist/components/slider.js +7 -6
  236. package/dist/components/slider.js.map +1 -1
  237. package/dist/components/sparkline.d.ts +5 -1
  238. package/dist/components/sparkline.js +4 -2
  239. package/dist/components/sparkline.js.map +1 -1
  240. package/dist/components/stat-tile.d.ts +17 -1
  241. package/dist/components/stat-tile.js +48 -20
  242. package/dist/components/stat-tile.js.map +1 -1
  243. package/dist/components/status-dot.d.ts +4 -0
  244. package/dist/components/status-dot.js +9 -2
  245. package/dist/components/status-dot.js.map +1 -1
  246. package/dist/components/swipeable-row.js +10 -2
  247. package/dist/components/swipeable-row.js.map +1 -1
  248. package/dist/components/switch.d.ts +21 -7
  249. package/dist/components/switch.js +24 -11
  250. package/dist/components/switch.js.map +1 -1
  251. package/dist/components/table.js +2 -2
  252. package/dist/components/table.js.map +1 -1
  253. package/dist/components/text-link.d.ts +8 -3
  254. package/dist/components/text-link.js +1 -1
  255. package/dist/components/text-link.js.map +1 -1
  256. package/dist/components/text.d.ts +7 -2
  257. package/dist/components/text.js +5 -5
  258. package/dist/components/text.js.map +1 -1
  259. package/dist/components/tile-radio.js +2 -1
  260. package/dist/components/tile-radio.js.map +1 -1
  261. package/dist/components/time-input.d.ts +8 -3
  262. package/dist/components/toast.d.ts +18 -2
  263. package/dist/components/toast.js +8 -5
  264. package/dist/components/toast.js.map +1 -1
  265. package/dist/components/toggle-group.d.ts +25 -3
  266. package/dist/components/toggle-group.js +40 -11
  267. package/dist/components/toggle-group.js.map +1 -1
  268. package/dist/components/toggle-legend.d.ts +10 -1
  269. package/dist/components/toggle-legend.js +21 -7
  270. package/dist/components/toggle-legend.js.map +1 -1
  271. package/dist/components/translation-review-editor.js +1 -1
  272. package/dist/components/translation-review-editor.js.map +1 -1
  273. package/dist/components/translation-review-parts.d.ts +4 -0
  274. package/dist/components/translation-review.d.ts +4 -0
  275. package/dist/components/translation-review.js +3 -3
  276. package/dist/components/translation-review.js.map +1 -1
  277. package/dist/components/tree-view.js +3 -2
  278. package/dist/components/tree-view.js.map +1 -1
  279. package/dist/components/treemap.d.ts +5 -2
  280. package/dist/components/treemap.js +33 -16
  281. package/dist/components/treemap.js.map +1 -1
  282. package/dist/components/ui.d.ts +8 -3
  283. package/dist/components/ui.js +207 -119
  284. package/dist/components/ui.js.map +1 -1
  285. package/dist/components/use-table-state.d.ts +1 -1
  286. package/dist/components/user-avatar.d.ts +5 -1
  287. package/dist/components/user-avatar.js +1 -1
  288. package/dist/components/user-avatar.js.map +1 -1
  289. package/dist/components/wizard-stepper.js +20 -14
  290. package/dist/components/wizard-stepper.js.map +1 -1
  291. package/dist/{data-table-labels-DUELeXpa.d.ts → data-table-labels-kpAAVi6g.d.ts} +26 -2
  292. package/dist/data-table.d.ts +1 -1
  293. package/dist/{delete-account-setting-CpIhzxxZ.d.ts → delete-account-setting-FLyJf-kZ.d.ts} +642 -60
  294. package/dist/demo/demo-banner.js +1 -0
  295. package/dist/demo/demo-banner.js.map +1 -1
  296. package/dist/feedback/feedback-attachment.d.ts +8 -3
  297. package/dist/feedback/feedback-context.d.ts +8 -3
  298. package/dist/feedback/feedback-crash.d.ts +8 -3
  299. package/dist/feedback/feedback-dialog.d.ts +8 -3
  300. package/dist/feedback/feedback-dialog.js +3 -3
  301. package/dist/feedback/feedback-dialog.js.map +1 -1
  302. package/dist/feedback/feedback-inbox.d.ts +8 -3
  303. package/dist/feedback/feedback-inbox.js +2 -2
  304. package/dist/feedback/feedback-inbox.js.map +1 -1
  305. package/dist/feedback/feedback-labels.d.ts +8 -3
  306. package/dist/feedback/feedback-menu.d.ts +9 -4
  307. package/dist/feedback/feedback-record.d.ts +8 -3
  308. package/dist/feedback/feedback-row-detail.d.ts +8 -3
  309. package/dist/feedback/feedback-status-undo.d.ts +8 -3
  310. package/dist/feedback/feedback-submit.d.ts +8 -3
  311. package/dist/feedback/feedback-swipe.d.ts +8 -3
  312. package/dist/feedback/feedback-table.d.ts +8 -3
  313. package/dist/feedback/feedback-thread.d.ts +8 -3
  314. package/dist/feedback/feedback-thread.js +3 -3
  315. package/dist/feedback/feedback-thread.js.map +1 -1
  316. package/dist/feedback.d.ts +8 -3
  317. package/dist/hooks/use-anchored-panel.d.ts +32 -3
  318. package/dist/hooks/use-anchored-panel.js +21 -3
  319. package/dist/hooks/use-anchored-panel.js.map +1 -1
  320. package/dist/hooks/use-breakpoint.d.ts +88 -0
  321. package/dist/hooks/use-breakpoint.js +50 -0
  322. package/dist/hooks/use-breakpoint.js.map +1 -0
  323. package/dist/hooks/use-file-drop.d.ts +8 -3
  324. package/dist/hooks/use-large-text.d.ts +29 -0
  325. package/dist/hooks/use-large-text.js +20 -0
  326. package/dist/hooks/use-large-text.js.map +1 -0
  327. package/dist/hooks/use-windowed-rows.d.ts +52 -8
  328. package/dist/hooks/use-windowed-rows.js +94 -5
  329. package/dist/hooks/use-windowed-rows.js.map +1 -1
  330. package/dist/i18n/defaults.d.ts +8 -3
  331. package/dist/i18n/defaults.js +8 -0
  332. package/dist/i18n/defaults.js.map +1 -1
  333. package/dist/i18n/german.d.ts +8 -3
  334. package/dist/i18n/german.js +94 -3
  335. package/dist/i18n/german.js.map +1 -1
  336. package/dist/i18n/kit-labels.d.ts +8 -3
  337. package/dist/i18n/kit-labels.js.map +1 -1
  338. package/dist/i18n/languages.d.ts +10 -5
  339. package/dist/i18n/locales/de-CH.d.ts +8 -3
  340. package/dist/i18n/locales/en.d.ts +8 -3
  341. package/dist/i18n/locales/es.d.ts +8 -3
  342. package/dist/i18n/locales/es.js +89 -3
  343. package/dist/i18n/locales/es.js.map +1 -1
  344. package/dist/i18n/locales/fr.d.ts +8 -3
  345. package/dist/i18n/locales/fr.js +91 -3
  346. package/dist/i18n/locales/fr.js.map +1 -1
  347. package/dist/i18n/locales/hu.d.ts +8 -3
  348. package/dist/i18n/locales/hu.js +91 -3
  349. package/dist/i18n/locales/hu.js.map +1 -1
  350. package/dist/i18n/locales/it.d.ts +8 -3
  351. package/dist/i18n/locales/it.js +92 -3
  352. package/dist/i18n/locales/it.js.map +1 -1
  353. package/dist/i18n/locales/zh.d.ts +8 -3
  354. package/dist/i18n/locales/zh.js +89 -3
  355. package/dist/i18n/locales/zh.js.map +1 -1
  356. package/dist/i18n/review.d.ts +8 -3
  357. package/dist/i18n/review.js +15 -1
  358. package/dist/i18n/review.js.map +1 -1
  359. package/dist/index.d.ts +27 -11
  360. package/dist/index.js +77 -4
  361. package/dist/index.js.map +1 -1
  362. package/dist/landing/landing-actions.d.ts +8 -3
  363. package/dist/landing/landing-sections.d.ts +8 -3
  364. package/dist/landing/landing-sections.js +7 -3
  365. package/dist/landing/landing-sections.js.map +1 -1
  366. package/dist/landing/public-header.d.ts +9 -4
  367. package/dist/landing/public-header.js +3 -3
  368. package/dist/landing/public-header.js.map +1 -1
  369. package/dist/landing/routing.d.ts +8 -3
  370. package/dist/lib/cn.js +3 -2
  371. package/dist/lib/cn.js.map +1 -1
  372. package/dist/rhf/fields.d.ts +8 -3
  373. package/dist/rhf/fields.js +1 -1
  374. package/dist/rhf/fields.js.map +1 -1
  375. package/dist/rhf/form.d.ts +8 -3
  376. package/dist/rhf/form.js +2 -2
  377. package/dist/rhf/form.js.map +1 -1
  378. package/dist/rhf.d.ts +8 -3
  379. package/dist/search/command-palette.d.ts +1 -1
  380. package/dist/search/command-palette.js +9 -8
  381. package/dist/search/command-palette.js.map +1 -1
  382. package/dist/settings/settings-layout.js +2 -1
  383. package/dist/settings/settings-layout.js.map +1 -1
  384. package/dist/settings/settings-section.d.ts +8 -3
  385. package/dist/settings/use-account-appearance.d.ts +92 -0
  386. package/dist/settings/use-account-appearance.js +63 -0
  387. package/dist/settings/use-account-appearance.js.map +1 -0
  388. package/dist/settings/use-settings-route.d.ts +1 -1
  389. package/dist/settings/use-settings-route.js +2 -3
  390. package/dist/settings/use-settings-route.js.map +1 -1
  391. package/dist/shell/app-shell.d.ts +12 -171
  392. package/dist/shell/app-shell.js +144 -28
  393. package/dist/shell/app-shell.js.map +1 -1
  394. package/dist/shell/auth-layout.js +1 -1
  395. package/dist/shell/auth-layout.js.map +1 -1
  396. package/dist/shell/option-switcher-menu.js +1 -1
  397. package/dist/shell/option-switcher-menu.js.map +1 -1
  398. package/dist/shell/top-bar-brand.d.ts +8 -3
  399. package/dist/shell/top-bar-brand.js +3 -1
  400. package/dist/shell/top-bar-brand.js.map +1 -1
  401. package/dist/shell/topbar-action-menu.d.ts +4 -0
  402. package/dist/shell/topbar-action-menu.js +3 -3
  403. package/dist/shell/topbar-action-menu.js.map +1 -1
  404. package/dist/shell/topbar-controls.js +4 -4
  405. package/dist/shell/topbar-controls.js.map +1 -1
  406. package/dist/shell.d.ts +9 -5
  407. package/dist/theme/contrast-tokens.d.ts +41 -0
  408. package/dist/theme/contrast-tokens.js +59 -0
  409. package/dist/theme/contrast-tokens.js.map +1 -0
  410. package/dist/theme/contrast.d.ts +110 -0
  411. package/dist/theme/contrast.js +128 -0
  412. package/dist/theme/contrast.js.map +1 -0
  413. package/dist/theme/palette-presets.d.ts +12 -1
  414. package/dist/theme/palette-presets.js +9 -4
  415. package/dist/theme/palette-presets.js.map +1 -1
  416. package/dist/theme/palette-store.js.map +1 -1
  417. package/dist/theme/text-size.d.ts +173 -0
  418. package/dist/theme/text-size.js +121 -0
  419. package/dist/theme/text-size.js.map +1 -0
  420. package/dist/wizard/stepper-nav.d.ts +8 -3
  421. package/dist/wizard/wizard-summary.js +1 -1
  422. package/dist/wizard/wizard-summary.js.map +1 -1
  423. package/dist/wizard.d.ts +8 -3
  424. package/package.json +1 -1
  425. package/src/admin/admin-action-log.tsx +6 -1
  426. package/src/admin/invitations-panel.tsx +36 -29
  427. package/src/admin/user-roster.tsx +17 -72
  428. package/src/auth/auth-errors.ts +68 -7
  429. package/src/billing/billing-banner.tsx +205 -0
  430. package/src/billing/billing-labels.ts +240 -0
  431. package/src/billing/billing-lock.ts +75 -0
  432. package/src/billing/billing-standing.ts +72 -0
  433. package/src/billing/plan-card.tsx +458 -0
  434. package/src/billing/plan-limit-notice.tsx +145 -0
  435. package/src/billing/plan-limit.ts +53 -0
  436. package/src/billing/plan-price.ts +101 -0
  437. package/src/billing/subscription-actions.tsx +76 -0
  438. package/src/billing/subscription-status-chip.tsx +95 -0
  439. package/src/chart.ts +2 -0
  440. package/src/components/account-chips.tsx +36 -10
  441. package/src/components/alert-banner.tsx +16 -7
  442. package/src/components/amount-input.tsx +5 -5
  443. package/src/components/appearance-labels.ts +47 -0
  444. package/src/components/autocomplete.tsx +2 -2
  445. package/src/components/breadcrumbs.tsx +3 -3
  446. package/src/components/bulk-action-bar.tsx +11 -14
  447. package/src/components/button-group.tsx +21 -0
  448. package/src/components/calculator.tsx +4 -3
  449. package/src/components/calendar-heatmap.tsx +5 -5
  450. package/src/components/chart-height.ts +30 -0
  451. package/src/components/chart-zoom.tsx +6 -1
  452. package/src/components/checkbox-group.tsx +2 -2
  453. package/src/components/checkbox.tsx +62 -20
  454. package/src/components/chip.tsx +139 -30
  455. package/src/components/choice-card.tsx +16 -11
  456. package/src/components/column-mapper.tsx +1 -1
  457. package/src/components/combobox-core.tsx +8 -7
  458. package/src/components/combobox.tsx +8 -8
  459. package/src/components/country-select.tsx +1 -1
  460. package/src/components/currency-select.tsx +3 -3
  461. package/src/components/data-table-filter-popover.tsx +7 -1
  462. package/src/components/data-table-pagination.tsx +60 -31
  463. package/src/components/data-table.tsx +164 -43
  464. package/src/components/date-picker.tsx +44 -10
  465. package/src/components/description-list.tsx +2 -2
  466. package/src/components/dialog-frame.tsx +2 -1
  467. package/src/components/disclosure.tsx +3 -2
  468. package/src/components/dropdown.tsx +10 -2
  469. package/src/components/facing-pair.ts +12 -5
  470. package/src/components/field-parts.tsx +190 -7
  471. package/src/components/field-sync.tsx +17 -3
  472. package/src/components/field.tsx +2 -2
  473. package/src/components/floating-panel.tsx +7 -4
  474. package/src/components/focus-ring.ts +12 -0
  475. package/src/components/form-actions.tsx +55 -18
  476. package/src/components/full-bleed-dialog.tsx +18 -4
  477. package/src/components/image-grid.tsx +1 -1
  478. package/src/components/inline-edit-field.tsx +26 -4
  479. package/src/components/legal-page.tsx +28 -2
  480. package/src/components/legal.tsx +19 -1
  481. package/src/components/lightbox.tsx +24 -2
  482. package/src/components/line-items.tsx +7 -1
  483. package/src/components/list.tsx +36 -10
  484. package/src/components/measured-grid.tsx +49 -24
  485. package/src/components/menu-item.tsx +2 -2
  486. package/src/components/mini-calendar.tsx +8 -4
  487. package/src/components/month-picker.tsx +15 -6
  488. package/src/components/nav-pills.tsx +1 -1
  489. package/src/components/number-input.tsx +14 -7
  490. package/src/components/numpad-sheet.tsx +2 -2
  491. package/src/components/one-time-code-input.tsx +2 -2
  492. package/src/components/page-contents.tsx +2 -2
  493. package/src/components/phone-input.tsx +9 -4
  494. package/src/components/pie-chart.tsx +19 -9
  495. package/src/components/popover.tsx +22 -5
  496. package/src/components/row-actions.tsx +450 -0
  497. package/src/components/scroll-area.tsx +1 -1
  498. package/src/components/search-field.tsx +4 -2
  499. package/src/components/series-chart-budget.ts +40 -8
  500. package/src/components/series-chart-ticks.ts +10 -0
  501. package/src/components/series-chart.tsx +123 -72
  502. package/src/components/settings-fields.tsx +89 -5
  503. package/src/components/share-card.tsx +31 -2
  504. package/src/components/sign-chip.tsx +28 -4
  505. package/src/components/signature-pad.tsx +13 -7
  506. package/src/components/slider.tsx +22 -9
  507. package/src/components/sparkline.tsx +11 -3
  508. package/src/components/stat-tile.tsx +70 -14
  509. package/src/components/status-dot.tsx +25 -2
  510. package/src/components/swipeable-row.tsx +10 -2
  511. package/src/components/switch.tsx +57 -23
  512. package/src/components/table.tsx +2 -2
  513. package/src/components/text-link.tsx +1 -1
  514. package/src/components/text.tsx +11 -6
  515. package/src/components/tile-radio.tsx +4 -1
  516. package/src/components/toast.tsx +27 -7
  517. package/src/components/toggle-group.tsx +115 -14
  518. package/src/components/toggle-legend.tsx +53 -8
  519. package/src/components/translation-review-editor.tsx +1 -1
  520. package/src/components/translation-review.tsx +11 -5
  521. package/src/components/tree-view.tsx +3 -2
  522. package/src/components/treemap.tsx +27 -10
  523. package/src/components/ui.tsx +353 -153
  524. package/src/components/user-avatar.tsx +1 -1
  525. package/src/components/wizard-stepper.tsx +5 -1
  526. package/src/demo/demo-banner.tsx +5 -0
  527. package/src/feedback/feedback-dialog.tsx +3 -3
  528. package/src/feedback/feedback-inbox.tsx +2 -2
  529. package/src/feedback/feedback-thread.tsx +3 -3
  530. package/src/hooks/use-anchored-panel.ts +67 -5
  531. package/src/hooks/use-breakpoint.ts +132 -0
  532. package/src/hooks/use-large-text.ts +41 -0
  533. package/src/hooks/use-windowed-rows.ts +168 -12
  534. package/src/i18n/defaults.ts +8 -0
  535. package/src/i18n/german.ts +107 -0
  536. package/src/i18n/kit-labels.tsx +12 -0
  537. package/src/i18n/locales/es.ts +98 -0
  538. package/src/i18n/locales/fr.ts +102 -0
  539. package/src/i18n/locales/hu.ts +100 -0
  540. package/src/i18n/locales/it.ts +103 -0
  541. package/src/i18n/locales/zh.ts +94 -0
  542. package/src/i18n/review.ts +14 -0
  543. package/src/index.ts +76 -6
  544. package/src/landing/landing-sections.tsx +7 -3
  545. package/src/landing/public-header.tsx +4 -4
  546. package/src/lib/cn.ts +13 -2
  547. package/src/rhf/fields.tsx +1 -1
  548. package/src/rhf/form.tsx +2 -2
  549. package/src/search/command-palette.tsx +10 -9
  550. package/src/settings/settings-layout.tsx +2 -1
  551. package/src/settings/use-account-appearance.ts +148 -0
  552. package/src/settings/use-settings-route.ts +3 -4
  553. package/src/shell/app-shell.tsx +278 -54
  554. package/src/shell/auth-layout.tsx +1 -1
  555. package/src/shell/option-switcher-menu.tsx +1 -1
  556. package/src/shell/top-bar-brand.tsx +3 -1
  557. package/src/shell/topbar-action-menu.tsx +3 -3
  558. package/src/shell/topbar-controls.tsx +4 -4
  559. package/src/theme/contrast-tokens.ts +112 -0
  560. package/src/theme/contrast.ts +225 -0
  561. package/src/theme/palette-presets.ts +24 -4
  562. package/src/theme/palette-store.ts +6 -1
  563. package/src/theme/text-size.ts +283 -0
  564. package/src/wizard/wizard-summary.tsx +4 -2
  565. package/tokens.css +158 -8
@@ -1,10 +1,12 @@
1
1
  import { createContext, forwardRef, useContext, useId, useLayoutEffect, useRef, useState } from "react";
2
- import { ChevronDown, Eye, EyeOff, HelpCircle, Plus, X } from "lucide-react";
2
+ import { ChevronDown, Eye, EyeOff, Plus, X } from "lucide-react";
3
3
  import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ChangeEvent, ComponentPropsWithoutRef, CSSProperties, InputHTMLAttributes, KeyboardEvent, MouseEvent, ReactElement, ReactNode, Ref, SelectHTMLAttributes, TextareaHTMLAttributes } from "react";
4
4
  import { cn } from "../lib/cn";
5
+ import { FOCUS_RING, FOCUS_RING_WIDTH } from "./focus-ring";
5
6
  import { scrollIntoStrip, useStripFade } from "../lib/strip-fade";
6
7
  import { horizontalStep } from "../lib/direction";
7
- import { useMediaQuery } from "../hooks/use-media-query";
8
+ import { usePhoneLayout } from "../hooks/use-breakpoint";
9
+ import { useLargeText } from "../hooks/use-large-text";
8
10
  import { Tooltip, type TooltipSide } from "./tooltip";
9
11
  import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from "../i18n/kit-labels";
10
12
  import type { KitLinkComponent, KitLinkProps } from "../i18n/kit-labels";
@@ -12,9 +14,13 @@ import { pickLinkRenderer, replacingClick, routerLinkNavigation } from "./text-l
12
14
  import { useCommitReason } from "./write-lock";
13
15
  import { mergeDescribedBy } from "./choice-parts";
14
16
  import {
17
+ CompactControlsContext,
18
+ DisabledReasonLine,
15
19
  EndHintRow,
16
20
  FieldCaption,
17
21
  LockedReason,
22
+ useDisabledReasonLine,
23
+ type DisabledReasonDisplay,
18
24
  useFieldHint,
19
25
  useLockReason,
20
26
  FLOATING_LABEL_STATIC,
@@ -24,16 +30,35 @@ import {
24
30
 
25
31
  // The static label lives in field-parts since 0.23 (it is what the field anatomy
26
32
  // there draws, and field-parts must not import this file); public from here as before.
27
- export { FieldLabel, FLOATING_LABEL_STATIC } from "./field-parts";
28
- export type { FieldLabelProps } from "./field-parts";
33
+ export { FieldLabel, FLOATING_LABEL_STATIC, FieldHint } from "./field-parts";
34
+ export type { FieldLabelProps, FieldHintProps } from "./field-parts";
35
+
36
+ // The kit's focus frame (§5) lives in its own module so the parts ui.tsx itself imports
37
+ // (field-parts, the choice controls) can draw it too; public from here as before.
38
+ export { FOCUS_RING, FOCUS_RING_WIDTH } from "./focus-ring";
29
39
 
30
40
  export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "brand" | "link";
31
41
 
32
42
  // Shared base ring for every button-styled element. Kept as a named const so the
33
43
  // <Button> component and the {@link buttonClasses} helper draw from one source and
34
44
  // can never drift apart.
35
- const BUTTON_BASE =
36
- "inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed";
45
+ //
46
+ // The ring is the kit's focus frame (§5): {@link FOCUS_RING_WIDTH} wide, on keyboard focus
47
+ // (`focus-visible`) — a tap or a click no longer leaves a ring on the button — in each
48
+ // variant's own colour below.
49
+ const BUTTON_BASE = cn(
50
+ "inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed",
51
+ FOCUS_RING_WIDTH,
52
+ );
53
+
54
+ /**
55
+ * The touch target at Large and Extra large (docs/text-size-harmonization.md §4): at
56
+ * least 48 px, the size a finger needs whatever the type. In px on purpose, like the
57
+ * field floor ({@link FIELD_TOUCH_TEXT}): it is a physical threshold, not a size that
58
+ * follows the text. Normal keeps the kit's own heights; a `large:` variant, so it costs
59
+ * no render and is right on the first paint.
60
+ */
61
+ export const TOUCH_TARGET_LARGE = "large:min-h-[48px] large:min-w-[48px]";
37
62
 
38
63
  export type ButtonSize = "sm" | "md";
39
64
 
@@ -44,10 +69,19 @@ export type ButtonSize = "sm" | "md";
44
69
  // enough, so the two spellings meet at `py-1`. There is no `lg`: no app has asked for
45
70
  // a bigger text button (IconButton's `lg` is a touch target, not a text size). The
46
71
  // variant map comes AFTER the size, so `link`'s `p-0` still wins at either size.
72
+ //
73
+ // At Large and above (§4) `sm` grows to `md`'s height — the padding that makes its
74
+ // 1rem line as tall as `md`'s 1.25rem one — and both reach the 48 px touch target. The
75
+ // type stays `sm`'s: the compact button is still the quieter one. Not on `link`, which
76
+ // is a word in a sentence and has no box to grow (see `buttonLook`).
47
77
  const BUTTON_SIZES: Record<ButtonSize, string> = {
48
78
  md: "gap-2 px-3 py-2 text-sm",
49
79
  sm: "gap-1.5 px-2 py-1 text-xs",
50
80
  };
81
+ const BUTTON_SIZES_LARGE: Record<ButtonSize, string> = {
82
+ md: "large:min-h-[48px]",
83
+ sm: "large:min-h-[48px] large:py-2.5",
84
+ };
51
85
 
52
86
  // Warm, palette-token-driven so buttons blend with the fields + cards in every theme.
53
87
  // Actions default to a warm bordered look (primary = filled warm chip, secondary =
@@ -57,24 +91,24 @@ const BUTTON_SIZES: Record<ButtonSize, string> = {
57
91
  // variant's own accent: brand for the four neutral ones, danger for `danger`.
58
92
  const buttonVariantClasses: Record<ButtonVariant, string> = {
59
93
  primary:
60
- "border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-primary)] hover:bg-[var(--border)] focus:ring-[var(--brand)]",
94
+ "border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-primary)] hover:bg-[var(--border)] focus-visible:ring-[var(--brand)]",
61
95
  secondary:
62
- "border border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus:ring-[var(--brand)]",
96
+ "border border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus-visible:ring-[var(--brand)]",
63
97
  ghost:
64
- "bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus:ring-[var(--brand)]",
98
+ "bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus-visible:ring-[var(--brand)]",
65
99
  danger:
66
- "bg-[var(--danger)] text-[var(--danger-contrast)] hover:bg-[var(--danger-hover)] focus:ring-[var(--danger-border-strong)]",
100
+ "bg-[var(--danger)] text-[var(--danger-contrast)] hover:bg-[var(--danger-hover)] focus-visible:ring-[var(--danger-border-strong)]",
67
101
  brand:
68
- "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)] focus:ring-[var(--brand)]",
102
+ "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)] focus-visible:ring-[var(--brand)]",
69
103
  // A text link that is still a `<button>` — keksdose's six hand-rolled
70
104
  // `<button className="text-brand underline">` sites ("Resend code", "Show all",
71
105
  // "Undo" in a toast…), which act rather than navigate and so must not be anchors.
72
106
  // The box padding goes (`p-0`) so it sits in a sentence at the text's own size, but
73
- // the base's `focus:ring-2` stays: those copies had `outline-none` and no ring, so
107
+ // the base's focus ring stays: those copies had `outline-none` and no ring, so
74
108
  // a keyboard user tabbing onto them saw nothing at all. `rounded-sm` keeps that
75
109
  // ring hugging the word instead of drawing a pill round it.
76
110
  link:
77
- "rounded-sm p-0 bg-transparent text-[var(--brand)] underline-offset-4 hover:underline focus:ring-[var(--brand)]",
111
+ "rounded-sm p-0 bg-transparent text-[var(--brand)] underline-offset-4 hover:underline focus-visible:ring-[var(--brand)]",
78
112
  };
79
113
 
80
114
  /**
@@ -109,7 +143,7 @@ const BUTTON_TONES: Record<Exclude<ButtonTone, "default">, string> = {
109
143
  muted:
110
144
  "text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:hover:text-[var(--text-secondary)]",
111
145
  danger:
112
- "text-[var(--text-secondary)] hover:text-[var(--danger)] focus:ring-[var(--danger-border-strong)] disabled:hover:text-[var(--text-secondary)]",
146
+ "text-[var(--text-secondary)] hover:text-[var(--danger)] focus-visible:ring-[var(--danger-border-strong)] disabled:hover:text-[var(--text-secondary)]",
113
147
  };
114
148
 
115
149
  /** Only the two transparent variants take every tone; see {@link ButtonTone}. */
@@ -120,9 +154,9 @@ const TONED_VARIANTS = new Set<ButtonVariant>(["link", "ghost"]);
120
154
  // the neutral ones survives the merge.
121
155
  const BUTTON_BOXED_DANGER: Partial<Record<ButtonVariant, string>> = {
122
156
  secondary:
123
- "border-[var(--danger-border)] bg-transparent text-[var(--danger)] hover:bg-[var(--danger-bg)] focus:ring-[var(--danger-border-strong)]",
157
+ "border-[var(--danger-border)] bg-transparent text-[var(--danger)] hover:bg-[var(--danger-bg)] focus-visible:ring-[var(--danger-border-strong)]",
124
158
  primary:
125
- "border-[var(--danger-border)] bg-[var(--danger-bg)] text-[var(--danger)] hover:border-[var(--danger)] hover:bg-[var(--danger)] hover:text-[var(--danger-contrast)] focus:ring-[var(--danger-border-strong)] disabled:hover:border-[var(--danger-border)] disabled:hover:bg-[var(--danger-bg)] disabled:hover:text-[var(--danger)]",
159
+ "border-[var(--danger-border)] bg-[var(--danger-bg)] text-[var(--danger)] hover:border-[var(--danger)] hover:bg-[var(--danger)] hover:text-[var(--danger-contrast)] focus-visible:ring-[var(--danger-border-strong)] disabled:hover:border-[var(--danger-border)] disabled:hover:bg-[var(--danger-bg)] disabled:hover:text-[var(--danger)]",
126
160
  };
127
161
 
128
162
  // `pressed` on a `link`: the brand colour and a heavier weight, which is exactly what
@@ -155,7 +189,13 @@ export function buttonClasses(
155
189
  ): string {
156
190
  const { size = "md", className } =
157
191
  typeof classNameOrOptions === "object" ? classNameOrOptions : { className: classNameOrOptions };
158
- return cn(BUTTON_BASE, BUTTON_SIZES[size], buttonVariantClasses[variant], className);
192
+ return cn(
193
+ BUTTON_BASE,
194
+ BUTTON_SIZES[size],
195
+ variant !== "link" && BUTTON_SIZES_LARGE[size],
196
+ buttonVariantClasses[variant],
197
+ className,
198
+ );
159
199
  }
160
200
 
161
201
  /**
@@ -213,6 +253,12 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
213
253
  * DangerConfirm keeps its own.
214
254
  */
215
255
  disabledReason?: ReactNode;
256
+ /**
257
+ * Where `disabledReason` shows (0.32, §4 "No fact only in a tooltip"): by default a
258
+ * line under the button at Large and on a touch screen, the tooltip otherwise. See
259
+ * {@link DisabledReasonDisplay}.
260
+ */
261
+ disabledReasonDisplay?: DisabledReasonDisplay;
216
262
  /**
217
263
  * This button COMMITS — it saves, creates, deletes. Under a locked
218
264
  * {@link WriteLockProvider} it is disabled the `disabledReason` way, with the lock's
@@ -295,6 +341,7 @@ export interface ButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorEle
295
341
  pending?: never;
296
342
  pressed?: never;
297
343
  disabledReason?: never;
344
+ disabledReasonDisplay?: never;
298
345
  commit?: never;
299
346
  }
300
347
 
@@ -302,11 +349,24 @@ function hasContent(node: ReactNode): boolean {
302
349
  return node !== undefined && node !== null && node !== false && node !== "";
303
350
  }
304
351
 
352
+ // No fact only in a tooltip (§4): the reason line and the compact region live in
353
+ // field-parts, where the self-saving controls (Switch, Checkbox) reach them too.
354
+ export {
355
+ CompactControls,
356
+ DisabledReasonLine,
357
+ DISABLED_REASON_LINE_CLASS,
358
+ useDisabledReasonLine,
359
+ } from "./field-parts";
360
+ export type { DisabledReasonDisplay } from "./field-parts";
361
+
305
362
  /** The button look shared by the `<button>` and the `<a>` form. */
306
363
  function buttonLook(variant: ButtonVariant, size: ButtonSize, stretch: boolean | undefined, tone: ButtonTone) {
307
364
  return cn(
308
365
  BUTTON_BASE,
309
366
  BUTTON_SIZES[size],
367
+ // §4: `sm` at `md`'s height and the 48 px target at Large — not on a `link`. In the
368
+ // order `buttonClasses` has it, so the two strings stay identical.
369
+ variant !== "link" && BUTTON_SIZES_LARGE[size],
310
370
  // In a flex row next to a taller labelled field, `stretch` makes the button
311
371
  // fill the field's height so the two line up (self-stretch overrides the row's
312
372
  // align-items). No effect outside a flex row / when it's already the tallest.
@@ -422,6 +482,7 @@ function ButtonElement({
422
482
  tone = "default",
423
483
  pressed,
424
484
  disabledReason: ownDisabledReason,
485
+ disabledReasonDisplay,
425
486
  commit,
426
487
  pending,
427
488
  className,
@@ -486,15 +547,60 @@ function ButtonElement({
486
547
  );
487
548
  if (!locked) return button;
488
549
  return (
489
- // The button in a FRAGMENT so Tooltip does not clone its bubble into the
490
- // description as well — the hidden copy below already describes it, and the same
491
- // sentence twice would be read on every focus. `stretch` moves to the wrapper,
492
- // which is now the flex item.
493
- <Tooltip label={disabledReason} className={stretch ? "self-stretch" : undefined}>
550
+ <ReasonFrame
551
+ reason={disabledReason}
552
+ reasonId={reasonId}
553
+ display={disabledReasonDisplay}
554
+ className={stretch ? "self-stretch" : undefined}
555
+ >
556
+ {button}
557
+ </ReasonFrame>
558
+ );
559
+ }
560
+
561
+ /**
562
+ * A locked control and its reason: the line under it, or the kit {@link Tooltip} with a
563
+ * hidden copy for `aria-describedby` — see {@link DisabledReasonDisplay}. A component of
564
+ * its own so only a LOCKED control asks the text size and the pointer; the hundreds of
565
+ * unlocked buttons on a page subscribe to nothing.
566
+ *
567
+ * Tooltip form: the control in a FRAGMENT so Tooltip does not clone its bubble into the
568
+ * description as well — the hidden copy already describes it, and the same sentence
569
+ * twice would be read on every focus. Line form: the line is the description, and no
570
+ * bubble repeats what is on screen. Either way `className` (the control's `stretch`)
571
+ * moves to the wrapper, which is now the flex item.
572
+ */
573
+ function ReasonFrame({
574
+ reason,
575
+ reasonId,
576
+ display,
577
+ className,
578
+ side,
579
+ portal,
580
+ children,
581
+ }: {
582
+ reason: ReactNode;
583
+ reasonId: string;
584
+ display: DisabledReasonDisplay | undefined;
585
+ className?: string;
586
+ side?: TooltipSide;
587
+ portal?: boolean;
588
+ children: ReactNode;
589
+ }) {
590
+ const line = useDisabledReasonLine(display);
591
+ if (line) {
592
+ return (
593
+ <DisabledReasonLine id={reasonId} reason={reason} className={className}>
594
+ {children}
595
+ </DisabledReasonLine>
596
+ );
597
+ }
598
+ return (
599
+ <Tooltip label={reason} side={side} portal={portal} className={className}>
494
600
  <>
495
- {button}
601
+ {children}
496
602
  <span id={reasonId} hidden>
497
- {disabledReason}
603
+ {reason}
498
604
  </span>
499
605
  </>
500
606
  </Tooltip>
@@ -511,8 +617,10 @@ function ButtonElement({
511
617
  //
512
618
  // Draws its colours from the same `buttonVariantClasses` map as <Button>, so the
513
619
  // two re-skin together with the palette.
514
- const ICON_BUTTON_BASE =
515
- "inline-flex items-center justify-center rounded-md transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed";
620
+ const ICON_BUTTON_BASE = cn(
621
+ "inline-flex items-center justify-center rounded-md transition-colors focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed",
622
+ FOCUS_RING_WIDTH,
623
+ );
516
624
 
517
625
  export type IconButtonSize = "2xl" | "xl" | "lg" | "md" | "sm" | "xs" | "2xs";
518
626
 
@@ -548,6 +656,45 @@ const ICON_BUTTON_STRETCH_SIZES: Record<IconButtonSize, string> = {
548
656
  "2xs": "w-6 min-h-6 self-stretch rounded [&_svg]:size-3.5",
549
657
  };
550
658
 
659
+ // The label as visible text beside the glyph (0.32, §4: at Large "IconButton shows its
660
+ // label"): the same height and glyph as the square, a width taken from the words. A
661
+ // map of its own rather than `w-auto` over `size-*`, which tailwind-merge cannot undo
662
+ // in one direction.
663
+ const ICON_BUTTON_TEXT_SIZES: Record<IconButtonSize, string> = {
664
+ "2xl": "h-16 gap-2 px-4 text-base [&_svg]:size-7",
665
+ xl: "h-12 gap-2 px-3.5 text-sm [&_svg]:size-6",
666
+ lg: "h-11 gap-2 px-3 text-sm [&_svg]:size-5",
667
+ md: "h-9 gap-1.5 px-2.5 text-sm [&_svg]:size-5",
668
+ sm: "h-8 gap-1.5 px-2 text-sm [&_svg]:size-5",
669
+ xs: "h-7 gap-1 rounded px-1.5 text-xs [&_svg]:size-4",
670
+ "2xs": "h-6 gap-1 rounded px-1.5 text-xs [&_svg]:size-3.5",
671
+ };
672
+ const ICON_BUTTON_TEXT_STRETCH_SIZES: Record<IconButtonSize, string> = {
673
+ "2xl": "min-h-16 self-stretch gap-2 px-4 text-base [&_svg]:size-7",
674
+ xl: "min-h-12 self-stretch gap-2 px-3.5 text-sm [&_svg]:size-6",
675
+ lg: "min-h-11 self-stretch gap-2 px-3 text-sm [&_svg]:size-5",
676
+ md: "min-h-9 self-stretch gap-1.5 px-2.5 text-sm [&_svg]:size-5",
677
+ sm: "min-h-8 self-stretch gap-1.5 px-2 text-sm [&_svg]:size-5",
678
+ xs: "min-h-7 self-stretch gap-1 rounded px-1.5 text-xs [&_svg]:size-4",
679
+ "2xs": "min-h-6 self-stretch gap-1 rounded px-1.5 text-xs [&_svg]:size-3.5",
680
+ };
681
+
682
+ // The 48 px touch target at Large (§4), per size. `lg` and up are past it already
683
+ // (2.75rem is 55 px at Large). `md` … `xs` grow their box to it. `2xs` is the action ON
684
+ // a chip or a tab, whose box cannot grow without growing the chip: it keeps its box and
685
+ // gets a 48 px hit area instead — an invisible `::after` centred on it, which takes the
686
+ // taps a finger lands round the glyph.
687
+ const ICON_BUTTON_TOUCH_LARGE: Record<IconButtonSize, string> = {
688
+ "2xl": "",
689
+ xl: "",
690
+ lg: "",
691
+ md: TOUCH_TARGET_LARGE,
692
+ sm: TOUCH_TARGET_LARGE,
693
+ xs: TOUCH_TARGET_LARGE,
694
+ "2xs":
695
+ "relative large:after:absolute large:after:inset-[calc((100%-48px)/2)] large:after:content-['']",
696
+ };
697
+
551
698
  // A tone re-colours the glyph without changing what the variant draws around it.
552
699
  // Each coloured tone comes in two resting looks, picked by `quiet` (see the prop):
553
700
  // QUIET is placeholder grey until the pointer or focus arrives, then the tone's
@@ -570,32 +717,32 @@ type ColouredTone = "danger" | "warning" | "info" | "success" | "custom";
570
717
  const ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }> = {
571
718
  danger: {
572
719
  quiet:
573
- "text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border-strong)]",
574
- toned: "text-[var(--danger)] hover:bg-[var(--danger-bg)] focus:ring-[var(--danger-border-strong)]",
720
+ "text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus-visible:ring-[var(--danger-border-strong)]",
721
+ toned: "text-[var(--danger)] hover:bg-[var(--danger-bg)] focus-visible:ring-[var(--danger-border-strong)]",
575
722
  },
576
723
  warning: {
577
724
  quiet:
578
- "text-[var(--text-placeholder)] hover:bg-[var(--warning-bg)] hover:text-[var(--warning)] focus:ring-[var(--warning-border)]",
579
- toned: "text-[var(--warning)] hover:bg-[var(--warning-bg)] focus:ring-[var(--warning-border)]",
725
+ "text-[var(--text-placeholder)] hover:bg-[var(--warning-bg)] hover:text-[var(--warning)] focus-visible:ring-[var(--warning-border)]",
726
+ toned: "text-[var(--warning)] hover:bg-[var(--warning-bg)] focus-visible:ring-[var(--warning-border)]",
580
727
  },
581
728
  info: {
582
729
  quiet:
583
- "text-[var(--text-placeholder)] hover:bg-[var(--info-bg)] hover:text-[var(--info)] focus:ring-[var(--info-border)]",
584
- toned: "text-[var(--info)] hover:bg-[var(--info-bg)] focus:ring-[var(--info-border)]",
730
+ "text-[var(--text-placeholder)] hover:bg-[var(--info-bg)] hover:text-[var(--info)] focus-visible:ring-[var(--info-border)]",
731
+ toned: "text-[var(--info)] hover:bg-[var(--info-bg)] focus-visible:ring-[var(--info-border)]",
585
732
  },
586
733
  success: {
587
734
  quiet:
588
- "text-[var(--text-placeholder)] hover:bg-[var(--success-bg)] hover:text-[var(--success)] focus:ring-[var(--success-border)]",
589
- toned: "text-[var(--success)] hover:bg-[var(--success-bg)] focus:ring-[var(--success-border)]",
735
+ "text-[var(--text-placeholder)] hover:bg-[var(--success-bg)] hover:text-[var(--success)] focus-visible:ring-[var(--success-border)]",
736
+ toned: "text-[var(--success)] hover:bg-[var(--success-bg)] focus-visible:ring-[var(--success-border)]",
590
737
  },
591
738
  // The hover fill and the ring are MIXED from the one colour, since a custom tone
592
739
  // brings no `-bg` / `-border` pair of its own. The fallback is the body colour, so a
593
740
  // `custom` tone with no colour set is the default look rather than invisible.
594
741
  custom: {
595
742
  quiet:
596
- "text-[var(--text-placeholder)] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] hover:text-[var(--icon-button-tone,var(--text-primary))] focus:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]",
743
+ "text-[var(--text-placeholder)] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] hover:text-[var(--icon-button-tone,var(--text-primary))] focus-visible:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]",
597
744
  toned:
598
- "text-[var(--icon-button-tone,var(--text-primary))] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] focus:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]",
745
+ "text-[var(--icon-button-tone,var(--text-primary))] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] focus-visible:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]",
599
746
  },
600
747
  };
601
748
 
@@ -656,7 +803,7 @@ const ICON_BUTTON_PRESSED =
656
803
  // themes. The disc is the inverse at 60% (`color-mix`, so it stays a token a palette
657
804
  // can re-point); the blur keeps a busy background from breaking the glyph's edge.
658
805
  const ICON_BUTTON_OVERLAY =
659
- "rounded-full bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)] text-[var(--text-inverse)] backdrop-blur-sm hover:bg-[color-mix(in_srgb,var(--bg-inverse)_75%,transparent)] focus:ring-[var(--text-inverse)]";
806
+ "rounded-full bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)] text-[var(--text-inverse)] backdrop-blur-sm hover:bg-[color-mix(in_srgb,var(--bg-inverse)_75%,transparent)] focus-visible:ring-[var(--text-inverse)]";
660
807
 
661
808
  // `variant="shutter"`: the camera's release — keksdose's camera-capture.tsx:248 draws
662
809
  // it by hand as a 64px circle with a thick white ring round a translucent fill. It sits
@@ -665,7 +812,7 @@ const ICON_BUTTON_OVERLAY =
665
812
  // `overlay` uses would give a dark ring on a dark viewfinder in dark mode. The focus ring stands off by 2px, or it would
666
813
  // merge into the ring that is part of the look.
667
814
  const ICON_BUTTON_SHUTTER =
668
- "rounded-full border-4 border-[var(--media-ink)] bg-[var(--media-scrim)] text-[var(--media-ink)] backdrop-blur-sm hover:bg-[var(--media-scrim-hover)] focus:ring-[var(--media-ink)] focus:ring-offset-2 focus:ring-offset-[var(--media-scrim-hover)]";
815
+ "rounded-full border-4 border-[var(--media-ink)] bg-[var(--media-scrim)] text-[var(--media-ink)] backdrop-blur-sm hover:bg-[var(--media-scrim-hover)] focus-visible:ring-[var(--media-ink)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--media-scrim-hover)]";
669
816
 
670
817
  export type IconButtonVariant = ButtonVariant | "overlay" | "shutter";
671
818
 
@@ -843,9 +990,25 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
843
990
  * unless `tooltipPortal` says otherwise.
844
991
  */
845
992
  label?: string;
993
+ /**
994
+ * Show `label` as visible text beside the icon at Large and Extra large (0.32,
995
+ * docs/text-size-harmonization.md §4, §10.8). A reader who asked for bigger type is
996
+ * the one an unlabelled glyph fails, and on touch the tooltip never opens. Then there
997
+ * is no tooltip: the words are on the button.
998
+ *
999
+ * Default `true` — except for `variant="overlay"` and `"shutter"`, which sit ON a
1000
+ * picture, and inside {@link CompactControls} (AppShell's top bar, `RowActions`'
1001
+ * inline icons). `false`: the icon alone at every size, for an icon on content — a
1002
+ * receipt overlay, a map's zoom, a viewfinder, a photo thumbnail. That must be rare;
1003
+ * a dense row's actions belong in `RowActions`, which collapses them into a "⋯" menu
1004
+ * at Large instead (§10.8). Normal never shows it. `tooltip={false}` (a universally
1005
+ * read glyph, a dialog's ✕) keeps the icon alone too.
1006
+ */
1007
+ labelVisible?: boolean;
846
1008
  /** Show `label` as a tooltip. Default `true`; `false` keeps `label` as the
847
1009
  * accessible name only — for a button whose glyph is universally read (a close ✕ in
848
- * a dialog header) or that already sits under a tooltip of its own. */
1010
+ * a dialog header) or that already sits under a tooltip of its own. It also keeps the
1011
+ * label from showing as text at Large (see `labelVisible`). */
849
1012
  tooltip?: boolean;
850
1013
  /** Where the `label` tooltip opens. See {@link Tooltip}'s `side`. */
851
1014
  tooltipSide?: TooltipSide;
@@ -899,8 +1062,13 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
899
1062
  * bubble shows the reason in place of the label, since the glyph already says what
900
1063
  * the button is and the reason is the news. Shown even with `tooltip={false}`: a
901
1064
  * reason nobody can see is not one. Wins over `disabled`, as on Button.
1065
+ *
1066
+ * 0.32: at Large and on a touch screen the reason is a line under the button rather
1067
+ * than a bubble — see `disabledReasonDisplay`.
902
1068
  */
903
1069
  disabledReason?: ReactNode;
1070
+ /** Where `disabledReason` shows — {@link Button}'s `disabledReasonDisplay`. */
1071
+ disabledReasonDisplay?: DisabledReasonDisplay;
904
1072
  /** This action COMMITS — {@link Button}'s `commit`: under a locked
905
1073
  * {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
906
1074
  * reason. No effect without a lock. */
@@ -947,6 +1115,7 @@ export interface IconButtonLinkProps
947
1115
  | "quiet"
948
1116
  | "stopPropagation"
949
1117
  | "label"
1118
+ | "labelVisible"
950
1119
  | "tooltip"
951
1120
  | "tooltipSide"
952
1121
  | "tooltipPortal"
@@ -968,11 +1137,35 @@ export interface IconButtonLinkProps
968
1137
  form?: never;
969
1138
  pressed?: never;
970
1139
  disabledReason?: never;
1140
+ disabledReasonDisplay?: never;
971
1141
  commit?: never;
972
1142
  disabledStyle?: never;
973
1143
  pending?: never;
974
1144
  }
975
1145
 
1146
+ /**
1147
+ * Whether an IconButton draws its `label` as text (see `labelVisible`): at Large and
1148
+ * above, with a label to draw, not switched off by the caller, the variant (`overlay`,
1149
+ * `shutter`), `tooltip={false}` or a {@link CompactControls} region.
1150
+ */
1151
+ function useIconButtonText(
1152
+ label: string | undefined,
1153
+ labelVisible: boolean | undefined,
1154
+ variant: IconButtonVariant,
1155
+ tooltip: boolean,
1156
+ ): boolean {
1157
+ const large = useLargeText();
1158
+ const compact = useContext(CompactControlsContext);
1159
+ if (!large || label === undefined || label === "" || !tooltip) return false;
1160
+ const visible = labelVisible ?? (!compact && variant !== "overlay" && variant !== "shutter");
1161
+ return visible;
1162
+ }
1163
+
1164
+ /** The visible label (see `labelVisible`). */
1165
+ function IconButtonText({ label }: { label: string }) {
1166
+ return <span data-slot="icon-button-label">{label}</span>;
1167
+ }
1168
+
976
1169
  /**
977
1170
  * `toneColor`: one CSS colour, or one per theme.
978
1171
  *
@@ -1008,6 +1201,7 @@ function iconButtonLook({
1008
1201
  tone,
1009
1202
  quiet,
1010
1203
  toneColor,
1204
+ withText = false,
1011
1205
  }: {
1012
1206
  variant: IconButtonVariant;
1013
1207
  size: IconButtonSize;
@@ -1017,10 +1211,18 @@ function iconButtonLook({
1017
1211
  tone: IconButtonTone;
1018
1212
  quiet: boolean | undefined;
1019
1213
  toneColor: IconButtonToneColor | undefined;
1214
+ /** The label is drawn beside the glyph (see `labelVisible`). */
1215
+ withText?: boolean;
1020
1216
  }) {
1021
1217
  return cn(
1022
1218
  ICON_BUTTON_BASE,
1023
- stretch ? ICON_BUTTON_STRETCH_SIZES[size] : ICON_BUTTON_SIZES[size],
1219
+ withText
1220
+ ? cn("font-medium whitespace-nowrap", stretch ? ICON_BUTTON_TEXT_STRETCH_SIZES[size] : ICON_BUTTON_TEXT_SIZES[size])
1221
+ : stretch
1222
+ ? ICON_BUTTON_STRETCH_SIZES[size]
1223
+ : ICON_BUTTON_SIZES[size],
1224
+ // With text the pill is a 48 px target at any size; the square follows its size.
1225
+ withText ? TOUCH_TARGET_LARGE : ICON_BUTTON_TOUCH_LARGE[size],
1024
1226
  glyphSize !== undefined && ICON_BUTTON_GLYPH_SIZES[glyphSize],
1025
1227
  // After the size, so the overlay's `rounded-full` beats the small sizes' `rounded`.
1026
1228
  variant === "overlay"
@@ -1079,6 +1281,7 @@ function IconButtonLink({
1079
1281
  shape = "square",
1080
1282
  toneColor,
1081
1283
  label,
1284
+ labelVisible,
1082
1285
  tooltip = true,
1083
1286
  tooltipSide,
1084
1287
  tooltipPortal,
@@ -1091,10 +1294,19 @@ function IconButtonLink({
1091
1294
  rel,
1092
1295
  onClick,
1093
1296
  onKeyDown,
1094
- children,
1297
+ children: glyph,
1095
1298
  ref,
1096
1299
  ...rest
1097
1300
  }: IconButtonLinkProps) {
1301
+ const withText = useIconButtonText(label, labelVisible, variant, tooltip);
1302
+ const children = withText ? (
1303
+ <>
1304
+ {glyph}
1305
+ <IconButtonText label={label!} />
1306
+ </>
1307
+ ) : (
1308
+ glyph
1309
+ );
1098
1310
  const kitLink = useKitLink();
1099
1311
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
1100
1312
  const tone = toneProp ?? (toneColor !== undefined ? "custom" : "default");
@@ -1116,7 +1328,7 @@ function IconButtonLink({
1116
1328
  onKeyDown?.(e);
1117
1329
  };
1118
1330
  const cls = cn(
1119
- iconButtonLook({ variant, size, stretch, glyphSize, shape, tone, quiet, toneColor }),
1331
+ iconButtonLook({ variant, size, stretch, glyphSize, shape, tone, quiet, toneColor, withText }),
1120
1332
  // BASE's `disabled:` classes never match an `<a>` — Button's disabled link look.
1121
1333
  disabled && "pointer-events-none cursor-not-allowed opacity-50",
1122
1334
  className,
@@ -1165,7 +1377,8 @@ function IconButtonLink({
1165
1377
  );
1166
1378
  }
1167
1379
  control = withIconButtonBadge(control, badge, stretch);
1168
- if (!tooltip || label === undefined || label === "") return control;
1380
+ // The words are on the link at Large: a bubble would only repeat them.
1381
+ if (!tooltip || withText || label === undefined || label === "") return control;
1169
1382
  return (
1170
1383
  // A fragment, as on the button: the bubble says what `aria-label` already does.
1171
1384
  <Tooltip
@@ -1192,11 +1405,13 @@ function IconButtonElement(
1192
1405
  shape = "square",
1193
1406
  toneColor,
1194
1407
  label,
1408
+ labelVisible,
1195
1409
  tooltip = true,
1196
1410
  tooltipSide,
1197
1411
  tooltipPortal,
1198
1412
  tooltipLazy = true,
1199
1413
  disabledReason: ownDisabledReason,
1414
+ disabledReasonDisplay,
1200
1415
  commit,
1201
1416
  pending,
1202
1417
  glyphSize,
@@ -1218,6 +1433,7 @@ function IconButtonElement(
1218
1433
  ) {
1219
1434
  const tone = toneProp ?? (toneColor !== undefined ? "custom" : "default");
1220
1435
  const keep = disabledStyle === "keep";
1436
+ const withText = useIconButtonText(label, labelVisible, variant, tooltip);
1221
1437
  const reasonId = useId();
1222
1438
  const disabledReason = useCommitReason(commit, ownDisabledReason);
1223
1439
  const locked = hasContent(disabledReason);
@@ -1252,7 +1468,7 @@ function IconButtonElement(
1252
1468
  onKeyDown?.(e);
1253
1469
  }}
1254
1470
  className={cn(
1255
- iconButtonLook({ variant, size, stretch, glyphSize, shape, tone, quiet, toneColor }),
1471
+ iconButtonLook({ variant, size, stretch, glyphSize, shape, tone, quiet, toneColor, withText }),
1256
1472
  pressed && ICON_BUTTON_PRESSED,
1257
1473
  // The disabled look for the focusable kind of disabled, as on Button.
1258
1474
  locked && (keep ? "cursor-default" : "cursor-not-allowed opacity-50"),
@@ -1274,29 +1490,28 @@ function IconButtonElement(
1274
1490
  ) : (
1275
1491
  children
1276
1492
  )}
1493
+ {withText && <IconButtonText label={label!} />}
1277
1494
  </button>
1278
1495
  );
1279
1496
  const control = withIconButtonBadge(button, badge, stretch);
1280
1497
  if (locked) {
1281
1498
  return (
1282
- // Button's shape: the button and the hidden reason in a FRAGMENT, so Tooltip does
1283
- // not add its bubble to the description as well.
1284
- <Tooltip
1285
- label={disabledReason}
1499
+ // Button's shape: the reason as a line under it, or the bubble with a hidden copy
1500
+ // for the description (see ReasonFrame).
1501
+ <ReasonFrame
1502
+ reason={disabledReason}
1503
+ reasonId={reasonId}
1504
+ display={disabledReasonDisplay}
1286
1505
  side={tooltipSide}
1287
1506
  portal={tooltipPortal}
1288
1507
  className={stretch ? "self-stretch" : undefined}
1289
1508
  >
1290
- <>
1291
- {control}
1292
- <span id={reasonId} hidden>
1293
- {disabledReason}
1294
- </span>
1295
- </>
1296
- </Tooltip>
1509
+ {control}
1510
+ </ReasonFrame>
1297
1511
  );
1298
1512
  }
1299
- if (!tooltip || label === undefined || label === "") return control;
1513
+ // The words are on the button at Large: a bubble would only repeat them.
1514
+ if (!tooltip || withText || label === undefined || label === "") return control;
1300
1515
  return (
1301
1516
  // A fragment, so Tooltip leaves the button's description alone: the bubble says
1302
1517
  // exactly what `aria-label` already does. Lazy by default — see `tooltipLazy`.
@@ -1312,8 +1527,25 @@ function IconButtonElement(
1312
1527
  );
1313
1528
  }
1314
1529
 
1530
+ /**
1531
+ * The 16 px floor on touch (docs/text-size-harmonization.md §10.1). iOS zooms the page
1532
+ * when a field under 16 px takes focus, and keksdose's `maximum-scale=1` — which stopped
1533
+ * it — goes so pinch zoom works again (WCAG 1.4.4). A zoomed page drops the keyboard
1534
+ * inset to 0 (§10.2), so the zoom would put a pinned editor footer behind the keyboard
1535
+ * (keksdose #154). So on a coarse pointer a field is at least 16 px, at every text size:
1536
+ * `max()` leaves Large and Extra large alone, where `text-sm` is already 17.5 / 21 px.
1537
+ *
1538
+ * In px on purpose: the threshold is the browser's, in CSS px, whatever the root size.
1539
+ * Font size only — the line height keeps `text-sm`'s ratio. Part of {@link FIELD_BASE};
1540
+ * add it to any other text-entry control whose type is below 16 px.
1541
+ */
1542
+ export const FIELD_TOUCH_TEXT = "pointer-coarse:text-[length:max(0.875rem,16px)]";
1543
+
1315
1544
  export const FIELD_BASE =
1316
1545
  "block w-full rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-3 py-2 text-sm text-[var(--text-primary)] shadow-sm placeholder:text-[var(--text-placeholder)] focus:border-[var(--brand)] focus:ring-[var(--brand)] " +
1546
+ // §10.1: at least 16 px on a touch screen, or iOS zooms the page on focus.
1547
+ FIELD_TOUCH_TEXT +
1548
+ " " +
1317
1549
  // A field the user cannot change has to LOOK settled. Without this, `disabled`
1318
1550
  // dimmed the floating label and nothing else — FIELD_BASE's own
1319
1551
  // `text-[var(--text-primary)]` overrides the browser's grey — so a read-only value
@@ -1375,8 +1607,12 @@ export const FIELD_INVALID =
1375
1607
 
1376
1608
  export const FLOATING_INPUT_CLASS = cn(FIELD_BASE, FIELD_FLOATING_PAD, "peer placeholder:text-transparent");
1377
1609
 
1378
- /** The phone breakpoint the display treatment below keys off — the same one the
1379
- * numpad sheet uses, kept in one place so the two can't drift apart. */
1610
+ /** The phone breakpoint at Normal text size, as a media query.
1611
+ *
1612
+ * @deprecated since 0.32 — it does not follow the text size (§3.3): at Large the CSS
1613
+ * `max-md:` reaches 959 px and this still stops at 767. Use `usePhoneLayout()`, which
1614
+ * agrees with `max-md:` at every size, or `breakpointQuery("max-md", scale)` where a
1615
+ * query string is needed. Kept exported, unchanged, for the apps that import it. */
1380
1616
  export const PHONE_QUERY = "(max-width: 767px)";
1381
1617
 
1382
1618
  /**
@@ -1385,8 +1621,9 @@ export const PHONE_QUERY = "(max-width: 767px)";
1385
1621
  * about reads as the thing itself, not as another boxed row in a stack.
1386
1622
  *
1387
1623
  * Every control that takes `variant="display"` — {@link Input}, `NumberInput`,
1388
- * `AmountInput` — means exactly the same thing by it: the treatment applies below
1389
- * {@link PHONE_QUERY} and the field is untouched above it, so a caller never has
1624
+ * `AmountInput` — means exactly the same thing by it: the treatment applies in the
1625
+ * phone layout (`usePhoneLayout()`, `max-md:` at the text size in force) and the field
1626
+ * is untouched above it, so a caller never has
1390
1627
  * to ask the viewport, and a form can't end up half-treated across breakpoints.
1391
1628
  *
1392
1629
  * What stays, deliberately:
@@ -1466,8 +1703,8 @@ export function FieldChevron({ className, ...rest }: FieldChevronProps) {
1466
1703
  export const FLOATING_LABEL_CLASS = cn(
1467
1704
  "pointer-events-none absolute start-3 top-2.5 text-sm text-[var(--text-placeholder)] transition-all",
1468
1705
  "max-w-[calc(100%-1.5rem)] truncate",
1469
- "peer-focus:top-1 peer-focus:text-[11px] peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]",
1470
- "peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]",
1706
+ "peer-focus:top-1 peer-focus:text-caption peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]",
1707
+ "peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-caption peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]",
1471
1708
  "peer-disabled:opacity-50",
1472
1709
  );
1473
1710
 
@@ -1485,8 +1722,8 @@ export const FLOATING_LABEL_CLASS = cn(
1485
1722
  const FLOATING_ROW_CLASS = cn(
1486
1723
  "pointer-events-none absolute start-3 top-2.5 flex items-center gap-1 transition-all",
1487
1724
  "max-w-[calc(100%-3rem)] text-sm text-[var(--text-placeholder)]",
1488
- "peer-focus:top-1 peer-focus:text-[11px] peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]",
1489
- "peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]",
1725
+ "peer-focus:top-1 peer-focus:text-caption peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]",
1726
+ "peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-caption peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]",
1490
1727
  "peer-disabled:opacity-50",
1491
1728
  );
1492
1729
 
@@ -1581,69 +1818,6 @@ export function FloatingField({
1581
1818
  );
1582
1819
  }
1583
1820
 
1584
- /**
1585
- * The "?" that explains a field, on the field's own label line (dev#468).
1586
- *
1587
- * Pass it to a labelled {@link Select} / {@link Input} as `hint`. It exists as a
1588
- * component rather than as a snippet each form repeats because the previous
1589
- * version was exactly that snippet — an absolutely-positioned button whose
1590
- * `top-1.5` was one guess at where an 11px label sits — and the reporter's
1591
- * follow-up was *"question mark is not centered. Is it part of the hoc?
1592
- * positioning problems seem quite frequently."* It was not part of the HOC. Now
1593
- * it is, and there is one place left where the answer can be wrong.
1594
- *
1595
- * A `<button>` rather than a bare icon: hover alone puts the explanation out of
1596
- * reach of a keyboard and of every touch device, and the tooltip shows on focus
1597
- * too. The text is also its accessible name, so a screen reader gets it without
1598
- * the bubble ever opening.
1599
- */
1600
- export interface FieldHintProps extends Omit<ComponentPropsWithoutRef<"button">, "children"> {
1601
- /** The explanation. It is both the tooltip's text and, by default, the button's
1602
- * accessible name, so a screen reader gets it without the bubble ever opening. */
1603
- label: string;
1604
- /** Default `"start"`: before the hint in the reading direction (the left in LTR, the
1605
- * right in RTL). `left` / `right` stay physical. */
1606
- side?: TooltipSide;
1607
- }
1608
-
1609
- export function FieldHint({
1610
- label,
1611
- side = "start",
1612
- className,
1613
- "aria-label": ariaLabel,
1614
- ...rest
1615
- }: FieldHintProps) {
1616
- return (
1617
- // `tap="toggle"` (0.25): the "?" exists only to explain, so a tap on a phone shows
1618
- // the bubble and the next tap hides it — the touch rule (no bubble left behind by a
1619
- // tap) would otherwise make it show nothing at all.
1620
- <Tooltip label={label} side={side} portal tap="toggle">
1621
- <button
1622
- {...rest}
1623
- // `type` after the spread, not before. These render inside forms — that is the
1624
- // only place a field has a label line — and a hint that defaulted to `submit`
1625
- // because a caller spread a props object at it would save the form on a click
1626
- // meant to explain a field.
1627
- type="button"
1628
- // The explanation names the button unless the caller says otherwise; passing
1629
- // `aria-label` is how you shorten it for a screen reader without shortening
1630
- // what the bubble shows.
1631
- aria-label={ariaLabel ?? label}
1632
- // Nothing to activate: the tooltip opens on hover and on focus, and a
1633
- // click that did something as well would be a second, undiscoverable
1634
- // behaviour on the same target.
1635
- onClick={(e) => e.preventDefault()}
1636
- className={cn(
1637
- "flex text-[var(--text-placeholder)] transition-colors hover:text-[var(--text-secondary)]",
1638
- className,
1639
- )}
1640
- >
1641
- <HelpCircle className="size-3.5" />
1642
- </button>
1643
- </Tooltip>
1644
- );
1645
- }
1646
-
1647
1821
  export interface LabelProps extends ComponentPropsWithoutRef<"label"> {
1648
1822
  /** Draw the required mark after the text. The mark is `aria-hidden`: what a
1649
1823
  * screen reader hears is the control's own `required` / `aria-required`, which
@@ -1717,7 +1891,7 @@ export function Label({ required, size = "md", disabled, className, children, ..
1717
1891
  * whatever is being read at the time, on every keystroke of a form that re-validates
1718
1892
  * as you type.
1719
1893
  */
1720
- const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
1894
+ const FIELD_ERROR_CLASS = "mt-1 text-caption leading-tight text-[var(--danger)]";
1721
1895
 
1722
1896
  function useFieldError(
1723
1897
  error: ReactNode,
@@ -1921,7 +2095,7 @@ function CharacterCount({
1921
2095
  <span
1922
2096
  aria-hidden
1923
2097
  className={cn(
1924
- "shrink-0 text-[11px] leading-tight tabular-nums",
2098
+ "shrink-0 text-caption leading-tight tabular-nums",
1925
2099
  zone === "limit"
1926
2100
  ? "text-[var(--danger)]"
1927
2101
  : zone === "near"
@@ -2091,7 +2265,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(pro
2091
2265
  rest.onChange?.(e);
2092
2266
  }
2093
2267
  : rest.onChange;
2094
- const asDisplay = useMediaQuery(PHONE_QUERY, false) && variant === "display";
2268
+ const asDisplay = usePhoneLayout() && variant === "display";
2095
2269
  // Password fields get a reveal toggle so users can check what they typed.
2096
2270
  const isPassword = type === "password";
2097
2271
  const [revealed, setRevealed] = useState(false);
@@ -2124,7 +2298,10 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(pro
2124
2298
  disabled={rest.disabled}
2125
2299
  aria-label={revealed ? passwordText.hide : passwordText.show}
2126
2300
  aria-pressed={revealed}
2127
- className="absolute inset-y-0 end-0 flex items-center rounded-e-md px-2.5 text-[var(--text-placeholder)] transition-colors hover:text-[var(--text-secondary)] focus:outline-none focus:ring-2 focus:ring-[var(--brand)] disabled:cursor-default disabled:opacity-50"
2301
+ className={cn(
2302
+ "absolute inset-y-0 end-0 flex items-center rounded-e-md px-2.5 text-[var(--text-placeholder)] transition-colors hover:text-[var(--text-secondary)] focus:outline-none disabled:cursor-default disabled:opacity-50",
2303
+ FOCUS_RING,
2304
+ )}
2128
2305
  >
2129
2306
  {revealed ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
2130
2307
  </button>
@@ -2346,7 +2523,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
2346
2523
  textHint || errorEl !== null ? (
2347
2524
  <>
2348
2525
  {textHint && (
2349
- <p id={hintId} className="mt-1 text-[11px] leading-tight text-[var(--text-muted)]">
2526
+ <p id={hintId} className="mt-1 text-caption leading-tight text-[var(--text-muted)]">
2350
2527
  {hint}
2351
2528
  </p>
2352
2529
  )}
@@ -2601,9 +2778,11 @@ function labelEdge(scrollTop: number, lineHeight: number): { height: number; sol
2601
2778
  * on the same ATTRIBUTE selector FIELD_BASE uses, for the reason given there. Focus and
2602
2779
  * `invalid` change only the border and the ring, which are outside the layer, so they
2603
2780
  * need nothing. Both themes come with the variables. The inner corners are the field's
2604
- * radius (`rounded-md`, 6px) less the 1px border, so the rounded border is never cut
2605
- * into — a literal 5px rather than `var(--radius-md)`, which Tailwind emits only where
2606
- * a utility uses it and the kit's token check refuses (token-vars-declared).
2781
+ * radius (`rounded-md`, 0.375rem) less the 1px border, so the rounded border is never
2782
+ * cut into — `calc(0.375rem - 1px)` rather than `var(--radius-md)`, which Tailwind emits
2783
+ * only where a utility uses it and the kit's token check refuses (token-vars-declared).
2784
+ * Since 0.32 in rem (§3.2): the field's own corner grows with the text, so a fixed 5px
2785
+ * left the strip's corner square of the border's at 125 %.
2607
2786
  *
2608
2787
  * Hidden (`display: none`) until the label floats: an empty, unfocused field has
2609
2788
  * nothing that could scroll, and it renders exactly as it did before 0.24. Pointer
@@ -2616,7 +2795,7 @@ function labelEdge(scrollTop: number, lineHeight: number): { height: number; sol
2616
2795
  * is {@link TEXTAREA_LABEL_EDGE} (0.25).
2617
2796
  */
2618
2797
  const TEXTAREA_LABEL_STRIP = cn(
2619
- "pointer-events-none absolute inset-x-px top-px hidden h-4 rounded-t-[5px] bg-[var(--bg-surface)]",
2798
+ "pointer-events-none absolute inset-x-px top-px hidden h-4 rounded-t-[calc(0.375rem-1px)] bg-[var(--bg-surface)]",
2620
2799
  "peer-focus:block peer-[:not(:placeholder-shown)]:block",
2621
2800
  "peer-disabled:bg-[var(--bg-surface-2)] peer-[[readonly]]:bg-[var(--bg-surface-2)]",
2622
2801
  TEXTAREA_LABEL_EDGE,
@@ -2931,10 +3110,13 @@ const CARD_PADDING: Record<NonNullable<CardProps["padding"]>, string> = {
2931
3110
 
2932
3111
  // The same padding one pixel short, for a 2px `toneStrength="strong"` frame: the
2933
3112
  // border grows inwards by that pixel, so the content stays where the 1px card had it.
3113
+ // `calc(rem − 1px)` (0.32, §3.2): the padding grows with the text like `p-3` / `p-4`,
3114
+ // the border does not — a fixed 11 / 15 px stopped matching them at 125 %, and toggling
3115
+ // the tone shifted the content.
2934
3116
  const CARD_PADDING_STRONG: Record<NonNullable<CardProps["padding"]>, string> = {
2935
3117
  none: "p-0",
2936
- sm: "p-[11px]",
2937
- md: "p-[15px]",
3118
+ sm: "p-[calc(0.75rem-1px)]",
3119
+ md: "p-[calc(1rem-1px)]",
2938
3120
  };
2939
3121
 
2940
3122
  // The wash as a background IMAGE over the card's opaque `--bg-surface` — see
@@ -3036,8 +3218,14 @@ export interface CardHeaderProps extends ComponentPropsWithoutRef<"div"> {
3036
3218
  * description instead of beside them. For an action that is wider than an icon —
3037
3219
  * a "Set up two-factor" button, a checkbox and a filter — which, kept in the
3038
3220
  * top-end column on a 390px phone, squeezed the title into a ribbon two words wide
3039
- * (keksdose's settings cards). Off by default: an icon action (the wizard summary's
3040
- * pencil) is narrow enough to stay beside the title at every width.
3221
+ * (keksdose's settings cards). Off by default at Normal: an icon action (the wizard
3222
+ * summary's pencil) is narrow enough to stay beside the title at every width.
3223
+ *
3224
+ * Unset, it stacks at Large and Extra large only (0.32.1, the 360 px Extra-large
3225
+ * sweep; docs/text-size-harmonization.md §4): there an IconButton shows its label as
3226
+ * text, so the "icon" action is a word-wide pill, and "Refresh sessions" beside
3227
+ * "Sessions" ran 47 px past a 360 px phone. `false` keeps the action beside the title
3228
+ * at every size, for an action the caller knows stays narrow (`labelVisible={false}`).
3041
3229
  */
3042
3230
  stackAction?: boolean;
3043
3231
  }
@@ -3057,7 +3245,18 @@ export type CardActionProps = ComponentPropsWithoutRef<"div">;
3057
3245
  export type CardContentProps = ComponentPropsWithoutRef<"div">;
3058
3246
  export type CardFooterProps = ComponentPropsWithoutRef<"div">;
3059
3247
 
3060
- export function CardHeader({ className, stackAction = false, ...props }: CardHeaderProps) {
3248
+ /** {@link CardHeaderProps.stackAction}: one column under `sm`, and the action back into
3249
+ * the flow (DOM order, so after the description when it is written after it), at the
3250
+ * start edge. The descendant selector outranks CardAction's own placement classes. */
3251
+ const CARD_STACK_ACTION =
3252
+ "max-sm:grid-cols-1 max-sm:[&>[data-slot=card-action]]:col-start-1 max-sm:[&>[data-slot=card-action]]:row-span-1 max-sm:[&>[data-slot=card-action]]:row-start-auto max-sm:[&>[data-slot=card-action]]:justify-self-start";
3253
+ /** The same at Large and Extra large only — `stackAction` unset (see its note). A
3254
+ * `large:` class over the scaled `max-sm`, so it costs no render and is right on the
3255
+ * first paint. */
3256
+ const CARD_STACK_ACTION_AT_LARGE =
3257
+ "large:max-sm:grid-cols-1 large:max-sm:[&>[data-slot=card-action]]:col-start-1 large:max-sm:[&>[data-slot=card-action]]:row-span-1 large:max-sm:[&>[data-slot=card-action]]:row-start-auto large:max-sm:[&>[data-slot=card-action]]:justify-self-start";
3258
+
3259
+ export function CardHeader({ className, stackAction, ...props }: CardHeaderProps) {
3061
3260
  const padded = useContext(CardPaddedContext);
3062
3261
  const compact = useContext(CardDensityContext) === "compact";
3063
3262
  return (
@@ -3079,11 +3278,8 @@ export function CardHeader({ className, stackAction = false, ...props }: CardHea
3079
3278
  "grid auto-rows-min items-start gap-1.5 [--card-header-cols:1fr] has-data-[slot=card-action]:[--card-header-cols:1fr_auto] grid-cols-[var(--card-header-cols,1fr)]",
3080
3279
  padded ? "px-0 pt-0" : "px-6 pt-6",
3081
3280
  compact && "gap-0.5",
3082
- // One column under `sm`, and the action back into the flow (DOM order, so
3083
- // after the description when it is written after it), at the start edge.
3084
- // The descendant selector outranks CardAction's own placement classes.
3085
- stackAction &&
3086
- "max-sm:grid-cols-1 max-sm:[&>[data-slot=card-action]]:col-start-1 max-sm:[&>[data-slot=card-action]]:row-span-1 max-sm:[&>[data-slot=card-action]]:row-start-auto max-sm:[&>[data-slot=card-action]]:justify-self-start",
3281
+ stackAction === true && CARD_STACK_ACTION,
3282
+ stackAction === undefined && CARD_STACK_ACTION_AT_LARGE,
3087
3283
  className,
3088
3284
  )}
3089
3285
  {...props}
@@ -3544,7 +3740,7 @@ export interface TabsProps<T extends string>
3544
3740
  * `onRemove`, `onAdd` — means exactly what it means on the horizontal strip; the
3545
3741
  * badge moves to the row's end.
3546
3742
  *
3547
- * **On a phone ({@link PHONE_QUERY}) it becomes the horizontal strip**, `wrap` and
3743
+ * **On a phone (`usePhoneLayout()`) it becomes the horizontal strip**, `wrap` and
3548
3744
  * all, rather than staying a column. A side nav only works beside its content; on
3549
3745
  * a phone it has to go ABOVE it, and eight full-width rows there push the panel
3550
3746
  * the user picked below the fold on every visit — the two keksdose pages both
@@ -3573,7 +3769,8 @@ const TABLIST_CLASSES =
3573
3769
  // admin page's nav and the app's own sidebar mark "you are here" the same way.
3574
3770
  const TABLIST_VERTICAL_CLASSES = "flex flex-col gap-0.5";
3575
3771
  const TAB_VERTICAL_CLASSES =
3576
- "flex w-full items-center rounded-md px-3 py-2 text-start text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)]";
3772
+ FOCUS_RING_WIDTH +
3773
+ " flex w-full items-center rounded-md px-3 py-2 text-start text-sm font-medium transition-colors focus:outline-none focus-visible:ring-[var(--border-strong)]";
3577
3774
  const TAB_VERTICAL_ACTIVE_CLASSES = "bg-[var(--bg-active)] text-[var(--text-primary)]";
3578
3775
  const TAB_VERTICAL_INACTIVE_CLASSES =
3579
3776
  "text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]";
@@ -3586,7 +3783,8 @@ const TABLIST_WRAP_CLASSES =
3586
3783
  // than the strip cannot be scrolled into view — its end was simply off screen
3587
3784
  // (lenkbank L3). The label and detail truncate inside it instead; see `inner`.
3588
3785
  const TAB_CLASSES =
3589
- "max-w-full whitespace-nowrap px-3 py-2 text-sm font-medium transition-colors border-b-2 -mb-px focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)]";
3786
+ FOCUS_RING_WIDTH +
3787
+ " max-w-full whitespace-nowrap px-3 py-2 text-sm font-medium transition-colors border-b-2 -mb-px focus:outline-none focus-visible:ring-[var(--border-strong)]";
3590
3788
  const TAB_ACTIVE_CLASSES = "border-[var(--text-primary)] text-[var(--text-primary)]";
3591
3789
  const TAB_INACTIVE_CLASSES =
3592
3790
  "border-transparent text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:border-[var(--border-strong)]";
@@ -3600,7 +3798,8 @@ const TAB_INACTIVE_CLASSES =
3600
3798
  // all ten have to stay readable; it is the FILL, not the text weight, that says
3601
3799
  // which one is open.
3602
3800
  const TAB_WRAP_CLASSES =
3603
- "max-w-full whitespace-nowrap rounded-md px-2.5 py-2 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)] md:rounded-none md:border-b-2 md:-mb-px md:bg-transparent md:px-3 md:py-2 md:text-sm";
3801
+ FOCUS_RING_WIDTH +
3802
+ " max-w-full whitespace-nowrap rounded-md px-2.5 py-2 text-xs font-medium transition-colors focus:outline-none focus-visible:ring-[var(--border-strong)] md:rounded-none md:border-b-2 md:-mb-px md:bg-transparent md:px-3 md:py-2 md:text-sm";
3604
3803
  const TAB_WRAP_ACTIVE_CLASSES =
3605
3804
  "bg-[var(--brand)] text-[var(--brand-contrast)] md:border-[var(--text-primary)] md:text-[var(--text-primary)]";
3606
3805
  const TAB_WRAP_INACTIVE_CLASSES =
@@ -3612,7 +3811,8 @@ const TAB_WRAP_INACTIVE_CLASSES =
3612
3811
  const TAB_REMOVABLE_CLASSES = "pe-8 md:pe-8";
3613
3812
  const TAB_EMPTY_CLASSES = "-mx-1.5 rounded border border-dashed border-[var(--border-strong)] px-1.5";
3614
3813
  const TAB_ADD_CLASSES =
3615
- "inline-flex items-center gap-1 whitespace-nowrap rounded-md px-3 py-2 text-sm font-medium text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)] disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4";
3814
+ FOCUS_RING_WIDTH +
3815
+ " inline-flex items-center gap-1 whitespace-nowrap rounded-md px-3 py-2 text-sm font-medium text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)] focus:outline-none focus-visible:ring-[var(--border-strong)] disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4";
3616
3816
  const TAB_ADD_WRAP_CLASSES = "px-2.5 text-xs md:px-3 md:text-sm";
3617
3817
  // `relative`: the phone's visually hidden label needs a positioned ancestor (see the
3618
3818
  // sr-only rule in CONTRIBUTING); `max-md:px-2` squares the lone "+".
@@ -3639,7 +3839,7 @@ export function Tabs<T extends string>({
3639
3839
  }: TabsProps<T>) {
3640
3840
  const text = useKitLabels("tabs", DEFAULT_TABS_LABELS, labels);
3641
3841
  // See `orientation`: a vertical strip is the horizontal one on a phone.
3642
- const phone = useMediaQuery(PHONE_QUERY, false);
3842
+ const phone = usePhoneLayout();
3643
3843
  const vertical = orientation === "vertical" && !phone;
3644
3844
  const stripRef = useRef<HTMLDivElement>(null);
3645
3845
  const fade = useStripFade(stripRef);
@@ -3836,7 +4036,7 @@ export function Tabs<T extends string>({
3836
4036
  word for a screen reader ("80 km/hloop 2"). */}{" "}
3837
4037
  <span
3838
4038
  className={cn(
3839
- "max-w-full truncate text-[11px] font-normal leading-tight text-[var(--text-muted)]",
4039
+ "max-w-full truncate text-caption font-normal leading-tight text-[var(--text-muted)]",
3840
4040
  // On a wrapped strip's filled chip (below `md`) the muted grey
3841
4041
  // was dark text on the brand fill — "loop 2" all but vanished
3842
4042
  // under the one label that most needed reading. It takes the