@eifi1/ui-kit 0.51.4 → 0.51.6

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 (759) hide show
  1. package/README.md +5 -5
  2. package/dist/account/account-settings-labels.js.map +1 -1
  3. package/dist/account/account-settings.js +11 -9
  4. package/dist/account/account-settings.js.map +1 -1
  5. package/dist/account/delete-account-setting.js +56 -65
  6. package/dist/account/delete-account-setting.js.map +1 -1
  7. package/dist/account/email-change-setting-labels.js.map +1 -1
  8. package/dist/account/email-change-setting.js.map +1 -1
  9. package/dist/account/passkeys-setting.js.map +1 -1
  10. package/dist/account/sessions-setting.js.map +1 -1
  11. package/dist/admin/admin-action-confirm.js +1 -3
  12. package/dist/admin/admin-action-confirm.js.map +1 -1
  13. package/dist/admin/admin-action-log.js.map +1 -1
  14. package/dist/admin/admin-area-labels.js +4 -1
  15. package/dist/admin/admin-area-labels.js.map +1 -1
  16. package/dist/admin/admin-overview.js +19 -11
  17. package/dist/admin/admin-overview.js.map +1 -1
  18. package/dist/admin/admin-sections.js.map +1 -1
  19. package/dist/admin/admin-tool-links.d.ts +1 -1
  20. package/dist/admin/admin-tool-links.js +7 -1
  21. package/dist/admin/admin-tool-links.js.map +1 -1
  22. package/dist/admin/invitations-panel.js +2 -13
  23. package/dist/admin/invitations-panel.js.map +1 -1
  24. package/dist/admin/mail-test-card.js.map +1 -1
  25. package/dist/admin/reviewer-scope-editor.js +4 -11
  26. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  27. package/dist/admin/role-select.js.map +1 -1
  28. package/dist/admin/user-roster.js +1 -9
  29. package/dist/admin/user-roster.js.map +1 -1
  30. package/dist/auth/accept-invitation.js.map +1 -1
  31. package/dist/auth/auth-errors.js.map +1 -1
  32. package/dist/auth/forgot-password-form.js +23 -31
  33. package/dist/auth/forgot-password-form.js.map +1 -1
  34. package/dist/auth/register-form.js.map +1 -1
  35. package/dist/auth/reset-password-form.js.map +1 -1
  36. package/dist/auth/sign-in-form.js +13 -5
  37. package/dist/auth/sign-in-form.js.map +1 -1
  38. package/dist/auth/verify-email.js +1 -8
  39. package/dist/auth/verify-email.js.map +1 -1
  40. package/dist/billing/billing-banner.js.map +1 -1
  41. package/dist/billing/billing-labels.js.map +1 -1
  42. package/dist/billing/checkout-processing.js.map +1 -1
  43. package/dist/billing/plan-card.js +91 -85
  44. package/dist/billing/plan-card.js.map +1 -1
  45. package/dist/billing/plan-limit-toast.js.map +1 -1
  46. package/dist/billing/plan-limit.js.map +1 -1
  47. package/dist/billing/plan-price.js.map +1 -1
  48. package/dist/billing/plan-switch.js.map +1 -1
  49. package/dist/billing/subscription-actions.js.map +1 -1
  50. package/dist/bin/eifi1-pay-page.mjs +6 -1
  51. package/dist/components/account-chips.js.map +1 -1
  52. package/dist/components/alert-banner.js +1 -5
  53. package/dist/components/alert-banner.js.map +1 -1
  54. package/dist/components/amount-input.js +29 -12
  55. package/dist/components/amount-input.js.map +1 -1
  56. package/dist/components/authed-image.js.map +1 -1
  57. package/dist/components/autocomplete.js.map +1 -1
  58. package/dist/components/bulk-action-bar.js +1 -4
  59. package/dist/components/bulk-action-bar.js.map +1 -1
  60. package/dist/components/button-group.js.map +1 -1
  61. package/dist/components/button-look.js.map +1 -1
  62. package/dist/components/button.js +1 -7
  63. package/dist/components/button.js.map +1 -1
  64. package/dist/components/calculator.js +5 -5
  65. package/dist/components/calculator.js.map +1 -1
  66. package/dist/components/calendar-heatmap.js +4 -10
  67. package/dist/components/calendar-heatmap.js.map +1 -1
  68. package/dist/components/card.js +1 -8
  69. package/dist/components/card.js.map +1 -1
  70. package/dist/components/chart-zoom-core.js +3 -17
  71. package/dist/components/chart-zoom-core.js.map +1 -1
  72. package/dist/components/chart.d.ts +1 -1
  73. package/dist/components/chart.js +4 -22
  74. package/dist/components/chart.js.map +1 -1
  75. package/dist/components/checkbox-group.js +1 -4
  76. package/dist/components/checkbox-group.js.map +1 -1
  77. package/dist/components/chip-input.js +4 -1
  78. package/dist/components/chip-input.js.map +1 -1
  79. package/dist/components/chip.js +10 -5
  80. package/dist/components/chip.js.map +1 -1
  81. package/dist/components/choice-card.js +58 -74
  82. package/dist/components/choice-card.js.map +1 -1
  83. package/dist/components/column-mapper-labels.js.map +1 -1
  84. package/dist/components/column-mapper.js +167 -227
  85. package/dist/components/column-mapper.js.map +1 -1
  86. package/dist/components/combobox-core.js +1 -4
  87. package/dist/components/combobox-core.js.map +1 -1
  88. package/dist/components/combobox.js +1 -4
  89. package/dist/components/combobox.js.map +1 -1
  90. package/dist/components/confirm-dialog.js.map +1 -1
  91. package/dist/components/copy-button.js.map +1 -1
  92. package/dist/components/country-select.d.ts +5 -4
  93. package/dist/components/country-select.js +3 -19
  94. package/dist/components/country-select.js.map +1 -1
  95. package/dist/components/currency-option-list.js +10 -8
  96. package/dist/components/currency-option-list.js.map +1 -1
  97. package/dist/components/currency-select.js +1 -6
  98. package/dist/components/currency-select.js.map +1 -1
  99. package/dist/components/danger-confirm.js.map +1 -1
  100. package/dist/components/data-table-cells.js.map +1 -1
  101. package/dist/components/data-table-column-settings.js.map +1 -1
  102. package/dist/components/data-table-filter-popover.js +2 -18
  103. package/dist/components/data-table-filter-popover.js.map +1 -1
  104. package/dist/components/data-table-filters.js.map +1 -1
  105. package/dist/components/data-table-fit.js.map +1 -1
  106. package/dist/components/data-table-groups.js +3 -1
  107. package/dist/components/data-table-groups.js.map +1 -1
  108. package/dist/components/data-table-head.js +44 -56
  109. package/dist/components/data-table-head.js.map +1 -1
  110. package/dist/components/data-table-mobile.js +31 -44
  111. package/dist/components/data-table-mobile.js.map +1 -1
  112. package/dist/components/data-table-pagination.js.map +1 -1
  113. package/dist/components/data-table-row-actions.js +19 -16
  114. package/dist/components/data-table-row-actions.js.map +1 -1
  115. package/dist/components/data-table-row.js.map +1 -1
  116. package/dist/components/data-table-shared.js.map +1 -1
  117. package/dist/components/data-table-sort.js.map +1 -1
  118. package/dist/components/data-table-virtual.js +4 -1
  119. package/dist/components/data-table-virtual.js.map +1 -1
  120. package/dist/components/data-table.js +30 -39
  121. package/dist/components/data-table.js.map +1 -1
  122. package/dist/components/date-picker.js +1 -6
  123. package/dist/components/date-picker.js.map +1 -1
  124. package/dist/components/date-range-picker.js.map +1 -1
  125. package/dist/components/description-list.js +10 -10
  126. package/dist/components/description-list.js.map +1 -1
  127. package/dist/components/dialog-frame.js +9 -2
  128. package/dist/components/dialog-frame.js.map +1 -1
  129. package/dist/components/disclosure.js +17 -27
  130. package/dist/components/disclosure.js.map +1 -1
  131. package/dist/components/dropdown.js +25 -35
  132. package/dist/components/dropdown.js.map +1 -1
  133. package/dist/components/empty-state.js.map +1 -1
  134. package/dist/components/entity-combobox.js.map +1 -1
  135. package/dist/components/error-boundary.js.map +1 -1
  136. package/dist/components/field-base.js +5 -5
  137. package/dist/components/field-base.js.map +1 -1
  138. package/dist/components/field-parts.d.ts +1 -1
  139. package/dist/components/field-parts.js +10 -14
  140. package/dist/components/field-parts.js.map +1 -1
  141. package/dist/components/field-sync.js +8 -10
  142. package/dist/components/field-sync.js.map +1 -1
  143. package/dist/components/file-button.js.map +1 -1
  144. package/dist/components/file-dropzone.js.map +1 -1
  145. package/dist/components/floating-panel.js +4 -1
  146. package/dist/components/floating-panel.js.map +1 -1
  147. package/dist/components/focus-ring.js.map +1 -1
  148. package/dist/components/form-actions.js.map +1 -1
  149. package/dist/components/grouped-picker.js +6 -8
  150. package/dist/components/grouped-picker.js.map +1 -1
  151. package/dist/components/hover-menu.js +1 -4
  152. package/dist/components/hover-menu.js.map +1 -1
  153. package/dist/components/iban-input-labels.js.map +1 -1
  154. package/dist/components/icon-button.js +4 -1
  155. package/dist/components/icon-button.js.map +1 -1
  156. package/dist/components/icon-picker.js +2 -7
  157. package/dist/components/icon-picker.js.map +1 -1
  158. package/dist/components/image-grid.js +41 -50
  159. package/dist/components/image-grid.js.map +1 -1
  160. package/dist/components/inline-edit-field.js.map +1 -1
  161. package/dist/components/inline-entity-combobox.js +2 -8
  162. package/dist/components/inline-entity-combobox.js.map +1 -1
  163. package/dist/components/input.js +14 -2
  164. package/dist/components/input.js.map +1 -1
  165. package/dist/components/jump-calendar.js +1 -7
  166. package/dist/components/jump-calendar.js.map +1 -1
  167. package/dist/components/legal-labels.js.map +1 -1
  168. package/dist/components/legal-page.js +14 -2
  169. package/dist/components/legal-page.js.map +1 -1
  170. package/dist/components/legal.js +12 -2
  171. package/dist/components/legal.js.map +1 -1
  172. package/dist/components/lightbox.js.map +1 -1
  173. package/dist/components/line-items.js +26 -36
  174. package/dist/components/line-items.js.map +1 -1
  175. package/dist/components/list.js +22 -10
  176. package/dist/components/list.js.map +1 -1
  177. package/dist/components/measured-grid.js +29 -53
  178. package/dist/components/measured-grid.js.map +1 -1
  179. package/dist/components/menu-item.js.map +1 -1
  180. package/dist/components/mini-calendar.js +13 -31
  181. package/dist/components/mini-calendar.js.map +1 -1
  182. package/dist/components/money-field.js.map +1 -1
  183. package/dist/components/month-picker.js +10 -21
  184. package/dist/components/month-picker.js.map +1 -1
  185. package/dist/components/multi-entity-combobox.js +3 -16
  186. package/dist/components/multi-entity-combobox.js.map +1 -1
  187. package/dist/components/multi-select.js.map +1 -1
  188. package/dist/components/nav-pills.js +10 -1
  189. package/dist/components/nav-pills.js.map +1 -1
  190. package/dist/components/number-field.js.map +1 -1
  191. package/dist/components/number-input.js +1 -7
  192. package/dist/components/number-input.js.map +1 -1
  193. package/dist/components/numpad-sheet.js +1 -4
  194. package/dist/components/numpad-sheet.js.map +1 -1
  195. package/dist/components/one-time-code-input.js +1 -7
  196. package/dist/components/one-time-code-input.js.map +1 -1
  197. package/dist/components/page-contents.js +13 -22
  198. package/dist/components/page-contents.js.map +1 -1
  199. package/dist/components/page-header.js +1 -11
  200. package/dist/components/page-header.js.map +1 -1
  201. package/dist/components/password-strength-labels.js.map +1 -1
  202. package/dist/components/password-strength.js +2 -8
  203. package/dist/components/password-strength.js.map +1 -1
  204. package/dist/components/phone-input.js +42 -57
  205. package/dist/components/phone-input.js.map +1 -1
  206. package/dist/components/picker-sheet.js.map +1 -1
  207. package/dist/components/pie-chart.js +1 -8
  208. package/dist/components/pie-chart.js.map +1 -1
  209. package/dist/components/popover.js.map +1 -1
  210. package/dist/components/progress-bar.js +16 -9
  211. package/dist/components/progress-bar.js.map +1 -1
  212. package/dist/components/qr-code.js +1 -8
  213. package/dist/components/qr-code.js.map +1 -1
  214. package/dist/components/row-actions.js +5 -1
  215. package/dist/components/row-actions.js.map +1 -1
  216. package/dist/components/search-field.js.map +1 -1
  217. package/dist/components/select.js +28 -38
  218. package/dist/components/select.js.map +1 -1
  219. package/dist/components/separator.js +1 -5
  220. package/dist/components/separator.js.map +1 -1
  221. package/dist/components/series-chart-axes.js +4 -1
  222. package/dist/components/series-chart-axes.js.map +1 -1
  223. package/dist/components/series-chart-budget.js.map +1 -1
  224. package/dist/components/series-chart-ticks.js.map +1 -1
  225. package/dist/components/series-chart.js +29 -45
  226. package/dist/components/series-chart.js.map +1 -1
  227. package/dist/components/settings-fields.js +21 -17
  228. package/dist/components/settings-fields.js.map +1 -1
  229. package/dist/components/share-card.js +4 -9
  230. package/dist/components/share-card.js.map +1 -1
  231. package/dist/components/sign-chip.js.map +1 -1
  232. package/dist/components/signature-pad.js +4 -13
  233. package/dist/components/signature-pad.js.map +1 -1
  234. package/dist/components/signed-amount.js +12 -5
  235. package/dist/components/signed-amount.js.map +1 -1
  236. package/dist/components/skeleton.js +2 -17
  237. package/dist/components/skeleton.js.map +1 -1
  238. package/dist/components/slider.js +13 -36
  239. package/dist/components/slider.js.map +1 -1
  240. package/dist/components/sparkline.js.map +1 -1
  241. package/dist/components/stat-tile.js +14 -10
  242. package/dist/components/stat-tile.js.map +1 -1
  243. package/dist/components/swatch-picker.js +14 -4
  244. package/dist/components/swatch-picker.js.map +1 -1
  245. package/dist/components/swipeable-row.d.ts +1 -1
  246. package/dist/components/swipeable-row.js +1 -8
  247. package/dist/components/swipeable-row.js.map +1 -1
  248. package/dist/components/table-skeleton.js +4 -1
  249. package/dist/components/table-skeleton.js.map +1 -1
  250. package/dist/components/table.js +12 -9
  251. package/dist/components/table.js.map +1 -1
  252. package/dist/components/tabs.js +5 -24
  253. package/dist/components/tabs.js.map +1 -1
  254. package/dist/components/text-link.js.map +1 -1
  255. package/dist/components/text.js.map +1 -1
  256. package/dist/components/tile-radio.js +1 -7
  257. package/dist/components/tile-radio.js.map +1 -1
  258. package/dist/components/time-input.js.map +1 -1
  259. package/dist/components/toast.js.map +1 -1
  260. package/dist/components/toggle-group.js +6 -1
  261. package/dist/components/toggle-group.js.map +1 -1
  262. package/dist/components/toggle-legend.js +2 -11
  263. package/dist/components/toggle-legend.js.map +1 -1
  264. package/dist/components/tooltip.js +2 -22
  265. package/dist/components/tooltip.js.map +1 -1
  266. package/dist/components/translation-review-editor.js +1 -4
  267. package/dist/components/translation-review-editor.js.map +1 -1
  268. package/dist/components/translation-review-parts.d.ts +1 -1
  269. package/dist/components/translation-review-parts.js +7 -1
  270. package/dist/components/translation-review-parts.js.map +1 -1
  271. package/dist/components/translation-review.js +18 -7
  272. package/dist/components/translation-review.js.map +1 -1
  273. package/dist/components/tree-view.js +14 -10
  274. package/dist/components/tree-view.js.map +1 -1
  275. package/dist/components/treemap.d.ts +24 -2
  276. package/dist/components/treemap.js +28 -4
  277. package/dist/components/treemap.js.map +1 -1
  278. package/dist/components/trigger-aria.js.map +1 -1
  279. package/dist/components/use-table-state.js +14 -11
  280. package/dist/components/use-table-state.js.map +1 -1
  281. package/dist/components/user-avatar.js +3 -26
  282. package/dist/components/user-avatar.js.map +1 -1
  283. package/dist/demo/demo-start.js +8 -4
  284. package/dist/demo/demo-start.js.map +1 -1
  285. package/dist/eslint/index.js +1 -1
  286. package/dist/feedback/feedback-attachment-labels.js.map +1 -1
  287. package/dist/feedback/feedback-attachment.js +1 -8
  288. package/dist/feedback/feedback-attachment.js.map +1 -1
  289. package/dist/feedback/feedback-capture.d.ts +1 -1
  290. package/dist/feedback/feedback-capture.js +3 -1
  291. package/dist/feedback/feedback-capture.js.map +1 -1
  292. package/dist/feedback/feedback-comment-thread.js +27 -13
  293. package/dist/feedback/feedback-comment-thread.js.map +1 -1
  294. package/dist/feedback/feedback-context.js +1 -4
  295. package/dist/feedback/feedback-context.js.map +1 -1
  296. package/dist/feedback/feedback-dialog.js.map +1 -1
  297. package/dist/feedback/feedback-inbox.js.map +1 -1
  298. package/dist/feedback/feedback-public-list.js +7 -1
  299. package/dist/feedback/feedback-public-list.js.map +1 -1
  300. package/dist/feedback/feedback-record.js +1 -5
  301. package/dist/feedback/feedback-record.js.map +1 -1
  302. package/dist/feedback/feedback-replies.js +25 -10
  303. package/dist/feedback/feedback-replies.js.map +1 -1
  304. package/dist/feedback/feedback-row-detail.js.map +1 -1
  305. package/dist/feedback/feedback-status-undo.js.map +1 -1
  306. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  307. package/dist/feedback/feedback-table.js +4 -1
  308. package/dist/feedback/feedback-table.js.map +1 -1
  309. package/dist/feedback/feedback-thread.js +2 -13
  310. package/dist/feedback/feedback-thread.js.map +1 -1
  311. package/dist/feedback/feedback-vocabulary.js.map +1 -1
  312. package/dist/feedback/feedback-vote-button.js.map +1 -1
  313. package/dist/hooks/use-anchored-rect.js.map +1 -1
  314. package/dist/hooks/use-document-title.js.map +1 -1
  315. package/dist/hooks/use-file-drop.js.map +1 -1
  316. package/dist/hooks/use-file-picker-labels.js.map +1 -1
  317. package/dist/hooks/use-file-picker.js.map +1 -1
  318. package/dist/hooks/use-focus-trap.js.map +1 -1
  319. package/dist/hooks/use-overlay-history.js +10 -5
  320. package/dist/hooks/use-overlay-history.js.map +1 -1
  321. package/dist/hooks/use-row-swipe.d.ts +1 -1
  322. package/dist/hooks/use-row-swipe.js +1 -6
  323. package/dist/hooks/use-row-swipe.js.map +1 -1
  324. package/dist/i18n/defaults.js +1 -4
  325. package/dist/i18n/defaults.js.map +1 -1
  326. package/dist/i18n/german.js +4 -1
  327. package/dist/i18n/german.js.map +1 -1
  328. package/dist/i18n/kit-labels.js +2 -8
  329. package/dist/i18n/kit-labels.js.map +1 -1
  330. package/dist/i18n/languages.js.map +1 -1
  331. package/dist/i18n/locales/en.js +4 -1
  332. package/dist/i18n/locales/en.js.map +2 -2
  333. package/dist/i18n/locales/es.js +4 -1
  334. package/dist/i18n/locales/es.js.map +1 -1
  335. package/dist/i18n/locales/fr.js +4 -1
  336. package/dist/i18n/locales/fr.js.map +1 -1
  337. package/dist/i18n/locales/hu.js +4 -1
  338. package/dist/i18n/locales/hu.js.map +1 -1
  339. package/dist/i18n/locales/it.js +4 -1
  340. package/dist/i18n/locales/it.js.map +1 -1
  341. package/dist/i18n/locales/zh.js.map +1 -1
  342. package/dist/i18n/plurals.js.map +1 -1
  343. package/dist/i18n/review.js +13 -7
  344. package/dist/i18n/review.js.map +1 -1
  345. package/dist/i18n/shared-labels.js.map +1 -1
  346. package/dist/index.d.ts +16 -16
  347. package/dist/index.js +40 -23
  348. package/dist/index.js.map +1 -1
  349. package/dist/landing/access.js +1 -4
  350. package/dist/landing/access.js.map +1 -1
  351. package/dist/landing/landing-sections.js +35 -38
  352. package/dist/landing/landing-sections.js.map +1 -1
  353. package/dist/landing/page-seo.js.map +1 -1
  354. package/dist/lib/calc.js.map +1 -1
  355. package/dist/lib/clipboard-minus.js.map +1 -1
  356. package/dist/lib/dates.js.map +1 -1
  357. package/dist/lib/decimal-marks.js.map +1 -1
  358. package/dist/lib/device-settings.js +2 -1
  359. package/dist/lib/device-settings.js.map +1 -1
  360. package/dist/lib/direction.js.map +1 -1
  361. package/dist/lib/iban.js.map +1 -1
  362. package/dist/lib/logger.js +1 -5
  363. package/dist/lib/logger.js.map +1 -1
  364. package/dist/lib/person-name.js.map +1 -1
  365. package/dist/lib/phone.js.map +1 -1
  366. package/dist/lib/qr-encode.js +344 -8
  367. package/dist/lib/qr-encode.js.map +1 -1
  368. package/dist/lib/session-refresh.js.map +1 -1
  369. package/dist/lib/table-text.js +15 -3
  370. package/dist/lib/table-text.js.map +1 -1
  371. package/dist/lib/text-match.d.ts +4 -0
  372. package/dist/lib/text-match.js.map +1 -1
  373. package/dist/lib/translation-review.js.map +1 -1
  374. package/dist/notifications/notification-inbox.js +63 -44
  375. package/dist/notifications/notification-inbox.js.map +1 -1
  376. package/dist/notifications/notification-parts.js.map +1 -1
  377. package/dist/notifications/notification-types-setting.js.map +1 -1
  378. package/dist/notifications/notifications-labels.js.map +1 -1
  379. package/dist/notifications/push-device-setting.js +6 -1
  380. package/dist/notifications/push-device-setting.js.map +1 -1
  381. package/dist/notifications/push-devices-setting.js.map +1 -1
  382. package/dist/notifications/push-worker.js.map +1 -1
  383. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  384. package/dist/notifications/use-push-device.js +4 -1
  385. package/dist/notifications/use-push-device.js.map +1 -1
  386. package/dist/offline/pwa-update-prompt.d.ts +1 -1
  387. package/dist/offline/pwa-update-prompt.js +7 -1
  388. package/dist/offline/pwa-update-prompt.js.map +1 -1
  389. package/dist/offline/query-persistence.js.map +1 -1
  390. package/dist/offline.d.ts +1 -1
  391. package/dist/offline.js +6 -1
  392. package/dist/offline.js.map +1 -1
  393. package/dist/pay/pay.css +8 -1
  394. package/dist/rhf/fields.d.ts +1 -2
  395. package/dist/rhf/fields.js +1 -10
  396. package/dist/rhf/fields.js.map +1 -1
  397. package/dist/rhf/form-core.js +1 -4
  398. package/dist/rhf/form-core.js.map +1 -1
  399. package/dist/rhf/line-items.js.map +1 -1
  400. package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
  401. package/dist/search/command-palette.js +71 -62
  402. package/dist/search/command-palette.js.map +1 -1
  403. package/dist/search/global-search.js +34 -5
  404. package/dist/search/global-search.js.map +1 -1
  405. package/dist/search/search-index.js.map +1 -1
  406. package/dist/settings/device-reset-setting.js +1 -6
  407. package/dist/settings/device-reset-setting.js.map +1 -1
  408. package/dist/settings/settings-heading.js.map +1 -1
  409. package/dist/settings/settings-layout.js +9 -20
  410. package/dist/settings/settings-layout.js.map +1 -1
  411. package/dist/settings/use-settings-route.js.map +1 -1
  412. package/dist/shell/app-shell.js +62 -31
  413. package/dist/shell/app-shell.js.map +1 -1
  414. package/dist/shell/auth-layout.js +1 -10
  415. package/dist/shell/auth-layout.js.map +1 -1
  416. package/dist/shell/dev-menu.js +12 -6
  417. package/dist/shell/dev-menu.js.map +1 -1
  418. package/dist/shell/option-switcher-menu.js.map +1 -1
  419. package/dist/shell/sync-status-indicator.js +7 -6
  420. package/dist/shell/sync-status-indicator.js.map +1 -1
  421. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  422. package/dist/shell/topbar-action-menu.js.map +1 -1
  423. package/dist/shell/topbar-controls.js +1 -10
  424. package/dist/shell/topbar-controls.js.map +1 -1
  425. package/dist/shell/tour-menu.js +9 -1
  426. package/dist/shell/tour-menu.js.map +1 -1
  427. package/dist/sync/db.js.map +1 -1
  428. package/dist/sync/engine.js +24 -6
  429. package/dist/sync/engine.js.map +1 -1
  430. package/dist/sync.d.ts +2 -2
  431. package/dist/sync.js +7 -1
  432. package/dist/sync.js.map +1 -1
  433. package/dist/testing/css-variables.js.map +1 -1
  434. package/dist/testing/index.d.ts +1 -1
  435. package/dist/testing/index.js +1 -3
  436. package/dist/testing/index.js.map +1 -1
  437. package/dist/testing/react.js.map +1 -1
  438. package/dist/testing/viewport.js +7 -4
  439. package/dist/testing/viewport.js.map +1 -1
  440. package/dist/theme/contrast-tokens.js +13 -2
  441. package/dist/theme/contrast-tokens.js.map +1 -1
  442. package/dist/theme/contrast.d.ts +2 -2
  443. package/dist/theme/palette-derive.d.ts +1 -1
  444. package/dist/theme/palette-derive.js +18 -29
  445. package/dist/theme/palette-derive.js.map +1 -1
  446. package/dist/theme/palette-presets.js.map +1 -1
  447. package/dist/theme/palette-store.js.map +1 -1
  448. package/dist/theme/text-size.js.map +1 -1
  449. package/dist/theme/theme-store.js +1 -3
  450. package/dist/theme/theme-store.js.map +1 -1
  451. package/dist/tour/tour-catalogue.js.map +1 -1
  452. package/dist/tour/tour-dock.js +5 -1
  453. package/dist/tour/tour-dock.js.map +1 -1
  454. package/dist/tour/tour-engine.js +1 -4
  455. package/dist/tour/tour-engine.js.map +1 -1
  456. package/dist/tour/tour.js +21 -12
  457. package/dist/tour/tour.js.map +1 -1
  458. package/dist/tour/use-tour-progress.js.map +1 -1
  459. package/dist/tour/use-tour-url-sync.js.map +1 -1
  460. package/dist/wizard/stepper-nav.js +3 -24
  461. package/dist/wizard/stepper-nav.js.map +1 -1
  462. package/dist/wizard/types.js.map +1 -1
  463. package/dist/wizard/use-wizard.js +24 -28
  464. package/dist/wizard/use-wizard.js.map +1 -1
  465. package/dist/wizard/validation.js.map +1 -1
  466. package/dist/wizard/wizard-context.js.map +1 -1
  467. package/dist/wizard/wizard-step.d.ts +1 -1
  468. package/dist/wizard/wizard-step.js +1 -8
  469. package/dist/wizard/wizard-step.js.map +1 -1
  470. package/dist/wizard/wizard-summary.js.map +1 -1
  471. package/package.json +5 -2
  472. package/src/account/account-settings-labels.ts +1 -2
  473. package/src/account/account-settings.tsx +11 -17
  474. package/src/account/delete-account-setting.tsx +1 -6
  475. package/src/account/email-change-setting-labels.ts +1 -2
  476. package/src/account/email-change-setting.tsx +3 -1
  477. package/src/account/passkeys-setting.tsx +1 -3
  478. package/src/account/sessions-setting.tsx +4 -2
  479. package/src/admin/admin-action-confirm.tsx +4 -7
  480. package/src/admin/admin-action-log.tsx +29 -29
  481. package/src/admin/admin-area-labels.ts +4 -1
  482. package/src/admin/admin-overview.tsx +8 -2
  483. package/src/admin/admin-sections.tsx +3 -1
  484. package/src/admin/admin-tool-links.tsx +7 -1
  485. package/src/admin/invitations-panel.tsx +5 -11
  486. package/src/admin/mail-test-card.tsx +1 -2
  487. package/src/admin/reviewer-scope-editor.tsx +4 -9
  488. package/src/admin/role-select.tsx +4 -2
  489. package/src/admin/user-roster.tsx +2 -11
  490. package/src/auth/accept-invitation.tsx +1 -7
  491. package/src/auth/auth-errors.ts +1 -6
  492. package/src/auth/forgot-password-form.tsx +2 -7
  493. package/src/auth/register-form.tsx +7 -8
  494. package/src/auth/reset-password-form.tsx +2 -5
  495. package/src/auth/sign-in-form.tsx +15 -14
  496. package/src/auth/verify-email.tsx +2 -7
  497. package/src/billing/billing-banner.tsx +3 -7
  498. package/src/billing/billing-labels.ts +6 -4
  499. package/src/billing/checkout-processing.ts +0 -1
  500. package/src/billing/plan-card.tsx +9 -8
  501. package/src/billing/plan-limit-toast.tsx +1 -2
  502. package/src/billing/plan-limit.ts +1 -2
  503. package/src/billing/plan-price.ts +1 -4
  504. package/src/billing/plan-switch.tsx +1 -2
  505. package/src/billing/subscription-actions.tsx +5 -2
  506. package/src/components/account-chips.tsx +1 -7
  507. package/src/components/alert-banner.tsx +2 -8
  508. package/src/components/amount-input.tsx +206 -178
  509. package/src/components/authed-image.tsx +7 -4
  510. package/src/components/autocomplete.tsx +7 -9
  511. package/src/components/bulk-action-bar.tsx +2 -7
  512. package/src/components/button-group.tsx +1 -2
  513. package/src/components/button-look.ts +6 -17
  514. package/src/components/button.tsx +1 -4
  515. package/src/components/calculator.tsx +6 -9
  516. package/src/components/calendar-heatmap.tsx +5 -10
  517. package/src/components/card.tsx +9 -13
  518. package/src/components/chart-zoom-core.tsx +6 -33
  519. package/src/components/chart.tsx +8 -31
  520. package/src/components/checkbox-group.tsx +1 -4
  521. package/src/components/chip-input.tsx +6 -6
  522. package/src/components/chip.tsx +14 -26
  523. package/src/components/choice-card.tsx +4 -18
  524. package/src/components/column-mapper-labels.ts +3 -10
  525. package/src/components/column-mapper.tsx +33 -122
  526. package/src/components/combobox-core.tsx +12 -42
  527. package/src/components/combobox.tsx +2 -6
  528. package/src/components/confirm-dialog.tsx +1 -3
  529. package/src/components/copy-button.tsx +2 -4
  530. package/src/components/country-select.tsx +9 -21
  531. package/src/components/currency-option-list.tsx +13 -14
  532. package/src/components/currency-select.tsx +2 -7
  533. package/src/components/danger-confirm.tsx +8 -9
  534. package/src/components/data-table-cells.tsx +1 -2
  535. package/src/components/data-table-column-settings.tsx +1 -3
  536. package/src/components/data-table-filter-popover.tsx +5 -19
  537. package/src/components/data-table-filters.ts +1 -6
  538. package/src/components/data-table-fit.ts +1 -3
  539. package/src/components/data-table-groups.tsx +3 -1
  540. package/src/components/data-table-head.tsx +27 -45
  541. package/src/components/data-table-mobile.tsx +99 -108
  542. package/src/components/data-table-pagination.tsx +2 -6
  543. package/src/components/data-table-row-actions.tsx +25 -24
  544. package/src/components/data-table-row.tsx +16 -17
  545. package/src/components/data-table-shared.tsx +10 -16
  546. package/src/components/data-table-sort.ts +1 -2
  547. package/src/components/data-table-virtual.ts +9 -4
  548. package/src/components/data-table.tsx +489 -501
  549. package/src/components/date-picker.tsx +1 -7
  550. package/src/components/date-range-picker.tsx +3 -13
  551. package/src/components/description-list.tsx +7 -13
  552. package/src/components/dialog-frame.tsx +12 -3
  553. package/src/components/disclosure.tsx +8 -12
  554. package/src/components/dropdown.tsx +3 -11
  555. package/src/components/empty-state.tsx +1 -3
  556. package/src/components/entity-combobox.tsx +2 -6
  557. package/src/components/error-boundary.tsx +7 -7
  558. package/src/components/field-base.tsx +8 -14
  559. package/src/components/field-parts.tsx +12 -23
  560. package/src/components/field-sync.tsx +7 -11
  561. package/src/components/file-button.tsx +7 -3
  562. package/src/components/file-dropzone.tsx +7 -12
  563. package/src/components/floating-panel.tsx +44 -51
  564. package/src/components/focus-ring.ts +1 -2
  565. package/src/components/form-actions.tsx +1 -4
  566. package/src/components/grouped-picker.tsx +6 -8
  567. package/src/components/hover-menu.tsx +1 -4
  568. package/src/components/iban-input-labels.ts +1 -2
  569. package/src/components/icon-button.tsx +46 -48
  570. package/src/components/icon-picker.tsx +4 -17
  571. package/src/components/image-grid.tsx +1 -6
  572. package/src/components/inline-edit-field.tsx +1 -5
  573. package/src/components/inline-entity-combobox.tsx +6 -21
  574. package/src/components/input.tsx +14 -2
  575. package/src/components/jump-calendar.tsx +2 -6
  576. package/src/components/legal-labels.ts +1 -2
  577. package/src/components/legal-page.tsx +18 -7
  578. package/src/components/legal.tsx +14 -5
  579. package/src/components/lightbox.tsx +1 -3
  580. package/src/components/line-items.tsx +7 -15
  581. package/src/components/list.tsx +29 -25
  582. package/src/components/measured-grid.tsx +8 -30
  583. package/src/components/menu-item.tsx +1 -3
  584. package/src/components/mini-calendar.tsx +129 -159
  585. package/src/components/money-field.tsx +4 -2
  586. package/src/components/month-picker.tsx +14 -21
  587. package/src/components/multi-entity-combobox.tsx +6 -26
  588. package/src/components/multi-select.tsx +5 -9
  589. package/src/components/nav-pills.tsx +10 -2
  590. package/src/components/number-field.tsx +18 -15
  591. package/src/components/number-input.tsx +3 -19
  592. package/src/components/numpad-sheet.tsx +9 -13
  593. package/src/components/one-time-code-input.tsx +6 -11
  594. package/src/components/page-contents.tsx +2 -9
  595. package/src/components/page-header.tsx +5 -10
  596. package/src/components/password-strength-labels.ts +1 -2
  597. package/src/components/password-strength.tsx +6 -9
  598. package/src/components/phone-input.tsx +2 -13
  599. package/src/components/picker-sheet.tsx +2 -5
  600. package/src/components/pie-chart.tsx +6 -12
  601. package/src/components/popover.tsx +2 -1
  602. package/src/components/progress-bar.tsx +16 -25
  603. package/src/components/qr-code.tsx +2 -9
  604. package/src/components/row-actions.tsx +9 -1
  605. package/src/components/search-field.tsx +4 -5
  606. package/src/components/select.tsx +2 -10
  607. package/src/components/separator.tsx +1 -5
  608. package/src/components/series-chart-axes.ts +5 -5
  609. package/src/components/series-chart-budget.ts +1 -4
  610. package/src/components/series-chart-ticks.ts +1 -4
  611. package/src/components/series-chart.tsx +43 -83
  612. package/src/components/settings-fields.tsx +20 -15
  613. package/src/components/share-card.tsx +6 -14
  614. package/src/components/sign-chip.tsx +16 -16
  615. package/src/components/signature-pad.tsx +21 -13
  616. package/src/components/signed-amount.tsx +6 -14
  617. package/src/components/skeleton.tsx +2 -13
  618. package/src/components/slider.tsx +9 -20
  619. package/src/components/sparkline.tsx +2 -13
  620. package/src/components/stat-tile.tsx +13 -24
  621. package/src/components/swatch-picker.tsx +8 -3
  622. package/src/components/swipeable-row.tsx +2 -11
  623. package/src/components/table-skeleton.tsx +4 -1
  624. package/src/components/table.tsx +12 -8
  625. package/src/components/tabs.tsx +24 -39
  626. package/src/components/text-link.tsx +6 -6
  627. package/src/components/text.tsx +1 -2
  628. package/src/components/tile-radio.tsx +6 -19
  629. package/src/components/time-input.tsx +4 -5
  630. package/src/components/toast.tsx +3 -12
  631. package/src/components/toggle-group.tsx +9 -9
  632. package/src/components/toggle-legend.tsx +4 -16
  633. package/src/components/tooltip.tsx +9 -48
  634. package/src/components/translation-review-editor.tsx +4 -4
  635. package/src/components/translation-review-parts.tsx +7 -1
  636. package/src/components/translation-review.tsx +15 -16
  637. package/src/components/tree-view.tsx +17 -20
  638. package/src/components/treemap.tsx +59 -9
  639. package/src/components/trigger-aria.ts +1 -4
  640. package/src/components/use-table-state.ts +16 -17
  641. package/src/components/user-avatar.tsx +4 -21
  642. package/src/demo/demo-start.tsx +8 -4
  643. package/src/feedback/feedback-attachment-labels.ts +2 -1
  644. package/src/feedback/feedback-attachment.tsx +16 -29
  645. package/src/feedback/feedback-capture.ts +3 -1
  646. package/src/feedback/feedback-comment-thread.tsx +9 -6
  647. package/src/feedback/feedback-context.tsx +1 -4
  648. package/src/feedback/feedback-dialog.tsx +3 -4
  649. package/src/feedback/feedback-inbox.tsx +3 -9
  650. package/src/feedback/feedback-public-list.tsx +7 -1
  651. package/src/feedback/feedback-record.ts +6 -14
  652. package/src/feedback/feedback-replies.tsx +10 -5
  653. package/src/feedback/feedback-row-detail.tsx +2 -5
  654. package/src/feedback/feedback-status-undo.ts +1 -4
  655. package/src/feedback/feedback-submit-dialog.tsx +4 -4
  656. package/src/feedback/feedback-table.tsx +12 -8
  657. package/src/feedback/feedback-thread.tsx +4 -16
  658. package/src/feedback/feedback-vocabulary.ts +72 -82
  659. package/src/feedback/feedback-vote-button.tsx +9 -8
  660. package/src/hooks/use-anchored-rect.ts +1 -4
  661. package/src/hooks/use-document-title.ts +3 -1
  662. package/src/hooks/use-file-drop.ts +1 -3
  663. package/src/hooks/use-file-picker-labels.ts +1 -2
  664. package/src/hooks/use-file-picker.tsx +1 -5
  665. package/src/hooks/use-focus-trap.ts +1 -2
  666. package/src/hooks/use-overlay-history.ts +10 -5
  667. package/src/hooks/use-row-swipe.ts +1 -6
  668. package/src/i18n/defaults.ts +1 -4
  669. package/src/i18n/german.ts +33 -49
  670. package/src/i18n/kit-labels.tsx +5 -20
  671. package/src/i18n/languages.ts +2 -8
  672. package/src/i18n/locales/en.ts +11 -31
  673. package/src/i18n/locales/es.ts +35 -49
  674. package/src/i18n/locales/fr.ts +34 -51
  675. package/src/i18n/locales/hu.ts +26 -37
  676. package/src/i18n/locales/it.ts +35 -52
  677. package/src/i18n/locales/zh.ts +14 -32
  678. package/src/i18n/plurals.ts +4 -1
  679. package/src/i18n/review.ts +18 -7
  680. package/src/i18n/shared-labels.ts +1 -2
  681. package/src/index.ts +106 -32
  682. package/src/landing/access.ts +2 -7
  683. package/src/landing/landing-sections.tsx +14 -15
  684. package/src/landing/page-seo.ts +1 -3
  685. package/src/lib/calc.ts +4 -1
  686. package/src/lib/clipboard-minus.ts +4 -1
  687. package/src/lib/dates.ts +2 -10
  688. package/src/lib/decimal-marks.ts +1 -3
  689. package/src/lib/device-settings.ts +2 -1
  690. package/src/lib/direction.ts +1 -3
  691. package/src/lib/iban.ts +89 -9
  692. package/src/lib/logger.ts +1 -5
  693. package/src/lib/person-name.ts +1 -3
  694. package/src/lib/phone.ts +5 -8
  695. package/src/lib/qr-encode.ts +32 -8
  696. package/src/lib/session-refresh.ts +5 -6
  697. package/src/lib/table-text.ts +24 -6
  698. package/src/lib/text-match.ts +15 -3
  699. package/src/lib/translation-review.ts +1 -3
  700. package/src/notifications/notification-inbox.tsx +17 -4
  701. package/src/notifications/notification-parts.tsx +1 -2
  702. package/src/notifications/notification-types-setting.tsx +19 -18
  703. package/src/notifications/notifications-labels.ts +1 -2
  704. package/src/notifications/push-device-setting.tsx +7 -1
  705. package/src/notifications/push-devices-setting.tsx +7 -3
  706. package/src/notifications/push-worker.ts +10 -2
  707. package/src/notifications/quiet-hours-setting.tsx +4 -1
  708. package/src/notifications/use-push-device.ts +4 -1
  709. package/src/offline/pwa-update-prompt.tsx +7 -1
  710. package/src/offline/query-persistence.ts +9 -6
  711. package/src/offline.ts +6 -1
  712. package/src/rhf/fields.tsx +50 -19
  713. package/src/rhf/form-core.tsx +2 -6
  714. package/src/rhf/line-items.tsx +5 -7
  715. package/src/rhf/use-rhf-wizard-step.ts +3 -12
  716. package/src/search/command-palette.tsx +15 -23
  717. package/src/search/global-search.tsx +38 -8
  718. package/src/search/search-index.ts +5 -1
  719. package/src/settings/device-reset-setting.tsx +5 -8
  720. package/src/settings/settings-heading.tsx +3 -1
  721. package/src/settings/settings-layout.tsx +6 -13
  722. package/src/settings/use-settings-route.ts +4 -3
  723. package/src/shell/app-shell.tsx +48 -24
  724. package/src/shell/auth-layout.tsx +1 -6
  725. package/src/shell/dev-menu.tsx +12 -6
  726. package/src/shell/option-switcher-menu.tsx +1 -3
  727. package/src/shell/sync-status-indicator.tsx +14 -12
  728. package/src/shell/top-bar-parts-labels.ts +1 -3
  729. package/src/shell/topbar-action-menu.tsx +9 -8
  730. package/src/shell/topbar-controls.tsx +5 -19
  731. package/src/shell/tour-menu.tsx +9 -1
  732. package/src/sync/db.ts +5 -1
  733. package/src/sync/engine.ts +44 -12
  734. package/src/sync.ts +16 -2
  735. package/src/testing/css-variables.ts +1 -2
  736. package/src/testing/index.ts +1 -6
  737. package/src/testing/react.ts +1 -2
  738. package/src/testing/viewport.ts +9 -9
  739. package/src/theme/contrast-tokens.ts +13 -2
  740. package/src/theme/palette-derive.ts +24 -50
  741. package/src/theme/palette-presets.ts +0 -1
  742. package/src/theme/palette-store.ts +1 -4
  743. package/src/theme/text-size.ts +1 -1
  744. package/src/theme/theme-store.ts +2 -6
  745. package/src/tour/tour-catalogue.tsx +4 -2
  746. package/src/tour/tour-dock.ts +5 -1
  747. package/src/tour/tour-engine.ts +2 -6
  748. package/src/tour/tour.tsx +28 -12
  749. package/src/tour/use-tour-progress.ts +8 -2
  750. package/src/tour/use-tour-url-sync.ts +3 -1
  751. package/src/vite-env.d.ts +1 -4
  752. package/src/wizard/stepper-nav.tsx +10 -35
  753. package/src/wizard/types.ts +1 -4
  754. package/src/wizard/use-wizard.ts +28 -43
  755. package/src/wizard/validation.ts +1 -5
  756. package/src/wizard/wizard-context.tsx +2 -6
  757. package/src/wizard/wizard-step.tsx +1 -8
  758. package/src/wizard/wizard-summary.tsx +2 -6
  759. package/tokens.css +393 -152
@@ -101,28 +101,19 @@ function PageContents({
101
101
  }
102
102
  );
103
103
  if (variant === "disclosure") {
104
- return /* @__PURE__ */ jsxs(
105
- "details",
106
- {
107
- className: cn(
108
- "group rounded-md border border-subtle bg-surface",
109
- className
110
- ),
111
- children: [
112
- /* @__PURE__ */ jsxs("summary", { className: "flex cursor-pointer list-none items-center justify-between px-3 py-2 text-sm font-medium text-secondary [&::-webkit-details-marker]:hidden", children: [
113
- l.title,
114
- /* @__PURE__ */ jsx(
115
- ChevronDown,
116
- {
117
- "aria-hidden": true,
118
- className: "size-4 transition-transform duration-200 group-open:rotate-180 motion-reduce:transition-none"
119
- }
120
- )
121
- ] }),
122
- /* @__PURE__ */ jsx("nav", { ...rest, "aria-label": l.title, className: "border-t border-subtle px-2 py-2", children: list })
123
- ]
124
- }
125
- );
104
+ return /* @__PURE__ */ jsxs("details", { className: cn("group rounded-md border border-subtle bg-surface", className), children: [
105
+ /* @__PURE__ */ jsxs("summary", { className: "flex cursor-pointer list-none items-center justify-between px-3 py-2 text-sm font-medium text-secondary [&::-webkit-details-marker]:hidden", children: [
106
+ l.title,
107
+ /* @__PURE__ */ jsx(
108
+ ChevronDown,
109
+ {
110
+ "aria-hidden": true,
111
+ className: "size-4 transition-transform duration-200 group-open:rotate-180 motion-reduce:transition-none"
112
+ }
113
+ )
114
+ ] }),
115
+ /* @__PURE__ */ jsx("nav", { ...rest, "aria-label": l.title, className: "border-t border-subtle px-2 py-2", children: list })
116
+ ] });
126
117
  }
127
118
  return /* @__PURE__ */ jsxs("nav", { ...rest, "aria-label": l.title, className, children: [
128
119
  /* @__PURE__ */ jsx("p", { className: "mb-2 text-caption font-semibold uppercase tracking-wide text-muted", children: l.title }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/page-contents.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_PAGE_CONTENTS_LABELS } from \"./page-contents-labels\";\n\n/**\n * \"On this page\": a page's own table of contents, with the section being read marked.\n *\n * Three parts, because an app needs them separately:\n *\n * - {@link useScrollSpy} — which section is current, from the headings' positions in\n * a scroll container. No markup; usable for anything that follows the reader.\n * - {@link PageContents} — the list. `variant=\"rail\"` for beside the page,\n * `variant=\"disclosure\"` for a collapsed block under the title on narrow screens.\n * - {@link PageContentsLayout} — the page with the rail sticky on one side of it.\n *\n * Domain-free: the app supplies the entries and how an entry links (`hrefFor`), so it\n * works with any router, including a hash router where `#id` alone would be read as a\n * route.\n */\n\nexport interface PageContentsItem {\n /** The id of the heading the entry jumps to. */\n id: string;\n label: ReactNode;\n /** 2 indents the entry under the one before it. */\n level?: 1 | 2;\n}\n\nexport interface PageContentsLabels {\n /** The heading above the list, and the landmark's accessible name. */\n title: string;\n}\n\n// Defined in ./page-contents-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_PAGE_CONTENTS_LABELS };\n\nexport interface UseScrollSpyOptions {\n /** The element that scrolls. Defaults to the viewport. In `AppShell`, pass its\n * `<main>` — it is the page's scroll container from `md` up. */\n root?: Element | null;\n /**\n * How far down the scroller the \"reading line\" sits, as a fraction of its height.\n * A section becomes current once its heading passes above it, and stays current\n * until the next heading does — so a long section keeps its entry marked while it\n * is being read, not only while its heading is on screen. Default 0.15: close to\n * the top, so a short section followed by another does not hand its mark to the\n * next one the moment it is scrolled to.\n */\n line?: number;\n}\n\n/**\n * The id of the section being read: the last of `ids` whose element has scrolled\n * above the reading line — or the first, before any has.\n */\nexport function useScrollSpy(ids: readonly string[], options: UseScrollSpyOptions = {}): string | null {\n const { root = null, line = 0.15 } = options;\n const [current, setCurrent] = useState<string | null>(null);\n const key = ids.join(\"\\u0000\");\n\n useEffect(() => {\n if (ids.length === 0) return;\n const scroller: Element | Window = root ?? window;\n const measure = () => {\n const top = root ? root.getBoundingClientRect().top : 0;\n const height = root ? root.clientHeight : window.innerHeight;\n const reading = top + height * line;\n let next = ids[0];\n for (const id of ids) {\n const el = document.getElementById(id);\n if (el && el.getBoundingClientRect().top <= reading) next = id;\n }\n // At the very bottom the last sections may never reach the line; the reader\n // who scrolled to the end is reading the end.\n const atEnd = root\n ? root.scrollTop + root.clientHeight >= root.scrollHeight - 2\n : window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2;\n const scrolled = root ? root.scrollTop : window.scrollY;\n if (atEnd && scrolled > 0) next = ids[ids.length - 1];\n setCurrent(next);\n };\n let frame = 0;\n const onScroll = () => {\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(measure);\n };\n measure();\n scroller.addEventListener(\"scroll\", onScroll, { passive: true });\n window.addEventListener(\"resize\", onScroll);\n return () => {\n cancelAnimationFrame(frame);\n scroller.removeEventListener(\"scroll\", onScroll);\n window.removeEventListener(\"resize\", onScroll);\n };\n // `key` stands for `ids`: a new array with the same entries is the same list.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key, root, line]);\n\n return current;\n}\n\nexport interface PageContentsProps extends Omit<ComponentPropsWithRef<\"nav\">, \"children\"> {\n items: PageContentsItem[];\n /** The current entry — typically `useScrollSpy(items.map(i => i.id), …)`. */\n activeId?: string | null;\n /** The link for an entry. Default `#id`; under a hash router build it with the\n * router (react-router's `useHref({ hash: id })` gives `#/page#id`). */\n hrefFor?: (id: string) => string;\n /** `rail` — a list beside the page. `disclosure` — collapsed under a summary line,\n * for narrow screens where a rail has no room. */\n variant?: \"rail\" | \"disclosure\";\n labels?: LabelOverride<PageContentsLabels>;\n}\n\n/**\n * The list itself. The current entry gets a brand bar on the list's rule, the brand\n * colour, and `aria-current=\"location\"` — the marker documentation sites use, and the\n * one a screen reader announces.\n */\nexport function PageContents({\n items,\n activeId,\n hrefFor = (id) => `#${id}`,\n variant = \"rail\",\n labels,\n className,\n ...rest\n}: PageContentsProps) {\n const l = useKitLabels(\"pageContents\", DEFAULT_PAGE_CONTENTS_LABELS, labels);\n const listRef = useRef<HTMLUListElement>(null);\n // The entry just clicked is the current one until the reader scrolls on their own.\n // Without this, jumping to a short section near the end of a page marks whichever\n // later section the jump also carried past the reading line — you click one entry\n // and another lights up.\n const [clicked, setClicked] = useState<string | null>(null);\n useEffect(() => {\n if (!clicked) return;\n const release = () => setClicked(null);\n // A user scroll — wheel, touch, keys — releases it; the programmatic scroll the\n // jump itself causes does not fire these.\n window.addEventListener(\"wheel\", release, { passive: true, once: true });\n window.addEventListener(\"touchmove\", release, { passive: true, once: true });\n window.addEventListener(\"keydown\", release, { once: true });\n return () => {\n window.removeEventListener(\"wheel\", release);\n window.removeEventListener(\"touchmove\", release);\n window.removeEventListener(\"keydown\", release);\n };\n }, [clicked]);\n const shown = clicked ?? activeId;\n\n // Keep the current entry in view when the list is taller than the window.\n useEffect(() => {\n if (!shown || variant !== \"rail\") return;\n listRef.current\n ?.querySelector<HTMLElement>(`[data-entry=\"${CSS.escape(shown)}\"]`)\n ?.scrollIntoView?.({ block: \"nearest\" });\n }, [shown, variant]);\n\n if (items.length === 0) return null;\n\n const list = (\n <ul\n ref={listRef}\n className={cn(\n \"space-y-0.5 border-s border-subtle\",\n variant === \"rail\" && \"max-h-[calc(100dvh-10rem)] overflow-y-auto [scrollbar-width:thin]\",\n )}\n >\n {items.map((item) => {\n const current = item.id === shown;\n return (\n <li key={item.id} data-entry={item.id}>\n <a\n href={hrefFor(item.id)}\n // Entries WRAP in the rail rather than truncate: a cut-off heading needed a\n // native `title` to be read at all, which a keyboard never sees and which\n // broke keksdose's one-tooltip rule (dev#523).\n aria-current={current ? \"location\" : undefined}\n onClick={() => setClicked(item.id)}\n className={cn(\n \"-ms-px block border-s-2 py-1 pe-1 text-[0.8125rem] leading-snug transition-colors duration-150\",\n variant === \"rail\" && \"[overflow-wrap:anywhere]\",\n item.level === 2 ? \"ps-6\" : \"ps-3\",\n current\n ? \"border-brand font-medium text-brand\"\n : \"border-transparent text-muted hover:border-strong hover:text-primary\",\n )}\n >\n {item.label}\n </a>\n </li>\n );\n })}\n </ul>\n );\n\n if (variant === \"disclosure\") {\n return (\n <details\n className={cn(\n \"group rounded-md border border-subtle bg-surface\",\n className,\n )}\n >\n <summary className=\"flex cursor-pointer list-none items-center justify-between px-3 py-2 text-sm font-medium text-secondary [&::-webkit-details-marker]:hidden\">\n {l.title}\n <ChevronDown\n aria-hidden\n className=\"size-4 transition-transform duration-200 group-open:rotate-180 motion-reduce:transition-none\"\n />\n </summary>\n <nav {...rest} aria-label={l.title} className=\"border-t border-subtle px-2 py-2\">\n {list}\n </nav>\n </details>\n );\n }\n\n return (\n <nav {...rest} aria-label={l.title} className={className}>\n <p className=\"mb-2 text-caption font-semibold uppercase tracking-wide text-muted\">\n {l.title}\n </p>\n {list}\n </nav>\n );\n}\n\nexport interface PageContentsLayoutProps extends ComponentPropsWithRef<\"div\"> {\n /** What goes in the rail — typically a `<PageContents variant=\"rail\">`. */\n contents: ReactNode;\n /**\n * Which side of the page the rail sits on: `start` is the left in a left-to-right\n * language (next to the app's own sidebar, so both navigations are in one place),\n * `end` the right (the documentation-site convention, clear of the sidebar). Logical,\n * so both flip in a right-to-left UI.\n */\n position?: \"start\" | \"end\";\n children: ReactNode;\n}\n\n/**\n * The page with a sticky contents rail beside it, from `xl` (1280px) up. Below that\n * the rail would squeeze the content, so it is not rendered there — pair it with a\n * `<PageContents variant=\"disclosure\" className=\"xl:hidden\">` in the page.\n *\n * The rail is `sticky top-6`, which sticks inside the nearest scroll container — the\n * viewport, or `AppShell`'s `<main>`.\n */\nexport function PageContentsLayout({\n contents,\n position = \"start\",\n children,\n className,\n ...rest\n}: PageContentsLayoutProps) {\n // In DOM order on its side, not moved there with `order`: Tab and a screen reader\n // follow the source, and a rail painted on the left but read after the page would\n // put the page's index at its end.\n const rail = (\n <aside className=\"hidden xl:block\">\n <div className=\"sticky top-6\">{contents}</div>\n </aside>\n );\n return (\n <div\n {...rest}\n className={cn(\n \"xl:grid xl:gap-10\",\n // `fit-content(22rem)`: the rail is as wide as its longest entry, up to 22rem.\n // A fixed 13rem broke \"Select — unlabelled, labelled, invalid, disabled\" over\n // three lines beside a page with room to spare; an index you scan should read\n // one entry per line.\n position === \"start\"\n ? \"xl:grid-cols-[fit-content(22rem)_minmax(0,1fr)]\"\n : \"xl:grid-cols-[minmax(0,1fr)_fit-content(22rem)]\",\n className,\n )}\n >\n {position === \"start\" && rail}\n <div className=\"min-w-0\">{children}</div>\n {position === \"end\" && rail}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/page-contents.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_PAGE_CONTENTS_LABELS } from \"./page-contents-labels\";\n\n/**\n * \"On this page\": a page's own table of contents, with the section being read marked.\n *\n * Three parts, because an app needs them separately:\n *\n * - {@link useScrollSpy} — which section is current, from the headings' positions in\n * a scroll container. No markup; usable for anything that follows the reader.\n * - {@link PageContents} — the list. `variant=\"rail\"` for beside the page,\n * `variant=\"disclosure\"` for a collapsed block under the title on narrow screens.\n * - {@link PageContentsLayout} — the page with the rail sticky on one side of it.\n *\n * Domain-free: the app supplies the entries and how an entry links (`hrefFor`), so it\n * works with any router, including a hash router where `#id` alone would be read as a\n * route.\n */\n\nexport interface PageContentsItem {\n /** The id of the heading the entry jumps to. */\n id: string;\n label: ReactNode;\n /** 2 indents the entry under the one before it. */\n level?: 1 | 2;\n}\n\nexport interface PageContentsLabels {\n /** The heading above the list, and the landmark's accessible name. */\n title: string;\n}\n\n// Defined in ./page-contents-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_PAGE_CONTENTS_LABELS };\n\nexport interface UseScrollSpyOptions {\n /** The element that scrolls. Defaults to the viewport. In `AppShell`, pass its\n * `<main>` — it is the page's scroll container from `md` up. */\n root?: Element | null;\n /**\n * How far down the scroller the \"reading line\" sits, as a fraction of its height.\n * A section becomes current once its heading passes above it, and stays current\n * until the next heading does — so a long section keeps its entry marked while it\n * is being read, not only while its heading is on screen. Default 0.15: close to\n * the top, so a short section followed by another does not hand its mark to the\n * next one the moment it is scrolled to.\n */\n line?: number;\n}\n\n/**\n * The id of the section being read: the last of `ids` whose element has scrolled\n * above the reading line — or the first, before any has.\n */\nexport function useScrollSpy(ids: readonly string[], options: UseScrollSpyOptions = {}): string | null {\n const { root = null, line = 0.15 } = options;\n const [current, setCurrent] = useState<string | null>(null);\n const key = ids.join(\"\\u0000\");\n\n useEffect(() => {\n if (ids.length === 0) return;\n const scroller: Element | Window = root ?? window;\n const measure = () => {\n const top = root ? root.getBoundingClientRect().top : 0;\n const height = root ? root.clientHeight : window.innerHeight;\n const reading = top + height * line;\n let next = ids[0];\n for (const id of ids) {\n const el = document.getElementById(id);\n if (el && el.getBoundingClientRect().top <= reading) next = id;\n }\n // At the very bottom the last sections may never reach the line; the reader\n // who scrolled to the end is reading the end.\n const atEnd = root\n ? root.scrollTop + root.clientHeight >= root.scrollHeight - 2\n : window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2;\n const scrolled = root ? root.scrollTop : window.scrollY;\n if (atEnd && scrolled > 0) next = ids[ids.length - 1];\n setCurrent(next);\n };\n let frame = 0;\n const onScroll = () => {\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(measure);\n };\n measure();\n scroller.addEventListener(\"scroll\", onScroll, { passive: true });\n window.addEventListener(\"resize\", onScroll);\n return () => {\n cancelAnimationFrame(frame);\n scroller.removeEventListener(\"scroll\", onScroll);\n window.removeEventListener(\"resize\", onScroll);\n };\n // `key` stands for `ids`: a new array with the same entries is the same list.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key, root, line]);\n\n return current;\n}\n\nexport interface PageContentsProps extends Omit<ComponentPropsWithRef<\"nav\">, \"children\"> {\n items: PageContentsItem[];\n /** The current entry — typically `useScrollSpy(items.map(i => i.id), …)`. */\n activeId?: string | null;\n /** The link for an entry. Default `#id`; under a hash router build it with the\n * router (react-router's `useHref({ hash: id })` gives `#/page#id`). */\n hrefFor?: (id: string) => string;\n /** `rail` — a list beside the page. `disclosure` — collapsed under a summary line,\n * for narrow screens where a rail has no room. */\n variant?: \"rail\" | \"disclosure\";\n labels?: LabelOverride<PageContentsLabels>;\n}\n\n/**\n * The list itself. The current entry gets a brand bar on the list's rule, the brand\n * colour, and `aria-current=\"location\"` — the marker documentation sites use, and the\n * one a screen reader announces.\n */\nexport function PageContents({\n items,\n activeId,\n hrefFor = (id) => `#${id}`,\n variant = \"rail\",\n labels,\n className,\n ...rest\n}: PageContentsProps) {\n const l = useKitLabels(\"pageContents\", DEFAULT_PAGE_CONTENTS_LABELS, labels);\n const listRef = useRef<HTMLUListElement>(null);\n // The entry just clicked is the current one until the reader scrolls on their own.\n // Without this, jumping to a short section near the end of a page marks whichever\n // later section the jump also carried past the reading line — you click one entry\n // and another lights up.\n const [clicked, setClicked] = useState<string | null>(null);\n useEffect(() => {\n if (!clicked) return;\n const release = () => setClicked(null);\n // A user scroll — wheel, touch, keys — releases it; the programmatic scroll the\n // jump itself causes does not fire these.\n window.addEventListener(\"wheel\", release, { passive: true, once: true });\n window.addEventListener(\"touchmove\", release, { passive: true, once: true });\n window.addEventListener(\"keydown\", release, { once: true });\n return () => {\n window.removeEventListener(\"wheel\", release);\n window.removeEventListener(\"touchmove\", release);\n window.removeEventListener(\"keydown\", release);\n };\n }, [clicked]);\n const shown = clicked ?? activeId;\n\n // Keep the current entry in view when the list is taller than the window.\n useEffect(() => {\n if (!shown || variant !== \"rail\") return;\n listRef.current\n ?.querySelector<HTMLElement>(`[data-entry=\"${CSS.escape(shown)}\"]`)\n ?.scrollIntoView?.({ block: \"nearest\" });\n }, [shown, variant]);\n\n if (items.length === 0) return null;\n\n const list = (\n <ul\n ref={listRef}\n className={cn(\n \"space-y-0.5 border-s border-subtle\",\n variant === \"rail\" && \"max-h-[calc(100dvh-10rem)] overflow-y-auto [scrollbar-width:thin]\",\n )}\n >\n {items.map((item) => {\n const current = item.id === shown;\n return (\n <li key={item.id} data-entry={item.id}>\n <a\n href={hrefFor(item.id)}\n // Entries WRAP in the rail rather than truncate: a cut-off heading needed a\n // native `title` to be read at all, which a keyboard never sees and which\n // broke keksdose's one-tooltip rule (dev#523).\n aria-current={current ? \"location\" : undefined}\n onClick={() => setClicked(item.id)}\n className={cn(\n \"-ms-px block border-s-2 py-1 pe-1 text-[0.8125rem] leading-snug transition-colors duration-150\",\n variant === \"rail\" && \"[overflow-wrap:anywhere]\",\n item.level === 2 ? \"ps-6\" : \"ps-3\",\n current\n ? \"border-brand font-medium text-brand\"\n : \"border-transparent text-muted hover:border-strong hover:text-primary\",\n )}\n >\n {item.label}\n </a>\n </li>\n );\n })}\n </ul>\n );\n\n if (variant === \"disclosure\") {\n return (\n <details className={cn(\"group rounded-md border border-subtle bg-surface\", className)}>\n <summary className=\"flex cursor-pointer list-none items-center justify-between px-3 py-2 text-sm font-medium text-secondary [&::-webkit-details-marker]:hidden\">\n {l.title}\n <ChevronDown\n aria-hidden\n className=\"size-4 transition-transform duration-200 group-open:rotate-180 motion-reduce:transition-none\"\n />\n </summary>\n <nav {...rest} aria-label={l.title} className=\"border-t border-subtle px-2 py-2\">\n {list}\n </nav>\n </details>\n );\n }\n\n return (\n <nav {...rest} aria-label={l.title} className={className}>\n <p className=\"mb-2 text-caption font-semibold uppercase tracking-wide text-muted\">{l.title}</p>\n {list}\n </nav>\n );\n}\n\nexport interface PageContentsLayoutProps extends ComponentPropsWithRef<\"div\"> {\n /** What goes in the rail — typically a `<PageContents variant=\"rail\">`. */\n contents: ReactNode;\n /**\n * Which side of the page the rail sits on: `start` is the left in a left-to-right\n * language (next to the app's own sidebar, so both navigations are in one place),\n * `end` the right (the documentation-site convention, clear of the sidebar). Logical,\n * so both flip in a right-to-left UI.\n */\n position?: \"start\" | \"end\";\n children: ReactNode;\n}\n\n/**\n * The page with a sticky contents rail beside it, from `xl` (1280px) up. Below that\n * the rail would squeeze the content, so it is not rendered there — pair it with a\n * `<PageContents variant=\"disclosure\" className=\"xl:hidden\">` in the page.\n *\n * The rail is `sticky top-6`, which sticks inside the nearest scroll container — the\n * viewport, or `AppShell`'s `<main>`.\n */\nexport function PageContentsLayout({\n contents,\n position = \"start\",\n children,\n className,\n ...rest\n}: PageContentsLayoutProps) {\n // In DOM order on its side, not moved there with `order`: Tab and a screen reader\n // follow the source, and a rail painted on the left but read after the page would\n // put the page's index at its end.\n const rail = (\n <aside className=\"hidden xl:block\">\n <div className=\"sticky top-6\">{contents}</div>\n </aside>\n );\n return (\n <div\n {...rest}\n className={cn(\n \"xl:grid xl:gap-10\",\n // `fit-content(22rem)`: the rail is as wide as its longest entry, up to 22rem.\n // A fixed 13rem broke \"Select — unlabelled, labelled, invalid, disabled\" over\n // three lines beside a page with room to spare; an index you scan should read\n // one entry per line.\n position === \"start\"\n ? \"xl:grid-cols-[fit-content(22rem)_minmax(0,1fr)]\"\n : \"xl:grid-cols-[minmax(0,1fr)_fit-content(22rem)]\",\n className,\n )}\n >\n {position === \"start\" && rail}\n <div className=\"min-w-0\">{children}</div>\n {position === \"end\" && rail}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -78,17 +78,7 @@ function PageHeader({
78
78
  children: [
79
79
  /* @__PURE__ */ jsxs("div", { ref: titleRef, className: cn("flex-1", keepsWords ? "min-w-min" : "min-w-0"), children: [
80
80
  eyebrow != null && /* @__PURE__ */ jsx("p", { className: cn(SECTION_LABEL_CLASS.xs, "mb-1"), children: eyebrow }),
81
- /* @__PURE__ */ jsx(
82
- Heading,
83
- {
84
- className: cn(
85
- truncateTitle ? "truncate-until-large" : "break-words",
86
- "text-primary",
87
- TITLE[size]
88
- ),
89
- children: title
90
- }
91
- ),
81
+ /* @__PURE__ */ jsx(Heading, { className: cn(truncateTitle ? "truncate-until-large" : "break-words", "text-primary", TITLE[size]), children: title }),
92
82
  description != null && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted", children: description })
93
83
  ] }),
94
84
  actions != null && /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/page-header.tsx"],"sourcesContent":["import { useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, ElementType, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { SECTION_LABEL_CLASS } from \"./text\";\nimport { useDocumentTitle } from \"../hooks/use-document-title\";\nimport { PageHeaderTrailContext } from \"./page-header-context\";\n\n/**\n * `md` (default) is kastlan's `text-2xl` page title from `sm` up, a step smaller on a\n * phone; `sm` is lenkbank's `text-lg` title on a dense tool page (projects-page.tsx).\n *\n * `compact` (0.13) is keksdose's page title (keksdose F4): `text-xl font-semibold` at\n * EVERY width — budgets, rules, accounts, transactions and budget pages all write\n * exactly that `<h1>` by hand. It is `md`'s phone size without `md`'s step up to\n * `text-2xl` on a wider screen, and without its `tracking-tight`, which keksdose's\n * titles never had. A word and not a letter for the reason `ProgressBar`'s `slim` is\n * one: the letters are taken in order, and there is none between `sm` and `md` — an\n * `lg` or `xl` at `text-xl` would read as LARGER than `md`, which from `sm` up it is\n * not.\n */\nexport type PageHeaderSize = \"sm\" | \"compact\" | \"md\";\n\nconst TITLE: Record<PageHeaderSize, string> = {\n sm: \"text-lg font-semibold\",\n compact: \"text-xl font-semibold\",\n md: \"text-xl font-semibold tracking-tight sm:text-2xl\",\n};\n\n/**\n * How the title block and the actions sit below the `sm` breakpoint. From `sm` up both\n * layouts are one row: title block at the start, actions at the end.\n */\nexport type PageHeaderMobileLayout = \"stacked\" | \"inline\";\n\n/**\n * `stacked` at Large and Extra large on a phone (0.32.2, kastlan's 0.32.1 report;\n * docs/text-size-harmonization.md §4, as FormActions does): the actions take the full\n * width, one under the other. Side by side in 240 px of layout, two buttons each wrapped\n * their label onto three lines. `large:max-sm:`, the scaled phone width, so Normal and\n * every wider screen keep their row; `inline` is for actions known to fit, and stays.\n */\nconst STACK_ACTIONS_AT_LARGE =\n \"large:max-sm:flex-col large:max-sm:items-stretch large:max-sm:[&>*]:w-full\";\n\nconst ROW: Record<PageHeaderMobileLayout, string> = {\n stacked: \"flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4\",\n inline: \"flex-row items-center justify-between gap-2 sm:gap-4\",\n};\n\n/**\n * Where the actions sit on the title block's height, in the one row the header is from\n * `sm` up (and on a phone too when {@link PageHeaderMobileLayout} is `inline`, the only\n * layout that has a row there). Unset keeps each layout's own: `start` for `stacked`,\n * `center` for `inline`.\n */\nexport type PageHeaderActionsAlign = \"start\" | \"center\" | \"end\";\n\n/** Applied to the ROW, so the title block and the actions are aligned against each\n * other on one cross axis. 0.14.0 put `self-*` on the actions box alone, which moved\n * nothing when the actions were the taller item — they already filled the row, and\n * the title stayed at the top (keksdose H2, reports-page's two-line labelled\n * CurrencySelect). Per layout, because `stacked` is a column on a phone, where\n * `items-center` would centre everything HORIZONTALLY — so it aligns from `sm` up. */\nconst ROW_ALIGN: Record<PageHeaderMobileLayout, Record<PageHeaderActionsAlign, string>> = {\n stacked: { start: \"sm:items-start\", center: \"sm:items-center\", end: \"sm:items-end\" },\n inline: { start: \"items-start\", center: \"items-center\", end: \"items-end\" },\n};\n\nexport interface PageHeaderProps extends Omit<ComponentPropsWithRef<\"div\">, \"title\"> {\n title: ReactNode;\n /** A sentence under the title. */\n description?: ReactNode;\n /** Buttons for the page — \"New\", \"Export\". At the title's end from `sm` up; under it,\n * wrapping, on a phone — unless {@link mobileLayout} is `inline`. */\n actions?: ReactNode;\n /** A `Breadcrumbs` trail, drawn above everything else. Inside a header with a title\n * it hides its current entry (the title names it) and is drawn only when an entry\n * other than home is left — `Breadcrumbs.hideCurrent` (0.49 KIT-9). */\n breadcrumbs?: ReactNode;\n /**\n * The window's title (`document.title`) while the header is mounted (0.49 KIT-9),\n * through `useDocumentTitle` and the provider's format (\"{page} · {section} · {app}\",\n * `appName` as the app). A string is the page; an array is `[page, section]` (or\n * `[page, section, app]`); `false` leaves the window's title alone.\n *\n * Default: the `title` when it is a string, and only for the page's own header\n * (`as=\"h1\"`) — a tab's `h2` header does not rename the window.\n */\n documentTitle?: string | ReadonlyArray<string | null | undefined> | false;\n /** A small uppercase line above the title — the section or the kind of record\n * (\"Building\", \"Settings\"). The section-label type. */\n eyebrow?: ReactNode;\n /** The title's element. Default `h1`: a page has one, and this is it. `h2` for a\n * header inside a page that already has its `h1` (a tab's own header). */\n as?: \"h1\" | \"h2\" | \"h3\";\n size?: PageHeaderSize;\n /**\n * `stacked` (default): on a phone the actions drop under the title and wrap, so a\n * row of three buttons never pushes the title off the screen.\n *\n * `inline`: the actions stay beside the title on a phone too, vertically centred on\n * it — keksdose's headers (transactions-page.tsx:515, `flex items-center\n * justify-between gap-2`), which carry one or two icon-sized controls and were tuned\n * for the phone in live #263. Adopting `PageHeader` must not undo that tuning, so the\n * row stays a row (keksdose F4). The actions do NOT wrap or shrink here: `inline` is\n * for a header whose actions are known to fit beside a phone-width title (an icon\n * button, a month picker); a long title wraps to make room for them instead.\n */\n mobileLayout?: PageHeaderMobileLayout;\n /**\n * A second group of actions that gets its OWN full-width row under the header on a\n * phone, and joins the actions' row from `sm` up, between the title and\n * {@link actions}.\n *\n * keksdose's budget page (budget-page.tsx, feedback #60) is the case: title and month\n * navigation on row 1, the fold/unfold toggles on row 2 on a phone — \"two tidy rows\n * instead of the old ragged justify-between overflow\" — and one row, title | toggles |\n * month nav, on a wider screen. It builds that by hand with a wrapping flex row and\n * swapped order utilities; adopting `PageHeader` (keksdose G6b) must not lose it.\n * There: `actions` is the month navigation, this is the toggles.\n *\n * The DOM order is title, `actions`, `secondaryActions` — the phone's visual order,\n * and the one a screen reader and the Tab key follow at every width. From `sm` up the\n * two groups swap places visually only, which keeps the page's primary control (the\n * month) at the row's end, where keksdose had it, and first in reading order. The\n * breakpoint is `sm`, the one {@link mobileLayout} switches at, not keksdose's `md`:\n * one header, one breakpoint.\n */\n secondaryActions?: ReactNode;\n /**\n * The actions' vertical alignment against the title block — see\n * {@link PageHeaderActionsAlign}. Unset keeps the layout's own.\n *\n * keksdose's reports page (reports-page.tsx, keksdose G8) carries a LABELLED\n * `CurrencySelect` as its action: a field with its label on top is taller than the\n * title, and `stacked`'s default top alignment hangs it from the title's cap height\n * with its control well below the title's line. `center` sets it on the title's\n * middle, as that page's hand-written `items-center` row did. It aligns the whole row\n * (title block and actions against each other), since 0.14.1: 0.14.0 moved only the\n * actions box, which did nothing when the actions were the taller item (keksdose H2).\n */\n actionsAlign?: PageHeaderActionsAlign;\n /**\n * One line, cut with an ellipsis, instead of the default breaking of a long word onto\n * as many lines as it needs.\n *\n * keksdose's payees page (payees-page.tsx, live #263, keksdose G6a) wraps its title in\n * its own truncating span so a long name gives way to the `inline` header's actions\n * instead of pushing them. This is that span's behaviour on the heading itself; the\n * title block is already allowed to shrink below its content, which is what lets the\n * cut happen inside a flex row.\n *\n * The full title stays in the DOM as the heading's text, so a screen reader and the\n * document outline read all of it. There is deliberately no native `title` tooltip:\n * the kit does not use them (not on touch, not on keyboard focus, not styled).\n *\n * At Normal only: from Large up the title wraps (`truncate-until-large`, text size\n * §10.17; round 0.34 T1), as nothing is cut at Large — the actions then share the row\n * with a taller title block.\n */\n truncateTitle?: boolean;\n}\n\nconst TITLED = { titled: true };\nconst UNTITLED = { titled: false };\n\n/**\n * A page's head: optional breadcrumbs, an eyebrow, the title, a description, and the\n * page's actions.\n *\n * kastlan's (shared/components/display/page-header.tsx) is the model — title and\n * description on one side, actions on the other, stacked on a phone — and this adds\n * what its pages kept writing around it: a trail above, an eyebrow, a `ReactNode`\n * title (a status chip beside the name), and actions that WRAP when there are more\n * than fit a phone's width, instead of running off it. No outer margin: the page's\n * own rhythm (`space-y-*`, a grid gap) places it.\n */\nexport function PageHeader({\n title,\n description,\n actions,\n breadcrumbs,\n eyebrow,\n as = \"h1\",\n size = \"md\",\n mobileLayout = \"stacked\",\n secondaryActions,\n actionsAlign,\n truncateTitle = false,\n documentTitle,\n className,\n ...rest\n}: PageHeaderProps) {\n const Heading = as as ElementType;\n useDocumentTitle(\n documentTitle === false\n ? []\n : documentTitle === undefined\n ? as === \"h1\" && typeof title === \"string\"\n ? [title]\n : []\n : typeof documentTitle === \"string\"\n ? [documentTitle]\n : documentTitle,\n );\n const titled = title != null && title !== false && title !== \"\";\n const hasSecondary = secondaryActions != null;\n const wrapsSecondary = hasSecondary && mobileLayout === \"inline\";\n const align = actionsAlign != null ? ROW_ALIGN[mobileLayout][actionsAlign] : undefined;\n // keksdose H1: in `inline` the title block could shrink below its longest word, and\n // `break-words` then split \"Monatsbudget\" into \"Monatsbudg\" / \"et\" beside the actions.\n // Unless it truncates, the title keeps its longest word (min-content) and the ROW\n // wraps instead: actions that do not fit go to a line of their own, as the app's old\n // `flex-wrap` header did.\n const keepsWords = mobileLayout === \"inline\" && !truncateTitle;\n // keksdose, after H1: once the actions have wrapped below the title, the phone's\n // header was THREE rows — title / actions / secondary — where the app's old header\n // shared the second row between the month nav and the toggles (~48px of phone\n // height). So the secondary group's full-width basis (G6b: a row of its own) is\n // dropped exactly when the actions already sit below the title; then it joins their\n // row if it fits, and wraps under them if not. While the actions share the title's\n // row, nothing changes: the secondary group never climbs onto the title's row.\n //\n // CSS cannot see \"the previous item wrapped\", so it is measured — by a\n // ResizeObserver, whose first callback runs on `observe`. It is stable: the\n // secondary group comes after the actions, so its basis cannot move them. No\n // ResizeObserver (jsdom) or no layout keeps the basis, the 0.14.0 layout.\n const measuresWrap = keepsWords && hasSecondary && actions != null;\n const titleRef = useRef<HTMLDivElement>(null);\n const actionsRef = useRef<HTMLDivElement>(null);\n const [actionsWrapped, setActionsWrapped] = useState(false);\n useLayoutEffect(() => {\n const titleBox = titleRef.current;\n const actionsBox = actionsRef.current;\n if (!measuresWrap || !titleBox || !actionsBox || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n const t = titleBox.getBoundingClientRect();\n const a = actionsBox.getBoundingClientRect();\n // Unmeasured (display: none, no layout): keep the full-width basis.\n setActionsWrapped(t.height > 0 && a.height > 0 && a.top >= t.bottom - 1);\n });\n observer.observe(titleBox);\n observer.observe(actionsBox);\n if (titleBox.parentElement) observer.observe(titleBox.parentElement);\n return () => observer.disconnect();\n }, [measuresWrap]);\n return (\n <div {...rest} className={cn(\"flex min-w-0 flex-col gap-2\", className)}>\n {breadcrumbs != null && (\n <PageHeaderTrailContext.Provider value={titled ? TITLED : UNTITLED}>{breadcrumbs}</PageHeaderTrailContext.Provider>\n )}\n {/* With a second group, `inline` wraps so that group's full-width basis puts it\n on a row of its own on a phone (`stacked` is a column there already, and a\n basis would size a column item's HEIGHT); from `sm` up the row stops wrapping\n and the order utilities seat it between the title and the actions. Without\n one, nothing changes. */}\n <div\n className={cn(\n \"flex min-w-0\",\n ROW[mobileLayout],\n keepsWords ? \"flex-wrap\" : wrapsSecondary && \"flex-wrap sm:flex-nowrap\",\n align,\n )}\n >\n <div ref={titleRef} className={cn(\"flex-1\", keepsWords ? \"min-w-min\" : \"min-w-0\")}>\n {eyebrow != null && <p className={cn(SECTION_LABEL_CLASS.xs, \"mb-1\")}>{eyebrow}</p>}\n <Heading\n className={cn(\n truncateTitle ? \"truncate-until-large\" : \"break-words\",\n \"text-primary\",\n TITLE[size],\n )}\n >\n {title}\n </Heading>\n {description != null && <p className=\"mt-1 text-sm text-muted\">{description}</p>}\n </div>\n {actions != null && (\n <div\n ref={actionsRef}\n className={cn(\n \"flex shrink-0 flex-wrap items-center gap-2\",\n hasSecondary && \"sm:order-2\",\n mobileLayout === \"stacked\" && STACK_ACTIONS_AT_LARGE,\n )}\n >\n {actions}\n </div>\n )}\n {hasSecondary && (\n <div\n className={cn(\n \"flex flex-wrap items-center gap-2 sm:order-1 sm:shrink-0\",\n wrapsSecondary && (measuresWrap && actionsWrapped ? \"sm:basis-auto\" : \"basis-full sm:basis-auto\"),\n )}\n >\n {secondaryActions}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/page-header.tsx"],"sourcesContent":["import { useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, ElementType, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { SECTION_LABEL_CLASS } from \"./text\";\nimport { useDocumentTitle } from \"../hooks/use-document-title\";\nimport { PageHeaderTrailContext } from \"./page-header-context\";\n\n/**\n * `md` (default) is kastlan's `text-2xl` page title from `sm` up, a step smaller on a\n * phone; `sm` is lenkbank's `text-lg` title on a dense tool page (projects-page.tsx).\n *\n * `compact` (0.13) is keksdose's page title (keksdose F4): `text-xl font-semibold` at\n * EVERY width — budgets, rules, accounts, transactions and budget pages all write\n * exactly that `<h1>` by hand. It is `md`'s phone size without `md`'s step up to\n * `text-2xl` on a wider screen, and without its `tracking-tight`, which keksdose's\n * titles never had. A word and not a letter for the reason `ProgressBar`'s `slim` is\n * one: the letters are taken in order, and there is none between `sm` and `md` — an\n * `lg` or `xl` at `text-xl` would read as LARGER than `md`, which from `sm` up it is\n * not.\n */\nexport type PageHeaderSize = \"sm\" | \"compact\" | \"md\";\n\nconst TITLE: Record<PageHeaderSize, string> = {\n sm: \"text-lg font-semibold\",\n compact: \"text-xl font-semibold\",\n md: \"text-xl font-semibold tracking-tight sm:text-2xl\",\n};\n\n/**\n * How the title block and the actions sit below the `sm` breakpoint. From `sm` up both\n * layouts are one row: title block at the start, actions at the end.\n */\nexport type PageHeaderMobileLayout = \"stacked\" | \"inline\";\n\n/**\n * `stacked` at Large and Extra large on a phone (0.32.2, kastlan's 0.32.1 report;\n * docs/text-size-harmonization.md §4, as FormActions does): the actions take the full\n * width, one under the other. Side by side in 240 px of layout, two buttons each wrapped\n * their label onto three lines. `large:max-sm:`, the scaled phone width, so Normal and\n * every wider screen keep their row; `inline` is for actions known to fit, and stays.\n */\nconst STACK_ACTIONS_AT_LARGE = \"large:max-sm:flex-col large:max-sm:items-stretch large:max-sm:[&>*]:w-full\";\n\nconst ROW: Record<PageHeaderMobileLayout, string> = {\n stacked: \"flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4\",\n inline: \"flex-row items-center justify-between gap-2 sm:gap-4\",\n};\n\n/**\n * Where the actions sit on the title block's height, in the one row the header is from\n * `sm` up (and on a phone too when {@link PageHeaderMobileLayout} is `inline`, the only\n * layout that has a row there). Unset keeps each layout's own: `start` for `stacked`,\n * `center` for `inline`.\n */\nexport type PageHeaderActionsAlign = \"start\" | \"center\" | \"end\";\n\n/** Applied to the ROW, so the title block and the actions are aligned against each\n * other on one cross axis. 0.14.0 put `self-*` on the actions box alone, which moved\n * nothing when the actions were the taller item — they already filled the row, and\n * the title stayed at the top (keksdose H2, reports-page's two-line labelled\n * CurrencySelect). Per layout, because `stacked` is a column on a phone, where\n * `items-center` would centre everything HORIZONTALLY — so it aligns from `sm` up. */\nconst ROW_ALIGN: Record<PageHeaderMobileLayout, Record<PageHeaderActionsAlign, string>> = {\n stacked: { start: \"sm:items-start\", center: \"sm:items-center\", end: \"sm:items-end\" },\n inline: { start: \"items-start\", center: \"items-center\", end: \"items-end\" },\n};\n\nexport interface PageHeaderProps extends Omit<ComponentPropsWithRef<\"div\">, \"title\"> {\n title: ReactNode;\n /** A sentence under the title. */\n description?: ReactNode;\n /** Buttons for the page — \"New\", \"Export\". At the title's end from `sm` up; under it,\n * wrapping, on a phone — unless {@link mobileLayout} is `inline`. */\n actions?: ReactNode;\n /** A `Breadcrumbs` trail, drawn above everything else. Inside a header with a title\n * it hides its current entry (the title names it) and is drawn only when an entry\n * other than home is left — `Breadcrumbs.hideCurrent` (0.49 KIT-9). */\n breadcrumbs?: ReactNode;\n /**\n * The window's title (`document.title`) while the header is mounted (0.49 KIT-9),\n * through `useDocumentTitle` and the provider's format (\"{page} · {section} · {app}\",\n * `appName` as the app). A string is the page; an array is `[page, section]` (or\n * `[page, section, app]`); `false` leaves the window's title alone.\n *\n * Default: the `title` when it is a string, and only for the page's own header\n * (`as=\"h1\"`) — a tab's `h2` header does not rename the window.\n */\n documentTitle?: string | ReadonlyArray<string | null | undefined> | false;\n /** A small uppercase line above the title — the section or the kind of record\n * (\"Building\", \"Settings\"). The section-label type. */\n eyebrow?: ReactNode;\n /** The title's element. Default `h1`: a page has one, and this is it. `h2` for a\n * header inside a page that already has its `h1` (a tab's own header). */\n as?: \"h1\" | \"h2\" | \"h3\";\n size?: PageHeaderSize;\n /**\n * `stacked` (default): on a phone the actions drop under the title and wrap, so a\n * row of three buttons never pushes the title off the screen.\n *\n * `inline`: the actions stay beside the title on a phone too, vertically centred on\n * it — keksdose's headers (transactions-page.tsx:515, `flex items-center\n * justify-between gap-2`), which carry one or two icon-sized controls and were tuned\n * for the phone in live #263. Adopting `PageHeader` must not undo that tuning, so the\n * row stays a row (keksdose F4). The actions do NOT wrap or shrink here: `inline` is\n * for a header whose actions are known to fit beside a phone-width title (an icon\n * button, a month picker); a long title wraps to make room for them instead.\n */\n mobileLayout?: PageHeaderMobileLayout;\n /**\n * A second group of actions that gets its OWN full-width row under the header on a\n * phone, and joins the actions' row from `sm` up, between the title and\n * {@link actions}.\n *\n * keksdose's budget page (budget-page.tsx, feedback #60) is the case: title and month\n * navigation on row 1, the fold/unfold toggles on row 2 on a phone — \"two tidy rows\n * instead of the old ragged justify-between overflow\" — and one row, title | toggles |\n * month nav, on a wider screen. It builds that by hand with a wrapping flex row and\n * swapped order utilities; adopting `PageHeader` (keksdose G6b) must not lose it.\n * There: `actions` is the month navigation, this is the toggles.\n *\n * The DOM order is title, `actions`, `secondaryActions` — the phone's visual order,\n * and the one a screen reader and the Tab key follow at every width. From `sm` up the\n * two groups swap places visually only, which keeps the page's primary control (the\n * month) at the row's end, where keksdose had it, and first in reading order. The\n * breakpoint is `sm`, the one {@link mobileLayout} switches at, not keksdose's `md`:\n * one header, one breakpoint.\n */\n secondaryActions?: ReactNode;\n /**\n * The actions' vertical alignment against the title block — see\n * {@link PageHeaderActionsAlign}. Unset keeps the layout's own.\n *\n * keksdose's reports page (reports-page.tsx, keksdose G8) carries a LABELLED\n * `CurrencySelect` as its action: a field with its label on top is taller than the\n * title, and `stacked`'s default top alignment hangs it from the title's cap height\n * with its control well below the title's line. `center` sets it on the title's\n * middle, as that page's hand-written `items-center` row did. It aligns the whole row\n * (title block and actions against each other), since 0.14.1: 0.14.0 moved only the\n * actions box, which did nothing when the actions were the taller item (keksdose H2).\n */\n actionsAlign?: PageHeaderActionsAlign;\n /**\n * One line, cut with an ellipsis, instead of the default breaking of a long word onto\n * as many lines as it needs.\n *\n * keksdose's payees page (payees-page.tsx, live #263, keksdose G6a) wraps its title in\n * its own truncating span so a long name gives way to the `inline` header's actions\n * instead of pushing them. This is that span's behaviour on the heading itself; the\n * title block is already allowed to shrink below its content, which is what lets the\n * cut happen inside a flex row.\n *\n * The full title stays in the DOM as the heading's text, so a screen reader and the\n * document outline read all of it. There is deliberately no native `title` tooltip:\n * the kit does not use them (not on touch, not on keyboard focus, not styled).\n *\n * At Normal only: from Large up the title wraps (`truncate-until-large`, text size\n * §10.17; round 0.34 T1), as nothing is cut at Large — the actions then share the row\n * with a taller title block.\n */\n truncateTitle?: boolean;\n}\n\nconst TITLED = { titled: true };\nconst UNTITLED = { titled: false };\n\n/**\n * A page's head: optional breadcrumbs, an eyebrow, the title, a description, and the\n * page's actions.\n *\n * kastlan's (shared/components/display/page-header.tsx) is the model — title and\n * description on one side, actions on the other, stacked on a phone — and this adds\n * what its pages kept writing around it: a trail above, an eyebrow, a `ReactNode`\n * title (a status chip beside the name), and actions that WRAP when there are more\n * than fit a phone's width, instead of running off it. No outer margin: the page's\n * own rhythm (`space-y-*`, a grid gap) places it.\n */\nexport function PageHeader({\n title,\n description,\n actions,\n breadcrumbs,\n eyebrow,\n as = \"h1\",\n size = \"md\",\n mobileLayout = \"stacked\",\n secondaryActions,\n actionsAlign,\n truncateTitle = false,\n documentTitle,\n className,\n ...rest\n}: PageHeaderProps) {\n const Heading = as as ElementType;\n useDocumentTitle(\n documentTitle === false\n ? []\n : documentTitle === undefined\n ? as === \"h1\" && typeof title === \"string\"\n ? [title]\n : []\n : typeof documentTitle === \"string\"\n ? [documentTitle]\n : documentTitle,\n );\n const titled = title != null && title !== false && title !== \"\";\n const hasSecondary = secondaryActions != null;\n const wrapsSecondary = hasSecondary && mobileLayout === \"inline\";\n const align = actionsAlign != null ? ROW_ALIGN[mobileLayout][actionsAlign] : undefined;\n // keksdose H1: in `inline` the title block could shrink below its longest word, and\n // `break-words` then split \"Monatsbudget\" into \"Monatsbudg\" / \"et\" beside the actions.\n // Unless it truncates, the title keeps its longest word (min-content) and the ROW\n // wraps instead: actions that do not fit go to a line of their own, as the app's old\n // `flex-wrap` header did.\n const keepsWords = mobileLayout === \"inline\" && !truncateTitle;\n // keksdose, after H1: once the actions have wrapped below the title, the phone's\n // header was THREE rows — title / actions / secondary — where the app's old header\n // shared the second row between the month nav and the toggles (~48px of phone\n // height). So the secondary group's full-width basis (G6b: a row of its own) is\n // dropped exactly when the actions already sit below the title; then it joins their\n // row if it fits, and wraps under them if not. While the actions share the title's\n // row, nothing changes: the secondary group never climbs onto the title's row.\n //\n // CSS cannot see \"the previous item wrapped\", so it is measured — by a\n // ResizeObserver, whose first callback runs on `observe`. It is stable: the\n // secondary group comes after the actions, so its basis cannot move them. No\n // ResizeObserver (jsdom) or no layout keeps the basis, the 0.14.0 layout.\n const measuresWrap = keepsWords && hasSecondary && actions != null;\n const titleRef = useRef<HTMLDivElement>(null);\n const actionsRef = useRef<HTMLDivElement>(null);\n const [actionsWrapped, setActionsWrapped] = useState(false);\n useLayoutEffect(() => {\n const titleBox = titleRef.current;\n const actionsBox = actionsRef.current;\n if (!measuresWrap || !titleBox || !actionsBox || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n const t = titleBox.getBoundingClientRect();\n const a = actionsBox.getBoundingClientRect();\n // Unmeasured (display: none, no layout): keep the full-width basis.\n setActionsWrapped(t.height > 0 && a.height > 0 && a.top >= t.bottom - 1);\n });\n observer.observe(titleBox);\n observer.observe(actionsBox);\n if (titleBox.parentElement) observer.observe(titleBox.parentElement);\n return () => observer.disconnect();\n }, [measuresWrap]);\n return (\n <div {...rest} className={cn(\"flex min-w-0 flex-col gap-2\", className)}>\n {breadcrumbs != null && (\n <PageHeaderTrailContext.Provider value={titled ? TITLED : UNTITLED}>\n {breadcrumbs}\n </PageHeaderTrailContext.Provider>\n )}\n {/* With a second group, `inline` wraps so that group's full-width basis puts it\n on a row of its own on a phone (`stacked` is a column there already, and a\n basis would size a column item's HEIGHT); from `sm` up the row stops wrapping\n and the order utilities seat it between the title and the actions. Without\n one, nothing changes. */}\n <div\n className={cn(\n \"flex min-w-0\",\n ROW[mobileLayout],\n keepsWords ? \"flex-wrap\" : wrapsSecondary && \"flex-wrap sm:flex-nowrap\",\n align,\n )}\n >\n <div ref={titleRef} className={cn(\"flex-1\", keepsWords ? \"min-w-min\" : \"min-w-0\")}>\n {eyebrow != null && <p className={cn(SECTION_LABEL_CLASS.xs, \"mb-1\")}>{eyebrow}</p>}\n <Heading className={cn(truncateTitle ? \"truncate-until-large\" : \"break-words\", \"text-primary\", TITLE[size])}>\n {title}\n </Heading>\n {description != null && <p className=\"mt-1 text-sm text-muted\">{description}</p>}\n </div>\n {actions != null && (\n <div\n ref={actionsRef}\n className={cn(\n \"flex shrink-0 flex-wrap items-center gap-2\",\n hasSecondary && \"sm:order-2\",\n mobileLayout === \"stacked\" && STACK_ACTIONS_AT_LARGE,\n )}\n >\n {actions}\n </div>\n )}\n {hasSecondary && (\n <div\n className={cn(\n \"flex flex-wrap items-center gap-2 sm:order-1 sm:shrink-0\",\n wrapsSecondary && (measuresWrap && actionsWrapped ? \"sm:basis-auto\" : \"basis-full sm:basis-auto\"),\n )}\n >\n {secondaryActions}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/password-strength-labels.ts"],"sourcesContent":["// The English words of ./password-strength, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./password-strength re-exports them.\n\nimport type { PasswordStrengthLabels } from \"./password-strength\";\n\nexport const DEFAULT_PASSWORD_STRENGTH_LABELS: PasswordStrengthLabels = {\n tooShort: \"Too short\",\n weak: \"Weak\",\n fair: \"Fair\",\n good: \"Good\",\n strong: \"Strong\",\n announcement: (level) => `Password strength: ${level}`,\n ruleLength: (minLength) => `At least ${minLength} characters`,\n ruleCase: \"Upper and lower case\",\n ruleDigit: \"A number\",\n ruleSymbol: \"A symbol\",\n optional: (rule) => `${rule} (optional)`,\n met: \"Met:\",\n notMet: \"Not met:\",\n tooLong: (maxBytes) =>\n `At most ${maxBytes} characters (accents and emoji count for more than one).`,\n};\n"],"mappings":";AAMO,MAAM,mCAA2D;AAAA,EACtE,UAAU;AAAA,EACV,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,cAAc,CAAC,UAAU,sBAAsB,KAAK;AAAA,EACpD,YAAY,CAAC,cAAc,YAAY,SAAS;AAAA,EAChD,UAAU;AAAA,EACV,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,UAAU,CAAC,SAAS,GAAG,IAAI;AAAA,EAC3B,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,SAAS,CAAC,aACR,WAAW,QAAQ;AACvB;","names":[]}
1
+ {"version":3,"sources":["../../src/components/password-strength-labels.ts"],"sourcesContent":["// The English words of ./password-strength, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./password-strength re-exports them.\n\nimport type { PasswordStrengthLabels } from \"./password-strength\";\n\nexport const DEFAULT_PASSWORD_STRENGTH_LABELS: PasswordStrengthLabels = {\n tooShort: \"Too short\",\n weak: \"Weak\",\n fair: \"Fair\",\n good: \"Good\",\n strong: \"Strong\",\n announcement: (level) => `Password strength: ${level}`,\n ruleLength: (minLength) => `At least ${minLength} characters`,\n ruleCase: \"Upper and lower case\",\n ruleDigit: \"A number\",\n ruleSymbol: \"A symbol\",\n optional: (rule) => `${rule} (optional)`,\n met: \"Met:\",\n notMet: \"Not met:\",\n tooLong: (maxBytes) => `At most ${maxBytes} characters (accents and emoji count for more than one).`,\n};\n"],"mappings":";AAMO,MAAM,mCAA2D;AAAA,EACtE,UAAU;AAAA,EACV,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,cAAc,CAAC,UAAU,sBAAsB,KAAK;AAAA,EACpD,YAAY,CAAC,cAAc,YAAY,SAAS;AAAA,EAChD,UAAU;AAAA,EACV,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,UAAU,CAAC,SAAS,GAAG,IAAI;AAAA,EAC3B,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,SAAS,CAAC,aAAa,WAAW,QAAQ;AAC5C;","names":[]}
@@ -74,10 +74,7 @@ function PasswordStrengthMeter({
74
74
  /* @__PURE__ */ jsx("div", { className: "flex flex-1 gap-1", "aria-hidden": "true", "data-score": score, children: [1, 2, 3, 4].map((step) => /* @__PURE__ */ jsx(
75
75
  "div",
76
76
  {
77
- className: cn(
78
- "h-1 flex-1 rounded-full",
79
- step <= score ? FILL[score] : "bg-[var(--border)]"
80
- )
77
+ className: cn("h-1 flex-1 rounded-full", step <= score ? FILL[score] : "bg-[var(--border)]")
81
78
  },
82
79
  step
83
80
  )) }),
@@ -86,10 +83,7 @@ function PasswordStrengthMeter({
86
83
  showRequirements && /* @__PURE__ */ jsx("ul", { className: "flex flex-wrap gap-x-3 gap-y-0.5 text-xs", children: rules.map((r) => /* @__PURE__ */ jsxs(
87
84
  "li",
88
85
  {
89
- className: cn(
90
- "relative",
91
- r.met ? "text-success" : "text-muted"
92
- ),
86
+ className: cn("relative", r.met ? "text-success" : "text-muted"),
93
87
  "data-met": r.met,
94
88
  children: [
95
89
  /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/password-strength.tsx"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport type { ComponentPropsWithRef } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_PASSWORD_STRENGTH_LABELS } from \"./password-strength-labels\";\n\n/**\n * The bar, the word and the advisory checklist under a field where someone is\n * CHOOSING a secret.\n *\n * Ported from Keksdose (`features/auth/password-strength-meter.tsx`), where one meter\n * sits under the sign-in password and the encryption passphrase alike — a copy per\n * form is what had let the passphrase dialogs ship with no meter at all. Three things\n * changed on the way in:\n *\n * 1. **Tokens, not palette.** The four fills were red/amber/lime/emerald utilities;\n * they are `--danger` → `--warning` → `--success` now, so a consumer re-pointing\n * those families re-skins the meter with them.\n * 2. **Not colour alone.** The level is always written out beside the bar, and each\n * checklist item says \"met\" / \"not met\" to a screen reader — the ✓/○ glyphs are\n * decoration and hidden.\n * 3. **Announced.** A level change is spoken through a polite live region\n * ({@link useAnnounce}); the bar itself is `aria-hidden`, because four coloured\n * strips read out as four empty groups.\n *\n * Advisory only, as in Keksdose: nothing here blocks a submit. The caller decides what\n * is required — {@link passwordRules} marks `length` as the one rule that usually is,\n * so a form can gate on it with the same function the checklist draws from.\n *\n * Dependency-free on purpose. zxcvbn is ~400 kB of dictionaries, and neither consumer\n * ships it; a host that does can pass its scorer as `score` and keep the rest.\n */\n\n/* ── The scorer ──────────────────────────────────────────────────────────── */\n\n/** 0 = too short (or empty), 4 = strong. */\nexport type PasswordStrengthScore = 0 | 1 | 2 | 3 | 4;\n\nexport type PasswordRuleId = \"length\" | \"case\" | \"digit\" | \"symbol\";\n\nexport interface PasswordRule {\n id: PasswordRuleId;\n met: boolean;\n /** Only `length` is required; the rest are suggestions. */\n required: boolean;\n}\n\nexport interface PasswordScoreOptions {\n /** The length below which a secret is \"too short\", whatever else it has. Keep it\n * in step with the server's own minimum. Default 8. */\n minLength?: number;\n}\n\nconst DEFAULT_MIN_LENGTH = 8;\n\n/** The checklist's rules, evaluated. Pure; the order is the order they are shown in. */\nexport function passwordRules(value: string, { minLength = DEFAULT_MIN_LENGTH }: PasswordScoreOptions = {}): PasswordRule[] {\n return [\n // Code points, not UTF-16 units: an emoji is one character to the person typing it.\n { id: \"length\", met: [...value].length >= minLength, required: true },\n { id: \"case\", met: /\\p{Ll}/u.test(value) && /\\p{Lu}/u.test(value), required: false },\n { id: \"digit\", met: /\\p{Nd}/u.test(value), required: false },\n { id: \"symbol\", met: /[^\\p{L}\\p{N}\\s]/u.test(value), required: false },\n ];\n}\n\n/**\n * A 0–4 strength bucket. Length dominates, because it genuinely dominates real-world\n * guessing cost: one step each at `minLength`, `minLength + 4` and `minLength + 8`,\n * and one more for at least two of the three variety rules.\n *\n * One addition over Keksdose's curve: a secret with fewer than four DISTINCT\n * characters caps at 1. Sixteen `a`s are long, and nothing about them is strong.\n */\nexport function scorePassword(value: string, options: PasswordScoreOptions = {}): PasswordStrengthScore {\n const minLength = options.minLength ?? DEFAULT_MIN_LENGTH;\n const chars = [...value];\n // Under the minimum is never anything but \"too short\", however varied.\n if (chars.length === 0 || chars.length < minLength) return 0;\n let score = 1;\n if (chars.length >= minLength + 4) score += 1;\n if (chars.length >= minLength + 8) score += 1;\n const variety = passwordRules(value, { minLength }).filter((r) => !r.required && r.met).length;\n if (variety >= 2) score += 1;\n if (new Set(chars).size < 4) score = 1;\n return Math.min(score, 4) as PasswordStrengthScore;\n}\n\n/** UTF-8 byte length — what a bcrypt-style ceiling counts. \"ü\" is two, an emoji four. */\nexport function passwordByteLength(value: string): number {\n return new TextEncoder().encode(value).length;\n}\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the meter renders or speaks. */\nexport interface PasswordStrengthLabels {\n /** The five level names, shown beside the bar. Score 0 … 4. */\n tooShort: string;\n weak: string;\n fair: string;\n good: string;\n strong: string;\n /** What the live region says when the level changes, given the level's name. */\n announcement: (level: string) => string;\n /** The length rule, given the minimum. */\n ruleLength: (minLength: number) => string;\n ruleCase: string;\n ruleDigit: string;\n ruleSymbol: string;\n /** A suggestion rather than a requirement, given the rule's text. */\n optional: (rule: string) => string;\n /** Screen-reader-only state before each checklist item — the ✓/○ are hidden. */\n met: string;\n notMet: string;\n /** Shown when the value is over `maxBytes`, given that ceiling. */\n tooLong: (maxBytes: number) => string;\n}\n\n// Defined in ./password-strength-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_PASSWORD_STRENGTH_LABELS };\n\nconst LEVEL_KEYS = [\"tooShort\", \"weak\", \"fair\", \"good\", \"strong\"] as const;\n\n/* ── The meter ───────────────────────────────────────────────────────────── */\n\n// The filled segments' colour per score. \"Good\" sits between the warning and the\n// success hue rather than borrowing either, so 3 and 4 differ in colour as well as in\n// segment count. Index 0 is never drawn — a too-short secret fills no segment.\nconst FILL: Record<PasswordStrengthScore, string> = {\n 0: \"\",\n 1: \"bg-danger\",\n 2: \"bg-warning\",\n 3: \"bg-[color-mix(in_oklab,var(--success)_60%,var(--warning))]\",\n 4: \"bg-success\",\n};\n\nexport interface PasswordStrengthMeterProps extends ComponentPropsWithRef<\"div\"> {\n /** The secret being chosen. Nothing visible renders while it is empty. */\n value: string;\n /** Replace the built-in scorer (e.g. with zxcvbn's `score`). Must be pure. */\n score?: (value: string) => PasswordStrengthScore;\n /** See {@link PasswordScoreOptions.minLength}. Drives the built-in scorer and the\n * length rule's text. */\n minLength?: number;\n /** Show the checklist under the bar. Default `true`. */\n showRequirements?: boolean;\n /**\n * A byte ceiling to warn about, if the secret has one — bcrypt reads at most 72\n * BYTES. Leave it unset for a secret that has none (a passphrase stretched in the\n * browser): inventing one talks people out of the long passphrase they chose.\n */\n maxBytes?: number;\n /** User-facing strings; see {@link PasswordStrengthLabels}. */\n labels?: LabelOverride<PasswordStrengthLabels>;\n}\n\n/**\n * Render directly under the password field. To have the field itself describe its\n * strength, give this an `id` and point the input's `aria-describedby` at it.\n */\nexport function PasswordStrengthMeter({\n value,\n score: scoreProp,\n minLength = DEFAULT_MIN_LENGTH,\n showRequirements = true,\n maxBytes,\n labels: labelsProp,\n className,\n ...rest\n}: PasswordStrengthMeterProps) {\n // Prop > <UiKitProvider> > English, like every labelled component in the kit.\n const labels = useKitLabels(\"passwordStrength\", DEFAULT_PASSWORD_STRENGTH_LABELS, labelsProp);\n const { announce, regionProps } = useAnnounce();\n\n const empty = value.length === 0;\n const score = empty ? 0 : scoreProp ? scoreProp(value) : scorePassword(value, { minLength });\n const level = labels[LEVEL_KEYS[score]];\n\n // Speak on a CHANGE of level only — not on every keystroke, and not on mount (a\n // prefilled field has not changed anything). Clearing the field says nothing: the\n // user did that on purpose and the meter disappearing is the whole news.\n const previous = useRef(score);\n useEffect(() => {\n if (previous.current === score) return;\n previous.current = score;\n if (!empty) announce(labels.announcement(level));\n }, [score, empty, level, labels, announce]);\n\n const rules = passwordRules(value, { minLength });\n const ruleText: Record<PasswordRuleId, string> = {\n length: labels.ruleLength(minLength),\n case: labels.ruleCase,\n digit: labels.ruleDigit,\n symbol: labels.ruleSymbol,\n };\n const tooLong = maxBytes !== undefined && passwordByteLength(value) > maxBytes;\n\n return (\n <div {...rest} className={cn(\"mt-1.5 space-y-1\", className)}>\n {/* Rendered from the start and never unmounted — see useAnnounce. */}\n <div {...regionProps} />\n {!empty && (\n <>\n <div className=\"flex items-center gap-2\">\n <div className=\"flex flex-1 gap-1\" aria-hidden=\"true\" data-score={score}>\n {[1, 2, 3, 4].map((step) => (\n <div\n key={step}\n className={cn(\n \"h-1 flex-1 rounded-full\",\n step <= score ? FILL[score] : \"bg-[var(--border)]\",\n )}\n />\n ))}\n </div>\n <p className=\"min-w-[4.5rem] text-end text-xs text-secondary\">{level}</p>\n </div>\n {showRequirements && (\n <ul className=\"flex flex-wrap gap-x-3 gap-y-0.5 text-xs\">\n {rules.map((r) => (\n <li\n key={r.id}\n // `relative`: the sr-only state needs a local containing block.\n className={cn(\n \"relative\",\n r.met ? \"text-success\" : \"text-muted\",\n )}\n data-met={r.met}\n >\n <span className=\"sr-only\">{r.met ? labels.met : labels.notMet} </span>\n <span aria-hidden=\"true\">{r.met ? \"✓\" : \"○\"}</span>{\" \"}\n {r.required ? ruleText[r.id] : labels.optional(ruleText[r.id])}\n </li>\n ))}\n </ul>\n )}\n {tooLong && <p className=\"text-xs text-danger\">{labels.tooLong(maxBytes)}</p>}\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/password-strength.tsx"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport type { ComponentPropsWithRef } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_PASSWORD_STRENGTH_LABELS } from \"./password-strength-labels\";\n\n/**\n * The bar, the word and the advisory checklist under a field where someone is\n * CHOOSING a secret.\n *\n * Ported from Keksdose (`features/auth/password-strength-meter.tsx`), where one meter\n * sits under the sign-in password and the encryption passphrase alike — a copy per\n * form is what had let the passphrase dialogs ship with no meter at all. Three things\n * changed on the way in:\n *\n * 1. **Tokens, not palette.** The four fills were red/amber/lime/emerald utilities;\n * they are `--danger` → `--warning` → `--success` now, so a consumer re-pointing\n * those families re-skins the meter with them.\n * 2. **Not colour alone.** The level is always written out beside the bar, and each\n * checklist item says \"met\" / \"not met\" to a screen reader — the ✓/○ glyphs are\n * decoration and hidden.\n * 3. **Announced.** A level change is spoken through a polite live region\n * ({@link useAnnounce}); the bar itself is `aria-hidden`, because four coloured\n * strips read out as four empty groups.\n *\n * Advisory only, as in Keksdose: nothing here blocks a submit. The caller decides what\n * is required — {@link passwordRules} marks `length` as the one rule that usually is,\n * so a form can gate on it with the same function the checklist draws from.\n *\n * Dependency-free on purpose. zxcvbn is ~400 kB of dictionaries, and neither consumer\n * ships it; a host that does can pass its scorer as `score` and keep the rest.\n */\n\n/* ── The scorer ──────────────────────────────────────────────────────────── */\n\n/** 0 = too short (or empty), 4 = strong. */\nexport type PasswordStrengthScore = 0 | 1 | 2 | 3 | 4;\n\nexport type PasswordRuleId = \"length\" | \"case\" | \"digit\" | \"symbol\";\n\nexport interface PasswordRule {\n id: PasswordRuleId;\n met: boolean;\n /** Only `length` is required; the rest are suggestions. */\n required: boolean;\n}\n\nexport interface PasswordScoreOptions {\n /** The length below which a secret is \"too short\", whatever else it has. Keep it\n * in step with the server's own minimum. Default 8. */\n minLength?: number;\n}\n\nconst DEFAULT_MIN_LENGTH = 8;\n\n/** The checklist's rules, evaluated. Pure; the order is the order they are shown in. */\nexport function passwordRules(\n value: string,\n { minLength = DEFAULT_MIN_LENGTH }: PasswordScoreOptions = {},\n): PasswordRule[] {\n return [\n // Code points, not UTF-16 units: an emoji is one character to the person typing it.\n { id: \"length\", met: [...value].length >= minLength, required: true },\n { id: \"case\", met: /\\p{Ll}/u.test(value) && /\\p{Lu}/u.test(value), required: false },\n { id: \"digit\", met: /\\p{Nd}/u.test(value), required: false },\n { id: \"symbol\", met: /[^\\p{L}\\p{N}\\s]/u.test(value), required: false },\n ];\n}\n\n/**\n * A 0–4 strength bucket. Length dominates, because it genuinely dominates real-world\n * guessing cost: one step each at `minLength`, `minLength + 4` and `minLength + 8`,\n * and one more for at least two of the three variety rules.\n *\n * One addition over Keksdose's curve: a secret with fewer than four DISTINCT\n * characters caps at 1. Sixteen `a`s are long, and nothing about them is strong.\n */\nexport function scorePassword(value: string, options: PasswordScoreOptions = {}): PasswordStrengthScore {\n const minLength = options.minLength ?? DEFAULT_MIN_LENGTH;\n const chars = [...value];\n // Under the minimum is never anything but \"too short\", however varied.\n if (chars.length === 0 || chars.length < minLength) return 0;\n let score = 1;\n if (chars.length >= minLength + 4) score += 1;\n if (chars.length >= minLength + 8) score += 1;\n const variety = passwordRules(value, { minLength }).filter((r) => !r.required && r.met).length;\n if (variety >= 2) score += 1;\n if (new Set(chars).size < 4) score = 1;\n return Math.min(score, 4) as PasswordStrengthScore;\n}\n\n/** UTF-8 byte length — what a bcrypt-style ceiling counts. \"ü\" is two, an emoji four. */\nexport function passwordByteLength(value: string): number {\n return new TextEncoder().encode(value).length;\n}\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the meter renders or speaks. */\nexport interface PasswordStrengthLabels {\n /** The five level names, shown beside the bar. Score 0 … 4. */\n tooShort: string;\n weak: string;\n fair: string;\n good: string;\n strong: string;\n /** What the live region says when the level changes, given the level's name. */\n announcement: (level: string) => string;\n /** The length rule, given the minimum. */\n ruleLength: (minLength: number) => string;\n ruleCase: string;\n ruleDigit: string;\n ruleSymbol: string;\n /** A suggestion rather than a requirement, given the rule's text. */\n optional: (rule: string) => string;\n /** Screen-reader-only state before each checklist item — the ✓/○ are hidden. */\n met: string;\n notMet: string;\n /** Shown when the value is over `maxBytes`, given that ceiling. */\n tooLong: (maxBytes: number) => string;\n}\n\n// Defined in ./password-strength-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_PASSWORD_STRENGTH_LABELS };\n\nconst LEVEL_KEYS = [\"tooShort\", \"weak\", \"fair\", \"good\", \"strong\"] as const;\n\n/* ── The meter ───────────────────────────────────────────────────────────── */\n\n// The filled segments' colour per score. \"Good\" sits between the warning and the\n// success hue rather than borrowing either, so 3 and 4 differ in colour as well as in\n// segment count. Index 0 is never drawn — a too-short secret fills no segment.\nconst FILL: Record<PasswordStrengthScore, string> = {\n 0: \"\",\n 1: \"bg-danger\",\n 2: \"bg-warning\",\n 3: \"bg-[color-mix(in_oklab,var(--success)_60%,var(--warning))]\",\n 4: \"bg-success\",\n};\n\nexport interface PasswordStrengthMeterProps extends ComponentPropsWithRef<\"div\"> {\n /** The secret being chosen. Nothing visible renders while it is empty. */\n value: string;\n /** Replace the built-in scorer (e.g. with zxcvbn's `score`). Must be pure. */\n score?: (value: string) => PasswordStrengthScore;\n /** See {@link PasswordScoreOptions.minLength}. Drives the built-in scorer and the\n * length rule's text. */\n minLength?: number;\n /** Show the checklist under the bar. Default `true`. */\n showRequirements?: boolean;\n /**\n * A byte ceiling to warn about, if the secret has one — bcrypt reads at most 72\n * BYTES. Leave it unset for a secret that has none (a passphrase stretched in the\n * browser): inventing one talks people out of the long passphrase they chose.\n */\n maxBytes?: number;\n /** User-facing strings; see {@link PasswordStrengthLabels}. */\n labels?: LabelOverride<PasswordStrengthLabels>;\n}\n\n/**\n * Render directly under the password field. To have the field itself describe its\n * strength, give this an `id` and point the input's `aria-describedby` at it.\n */\nexport function PasswordStrengthMeter({\n value,\n score: scoreProp,\n minLength = DEFAULT_MIN_LENGTH,\n showRequirements = true,\n maxBytes,\n labels: labelsProp,\n className,\n ...rest\n}: PasswordStrengthMeterProps) {\n // Prop > <UiKitProvider> > English, like every labelled component in the kit.\n const labels = useKitLabels(\"passwordStrength\", DEFAULT_PASSWORD_STRENGTH_LABELS, labelsProp);\n const { announce, regionProps } = useAnnounce();\n\n const empty = value.length === 0;\n const score = empty ? 0 : scoreProp ? scoreProp(value) : scorePassword(value, { minLength });\n const level = labels[LEVEL_KEYS[score]];\n\n // Speak on a CHANGE of level only — not on every keystroke, and not on mount (a\n // prefilled field has not changed anything). Clearing the field says nothing: the\n // user did that on purpose and the meter disappearing is the whole news.\n const previous = useRef(score);\n useEffect(() => {\n if (previous.current === score) return;\n previous.current = score;\n if (!empty) announce(labels.announcement(level));\n }, [score, empty, level, labels, announce]);\n\n const rules = passwordRules(value, { minLength });\n const ruleText: Record<PasswordRuleId, string> = {\n length: labels.ruleLength(minLength),\n case: labels.ruleCase,\n digit: labels.ruleDigit,\n symbol: labels.ruleSymbol,\n };\n const tooLong = maxBytes !== undefined && passwordByteLength(value) > maxBytes;\n\n return (\n <div {...rest} className={cn(\"mt-1.5 space-y-1\", className)}>\n {/* Rendered from the start and never unmounted — see useAnnounce. */}\n <div {...regionProps} />\n {!empty && (\n <>\n <div className=\"flex items-center gap-2\">\n <div className=\"flex flex-1 gap-1\" aria-hidden=\"true\" data-score={score}>\n {[1, 2, 3, 4].map((step) => (\n <div\n key={step}\n className={cn(\"h-1 flex-1 rounded-full\", step <= score ? FILL[score] : \"bg-[var(--border)]\")}\n />\n ))}\n </div>\n <p className=\"min-w-[4.5rem] text-end text-xs text-secondary\">{level}</p>\n </div>\n {showRequirements && (\n <ul className=\"flex flex-wrap gap-x-3 gap-y-0.5 text-xs\">\n {rules.map((r) => (\n <li\n key={r.id}\n // `relative`: the sr-only state needs a local containing block.\n className={cn(\"relative\", r.met ? \"text-success\" : \"text-muted\")}\n data-met={r.met}\n >\n <span className=\"sr-only\">{r.met ? labels.met : labels.notMet} </span>\n <span aria-hidden=\"true\">{r.met ? \"✓\" : \"○\"}</span>{\" \"}\n {r.required ? ruleText[r.id] : labels.optional(ruleText[r.id])}\n </li>\n ))}\n </ul>\n )}\n {tooLong && <p className=\"text-xs text-danger\">{labels.tooLong(maxBytes)}</p>}\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -3,14 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { useKitLabels } from "../i18n/kit-labels.js";
5
5
  import { cn } from "../lib/cn.js";
6
- import {
7
- PHONE_COUNTRIES,
8
- PHONE_DIAL_CODES,
9
- formatNationalPhone,
10
- formatPhone,
11
- isE164,
12
- parsePhone
13
- } from "../lib/phone.js";
6
+ import { PHONE_COUNTRIES, PHONE_DIAL_CODES, formatNationalPhone, formatPhone, isE164, parsePhone } from "../lib/phone.js";
14
7
  import { useFieldMessages } from "./field-parts.js";
15
8
  import { Input } from "./input.js";
16
9
  import { Select } from "./select.js";
@@ -115,55 +108,47 @@ function PhoneInput({
115
108
  ariaInvalid: rest["aria-invalid"]
116
109
  });
117
110
  const named = typeof label === "string" && label !== "";
118
- return /* @__PURE__ */ jsxs(
119
- "div",
120
- {
121
- className,
122
- role: named ? "group" : void 0,
123
- "aria-label": named ? label : void 0,
124
- children: [
125
- /* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-2 large:max-sm:flex-wrap", children: [
126
- /* @__PURE__ */ jsxs(
127
- Select,
128
- {
129
- "aria-label": text.countryCode,
130
- value: country,
131
- disabled,
132
- onChange: (e) => handleCountry(e.target.value),
133
- className: "w-28 shrink-0",
134
- selectClassName: "h-full tabular-nums",
135
- children: [
136
- options.map((code) => /* @__PURE__ */ jsx("option", { value: code, children: `${code} +${PHONE_DIAL_CODES[code]}` }, code)),
137
- /* @__PURE__ */ jsx("option", { value: "other", children: text.other })
138
- ]
139
- }
140
- ),
141
- /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 large:max-sm:basis-full", children: /* @__PURE__ */ jsx(
142
- Input,
143
- {
144
- ref,
145
- ...rest,
146
- type: "tel",
147
- autoComplete: rest.autoComplete ?? "tel",
148
- label,
149
- hint: messages.labelHint,
150
- invalid: messages.isInvalid,
151
- "aria-describedby": messages.describedBy,
152
- disabled,
153
- value: display,
154
- onChange: handleChange,
155
- onBlur: (e) => {
156
- setDraft(null);
157
- onBlur?.(e);
158
- },
159
- inputClassName: cn("tabular-nums", inputClassName)
160
- }
161
- ) })
162
- ] }),
163
- messages.below
164
- ]
165
- }
166
- );
111
+ return /* @__PURE__ */ jsxs("div", { className, role: named ? "group" : void 0, "aria-label": named ? label : void 0, children: [
112
+ /* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-2 large:max-sm:flex-wrap", children: [
113
+ /* @__PURE__ */ jsxs(
114
+ Select,
115
+ {
116
+ "aria-label": text.countryCode,
117
+ value: country,
118
+ disabled,
119
+ onChange: (e) => handleCountry(e.target.value),
120
+ className: "w-28 shrink-0",
121
+ selectClassName: "h-full tabular-nums",
122
+ children: [
123
+ options.map((code) => /* @__PURE__ */ jsx("option", { value: code, children: `${code} +${PHONE_DIAL_CODES[code]}` }, code)),
124
+ /* @__PURE__ */ jsx("option", { value: "other", children: text.other })
125
+ ]
126
+ }
127
+ ),
128
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 large:max-sm:basis-full", children: /* @__PURE__ */ jsx(
129
+ Input,
130
+ {
131
+ ref,
132
+ ...rest,
133
+ type: "tel",
134
+ autoComplete: rest.autoComplete ?? "tel",
135
+ label,
136
+ hint: messages.labelHint,
137
+ invalid: messages.isInvalid,
138
+ "aria-describedby": messages.describedBy,
139
+ disabled,
140
+ value: display,
141
+ onChange: handleChange,
142
+ onBlur: (e) => {
143
+ setDraft(null);
144
+ onBlur?.(e);
145
+ },
146
+ inputClassName: cn("tabular-nums", inputClassName)
147
+ }
148
+ ) })
149
+ ] }),
150
+ messages.below
151
+ ] });
167
152
  }
168
153
  export {
169
154
  DEFAULT_PHONE_INPUT_LABELS,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/phone-input.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ChangeEvent, FocusEvent } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n PHONE_COUNTRIES,\n PHONE_DIAL_CODES,\n formatNationalPhone,\n formatPhone,\n isE164,\n parsePhone,\n} from \"../lib/phone\";\nimport type { ParsedPhone, PhoneCountry, PhoneCountryCode } from \"../lib/phone\";\nimport { useFieldMessages } from \"./field-parts\";\nimport { Input, type InputProps } from \"./input\";\nimport { Select } from \"./select\";\nimport { DEFAULT_PHONE_INPUT_LABELS } from \"./phone-input-labels\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words {@link PhoneInput} renders on its own behalf. */\nexport interface PhoneInputLabels {\n /** The country-code select's accessible name. */\n countryCode: string;\n /** The select's last option: a number from a country not listed, typed with its\n * own \"+\". */\n other: string;\n}\n\n// Defined in ./phone-input-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_PHONE_INPUT_LABELS };\n\n/**\n * The `phoneInput` namespace: prop > `<UiKitProvider labels>` > English (0.22.0).\n */\nfunction usePhoneInputLabels(prop: Partial<PhoneInputLabels> | undefined): PhoneInputLabels {\n const labels: PhoneInputLabels = useKitLabels(\"phoneInput\", DEFAULT_PHONE_INPUT_LABELS, prop);\n return labels;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\n/** A stored value read back: an E.164 number these rules can read, else `null`. */\nfunction storedNumber(value: string): ParsedPhone | null {\n return isE164(value) ? parsePhone(value, \"other\") : null;\n}\n\n/** The select's country for a stored E.164 value, or `null` for free text. */\nfunction countryOf(value: string): PhoneCountry | null {\n const parsed = storedNumber(value);\n return parsed ? (parsed.country ?? \"other\") : null;\n}\n\n/** What the field emits for typed text: E.164 when it reads, the text unchanged when\n * it does not. */\nfunction emitFor(raw: string, country: PhoneCountry): string {\n return parsePhone(raw, country)?.e164 ?? raw;\n}\n\n/**\n * The text a stored value shows, outside of typing. Under its own country: the national\n * number grouped (\"21 000 00 01\" beside \"CH +41\"). Under another (a \"+33\" number while\n * the select says \"Other\"): the whole international number. Free text: as it is.\n */\nfunction displayOf(value: string, country: PhoneCountry): string {\n const parsed = storedNumber(value);\n if (!parsed) return value;\n if (parsed.country !== null && parsed.country === country) {\n return formatNationalPhone(parsed.national, parsed.country);\n }\n return formatPhone(parsed.e164);\n}\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport interface PhoneInputProps extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\"> {\n /** The number: E.164 (\"+41210000001\") for one the field could read, or free text —\n * a value saved before the field existed is shown exactly as it is. `\"\"` is no\n * number. */\n value: string;\n /** On every change: E.164 when the typed text reads as a number of the selected\n * country (or carries its own \"+\"), otherwise the typed text UNCHANGED. Nothing is\n * refused — see {@link PhoneInput}. Plain `onChange` still fires with the raw event;\n * bind this one to a form. */\n onValueChange?: (value: string) => void;\n /** The country a number typed without \"+\" belongs to, until the value says\n * otherwise. Default `\"CH\"`. */\n defaultCountry?: PhoneCountry;\n /** The countries the select offers, in order; \"Other\" always follows. Default\n * {@link PHONE_COUNTRIES} (CH, LI, DE, AT, FR, IT). A stored number from a country\n * left out still shows under its own code. */\n countries?: readonly PhoneCountryCode[];\n /** See {@link PhoneInputLabels}. */\n labels?: LabelOverride<PhoneInputLabels>;\n /** Classes for the whole field — the select, the input and the lines under them.\n * `inputClassName` reaches the `<input>`. */\n className?: string;\n}\n\n/**\n * A phone number field (kastlan): a country-code select and the number, stored as\n * E.164 and shown grouped.\n *\n * kastlan's contacts kept whatever was typed — \"079 …\", \"+41 (0)79 …\", \"0041 79 …\" —\n * which nobody could dial from the app or match against the next import. This field\n * turns all of those into one stored form, \"+41210000001\", without taking anything\n * away from the person typing:\n *\n * - **A country to type against.** The select (default CH, then LI, DE, AT, FR, IT\n * and \"Other\") says which country a number without \"+\" belongs to; the national\n * trunk 0 may be typed or left out. Typing a \"+\" or \"00\" overrides it, and the\n * select follows the number (\"+49 …\" switches it to DE). Changing the select moves\n * the national number to the new country code.\n * - **E.164 when it reads, the text when it does not.** A number the rules read is\n * emitted as E.164; anything else (\"ask reception\", an extension) is emitted exactly\n * as typed. Never refused, never an error of its own: a contact's number is often\n * known only roughly, and a field that will not save \"079 … (mornings)\" makes\n * people type it into the notes instead. An app that needs a dialable number\n * checks {@link isE164} on submit.\n * - **Existing values untouched.** A free-text value from before is shown exactly as\n * it is, and normalised only when someone edits it — the field never rewrites a\n * record just by being opened.\n * - **Grouped when settled.** While typing the field shows what was typed; on blur a\n * number it read is regrouped by its country (\"21 000 00 01\").\n *\n * **The limitation** — there is no libphonenumber behind this (≈150 kB of metadata in\n * every bundle): the six countries have light rules (the trunk 0, a plausible length)\n * and every other country is any \"+\" with 8–15 digits. A well-formed number that does\n * not exist passes, and German and Austrian landlines, whose area codes vary in length,\n * are grouped only where the split is known (mobile prefixes, the largest cities) and\n * otherwise shown as one block. See `lib/phone.ts`.\n *\n * `label`, `hint`, `error` and `invalid` read as on {@link Input}: the label floats in\n * the number's box, and a text hint and the error go under the whole row, attached to\n * the number with `aria-describedby`. The select is named by `labels.countryCode`, and\n * the pair is a group named by a string `label`.\n */\nexport function PhoneInput({\n ref,\n value,\n onValueChange,\n onChange,\n onBlur,\n defaultCountry = \"CH\",\n countries = PHONE_COUNTRIES,\n labels,\n className,\n inputClassName,\n label,\n hint,\n help,\n error,\n invalid,\n disabled,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: PhoneInputProps) {\n const text = usePhoneInputLabels(labels);\n const [country, setCountry] = useState<PhoneCountry>(() => countryOf(value) ?? defaultCountry);\n // What was typed, shown for as long as it still reads as the value (AmountInput's\n // draft): the person's own spacing stays under their fingers, and is regrouped on\n // blur.\n const [draft, setDraft] = useState<string | null>(null);\n // The last value this field emitted, to tell its own echo from a value set outside.\n const [emitted, setEmitted] = useState<string | null>(null);\n const [seen, setSeen] = useState(value);\n if (seen !== value) {\n setSeen(value);\n if (value !== emitted) {\n // Set from outside (a reset, another record): its own country, none of our text.\n const own = countryOf(value);\n if (own) setCountry(own);\n setDraft(null);\n }\n }\n\n const emit = (next: string) => {\n setEmitted(next);\n onValueChange?.(next);\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n const raw = e.target.value;\n setDraft(raw);\n const parsed = parsePhone(raw, country);\n // A number typed with its own \"+\" takes the select with it.\n if (parsed && parsed.country !== country) setCountry(parsed.country ?? \"other\");\n emit(parsed?.e164 ?? raw);\n };\n\n const handleCountry = (next: PhoneCountry) => {\n setCountry(next);\n const stored = storedNumber(value);\n if (stored) {\n // \"Other\", the same country, or a generic number with its own \"+\": the number\n // stays, only the way it is shown changes.\n if (next === \"other\" || stored.country === null || stored.country === next) {\n setDraft(null);\n return;\n }\n // A different listed country: the national number moves to its code. If it does\n // not read there, the digits are kept as text for the person to finish.\n emit(parsePhone(stored.national, next)?.e164 ?? stored.national);\n setDraft(null);\n return;\n }\n // Free text (or a draft): read it again under the new country.\n const source = draft ?? value;\n const parsed = source.trim() === \"\" ? null : parsePhone(source, next);\n if (parsed && parsed.e164 !== value) {\n setDraft(source);\n emit(parsed.e164);\n }\n };\n\n const display = draft !== null && emitFor(draft, country) === value ? draft : displayOf(value, country);\n\n // The select offers the configured countries, plus the stored number's own if it\n // is not among them — a select cannot show a value it has no option for.\n const options: PhoneCountryCode[] = [...countries];\n if (country !== \"other\" && !options.includes(country)) options.push(country);\n\n // A FieldHint \"?\" rides the number's label line; text — and a FieldHint at Large and\n // on touch (§4) — goes under the whole row with the error. Through useFieldMessages\n // since 0.34, not a copy of its rule (DUP-2, DUP-8).\n const messages = useFieldMessages({\n hint,\n // The number's label line takes the help's \"?\" (0.36); the hint is then the caption.\n help,\n name: label ?? rest[\"aria-label\"],\n error,\n invalid,\n describedBy: ariaDescribedBy,\n ariaInvalid: rest[\"aria-invalid\"],\n });\n const named = typeof label === \"string\" && label !== \"\";\n\n return (\n <div\n className={className}\n role={named ? \"group\" : undefined}\n aria-label={named ? label : undefined}\n >\n {/* On a phone at Large the number takes a line of its own under the country (0.32.1,\n the 360 px Extra-large sweep; docs/text-size-harmonization.md §4 \"nothing\n truncates\"): the 7rem select is 168 px at 150 %, and on a 360 px phone it left\n the number 82 px — \"Ph…\" over \"021 (\". `large:max-sm:`, the scaled phone width,\n so a wider screen at Large and Normal keep the one row. */}\n <div className=\"flex items-stretch gap-2 large:max-sm:flex-wrap\">\n <Select\n aria-label={text.countryCode}\n value={country}\n disabled={disabled}\n onChange={(e) => handleCountry(e.target.value as PhoneCountry)}\n className=\"w-28 shrink-0\"\n // Stretched to the number's height: a labelled field is taller than an\n // unlabelled select, and the pair should read as one row.\n selectClassName=\"h-full tabular-nums\"\n >\n {options.map((code) => (\n <option key={code} value={code}>\n {`${code} +${PHONE_DIAL_CODES[code]}`}\n </option>\n ))}\n <option value=\"other\">{text.other}</option>\n </Select>\n {/* Its own flex item, so nothing Input wraps itself in can take the sizing. */}\n <div className=\"min-w-0 flex-1 large:max-sm:basis-full\">\n <Input\n ref={ref}\n {...rest}\n type=\"tel\"\n autoComplete={rest.autoComplete ?? \"tel\"}\n label={label}\n hint={messages.labelHint}\n invalid={messages.isInvalid}\n aria-describedby={messages.describedBy}\n disabled={disabled}\n value={display}\n onChange={handleChange}\n onBlur={(e: FocusEvent<HTMLInputElement>) => {\n setDraft(null);\n onBlur?.(e);\n }}\n inputClassName={cn(\"tabular-nums\", inputClassName)}\n />\n </div>\n </div>\n {messages.below}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/phone-input.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ChangeEvent, FocusEvent } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { PHONE_COUNTRIES, PHONE_DIAL_CODES, formatNationalPhone, formatPhone, isE164, parsePhone } from \"../lib/phone\";\nimport type { ParsedPhone, PhoneCountry, PhoneCountryCode } from \"../lib/phone\";\nimport { useFieldMessages } from \"./field-parts\";\nimport { Input, type InputProps } from \"./input\";\nimport { Select } from \"./select\";\nimport { DEFAULT_PHONE_INPUT_LABELS } from \"./phone-input-labels\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words {@link PhoneInput} renders on its own behalf. */\nexport interface PhoneInputLabels {\n /** The country-code select's accessible name. */\n countryCode: string;\n /** The select's last option: a number from a country not listed, typed with its\n * own \"+\". */\n other: string;\n}\n\n// Defined in ./phone-input-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_PHONE_INPUT_LABELS };\n\n/**\n * The `phoneInput` namespace: prop > `<UiKitProvider labels>` > English (0.22.0).\n */\nfunction usePhoneInputLabels(prop: Partial<PhoneInputLabels> | undefined): PhoneInputLabels {\n const labels: PhoneInputLabels = useKitLabels(\"phoneInput\", DEFAULT_PHONE_INPUT_LABELS, prop);\n return labels;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\n/** A stored value read back: an E.164 number these rules can read, else `null`. */\nfunction storedNumber(value: string): ParsedPhone | null {\n return isE164(value) ? parsePhone(value, \"other\") : null;\n}\n\n/** The select's country for a stored E.164 value, or `null` for free text. */\nfunction countryOf(value: string): PhoneCountry | null {\n const parsed = storedNumber(value);\n return parsed ? (parsed.country ?? \"other\") : null;\n}\n\n/** What the field emits for typed text: E.164 when it reads, the text unchanged when\n * it does not. */\nfunction emitFor(raw: string, country: PhoneCountry): string {\n return parsePhone(raw, country)?.e164 ?? raw;\n}\n\n/**\n * The text a stored value shows, outside of typing. Under its own country: the national\n * number grouped (\"21 000 00 01\" beside \"CH +41\"). Under another (a \"+33\" number while\n * the select says \"Other\"): the whole international number. Free text: as it is.\n */\nfunction displayOf(value: string, country: PhoneCountry): string {\n const parsed = storedNumber(value);\n if (!parsed) return value;\n if (parsed.country !== null && parsed.country === country) {\n return formatNationalPhone(parsed.national, parsed.country);\n }\n return formatPhone(parsed.e164);\n}\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport interface PhoneInputProps extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\"> {\n /** The number: E.164 (\"+41210000001\") for one the field could read, or free text —\n * a value saved before the field existed is shown exactly as it is. `\"\"` is no\n * number. */\n value: string;\n /** On every change: E.164 when the typed text reads as a number of the selected\n * country (or carries its own \"+\"), otherwise the typed text UNCHANGED. Nothing is\n * refused — see {@link PhoneInput}. Plain `onChange` still fires with the raw event;\n * bind this one to a form. */\n onValueChange?: (value: string) => void;\n /** The country a number typed without \"+\" belongs to, until the value says\n * otherwise. Default `\"CH\"`. */\n defaultCountry?: PhoneCountry;\n /** The countries the select offers, in order; \"Other\" always follows. Default\n * {@link PHONE_COUNTRIES} (CH, LI, DE, AT, FR, IT). A stored number from a country\n * left out still shows under its own code. */\n countries?: readonly PhoneCountryCode[];\n /** See {@link PhoneInputLabels}. */\n labels?: LabelOverride<PhoneInputLabels>;\n /** Classes for the whole field — the select, the input and the lines under them.\n * `inputClassName` reaches the `<input>`. */\n className?: string;\n}\n\n/**\n * A phone number field (kastlan): a country-code select and the number, stored as\n * E.164 and shown grouped.\n *\n * kastlan's contacts kept whatever was typed — \"079 …\", \"+41 (0)79 …\", \"0041 79 …\" —\n * which nobody could dial from the app or match against the next import. This field\n * turns all of those into one stored form, \"+41210000001\", without taking anything\n * away from the person typing:\n *\n * - **A country to type against.** The select (default CH, then LI, DE, AT, FR, IT\n * and \"Other\") says which country a number without \"+\" belongs to; the national\n * trunk 0 may be typed or left out. Typing a \"+\" or \"00\" overrides it, and the\n * select follows the number (\"+49 …\" switches it to DE). Changing the select moves\n * the national number to the new country code.\n * - **E.164 when it reads, the text when it does not.** A number the rules read is\n * emitted as E.164; anything else (\"ask reception\", an extension) is emitted exactly\n * as typed. Never refused, never an error of its own: a contact's number is often\n * known only roughly, and a field that will not save \"079 … (mornings)\" makes\n * people type it into the notes instead. An app that needs a dialable number\n * checks {@link isE164} on submit.\n * - **Existing values untouched.** A free-text value from before is shown exactly as\n * it is, and normalised only when someone edits it — the field never rewrites a\n * record just by being opened.\n * - **Grouped when settled.** While typing the field shows what was typed; on blur a\n * number it read is regrouped by its country (\"21 000 00 01\").\n *\n * **The limitation** — there is no libphonenumber behind this (≈150 kB of metadata in\n * every bundle): the six countries have light rules (the trunk 0, a plausible length)\n * and every other country is any \"+\" with 8–15 digits. A well-formed number that does\n * not exist passes, and German and Austrian landlines, whose area codes vary in length,\n * are grouped only where the split is known (mobile prefixes, the largest cities) and\n * otherwise shown as one block. See `lib/phone.ts`.\n *\n * `label`, `hint`, `error` and `invalid` read as on {@link Input}: the label floats in\n * the number's box, and a text hint and the error go under the whole row, attached to\n * the number with `aria-describedby`. The select is named by `labels.countryCode`, and\n * the pair is a group named by a string `label`.\n */\nexport function PhoneInput({\n ref,\n value,\n onValueChange,\n onChange,\n onBlur,\n defaultCountry = \"CH\",\n countries = PHONE_COUNTRIES,\n labels,\n className,\n inputClassName,\n label,\n hint,\n help,\n error,\n invalid,\n disabled,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: PhoneInputProps) {\n const text = usePhoneInputLabels(labels);\n const [country, setCountry] = useState<PhoneCountry>(() => countryOf(value) ?? defaultCountry);\n // What was typed, shown for as long as it still reads as the value (AmountInput's\n // draft): the person's own spacing stays under their fingers, and is regrouped on\n // blur.\n const [draft, setDraft] = useState<string | null>(null);\n // The last value this field emitted, to tell its own echo from a value set outside.\n const [emitted, setEmitted] = useState<string | null>(null);\n const [seen, setSeen] = useState(value);\n if (seen !== value) {\n setSeen(value);\n if (value !== emitted) {\n // Set from outside (a reset, another record): its own country, none of our text.\n const own = countryOf(value);\n if (own) setCountry(own);\n setDraft(null);\n }\n }\n\n const emit = (next: string) => {\n setEmitted(next);\n onValueChange?.(next);\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n const raw = e.target.value;\n setDraft(raw);\n const parsed = parsePhone(raw, country);\n // A number typed with its own \"+\" takes the select with it.\n if (parsed && parsed.country !== country) setCountry(parsed.country ?? \"other\");\n emit(parsed?.e164 ?? raw);\n };\n\n const handleCountry = (next: PhoneCountry) => {\n setCountry(next);\n const stored = storedNumber(value);\n if (stored) {\n // \"Other\", the same country, or a generic number with its own \"+\": the number\n // stays, only the way it is shown changes.\n if (next === \"other\" || stored.country === null || stored.country === next) {\n setDraft(null);\n return;\n }\n // A different listed country: the national number moves to its code. If it does\n // not read there, the digits are kept as text for the person to finish.\n emit(parsePhone(stored.national, next)?.e164 ?? stored.national);\n setDraft(null);\n return;\n }\n // Free text (or a draft): read it again under the new country.\n const source = draft ?? value;\n const parsed = source.trim() === \"\" ? null : parsePhone(source, next);\n if (parsed && parsed.e164 !== value) {\n setDraft(source);\n emit(parsed.e164);\n }\n };\n\n const display = draft !== null && emitFor(draft, country) === value ? draft : displayOf(value, country);\n\n // The select offers the configured countries, plus the stored number's own if it\n // is not among them — a select cannot show a value it has no option for.\n const options: PhoneCountryCode[] = [...countries];\n if (country !== \"other\" && !options.includes(country)) options.push(country);\n\n // A FieldHint \"?\" rides the number's label line; text — and a FieldHint at Large and\n // on touch (§4) — goes under the whole row with the error. Through useFieldMessages\n // since 0.34, not a copy of its rule (DUP-2, DUP-8).\n const messages = useFieldMessages({\n hint,\n // The number's label line takes the help's \"?\" (0.36); the hint is then the caption.\n help,\n name: label ?? rest[\"aria-label\"],\n error,\n invalid,\n describedBy: ariaDescribedBy,\n ariaInvalid: rest[\"aria-invalid\"],\n });\n const named = typeof label === \"string\" && label !== \"\";\n\n return (\n <div className={className} role={named ? \"group\" : undefined} aria-label={named ? label : undefined}>\n {/* On a phone at Large the number takes a line of its own under the country (0.32.1,\n the 360 px Extra-large sweep; docs/text-size-harmonization.md §4 \"nothing\n truncates\"): the 7rem select is 168 px at 150 %, and on a 360 px phone it left\n the number 82 px — \"Ph…\" over \"021 (\". `large:max-sm:`, the scaled phone width,\n so a wider screen at Large and Normal keep the one row. */}\n <div className=\"flex items-stretch gap-2 large:max-sm:flex-wrap\">\n <Select\n aria-label={text.countryCode}\n value={country}\n disabled={disabled}\n onChange={(e) => handleCountry(e.target.value as PhoneCountry)}\n className=\"w-28 shrink-0\"\n // Stretched to the number's height: a labelled field is taller than an\n // unlabelled select, and the pair should read as one row.\n selectClassName=\"h-full tabular-nums\"\n >\n {options.map((code) => (\n <option key={code} value={code}>\n {`${code} +${PHONE_DIAL_CODES[code]}`}\n </option>\n ))}\n <option value=\"other\">{text.other}</option>\n </Select>\n {/* Its own flex item, so nothing Input wraps itself in can take the sizing. */}\n <div className=\"min-w-0 flex-1 large:max-sm:basis-full\">\n <Input\n ref={ref}\n {...rest}\n type=\"tel\"\n autoComplete={rest.autoComplete ?? \"tel\"}\n label={label}\n hint={messages.labelHint}\n invalid={messages.isInvalid}\n aria-describedby={messages.describedBy}\n disabled={disabled}\n value={display}\n onChange={handleChange}\n onBlur={(e: FocusEvent<HTMLInputElement>) => {\n setDraft(null);\n onBlur?.(e);\n }}\n inputClassName={cn(\"tabular-nums\", inputClassName)}\n />\n </div>\n </div>\n {messages.below}\n </div>\n );\n}\n"],"mappings":"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","names":[]}