@eifi1/ui-kit 0.32.1 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. package/README.md +16 -8
  2. package/dist/account/delete-account-setting.d.ts +10 -10
  3. package/dist/account/delete-account-setting.js +2 -1
  4. package/dist/account/delete-account-setting.js.map +1 -1
  5. package/dist/account/email-change-setting.d.ts +5 -0
  6. package/dist/account/email-change-setting.js +4 -3
  7. package/dist/account/email-change-setting.js.map +1 -1
  8. package/dist/account/sessions-setting.d.ts +4 -0
  9. package/dist/account/sessions-setting.js +3 -2
  10. package/dist/account/sessions-setting.js.map +1 -1
  11. package/dist/admin/admin-action-confirm.d.ts +10 -10
  12. package/dist/admin/admin-action-confirm.js.map +1 -1
  13. package/dist/admin/invitations-panel.d.ts +10 -10
  14. package/dist/admin/invitations-panel.js +4 -3
  15. package/dist/admin/invitations-panel.js.map +1 -1
  16. package/dist/admin/plan-change.d.ts +100 -0
  17. package/dist/admin/plan-change.js +208 -0
  18. package/dist/admin/plan-change.js.map +1 -0
  19. package/dist/admin/reviewer-scope-editor.d.ts +10 -10
  20. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  21. package/dist/admin/role-select.d.ts +7 -7
  22. package/dist/admin/role-select.js.map +1 -1
  23. package/dist/admin/transfer-ownership-dialog.d.ts +10 -10
  24. package/dist/admin/user-roster.d.ts +7 -7
  25. package/dist/admin/user-roster.js +2 -2
  26. package/dist/admin/user-roster.js.map +1 -1
  27. package/dist/auth/auth-errors.d.ts +13 -2
  28. package/dist/auth/auth-errors.js +5 -1
  29. package/dist/auth/auth-errors.js.map +1 -1
  30. package/dist/auth/complete-name-dialog.d.ts +4 -1
  31. package/dist/auth/complete-name-dialog.js +2 -1
  32. package/dist/auth/complete-name-dialog.js.map +1 -1
  33. package/dist/auth/register-form.d.ts +10 -10
  34. package/dist/billing/billing-banner.d.ts +13 -11
  35. package/dist/billing/billing-banner.js.map +1 -1
  36. package/dist/billing/billing-labels.d.ts +10 -10
  37. package/dist/billing/billing-labels.js +10 -1
  38. package/dist/billing/billing-labels.js.map +1 -1
  39. package/dist/billing/billing-lock.d.ts +39 -12
  40. package/dist/billing/billing-lock.js +9 -3
  41. package/dist/billing/billing-lock.js.map +1 -1
  42. package/dist/billing/billing-overview.d.ts +154 -0
  43. package/dist/billing/billing-overview.js +2 -0
  44. package/dist/billing/billing-overview.js.map +1 -0
  45. package/dist/billing/billing-standing.d.ts +10 -10
  46. package/dist/billing/checkout-processing.d.ts +217 -0
  47. package/dist/billing/checkout-processing.js +198 -0
  48. package/dist/billing/checkout-processing.js.map +1 -0
  49. package/dist/billing/checkout-return.d.ts +51 -0
  50. package/dist/billing/checkout-return.js +43 -0
  51. package/dist/billing/checkout-return.js.map +1 -0
  52. package/dist/billing/pay-page.d.ts +210 -0
  53. package/dist/billing/pay-page.js +113 -0
  54. package/dist/billing/pay-page.js.map +1 -0
  55. package/dist/billing/plan-card.d.ts +10 -10
  56. package/dist/billing/plan-limit-notice.d.ts +10 -10
  57. package/dist/billing/plan-limit-toast.d.ts +143 -0
  58. package/dist/billing/plan-limit-toast.js +40 -0
  59. package/dist/billing/plan-limit-toast.js.map +1 -0
  60. package/dist/billing/subscription-actions.d.ts +38 -15
  61. package/dist/billing/subscription-actions.js +9 -2
  62. package/dist/billing/subscription-actions.js.map +1 -1
  63. package/dist/billing/subscription-status-chip.d.ts +9 -9
  64. package/dist/bin/eifi1-pay-page.mjs +166 -0
  65. package/dist/chart.d.ts +10 -10
  66. package/dist/components/account-chips.d.ts +6 -1
  67. package/dist/components/account-settings.d.ts +4 -1
  68. package/dist/components/account-settings.js +2 -1
  69. package/dist/components/account-settings.js.map +1 -1
  70. package/dist/components/amount-input.d.ts +10 -10
  71. package/dist/components/autocomplete.d.ts +2 -1
  72. package/dist/components/autocomplete.js +3 -1
  73. package/dist/components/autocomplete.js.map +1 -1
  74. package/dist/components/button-group.d.ts +10 -10
  75. package/dist/components/button-group.js +1 -1
  76. package/dist/components/button-group.js.map +1 -1
  77. package/dist/components/calculator.d.ts +10 -10
  78. package/dist/components/calculator.js +3 -3
  79. package/dist/components/calculator.js.map +1 -1
  80. package/dist/components/checkbox-group.d.ts +2 -1
  81. package/dist/components/checkbox-group.js.map +1 -1
  82. package/dist/components/checkbox.d.ts +2 -1
  83. package/dist/components/checkbox.js.map +1 -1
  84. package/dist/components/chip.d.ts +2 -1
  85. package/dist/components/chip.js +19 -17
  86. package/dist/components/chip.js.map +1 -1
  87. package/dist/components/choice-card.d.ts +3 -1
  88. package/dist/components/choice-card.js +1 -1
  89. package/dist/components/choice-card.js.map +1 -1
  90. package/dist/components/combobox.d.ts +4 -3
  91. package/dist/components/combobox.js.map +1 -1
  92. package/dist/components/confirm-dialog.d.ts +10 -10
  93. package/dist/components/confirm-dialog.js +6 -3
  94. package/dist/components/confirm-dialog.js.map +1 -1
  95. package/dist/components/copy-button.d.ts +11 -11
  96. package/dist/components/country-select.d.ts +2 -1
  97. package/dist/components/country-select.js.map +1 -1
  98. package/dist/components/danger-confirm.d.ts +10 -10
  99. package/dist/components/danger-confirm.js.map +1 -1
  100. package/dist/components/data-table-cells.d.ts +12 -1
  101. package/dist/components/data-table-filter-popover.d.ts +12 -1
  102. package/dist/components/data-table-filters.d.ts +12 -1
  103. package/dist/components/data-table-labels.d.ts +12 -1
  104. package/dist/components/data-table-pagination.d.ts +12 -1
  105. package/dist/components/data-table.d.ts +12 -1
  106. package/dist/components/data-table.js +3 -3
  107. package/dist/components/data-table.js.map +1 -1
  108. package/dist/components/date-picker.js +6 -4
  109. package/dist/components/date-picker.js.map +1 -1
  110. package/dist/components/entity-combobox.d.ts +3 -2
  111. package/dist/components/entity-combobox.js.map +1 -1
  112. package/dist/components/facing-pair.d.ts +10 -10
  113. package/dist/components/field-parts.d.ts +32 -4
  114. package/dist/components/field-parts.js +54 -19
  115. package/dist/components/field-parts.js.map +1 -1
  116. package/dist/components/file-button.d.ts +10 -10
  117. package/dist/components/file-button.js +3 -2
  118. package/dist/components/file-button.js.map +1 -1
  119. package/dist/components/file-dropzone.d.ts +11 -11
  120. package/dist/components/file-dropzone.js.map +1 -1
  121. package/dist/components/floating-panel.js +5 -5
  122. package/dist/components/floating-panel.js.map +1 -1
  123. package/dist/components/form-actions.d.ts +9 -9
  124. package/dist/components/form-actions.js +3 -3
  125. package/dist/components/form-actions.js.map +1 -1
  126. package/dist/components/iban-input.d.ts +10 -10
  127. package/dist/components/inline-edit-field.d.ts +2 -1
  128. package/dist/components/inline-edit-field.js +6 -6
  129. package/dist/components/inline-edit-field.js.map +1 -1
  130. package/dist/components/language-select.d.ts +10 -10
  131. package/dist/components/legal-page.d.ts +10 -10
  132. package/dist/components/legal.d.ts +10 -10
  133. package/dist/components/list.d.ts +2 -1
  134. package/dist/components/list.js +4 -5
  135. package/dist/components/list.js.map +1 -1
  136. package/dist/components/money-field.d.ts +10 -10
  137. package/dist/components/month-picker.js +54 -44
  138. package/dist/components/month-picker.js.map +1 -1
  139. package/dist/components/multi-entity-combobox.d.ts +3 -2
  140. package/dist/components/multi-entity-combobox.js.map +1 -1
  141. package/dist/components/number-field.d.ts +10 -10
  142. package/dist/components/number-input.d.ts +10 -10
  143. package/dist/components/numpad-sheet.d.ts +10 -10
  144. package/dist/components/numpad-sheet.js +2 -2
  145. package/dist/components/numpad-sheet.js.map +1 -1
  146. package/dist/components/page-header.js +6 -1
  147. package/dist/components/page-header.js.map +1 -1
  148. package/dist/components/phone-input.d.ts +10 -10
  149. package/dist/components/popover.d.ts +1 -1
  150. package/dist/components/popover.js +2 -0
  151. package/dist/components/popover.js.map +1 -1
  152. package/dist/components/row-actions.d.ts +5 -5
  153. package/dist/components/row-actions.js +130 -38
  154. package/dist/components/row-actions.js.map +1 -1
  155. package/dist/components/series-chart.d.ts +10 -10
  156. package/dist/components/settings-fields.d.ts +10 -10
  157. package/dist/components/share-card.d.ts +7 -7
  158. package/dist/components/share-card.js +26 -10
  159. package/dist/components/share-card.js.map +1 -1
  160. package/dist/components/status-dot.d.ts +1 -0
  161. package/dist/components/status-dot.js +2 -2
  162. package/dist/components/status-dot.js.map +1 -1
  163. package/dist/components/swatch-picker.d.ts +2 -1
  164. package/dist/components/swatch-picker.js.map +1 -1
  165. package/dist/components/swipeable-row.d.ts +88 -17
  166. package/dist/components/swipeable-row.js +46 -9
  167. package/dist/components/swipeable-row.js.map +1 -1
  168. package/dist/components/switch.d.ts +2 -1
  169. package/dist/components/switch.js.map +1 -1
  170. package/dist/components/table.d.ts +2 -1
  171. package/dist/components/table.js +6 -0
  172. package/dist/components/table.js.map +1 -1
  173. package/dist/components/text-link.d.ts +9 -9
  174. package/dist/components/time-input.d.ts +10 -10
  175. package/dist/components/toggle-group.d.ts +2 -1
  176. package/dist/components/toggle-group.js +1 -1
  177. package/dist/components/toggle-group.js.map +1 -1
  178. package/dist/components/translation-review-editor.d.ts +4 -0
  179. package/dist/components/translation-review-editor.js +4 -3
  180. package/dist/components/translation-review-editor.js.map +1 -1
  181. package/dist/components/translation-review-parts.d.ts +1 -0
  182. package/dist/components/translation-review.d.ts +1 -0
  183. package/dist/components/translation-review.js +15 -18
  184. package/dist/components/translation-review.js.map +1 -1
  185. package/dist/components/ui.d.ts +9 -9
  186. package/dist/components/ui.js +48 -28
  187. package/dist/components/ui.js.map +1 -1
  188. package/dist/components/use-table-state.d.ts +12 -1
  189. package/dist/components/user-avatar.d.ts +1 -0
  190. package/dist/components/write-lock.d.ts +101 -13
  191. package/dist/components/write-lock.js +37 -13
  192. package/dist/components/write-lock.js.map +1 -1
  193. package/dist/{data-table-labels-kpAAVi6g.d.ts → data-table-labels-BJ5JE_X2.d.ts} +17 -0
  194. package/dist/data-table.d.ts +12 -1
  195. package/dist/{delete-account-setting-FLyJf-kZ.d.ts → delete-account-setting-DiPX4Vue.d.ts} +365 -47
  196. package/dist/feedback/feedback-attachment.d.ts +10 -10
  197. package/dist/feedback/feedback-attachment.js +7 -7
  198. package/dist/feedback/feedback-attachment.js.map +1 -1
  199. package/dist/feedback/feedback-context.d.ts +10 -10
  200. package/dist/feedback/feedback-crash.d.ts +10 -10
  201. package/dist/feedback/feedback-dialog.d.ts +10 -10
  202. package/dist/feedback/feedback-inbox.d.ts +9 -9
  203. package/dist/feedback/feedback-inbox.js +7 -3
  204. package/dist/feedback/feedback-inbox.js.map +1 -1
  205. package/dist/feedback/feedback-labels.d.ts +10 -10
  206. package/dist/feedback/feedback-menu.d.ts +3 -3
  207. package/dist/feedback/feedback-record.d.ts +10 -10
  208. package/dist/feedback/feedback-row-detail.d.ts +10 -10
  209. package/dist/feedback/feedback-row-detail.js.map +1 -1
  210. package/dist/feedback/feedback-status-undo.d.ts +10 -10
  211. package/dist/feedback/feedback-submit.d.ts +10 -10
  212. package/dist/feedback/feedback-swipe.d.ts +10 -10
  213. package/dist/feedback/feedback-swipe.js +3 -6
  214. package/dist/feedback/feedback-swipe.js.map +1 -1
  215. package/dist/feedback/feedback-table.d.ts +10 -10
  216. package/dist/feedback/feedback-table.js.map +1 -1
  217. package/dist/feedback/feedback-thread.d.ts +10 -10
  218. package/dist/feedback.d.ts +10 -10
  219. package/dist/hooks/use-file-drop.d.ts +10 -10
  220. package/dist/i18n/defaults.d.ts +10 -10
  221. package/dist/i18n/defaults.js +2 -0
  222. package/dist/i18n/defaults.js.map +1 -1
  223. package/dist/i18n/german.d.ts +10 -10
  224. package/dist/i18n/german.js +34 -1
  225. package/dist/i18n/german.js.map +1 -1
  226. package/dist/i18n/kit-labels.d.ts +4 -4
  227. package/dist/i18n/kit-labels.js.map +1 -1
  228. package/dist/i18n/languages.d.ts +10 -10
  229. package/dist/i18n/locales/de-CH.d.ts +10 -10
  230. package/dist/i18n/locales/en.d.ts +10 -10
  231. package/dist/i18n/locales/es.d.ts +10 -10
  232. package/dist/i18n/locales/es.js +34 -1
  233. package/dist/i18n/locales/es.js.map +1 -1
  234. package/dist/i18n/locales/fr.d.ts +10 -10
  235. package/dist/i18n/locales/fr.js +35 -1
  236. package/dist/i18n/locales/fr.js.map +1 -1
  237. package/dist/i18n/locales/hu.d.ts +10 -10
  238. package/dist/i18n/locales/hu.js +36 -1
  239. package/dist/i18n/locales/hu.js.map +1 -1
  240. package/dist/i18n/locales/it.d.ts +10 -10
  241. package/dist/i18n/locales/it.js +34 -1
  242. package/dist/i18n/locales/it.js.map +1 -1
  243. package/dist/i18n/locales/zh.d.ts +10 -10
  244. package/dist/i18n/locales/zh.js +34 -1
  245. package/dist/i18n/locales/zh.js.map +1 -1
  246. package/dist/i18n/review.d.ts +10 -10
  247. package/dist/i18n/review.js +8 -1
  248. package/dist/i18n/review.js.map +1 -1
  249. package/dist/index.d.ts +10 -5
  250. package/dist/index.js +40 -2
  251. package/dist/index.js.map +1 -1
  252. package/dist/landing/landing-actions.d.ts +10 -10
  253. package/dist/landing/landing-sections.d.ts +10 -10
  254. package/dist/landing/public-header.d.ts +10 -10
  255. package/dist/landing/routing.d.ts +10 -10
  256. package/dist/pay/.well-known/apple-developer-merchantid-domain-association +1 -0
  257. package/dist/pay/index.html +30 -0
  258. package/dist/pay/pay.css +126 -0
  259. package/dist/pay/pay.js +198 -0
  260. package/dist/rhf/fields.d.ts +10 -10
  261. package/dist/rhf/form.d.ts +10 -10
  262. package/dist/rhf.d.ts +10 -10
  263. package/dist/settings/settings-section.d.ts +10 -10
  264. package/dist/shell/app-shell.d.ts +9 -9
  265. package/dist/shell/app-shell.js +2 -1
  266. package/dist/shell/app-shell.js.map +1 -1
  267. package/dist/shell/top-bar-brand.d.ts +10 -10
  268. package/dist/shell/topbar-action-menu.d.ts +1 -0
  269. package/dist/shell.d.ts +10 -10
  270. package/dist/testing/css-variables.d.ts +74 -0
  271. package/dist/testing/css-variables.generated.d.ts +7 -0
  272. package/dist/testing/css-variables.generated.js +8 -0
  273. package/dist/testing/css-variables.generated.js.map +1 -0
  274. package/dist/testing/css-variables.js +93 -0
  275. package/dist/testing/css-variables.js.map +1 -0
  276. package/dist/testing/index.d.ts +1 -0
  277. package/dist/testing/index.js +12 -0
  278. package/dist/testing/index.js.map +1 -0
  279. package/dist/theme/contrast-tokens.d.ts +44 -1
  280. package/dist/theme/contrast-tokens.js +21 -0
  281. package/dist/theme/contrast-tokens.js.map +1 -1
  282. package/dist/theme/contrast.d.ts +2 -2
  283. package/dist/theme/palette-derive.js +60 -11
  284. package/dist/theme/palette-derive.js.map +1 -1
  285. package/dist/theme/palette-presets.js +45 -43
  286. package/dist/theme/palette-presets.js.map +1 -1
  287. package/dist/wizard/stepper-nav.d.ts +11 -11
  288. package/dist/wizard/stepper-nav.js.map +1 -1
  289. package/dist/wizard.d.ts +10 -10
  290. package/package.json +10 -2
  291. package/src/account/delete-account-setting.tsx +6 -1
  292. package/src/account/email-change-setting.tsx +9 -3
  293. package/src/account/sessions-setting.tsx +7 -2
  294. package/src/admin/admin-action-confirm.tsx +2 -1
  295. package/src/admin/invitations-panel.tsx +9 -3
  296. package/src/admin/plan-change.tsx +459 -0
  297. package/src/admin/reviewer-scope-editor.tsx +2 -1
  298. package/src/admin/role-select.tsx +2 -1
  299. package/src/admin/user-roster.tsx +5 -3
  300. package/src/auth/auth-errors.ts +25 -2
  301. package/src/auth/complete-name-dialog.tsx +6 -2
  302. package/src/billing/billing-banner.tsx +3 -1
  303. package/src/billing/billing-labels.ts +45 -0
  304. package/src/billing/billing-lock.ts +36 -3
  305. package/src/billing/billing-overview.ts +58 -0
  306. package/src/billing/checkout-processing.ts +391 -0
  307. package/src/billing/checkout-return.ts +90 -0
  308. package/src/billing/pay-page.ts +242 -0
  309. package/src/billing/plan-limit-toast.tsx +91 -0
  310. package/src/billing/subscription-actions.tsx +41 -8
  311. package/src/components/account-settings.tsx +6 -2
  312. package/src/components/autocomplete.tsx +5 -2
  313. package/src/components/button-group.tsx +4 -1
  314. package/src/components/calculator.tsx +10 -6
  315. package/src/components/checkbox-group.tsx +2 -2
  316. package/src/components/checkbox.tsx +2 -1
  317. package/src/components/chip.tsx +37 -32
  318. package/src/components/choice-card.tsx +4 -2
  319. package/src/components/combobox.tsx +4 -4
  320. package/src/components/confirm-dialog.tsx +13 -6
  321. package/src/components/country-select.tsx +2 -2
  322. package/src/components/danger-confirm.tsx +2 -2
  323. package/src/components/data-table.tsx +20 -9
  324. package/src/components/date-picker.tsx +10 -7
  325. package/src/components/entity-combobox.tsx +3 -3
  326. package/src/components/field-parts.tsx +61 -14
  327. package/src/components/file-button.tsx +5 -4
  328. package/src/components/file-dropzone.tsx +2 -2
  329. package/src/components/floating-panel.tsx +7 -5
  330. package/src/components/form-actions.tsx +12 -5
  331. package/src/components/inline-edit-field.tsx +13 -13
  332. package/src/components/list.tsx +10 -9
  333. package/src/components/month-picker.tsx +8 -2
  334. package/src/components/multi-entity-combobox.tsx +3 -3
  335. package/src/components/numpad-sheet.tsx +8 -4
  336. package/src/components/page-header.tsx +15 -1
  337. package/src/components/popover.tsx +4 -0
  338. package/src/components/row-actions.tsx +201 -50
  339. package/src/components/share-card.tsx +67 -13
  340. package/src/components/status-dot.tsx +7 -7
  341. package/src/components/swatch-picker.tsx +2 -2
  342. package/src/components/swipeable-row.tsx +138 -27
  343. package/src/components/switch.tsx +2 -1
  344. package/src/components/table.tsx +8 -1
  345. package/src/components/toggle-group.tsx +5 -3
  346. package/src/components/translation-review-editor.tsx +8 -3
  347. package/src/components/translation-review.tsx +18 -22
  348. package/src/components/ui.tsx +98 -44
  349. package/src/components/write-lock.tsx +150 -24
  350. package/src/feedback/feedback-attachment.tsx +13 -9
  351. package/src/feedback/feedback-inbox.tsx +10 -6
  352. package/src/feedback/feedback-row-detail.tsx +3 -2
  353. package/src/feedback/feedback-swipe.tsx +6 -11
  354. package/src/feedback/feedback-table.tsx +2 -1
  355. package/src/i18n/defaults.ts +2 -0
  356. package/src/i18n/german.ts +38 -0
  357. package/src/i18n/kit-labels.tsx +4 -0
  358. package/src/i18n/locales/es.ts +34 -0
  359. package/src/i18n/locales/fr.ts +39 -0
  360. package/src/i18n/locales/hu.ts +37 -0
  361. package/src/i18n/locales/it.ts +35 -0
  362. package/src/i18n/locales/zh.ts +33 -0
  363. package/src/i18n/review.ts +7 -0
  364. package/src/index.ts +48 -4
  365. package/src/shell/app-shell.tsx +11 -6
  366. package/src/testing/css-variables.generated.ts +112 -0
  367. package/src/testing/css-variables.ts +191 -0
  368. package/src/testing/index.ts +13 -0
  369. package/src/theme/contrast-tokens.ts +53 -2
  370. package/src/theme/palette-derive.ts +87 -10
  371. package/src/theme/palette-presets.ts +45 -43
  372. package/src/wizard/stepper-nav.tsx +2 -1
  373. package/tokens.css +306 -46
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/account-settings.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { FormEvent, KeyboardEvent, ReactNode } from \"react\";\nimport { Button, Card, Input } from \"./ui\";\nimport { UserAvatar } from \"./user-avatar\";\nimport { CopyButton } from \"./copy-button\";\nimport { QrCode } from \"./qr-code\";\nimport { OneTimeCodeInput } from \"./one-time-code-input\";\nimport { usePromisePending } from \"./danger-confirm\";\nimport { personNameOk, PERSON_NAME_MAX_LENGTH } from \"../auth/form-rules\";\nimport {\n DEFAULT_PASSWORD_STRENGTH_LABELS,\n passwordByteLength,\n PasswordStrengthMeter,\n} from \"./password-strength\";\nimport type { PasswordStrengthMeterProps } from \"./password-strength\";\nimport { useKitLabelOverrides, useKitLabels } from \"../i18n/kit-labels\";\nimport { Chip } from \"./chip\";\nimport { useAccountSettingsLabels } from \"./account-settings-labels\";\nimport { SettingsCardTitle } from \"../settings/settings-heading\";\nimport type {\n PasswordSettingLabels,\n ProfileSettingLabels,\n TwoFactorSettingLabels,\n} from \"./account-settings-labels\";\n\n// The label types moved beside the namespace's defaults (0.12.0); re-exported here so\n// every existing `import type { ProfileSettingLabels }` keeps resolving.\nexport type {\n AccountSettingsLabels,\n PasskeysSettingLabels,\n PasswordSettingLabels,\n ProfileSettingLabels,\n TwoFactorSettingLabels,\n} from \"./account-settings-labels\";\nexport { DEFAULT_ACCOUNT_SETTINGS_LABELS } from \"./account-settings-labels\";\n\n/**\n * App-agnostic account-settings SECTIONS (feedback #333). The presentation lives\n * in @hb/ui; each app injects its own auth-API handlers + translated labels and\n * arranges these next to its own app-specific cards (appearance, budgets, …).\n * Every section is a self-contained <Card> owning its transient form state.\n *\n * Every `labels` prop is optional since 0.12.0: the strings come from the provider's\n * `accountSettings` namespace, then English (see account-settings-labels.ts).\n */\n\n/** What {@link ProfileSetting} hands `onSave` in its two-field form (0.30.0): both\n * trimmed, 1–120 characters each — the `PATCH /auth/me` body's `first_name` /\n * `last_name` (docs/user-admin-harmonization.md §6.1). */\nexport interface ProfileNameValues {\n firstName: string;\n lastName: string;\n}\n\n/** What both forms of {@link ProfileSetting} take. */\ninterface ProfileSettingCommonProps {\n name?: string | null;\n email?: string | null;\n role?: ReactNode;\n memberSince?: ReactNode;\n saving?: boolean;\n /** Prop > `<UiKitProvider labels={{ accountSettings: { profile } }}>` > English. */\n labels?: Partial<ProfileSettingLabels>;\n /** The card's DOM id — a settings catalogue `anchor`, so `?focus=` can ring it (0.31). */\n id?: string;\n}\n\n/**\n * The single display name, as before 0.30.0 — controlled through `value` / `onChange`,\n * saved by `onSave()`. Kept unchanged for an app that has not moved to first and last\n * name yet.\n */\nexport interface ProfileSettingDisplayNameProps extends ProfileSettingCommonProps {\n value: string;\n onChange: (value: string) => void;\n onSave: () => void;\n firstName?: undefined;\n lastName?: undefined;\n}\n\n/**\n * First and last name (0.30.0, docs/user-admin-harmonization.md §6.1): the saved\n * values in, the edited ones out through `onSave`. The card keeps the drafts itself.\n */\nexport interface ProfileSettingNamesProps extends ProfileSettingCommonProps {\n /** The saved given name — `first_name`. `null` reads as empty. */\n firstName: string | null;\n /** The saved family name — `last_name`. `null` (a migrated account, §3.3) reads as\n * empty, and Save then waits for one. */\n lastName: string | null;\n /**\n * Save the two names (`PATCH /auth/me`, which also clears `name_incomplete`). Return\n * a promise and Save is busy until it settles. Refresh `firstName` / `lastName` from\n * the answer: the card compares its drafts with them, so Save turns off once the new\n * names come back. A rejection keeps what was typed — the app says why (a toast), as\n * for `PasswordSetting`.\n */\n onSave: (values: ProfileNameValues) => void | Promise<unknown>;\n value?: undefined;\n onChange?: undefined;\n}\n\nexport type ProfileSettingProps = ProfileSettingDisplayNameProps | ProfileSettingNamesProps;\n\n/**\n * The profile section: avatar, address, role, member since, and the name.\n *\n * **Two forms.** Given `firstName` / `lastName` (0.30.0, §6.1) it edits first and last\n * name in two fields, side by side where the card is wide enough — the order and the\n * 1–120-character rule of `RegisterForm` (auth §3.1), so the name a person signed up\n * with is edited the way it was typed. Without them it is the single `displayName`\n * field it always was, `value` / `onChange` / `onSave()` unchanged, for an app that has\n * not moved yet (kastlan split the one name at its first space on save; Kurvenschmiede\n * built its own card instead).\n *\n * The two-field form owns its drafts, starting from the saved names and following them\n * when they change; Save waits for a change, holds — saying why — while a name is\n * blank, and is a `commit` (a {@link WriteLockProvider} locks it, keksdose's demo).\n */\nexport function ProfileSetting(props: ProfileSettingProps) {\n const { name, email, role, memberSince, labels: labelsProp, id } = props;\n const labels = useAccountSettingsLabels(\"profile\", labelsProp);\n const names = props.firstName !== undefined || props.lastName !== undefined;\n return (\n <Card id={id} className=\"p-4 space-y-3\">\n <div className=\"flex items-center gap-3\">\n <UserAvatar\n name={name}\n email={email}\n person={names ? { first: props.firstName, last: props.lastName } : undefined}\n size=\"lg\"\n />\n <div className=\"min-w-0\">\n {/* A heading inside a SettingsLayout (0.31, docs/settings-harmonization.md\n §3.7), the plain div it always was outside one. */}\n <SettingsCardTitle>{labels.title}</SettingsCardTitle>\n <div className=\"truncate font-mono text-xs text-[var(--text-muted)]\">\n {/* `email` was a required key that nothing rendered; it names the address\n for a screen reader, which otherwise hears a bare string under a title. */}\n <span className=\"sr-only\">{labels.email}: </span>\n {email ?? \"—\"}\n </div>\n </div>\n </div>\n <div className=\"grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1 text-sm\">\n <span className=\"text-[var(--text-muted)]\">{labels.role}</span>\n <span>{role ?? \"—\"}</span>\n <span className=\"text-[var(--text-muted)]\">{labels.memberSince}</span>\n <span>{memberSince ?? \"—\"}</span>\n </div>\n {names ? (\n <PersonNameFields\n firstName={props.firstName ?? \"\"}\n lastName={props.lastName ?? \"\"}\n onSave={(props as ProfileSettingNamesProps).onSave}\n saving={props.saving}\n labels={labels}\n />\n ) : (\n <DisplayNameField\n name={name}\n value={(props as ProfileSettingDisplayNameProps).value}\n onChange={(props as ProfileSettingDisplayNameProps).onChange}\n onSave={(props as ProfileSettingDisplayNameProps).onSave}\n saving={props.saving}\n labels={labels}\n />\n )}\n </Card>\n );\n}\n\n/** The single-name form — exactly the 0.29 card's field and button. */\nfunction DisplayNameField({\n name,\n value,\n onChange,\n onSave,\n saving,\n labels,\n}: {\n name?: string | null;\n value: string;\n onChange: (value: string) => void;\n onSave: () => void;\n saving?: boolean;\n labels: Required<ProfileSettingLabels>;\n}) {\n const dirty = value.trim().length > 0 && value.trim() !== (name ?? \"\");\n // Generated, not the literal \"display-name\" it was: two of these on one page (an\n // admin editing someone else's profile beside their own) shared an id, and the\n // second label pointed at the first field.\n const inputId = useId();\n return (\n <>\n <Input\n id={inputId}\n label={labels.displayName}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n maxLength={120}\n />\n <Button onClick={onSave} disabled={!dirty || saving}>\n {labels.save}\n </Button>\n </>\n );\n}\n\n/** The first-and-last form (0.30.0, §6.1). */\nfunction PersonNameFields({\n firstName,\n lastName,\n onSave,\n saving,\n labels,\n}: {\n firstName: string;\n lastName: string;\n onSave: (values: ProfileNameValues) => void | Promise<unknown>;\n saving?: boolean;\n labels: Required<ProfileSettingLabels>;\n}) {\n const [first, setFirst] = useState(firstName);\n const [last, setLast] = useState(lastName);\n // The saved names moved (the save came back, another tab renamed): the drafts follow\n // them. During render, as DangerConfirm resets its fields, so no frame shows the old\n // draft beside the new name.\n const [saved, setSaved] = useState({ firstName, lastName });\n if (saved.firstName !== firstName || saved.lastName !== lastName) {\n setSaved({ firstName, lastName });\n setFirst(firstName);\n setLast(lastName);\n }\n const { pending, run } = usePromisePending();\n const busy = Boolean(saving) || pending;\n const valid = personNameOk(first) && personNameOk(last);\n const dirty = first.trim() !== firstName.trim() || last.trim() !== lastName.trim();\n\n const submit = (e: FormEvent) => {\n e.preventDefault();\n if (!valid || !dirty || busy) return;\n // A rejection keeps the drafts; the app reports it, as PasswordSetting's does.\n run(onSave({ firstName: first.trim(), lastName: last.trim() }), () => {});\n };\n\n return (\n <form className=\"@container space-y-3\" onSubmit={submit} noValidate>\n {/* The order and the rule of RegisterForm's two fields (auth §3.1). */}\n <div className=\"grid gap-3 @xs:grid-cols-2\">\n <Input\n name=\"given-name\"\n autoComplete=\"given-name\"\n label={labels.firstName}\n value={first}\n onChange={(e) => setFirst(e.target.value)}\n maxLength={PERSON_NAME_MAX_LENGTH}\n readOnly={busy}\n required\n />\n <Input\n name=\"family-name\"\n autoComplete=\"family-name\"\n label={labels.lastName}\n value={last}\n onChange={(e) => setLast(e.target.value)}\n maxLength={PERSON_NAME_MAX_LENGTH}\n readOnly={busy}\n required\n />\n </div>\n <Button\n type=\"submit\"\n commit\n pending={busy}\n // A blank name is a reason the person can act on; an unchanged one is not —\n // there is nothing to save, and the button is simply off.\n disabled={valid && !dirty}\n disabledReason={valid ? undefined : labels.nameRequired}\n >\n {labels.save}\n </Button>\n </form>\n );\n}\n\n/**\n * What {@link PasswordSetting}'s `strength` takes besides `true`: the meter's own options,\n * minus the ones the card owns — `value` (the new password), `minLength` (the card's, so\n * the checklist's length rule and the submit's check are one number) and `maxBytes`\n * (the card's own prop, which the submit enforces as well).\n */\nexport type PasswordSettingStrength = Pick<PasswordStrengthMeterProps, \"score\" | \"showRequirements\" | \"labels\">;\n\nexport function PasswordSetting({\n onSubmit,\n pending,\n minLength = 8,\n strength,\n maxBytes,\n labels: labelsProp,\n id,\n}: {\n /** Called with the validated (current, new) pair; return a promise to auto-clear on success. */\n onSubmit: (currentPassword: string, newPassword: string) => void | Promise<unknown>;\n pending?: boolean;\n /** The shortest new password the card submits, in characters as a person counts them\n * (an emoji is one). Keep it in step with the server's own minimum. Default 8. */\n minLength?: number;\n /**\n * Show the {@link PasswordStrengthMeter} under the new-password field (0.22.0,\n * kastlan's input audit): the bar, the level in words and the checklist, with the\n * length rule drawn from `minLength`. `true` for the meter as it comes; an object for\n * its `score` (zxcvbn's, say), `showRequirements` or `labels`.\n *\n * Off by default, so a card that never asked for it does not grow one. The meter is\n * advisory, as everywhere in the kit: it blocks nothing — the card's own checks do\n * (`minLength`, the confirmation, `maxBytes`).\n */\n strength?: boolean | PasswordSettingStrength;\n /**\n * A byte ceiling on the new password — bcrypt reads at most 72 BYTES and ignores the\n * rest, which is kastlan's case: `maxBytes={72}`. A new password over it is refused\n * on submit with the meter's own \"too long\" sentence (the `passwordStrength`\n * namespace's `tooLong`), and the meter, when shown, warns while it is typed.\n *\n * Unset by default, and leave it unset for a secret with no ceiling: an invented one\n * talks people out of the long passphrase they chose.\n */\n maxBytes?: number;\n /** Prop > `<UiKitProvider labels={{ accountSettings: { password } }}>` > English. */\n labels?: Partial<PasswordSettingLabels>;\n /** The card's DOM id — a settings catalogue `anchor`, so `?focus=` can ring it (0.31). */\n id?: string;\n}) {\n const labels = useAccountSettingsLabels(\"password\", labelsProp);\n const strengthOptions = typeof strength === \"object\" ? strength : undefined;\n // Only for `tooLong`: the sentence the meter shows is the one the refusal says.\n const strengthLabels = useKitLabels(\"passwordStrength\", DEFAULT_PASSWORD_STRENGTH_LABELS, strengthOptions?.labels);\n const [current, setCurrent] = useState(\"\");\n const [next, setNext] = useState(\"\");\n const [confirm, setConfirm] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n\n const submit = async () => {\n // Code points, not UTF-16 units — the count the meter's checklist and a server's\n // `len()` use. `next.length` counted an emoji as two, so four of them passed a\n // minimum of eight that the meter beside the field said they missed.\n if ([...next].length < minLength) {\n setError(labels.tooShort);\n return;\n }\n if (maxBytes !== undefined && passwordByteLength(next) > maxBytes) {\n setError(strengthLabels.tooLong(maxBytes));\n return;\n }\n if (next !== confirm) {\n setError(labels.mismatch);\n return;\n }\n setError(null);\n try {\n await onSubmit(current, next);\n setCurrent(\"\");\n setNext(\"\");\n setConfirm(\"\");\n } catch {\n // The app surfaces the failure (e.g. a toast); keep the fields for a retry.\n }\n };\n\n const nextField = (\n <Input type=\"password\" autoComplete=\"new-password\" label={labels.next} value={next} onChange={(e) => setNext(e.target.value)} />\n );\n\n return (\n <Card id={id} className=\"p-4 space-y-3\">\n <SettingsCardTitle>{labels.title}</SettingsCardTitle>\n <Input type=\"password\" autoComplete=\"current-password\" label={labels.current} value={current} onChange={(e) => setCurrent(e.target.value)} />\n {strength ? (\n // One box for the field and its meter, so the card's `space-y-3` spaces the pair\n // from its neighbours and the meter keeps its own small gap under the field.\n <div>\n {nextField}\n <PasswordStrengthMeter\n value={next}\n minLength={minLength}\n maxBytes={maxBytes}\n score={strengthOptions?.score}\n showRequirements={strengthOptions?.showRequirements}\n labels={strengthOptions?.labels}\n />\n </div>\n ) : (\n nextField\n )}\n <Input type=\"password\" autoComplete=\"new-password\" label={labels.confirm} value={confirm} onChange={(e) => setConfirm(e.target.value)} />\n {error && <p className=\"text-xs text-[var(--danger)]\">{error}</p>}\n <Button onClick={() => void submit()} disabled={!current || !next || !confirm || pending}>\n {labels.submit}\n </Button>\n </Card>\n );\n}\n\n/**\n * What the app's setup call returned.\n *\n * `otpauthUri` (0.12.0) is what an authenticator backend actually hands out; the kit\n * draws the QR code itself ({@link QrCode}) and reads the manual-entry key out of the\n * URI's `secret` parameter when `secret` is not given. The `qrSvg` shape — a\n * base64-encoded SVG image — is what the section took before, which made every app\n * render a QR library to a static string and base64 it only for the kit to draw it back.\n */\nexport type TwoFactorSetupData =\n | { qrSvg: string; secret: string }\n | { otpauthUri: string; secret?: string };\n\n/** The `secret` parameter of an `otpauth://` URI, or null. */\nfunction otpauthSecret(uri: string): string | null {\n const match = /[?&]secret=([^&#]*)/i.exec(uri);\n if (!match) return null;\n try {\n return decodeURIComponent(match[1]) || null;\n } catch {\n return match[1] || null;\n }\n}\n\n/** \"JBSW Y3DP EHPK 3PXP\": the key in groups of four, which is how an authenticator's\n * manual-entry screen shows it and how a person copies it by eye. */\nfunction groupSecret(secret: string): string {\n return secret.replace(/\\s+/g, \"\").replace(/(.{4})(?=.)/g, \"$1 \");\n}\n\nexport function TwoFactorSetting({\n enabled,\n setup,\n onStartSetup,\n onEnable,\n onDisable,\n busy,\n renderQr,\n labels: labelsProp,\n id,\n}: {\n enabled: boolean;\n /** What the app's setup call returned; null before setup starts. */\n setup: TwoFactorSetupData | null;\n onStartSetup: () => void;\n onEnable: (code: string) => void;\n onDisable: (password: string, code: string) => void;\n busy?: boolean;\n /** Draw the code yourself (a branded QR, a library you already ship). Given the\n * `otpauthUri`; the kit's own {@link QrCode} otherwise. Unused with `qrSvg`. */\n renderQr?: (otpauthUri: string) => ReactNode;\n /** Prop > `<UiKitProvider labels={{ accountSettings: { twoFactor } }}>` > English. */\n labels?: Partial<TwoFactorSettingLabels>;\n /** The card's DOM id — a settings catalogue `anchor`, so `?focus=` can ring it (0.31). */\n id?: string;\n}) {\n const labels = useAccountSettingsLabels(\"twoFactor\", labelsProp);\n // The title (0.31, docs/settings-harmonization.md §3.7) is new, and an app that\n // translated this card before it named the setting in `status` — \"2FA\",\n // \"Zwei-Faktor-Authentifizierung\". Each source's `title` first, then its `status`, so\n // that app keeps its own word instead of meeting the English title.\n const overrides = useKitLabelOverrides(\"accountSettings\")?.twoFactor;\n const title = labelsProp?.title ?? overrides?.title ?? labelsProp?.status ?? overrides?.status ?? labels.title;\n const [code, setCode] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n // Both code fields are a OneTimeCodeInput (0.22.0) — the field this card wrote by hand\n // twice — and keep what they did:\n // - `readOnlyUntilFocus`: browsers autofill a one-time-code field with the saved\n // username (the disable form has a current-password field right above it), and a\n // read-only field is never autofilled. The guard drops on focus, and the field\n // keeps the look of an editable one meanwhile (FIELD_WRITABLE_LOOK) — see the prop.\n // - `length={8}`: the `maxLength={8}` they had, so an authenticator issuing eight\n // digits still fits. What changed is that they now take digits only: \"123 456\"\n // pasted from an authenticator that groups its digits used to reach `onEnable`\n // with the space in it.\n // - one `code` state for both, as before.\n const secret = setup\n ? (setup.secret ?? (\"otpauthUri\" in setup ? otpauthSecret(setup.otpauthUri) : null))\n : null;\n\n return (\n <Card id={id} className=\"p-4 space-y-3\">\n {/* The card had only \"Two-factor authentication: Off\". A settings page outlines its\n cards by their titles (§3.7), so the setting's name is the title now, and the\n state stands beside it as a chip instead of repeating the name. */}\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <SettingsCardTitle>{title}</SettingsCardTitle>\n <Chip size=\"sm\" tone={enabled ? \"success\" : \"neutral\"}>\n {enabled ? labels.enabledText : labels.disabledText}\n </Chip>\n </div>\n\n {!enabled && !setup && (\n <Button onClick={onStartSetup} disabled={busy}>\n {labels.enable}\n </Button>\n )}\n\n {setup && (\n <div className=\"space-y-2\">\n <div className=\"text-xs text-[var(--text-secondary)]\">{labels.scanHint}</div>\n {/* `bg-white`, NOT `--bg-surface`, and it must stay that way: a QR code is\n read by a camera looking for maximum contrast between the modules and\n their quiet zone. On the dark theme a surface-coloured backing makes it\n slow to acquire, and on a warm light preset it lowers the contrast ratio\n the spec is written against. This is the one place in the kit where a\n literal colour is the correct answer. `data-private`: the code IS the\n second factor, and demo mode blurs it like any other secret. */}\n {\"otpauthUri\" in setup ? (\n // No backing of its own: QrCode draws its quiet zone white inside the SVG.\n <div data-private className=\"inline-block overflow-hidden rounded-md\">\n {renderQr ? (\n renderQr(setup.otpauthUri)\n ) : (\n <QrCode value={setup.otpauthUri} label={labels.qrAlt} className=\"size-48\" />\n )}\n </div>\n ) : (\n <div data-private className=\"inline-block rounded-md bg-white p-2\">\n <img\n alt={labels.qrAlt}\n src={`data:image/svg+xml;base64,${setup.qrSvg}`}\n className=\"h-48 w-48\"\n />\n </div>\n )}\n {secret && (\n <div className=\"space-y-1\">\n <div className=\"text-xs text-[var(--text-secondary)]\">{labels.secretHint}</div>\n <div className=\"flex items-center gap-1\">\n {/* `dir=\"ltr\"`: a base32 key is Latin letters and digits in a fixed\n order, and in an RTL page the groups would otherwise be laid out\n right to left. */}\n <code data-private dir=\"ltr\" className=\"break-all font-mono text-xs select-all\">\n {groupSecret(secret)}\n </code>\n <CopyButton text={secret} label={labels.copySecret} size=\"xs\" tone=\"muted\" />\n </div>\n </div>\n )}\n <OneTimeCodeInput\n readOnlyUntilFocus\n length={8}\n label={labels.codeLabel}\n value={code}\n onChange={setCode}\n onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\" && code && !busy) {\n e.preventDefault();\n onEnable(code);\n }\n }}\n />\n <Button onClick={() => onEnable(code)} disabled={!code || busy}>\n {labels.verify}\n </Button>\n </div>\n )}\n\n {enabled && (\n <div className=\"space-y-3 border-t border-[var(--border)] pt-3\">\n <div className=\"text-sm font-medium\">{labels.disableSection}</div>\n <Input type=\"password\" autoComplete=\"current-password\" label={labels.password} value={password} onChange={(e) => setPassword(e.target.value)} />\n <OneTimeCodeInput\n readOnlyUntilFocus\n length={8}\n label={labels.codeLabel}\n value={code}\n onChange={setCode}\n onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\" && password && code && !busy) {\n e.preventDefault();\n onDisable(password, code);\n }\n }}\n />\n <Button variant=\"danger\" onClick={() => onDisable(password, code)} disabled={!password || !code || busy}>\n {labels.disable}\n </Button>\n </div>\n )}\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/account-settings.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { FormEvent, KeyboardEvent, ReactNode } from \"react\";\nimport { Button, Card, Input } from \"./ui\";\nimport { UserAvatar } from \"./user-avatar\";\nimport { CopyButton } from \"./copy-button\";\nimport { QrCode } from \"./qr-code\";\nimport { OneTimeCodeInput } from \"./one-time-code-input\";\nimport { usePromisePending } from \"./danger-confirm\";\nimport { personNameOk, PERSON_NAME_MAX_LENGTH } from \"../auth/form-rules\";\nimport {\n DEFAULT_PASSWORD_STRENGTH_LABELS,\n passwordByteLength,\n PasswordStrengthMeter,\n} from \"./password-strength\";\nimport type { PasswordStrengthMeterProps } from \"./password-strength\";\nimport { useKitLabelOverrides, useKitLabels } from \"../i18n/kit-labels\";\nimport { Chip } from \"./chip\";\nimport { useAccountSettingsLabels } from \"./account-settings-labels\";\nimport { SettingsCardTitle } from \"../settings/settings-heading\";\nimport type {\n PasswordSettingLabels,\n ProfileSettingLabels,\n TwoFactorSettingLabels,\n} from \"./account-settings-labels\";\nimport { COMMIT_EXCEPT_BILLING } from \"./write-lock\";\n\n// The label types moved beside the namespace's defaults (0.12.0); re-exported here so\n// every existing `import type { ProfileSettingLabels }` keeps resolving.\nexport type {\n AccountSettingsLabels,\n PasskeysSettingLabels,\n PasswordSettingLabels,\n ProfileSettingLabels,\n TwoFactorSettingLabels,\n} from \"./account-settings-labels\";\nexport { DEFAULT_ACCOUNT_SETTINGS_LABELS } from \"./account-settings-labels\";\n\n/**\n * App-agnostic account-settings SECTIONS (feedback #333). The presentation lives\n * in @hb/ui; each app injects its own auth-API handlers + translated labels and\n * arranges these next to its own app-specific cards (appearance, budgets, …).\n * Every section is a self-contained <Card> owning its transient form state.\n *\n * Every `labels` prop is optional since 0.12.0: the strings come from the provider's\n * `accountSettings` namespace, then English (see account-settings-labels.ts).\n */\n\n/** What {@link ProfileSetting} hands `onSave` in its two-field form (0.30.0): both\n * trimmed, 1–120 characters each — the `PATCH /auth/me` body's `first_name` /\n * `last_name` (docs/user-admin-harmonization.md §6.1). */\nexport interface ProfileNameValues {\n firstName: string;\n lastName: string;\n}\n\n/** What both forms of {@link ProfileSetting} take. */\ninterface ProfileSettingCommonProps {\n name?: string | null;\n email?: string | null;\n role?: ReactNode;\n memberSince?: ReactNode;\n saving?: boolean;\n /** Prop > `<UiKitProvider labels={{ accountSettings: { profile } }}>` > English. */\n labels?: Partial<ProfileSettingLabels>;\n /** The card's DOM id — a settings catalogue `anchor`, so `?focus=` can ring it (0.31). */\n id?: string;\n}\n\n/**\n * The single display name, as before 0.30.0 — controlled through `value` / `onChange`,\n * saved by `onSave()`. Kept unchanged for an app that has not moved to first and last\n * name yet.\n */\nexport interface ProfileSettingDisplayNameProps extends ProfileSettingCommonProps {\n value: string;\n onChange: (value: string) => void;\n onSave: () => void;\n firstName?: undefined;\n lastName?: undefined;\n}\n\n/**\n * First and last name (0.30.0, docs/user-admin-harmonization.md §6.1): the saved\n * values in, the edited ones out through `onSave`. The card keeps the drafts itself.\n */\nexport interface ProfileSettingNamesProps extends ProfileSettingCommonProps {\n /** The saved given name — `first_name`. `null` reads as empty. */\n firstName: string | null;\n /** The saved family name — `last_name`. `null` (a migrated account, §3.3) reads as\n * empty, and Save then waits for one. */\n lastName: string | null;\n /**\n * Save the two names (`PATCH /auth/me`, which also clears `name_incomplete`). Return\n * a promise and Save is busy until it settles. Refresh `firstName` / `lastName` from\n * the answer: the card compares its drafts with them, so Save turns off once the new\n * names come back. A rejection keeps what was typed — the app says why (a toast), as\n * for `PasswordSetting`.\n */\n onSave: (values: ProfileNameValues) => void | Promise<unknown>;\n value?: undefined;\n onChange?: undefined;\n}\n\nexport type ProfileSettingProps = ProfileSettingDisplayNameProps | ProfileSettingNamesProps;\n\n/**\n * The profile section: avatar, address, role, member since, and the name.\n *\n * **Two forms.** Given `firstName` / `lastName` (0.30.0, §6.1) it edits first and last\n * name in two fields, side by side where the card is wide enough — the order and the\n * 1–120-character rule of `RegisterForm` (auth §3.1), so the name a person signed up\n * with is edited the way it was typed. Without them it is the single `displayName`\n * field it always was, `value` / `onChange` / `onSave()` unchanged, for an app that has\n * not moved yet (kastlan split the one name at its first space on save; Kurvenschmiede\n * built its own card instead).\n *\n * The two-field form owns its drafts, starting from the saved names and following them\n * when they change; Save waits for a change, holds — saying why — while a name is\n * blank, and is a `commit` (a {@link WriteLockProvider} locks it, keksdose's demo) —\n * every lock but a lapsed plan's (`COMMIT_EXCEPT_BILLING`, 0.33): a profile is the\n * account's own settings, which billing never locks (docs/billing-harmonization.md §3.3,\n * §12.36).\n */\nexport function ProfileSetting(props: ProfileSettingProps) {\n const { name, email, role, memberSince, labels: labelsProp, id } = props;\n const labels = useAccountSettingsLabels(\"profile\", labelsProp);\n const names = props.firstName !== undefined || props.lastName !== undefined;\n return (\n <Card id={id} className=\"p-4 space-y-3\">\n <div className=\"flex items-center gap-3\">\n <UserAvatar\n name={name}\n email={email}\n person={names ? { first: props.firstName, last: props.lastName } : undefined}\n size=\"lg\"\n />\n <div className=\"min-w-0\">\n {/* A heading inside a SettingsLayout (0.31, docs/settings-harmonization.md\n §3.7), the plain div it always was outside one. */}\n <SettingsCardTitle>{labels.title}</SettingsCardTitle>\n <div className=\"truncate font-mono text-xs text-[var(--text-muted)]\">\n {/* `email` was a required key that nothing rendered; it names the address\n for a screen reader, which otherwise hears a bare string under a title. */}\n <span className=\"sr-only\">{labels.email}: </span>\n {email ?? \"—\"}\n </div>\n </div>\n </div>\n <div className=\"grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1 text-sm\">\n <span className=\"text-[var(--text-muted)]\">{labels.role}</span>\n <span>{role ?? \"—\"}</span>\n <span className=\"text-[var(--text-muted)]\">{labels.memberSince}</span>\n <span>{memberSince ?? \"—\"}</span>\n </div>\n {names ? (\n <PersonNameFields\n firstName={props.firstName ?? \"\"}\n lastName={props.lastName ?? \"\"}\n onSave={(props as ProfileSettingNamesProps).onSave}\n saving={props.saving}\n labels={labels}\n />\n ) : (\n <DisplayNameField\n name={name}\n value={(props as ProfileSettingDisplayNameProps).value}\n onChange={(props as ProfileSettingDisplayNameProps).onChange}\n onSave={(props as ProfileSettingDisplayNameProps).onSave}\n saving={props.saving}\n labels={labels}\n />\n )}\n </Card>\n );\n}\n\n/** The single-name form — exactly the 0.29 card's field and button. */\nfunction DisplayNameField({\n name,\n value,\n onChange,\n onSave,\n saving,\n labels,\n}: {\n name?: string | null;\n value: string;\n onChange: (value: string) => void;\n onSave: () => void;\n saving?: boolean;\n labels: Required<ProfileSettingLabels>;\n}) {\n const dirty = value.trim().length > 0 && value.trim() !== (name ?? \"\");\n // Generated, not the literal \"display-name\" it was: two of these on one page (an\n // admin editing someone else's profile beside their own) shared an id, and the\n // second label pointed at the first field.\n const inputId = useId();\n return (\n <>\n <Input\n id={inputId}\n label={labels.displayName}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n maxLength={120}\n />\n <Button onClick={onSave} disabled={!dirty || saving}>\n {labels.save}\n </Button>\n </>\n );\n}\n\n/** The first-and-last form (0.30.0, §6.1). */\nfunction PersonNameFields({\n firstName,\n lastName,\n onSave,\n saving,\n labels,\n}: {\n firstName: string;\n lastName: string;\n onSave: (values: ProfileNameValues) => void | Promise<unknown>;\n saving?: boolean;\n labels: Required<ProfileSettingLabels>;\n}) {\n const [first, setFirst] = useState(firstName);\n const [last, setLast] = useState(lastName);\n // The saved names moved (the save came back, another tab renamed): the drafts follow\n // them. During render, as DangerConfirm resets its fields, so no frame shows the old\n // draft beside the new name.\n const [saved, setSaved] = useState({ firstName, lastName });\n if (saved.firstName !== firstName || saved.lastName !== lastName) {\n setSaved({ firstName, lastName });\n setFirst(firstName);\n setLast(lastName);\n }\n const { pending, run } = usePromisePending();\n const busy = Boolean(saving) || pending;\n const valid = personNameOk(first) && personNameOk(last);\n const dirty = first.trim() !== firstName.trim() || last.trim() !== lastName.trim();\n\n const submit = (e: FormEvent) => {\n e.preventDefault();\n if (!valid || !dirty || busy) return;\n // A rejection keeps the drafts; the app reports it, as PasswordSetting's does.\n run(onSave({ firstName: first.trim(), lastName: last.trim() }), () => {});\n };\n\n return (\n <form className=\"@container space-y-3\" onSubmit={submit} noValidate>\n {/* The order and the rule of RegisterForm's two fields (auth §3.1). */}\n <div className=\"grid gap-3 @xs:grid-cols-2\">\n <Input\n name=\"given-name\"\n autoComplete=\"given-name\"\n label={labels.firstName}\n value={first}\n onChange={(e) => setFirst(e.target.value)}\n maxLength={PERSON_NAME_MAX_LENGTH}\n readOnly={busy}\n required\n />\n <Input\n name=\"family-name\"\n autoComplete=\"family-name\"\n label={labels.lastName}\n value={last}\n onChange={(e) => setLast(e.target.value)}\n maxLength={PERSON_NAME_MAX_LENGTH}\n readOnly={busy}\n required\n />\n </div>\n <Button\n type=\"submit\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={busy}\n // A blank name is a reason the person can act on; an unchanged one is not —\n // there is nothing to save, and the button is simply off.\n disabled={valid && !dirty}\n disabledReason={valid ? undefined : labels.nameRequired}\n >\n {labels.save}\n </Button>\n </form>\n );\n}\n\n/**\n * What {@link PasswordSetting}'s `strength` takes besides `true`: the meter's own options,\n * minus the ones the card owns — `value` (the new password), `minLength` (the card's, so\n * the checklist's length rule and the submit's check are one number) and `maxBytes`\n * (the card's own prop, which the submit enforces as well).\n */\nexport type PasswordSettingStrength = Pick<PasswordStrengthMeterProps, \"score\" | \"showRequirements\" | \"labels\">;\n\nexport function PasswordSetting({\n onSubmit,\n pending,\n minLength = 8,\n strength,\n maxBytes,\n labels: labelsProp,\n id,\n}: {\n /** Called with the validated (current, new) pair; return a promise to auto-clear on success. */\n onSubmit: (currentPassword: string, newPassword: string) => void | Promise<unknown>;\n pending?: boolean;\n /** The shortest new password the card submits, in characters as a person counts them\n * (an emoji is one). Keep it in step with the server's own minimum. Default 8. */\n minLength?: number;\n /**\n * Show the {@link PasswordStrengthMeter} under the new-password field (0.22.0,\n * kastlan's input audit): the bar, the level in words and the checklist, with the\n * length rule drawn from `minLength`. `true` for the meter as it comes; an object for\n * its `score` (zxcvbn's, say), `showRequirements` or `labels`.\n *\n * Off by default, so a card that never asked for it does not grow one. The meter is\n * advisory, as everywhere in the kit: it blocks nothing — the card's own checks do\n * (`minLength`, the confirmation, `maxBytes`).\n */\n strength?: boolean | PasswordSettingStrength;\n /**\n * A byte ceiling on the new password — bcrypt reads at most 72 BYTES and ignores the\n * rest, which is kastlan's case: `maxBytes={72}`. A new password over it is refused\n * on submit with the meter's own \"too long\" sentence (the `passwordStrength`\n * namespace's `tooLong`), and the meter, when shown, warns while it is typed.\n *\n * Unset by default, and leave it unset for a secret with no ceiling: an invented one\n * talks people out of the long passphrase they chose.\n */\n maxBytes?: number;\n /** Prop > `<UiKitProvider labels={{ accountSettings: { password } }}>` > English. */\n labels?: Partial<PasswordSettingLabels>;\n /** The card's DOM id — a settings catalogue `anchor`, so `?focus=` can ring it (0.31). */\n id?: string;\n}) {\n const labels = useAccountSettingsLabels(\"password\", labelsProp);\n const strengthOptions = typeof strength === \"object\" ? strength : undefined;\n // Only for `tooLong`: the sentence the meter shows is the one the refusal says.\n const strengthLabels = useKitLabels(\"passwordStrength\", DEFAULT_PASSWORD_STRENGTH_LABELS, strengthOptions?.labels);\n const [current, setCurrent] = useState(\"\");\n const [next, setNext] = useState(\"\");\n const [confirm, setConfirm] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n\n const submit = async () => {\n // Code points, not UTF-16 units — the count the meter's checklist and a server's\n // `len()` use. `next.length` counted an emoji as two, so four of them passed a\n // minimum of eight that the meter beside the field said they missed.\n if ([...next].length < minLength) {\n setError(labels.tooShort);\n return;\n }\n if (maxBytes !== undefined && passwordByteLength(next) > maxBytes) {\n setError(strengthLabels.tooLong(maxBytes));\n return;\n }\n if (next !== confirm) {\n setError(labels.mismatch);\n return;\n }\n setError(null);\n try {\n await onSubmit(current, next);\n setCurrent(\"\");\n setNext(\"\");\n setConfirm(\"\");\n } catch {\n // The app surfaces the failure (e.g. a toast); keep the fields for a retry.\n }\n };\n\n const nextField = (\n <Input type=\"password\" autoComplete=\"new-password\" label={labels.next} value={next} onChange={(e) => setNext(e.target.value)} />\n );\n\n return (\n <Card id={id} className=\"p-4 space-y-3\">\n <SettingsCardTitle>{labels.title}</SettingsCardTitle>\n <Input type=\"password\" autoComplete=\"current-password\" label={labels.current} value={current} onChange={(e) => setCurrent(e.target.value)} />\n {strength ? (\n // One box for the field and its meter, so the card's `space-y-3` spaces the pair\n // from its neighbours and the meter keeps its own small gap under the field.\n <div>\n {nextField}\n <PasswordStrengthMeter\n value={next}\n minLength={minLength}\n maxBytes={maxBytes}\n score={strengthOptions?.score}\n showRequirements={strengthOptions?.showRequirements}\n labels={strengthOptions?.labels}\n />\n </div>\n ) : (\n nextField\n )}\n <Input type=\"password\" autoComplete=\"new-password\" label={labels.confirm} value={confirm} onChange={(e) => setConfirm(e.target.value)} />\n {error && <p className=\"text-xs text-[var(--danger)]\">{error}</p>}\n <Button onClick={() => void submit()} disabled={!current || !next || !confirm || pending}>\n {labels.submit}\n </Button>\n </Card>\n );\n}\n\n/**\n * What the app's setup call returned.\n *\n * `otpauthUri` (0.12.0) is what an authenticator backend actually hands out; the kit\n * draws the QR code itself ({@link QrCode}) and reads the manual-entry key out of the\n * URI's `secret` parameter when `secret` is not given. The `qrSvg` shape — a\n * base64-encoded SVG image — is what the section took before, which made every app\n * render a QR library to a static string and base64 it only for the kit to draw it back.\n */\nexport type TwoFactorSetupData =\n | { qrSvg: string; secret: string }\n | { otpauthUri: string; secret?: string };\n\n/** The `secret` parameter of an `otpauth://` URI, or null. */\nfunction otpauthSecret(uri: string): string | null {\n const match = /[?&]secret=([^&#]*)/i.exec(uri);\n if (!match) return null;\n try {\n return decodeURIComponent(match[1]) || null;\n } catch {\n return match[1] || null;\n }\n}\n\n/** \"JBSW Y3DP EHPK 3PXP\": the key in groups of four, which is how an authenticator's\n * manual-entry screen shows it and how a person copies it by eye. */\nfunction groupSecret(secret: string): string {\n return secret.replace(/\\s+/g, \"\").replace(/(.{4})(?=.)/g, \"$1 \");\n}\n\nexport function TwoFactorSetting({\n enabled,\n setup,\n onStartSetup,\n onEnable,\n onDisable,\n busy,\n renderQr,\n labels: labelsProp,\n id,\n}: {\n enabled: boolean;\n /** What the app's setup call returned; null before setup starts. */\n setup: TwoFactorSetupData | null;\n onStartSetup: () => void;\n onEnable: (code: string) => void;\n onDisable: (password: string, code: string) => void;\n busy?: boolean;\n /** Draw the code yourself (a branded QR, a library you already ship). Given the\n * `otpauthUri`; the kit's own {@link QrCode} otherwise. Unused with `qrSvg`. */\n renderQr?: (otpauthUri: string) => ReactNode;\n /** Prop > `<UiKitProvider labels={{ accountSettings: { twoFactor } }}>` > English. */\n labels?: Partial<TwoFactorSettingLabels>;\n /** The card's DOM id — a settings catalogue `anchor`, so `?focus=` can ring it (0.31). */\n id?: string;\n}) {\n const labels = useAccountSettingsLabels(\"twoFactor\", labelsProp);\n // The title (0.31, docs/settings-harmonization.md §3.7) is new, and an app that\n // translated this card before it named the setting in `status` — \"2FA\",\n // \"Zwei-Faktor-Authentifizierung\". Each source's `title` first, then its `status`, so\n // that app keeps its own word instead of meeting the English title.\n const overrides = useKitLabelOverrides(\"accountSettings\")?.twoFactor;\n const title = labelsProp?.title ?? overrides?.title ?? labelsProp?.status ?? overrides?.status ?? labels.title;\n const [code, setCode] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n // Both code fields are a OneTimeCodeInput (0.22.0) — the field this card wrote by hand\n // twice — and keep what they did:\n // - `readOnlyUntilFocus`: browsers autofill a one-time-code field with the saved\n // username (the disable form has a current-password field right above it), and a\n // read-only field is never autofilled. The guard drops on focus, and the field\n // keeps the look of an editable one meanwhile (FIELD_WRITABLE_LOOK) — see the prop.\n // - `length={8}`: the `maxLength={8}` they had, so an authenticator issuing eight\n // digits still fits. What changed is that they now take digits only: \"123 456\"\n // pasted from an authenticator that groups its digits used to reach `onEnable`\n // with the space in it.\n // - one `code` state for both, as before.\n const secret = setup\n ? (setup.secret ?? (\"otpauthUri\" in setup ? otpauthSecret(setup.otpauthUri) : null))\n : null;\n\n return (\n <Card id={id} className=\"p-4 space-y-3\">\n {/* The card had only \"Two-factor authentication: Off\". A settings page outlines its\n cards by their titles (§3.7), so the setting's name is the title now, and the\n state stands beside it as a chip instead of repeating the name. */}\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <SettingsCardTitle>{title}</SettingsCardTitle>\n <Chip size=\"sm\" tone={enabled ? \"success\" : \"neutral\"}>\n {enabled ? labels.enabledText : labels.disabledText}\n </Chip>\n </div>\n\n {!enabled && !setup && (\n <Button onClick={onStartSetup} disabled={busy}>\n {labels.enable}\n </Button>\n )}\n\n {setup && (\n <div className=\"space-y-2\">\n <div className=\"text-xs text-[var(--text-secondary)]\">{labels.scanHint}</div>\n {/* `bg-white`, NOT `--bg-surface`, and it must stay that way: a QR code is\n read by a camera looking for maximum contrast between the modules and\n their quiet zone. On the dark theme a surface-coloured backing makes it\n slow to acquire, and on a warm light preset it lowers the contrast ratio\n the spec is written against. This is the one place in the kit where a\n literal colour is the correct answer. `data-private`: the code IS the\n second factor, and demo mode blurs it like any other secret. */}\n {\"otpauthUri\" in setup ? (\n // No backing of its own: QrCode draws its quiet zone white inside the SVG.\n <div data-private className=\"inline-block overflow-hidden rounded-md\">\n {renderQr ? (\n renderQr(setup.otpauthUri)\n ) : (\n <QrCode value={setup.otpauthUri} label={labels.qrAlt} className=\"size-48\" />\n )}\n </div>\n ) : (\n <div data-private className=\"inline-block rounded-md bg-white p-2\">\n <img\n alt={labels.qrAlt}\n src={`data:image/svg+xml;base64,${setup.qrSvg}`}\n className=\"h-48 w-48\"\n />\n </div>\n )}\n {secret && (\n <div className=\"space-y-1\">\n <div className=\"text-xs text-[var(--text-secondary)]\">{labels.secretHint}</div>\n <div className=\"flex items-center gap-1\">\n {/* `dir=\"ltr\"`: a base32 key is Latin letters and digits in a fixed\n order, and in an RTL page the groups would otherwise be laid out\n right to left. */}\n <code data-private dir=\"ltr\" className=\"break-all font-mono text-xs select-all\">\n {groupSecret(secret)}\n </code>\n <CopyButton text={secret} label={labels.copySecret} size=\"xs\" tone=\"muted\" />\n </div>\n </div>\n )}\n <OneTimeCodeInput\n readOnlyUntilFocus\n length={8}\n label={labels.codeLabel}\n value={code}\n onChange={setCode}\n onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\" && code && !busy) {\n e.preventDefault();\n onEnable(code);\n }\n }}\n />\n <Button onClick={() => onEnable(code)} disabled={!code || busy}>\n {labels.verify}\n </Button>\n </div>\n )}\n\n {enabled && (\n <div className=\"space-y-3 border-t border-[var(--border)] pt-3\">\n <div className=\"text-sm font-medium\">{labels.disableSection}</div>\n <Input type=\"password\" autoComplete=\"current-password\" label={labels.password} value={password} onChange={(e) => setPassword(e.target.value)} />\n <OneTimeCodeInput\n readOnlyUntilFocus\n length={8}\n label={labels.codeLabel}\n value={code}\n onChange={setCode}\n onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\" && password && code && !busy) {\n e.preventDefault();\n onDisable(password, code);\n }\n }}\n />\n <Button variant=\"danger\" onClick={() => onDisable(password, code)} disabled={!password || !code || busy}>\n {labels.disable}\n </Button>\n </div>\n )}\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
@@ -2,20 +2,25 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { CalculatorButtonLabels } from './calculator.js';
4
4
  import { NumberPadSheetLabels } from './numpad-sheet.js';
5
- import '../delete-account-setting-FLyJf-kZ.js';
5
+ import '../delete-account-setting-DiPX4Vue.js';
6
6
  import './tooltip.js';
7
7
  import '../hooks/use-anchored-rect.js';
8
8
  import '../lib/clipping.js';
9
- import '../data-table-labels-kpAAVi6g.js';
9
+ import '../data-table-labels-BJ5JE_X2.js';
10
10
  import './data-table-sort.js';
11
11
  import './swipeable-row.js';
12
+ import './status-dot.js';
13
+ import './progress-bar.js';
14
+ import './signed-amount.js';
15
+ import './stat-tile.js';
16
+ import './chip.js';
17
+ import 'lucide-react';
18
+ import './field-parts.js';
19
+ import './write-lock.js';
12
20
  import './mini-calendar.js';
13
21
  import './calendar-heatmap.js';
14
22
  import './popover.js';
15
23
  import '../hooks/use-breakpoint.js';
16
- import './chip.js';
17
- import 'lucide-react';
18
- import './field-parts.js';
19
24
  import './field-sync.js';
20
25
  import '../wizard/types.js';
21
26
  import '../tour/tour.js';
@@ -27,7 +32,6 @@ import './page-contents.js';
27
32
  import './series-chart-labels.js';
28
33
  import './pie-chart-labels.js';
29
34
  import './sparkline.js';
30
- import './stat-tile.js';
31
35
  import './signature-pad.js';
32
36
  import './password-strength.js';
33
37
  import './swatch-picker.js';
@@ -39,9 +43,6 @@ import '../hooks/use-search-param-state.js';
39
43
  import './measured-grid.js';
40
44
  import './user-avatar.js';
41
45
  import '../lib/person-name.js';
42
- import './status-dot.js';
43
- import './progress-bar.js';
44
- import './signed-amount.js';
45
46
  import '../shell/topbar-action-menu.js';
46
47
  import './menu-item.js';
47
48
  import './floating-panel.js';
@@ -57,7 +58,6 @@ import './error-boundary.js';
57
58
  import './authed-image.js';
58
59
  import './image-grid.js';
59
60
  import './lightbox.js';
60
- import './write-lock.js';
61
61
  import '../auth/sign-in-form.js';
62
62
  import '../landing/access.js';
63
63
  import '../landing/landing-labels.js';
@@ -1,5 +1,6 @@
1
1
  import { ComponentPropsWithoutRef, ReactNode, RefAttributes, ReactElement } from 'react';
2
2
  import { ComboOption } from './combobox-core.js';
3
+ import { CommitScope } from './write-lock.js';
3
4
  import '../hooks/use-anchored-rect.js';
4
5
  import '../hooks/use-anchored-panel.js';
5
6
 
@@ -126,7 +127,7 @@ interface AutocompleteProps<V extends string | number = string> extends Omit<Com
126
127
  * whose result is only draft state — the form's Save writes it — leaves this off and
127
128
  * stays live under the lock. No provider, or an unlocked one: no effect.
128
129
  */
129
- commit?: boolean;
130
+ commit?: CommitScope;
130
131
  }
131
132
  /**
132
133
  * An inline, async-capable text field with a suggestion list under it — the
@@ -138,7 +138,9 @@ function AutocompleteInner(props, ref) {
138
138
  // label's first letters, which float at the same start inset. On the
139
139
  // value line it is beside what it describes, and the label keeps the
140
140
  // start edge every other label in the form's column shares.
141
- hasLabel ? "top-[calc(1rem+1px)] h-5 items-center" : "top-1/2 -translate-y-1/2"
141
+ // At Large the label stands above the field and the strip is gone
142
+ // (0.33, §10.17): the value's line is under FIELD_BASE's `py-2`.
143
+ hasLabel ? "top-[calc(1rem+1px)] h-5 items-center large:top-[calc(0.5rem+1px)]" : "top-1/2 -translate-y-1/2"
142
144
  ),
143
145
  children: icon
144
146
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/autocomplete.tsx"],"sourcesContent":["import { Fragment, forwardRef, useId, useRef, useState } from \"react\";\nimport type {\n ComponentPropsWithoutRef,\n ForwardedRef,\n ReactElement,\n ReactNode,\n RefAttributes,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, Spinner } from \"./ui\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n isOptionEnabled,\n stepEnabled,\n useActiveOptionScroll,\n useComboboxFieldError,\n useOptionSource,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n StaticLabelRow,\n useFieldHint,\n useLockReason,\n} from \"./field-parts\";\nimport { mergeDescribedBy } from \"./choice-parts\";\n\n/**\n * `value`/`onChange` are the TEXT's, and `onSelect` is \"a suggestion was taken\" —\n * not the DOM's text-selection event — so the input's own spellings of those three\n * are omitted. Everything else reaches the `<input>`, the way it does on {@link Input}.\n */\nexport interface AutocompleteProps<V extends string | number = string>\n extends Omit<\n ComponentPropsWithoutRef<\"input\">,\n \"value\" | \"defaultValue\" | \"onChange\" | \"onSelect\" | \"children\" | \"type\" | \"size\"\n > {\n /** What is in the field. Controlled, and never reset by the component: opening,\n * closing and a failed lookup all leave it exactly as typed. */\n value: string;\n /** Every keystroke — and, with `fillOnSelect`, the label of a taken suggestion. */\n onChange: (text: string) => void;\n /** Suggestions the caller already has (e.g. from its own query hook). Narrowed by\n * the text unless `filter={false}`. An option with `disabled: true` is listed but\n * cannot be taken — see {@link ComboOption.disabled}. */\n options?: ComboOption<V>[];\n /** Or: fetch them. Debounced (`debounceMs`), race-safe, only at `minChars` and up,\n * never while the field is idle; a rejection shows `loadErrorLabel` and empties\n * the list rather than leaving the previous text's rows under it. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** Narrow `options` by label/sublabel substring. Default `true`. Pass `false` for\n * a list a server has already searched and ranked — it is shown as given. */\n filter?: boolean;\n /** External loading flag (a caller-side fetch), OR-ed with `loadOptions`'. */\n loading?: boolean;\n /** Below this many characters nothing is offered or fetched. Default 1: an empty\n * field asks nothing — focusing it is not a search. */\n minChars?: number;\n /** Quiet time before `loadOptions` runs. Default 300 ms — an ordinary typist's\n * inter-key interval is 150–250 ms, so the pickers' 150 fires on nearly every\n * letter, which a rate-limited geocoder cannot afford. */\n debounceMs?: number;\n /**\n * Whether the list MAY show — controlled. Omit (or pass `undefined`) and the field\n * decides: open while focused, until Escape, Tab or a taken row. `false` keeps it\n * shut whatever the user does, and stops `loadOptions` asking — keksdose's address\n * search, with its confirm map up, where it used to pass `minChars={Infinity}`.\n * `true` shows it even unfocused.\n *\n * Even open, the list only APPEARS when it has something to show (rows, or a\n * status line); `aria-expanded` follows what appears, not this flag.\n */\n open?: boolean;\n /** The field wants the list open or shut — focus, typing, ↓/↑ open it; blur,\n * Escape, Tab and taking a row shut it. Called on every change, controlled or not. */\n onOpenChange?: (open: boolean) => void;\n /**\n * `\"sm\"`: the 28px, 12px-type field of {@link Select}'s `size=\"sm\"`, with the icon,\n * spinner and list rows scaled to match — for a card or toolbar whose buttons are\n * small. `\"md\"` (default) is the form field. Unlabelled only, as on `Select`: a\n * floating label needs the tall box, so a labelled field ignores `\"sm\"`.\n *\n * A NUMBER is still the native `size` attribute (the width in characters), passed\n * straight through.\n */\n size?: \"sm\" | \"md\" | number;\n /** A suggestion was taken (click, or Enter on the highlighted row). */\n onSelect?: (option: ComboOption<V>) => void;\n /**\n * Put the taken suggestion's label into the field (`onChange(label)`). Default\n * `true` — the free-text case, where the text IS the value and a suggestion only\n * completes it (an address field). Pass `false` where taking a row is an ACTION\n * and the field is only the search that found it; the text then stays as typed.\n */\n fillOnSelect?: boolean;\n /** A floating label, as on {@link Combobox} — a real `<label for>` on the input.\n * Names the field (`aria-labelledby`) unless an `aria-label` is given. */\n label?: ReactNode;\n /** Leading decoration inside the field (a pin, a magnifier). Decorative: the\n * label names the field. An `<svg>` child is sized by the field — 16px, or 14px\n * at `size=\"sm\"` — so a lucide icon needs no class of its own. */\n icon?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}. */\n invalid?: boolean;\n /** What is wrong with the VALUE, as {@link Input}'s `error`: under the field, on\n * its `aria-describedby`, implies `invalid`. Not a failed lookup — that is\n * `loadErrorLabel`, shown in the list. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so this field reads like every other\n * (keksdose K4): plain TEXT is a caption under the field on the input's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the field's end edge, outside the box. Not the list's\n * `status` line, which talks about the lookup, not the field.\n *\n * Pass it — even as `undefined` — on a field whose hint comes and goes (0.24): the\n * unlabelled field then keeps its end-hint row whether or not a \"?\" is in it, so the\n * `<input>` is never re-parented, and focus and caret survive the toggle. The rule\n * the rest of the family has had since 0.23 (and the fields since 0.22).\n */\n hint?: ReactNode;\n /**\n * The caller's own line in the list, replacing the automatic one (loading / failed\n * / no results). For what only the caller knows: \"offline\", \"the address service\n * is switched off\", a privacy note, the hint a caller-fetched list needs below its\n * own minimum. While set, the list opens even with no rows. Announced politely.\n */\n status?: ReactNode;\n /** Default: `combobox.noResults` from the {@link UiKitProvider}, else English. */\n emptyLabel?: string;\n /** Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n /** Classes for the `<input>` itself; `className` styles the wrapper. */\n inputClassName?: string;\n /**\n * Why the field cannot be changed — {@link Button}'s `disabledReason`, the Combobox\n * family's write lock (keksdose G2, 0.23), for a field whose pick SAVES: an address\n * search whose taken row writes the address, a lookup that links a record.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey FIELD_BASE gives a `[readonly]` field), opens no list and asks no\n * `loadOptions` — whatever `open` says — and nothing reaches `onChange`, `onSelect` or\n * `onOpenChange(true)`. Enter is swallowed, so a locked field does not submit the form\n * around it, and a caller's `onKeyDown` does not hear it; every other key still\n * reaches the caller (an Escape that closes its panel) and the caret. The reason is in\n * the kit {@link Tooltip} and on the input's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /**\n * This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the\n * `disabledReason` way with the lock's reason (which wins over its own). A search\n * whose result is only draft state — the form's Save writes it — leaves this off and\n * stays live under the lock. No provider, or an unlocked one: no effect.\n */\n commit?: boolean;\n}\n\nfunction AutocompleteInner<V extends string | number = string>(\n props: AutocompleteProps<V>,\n ref: ForwardedRef<HTMLInputElement>,\n) {\n const {\n value,\n onChange,\n options,\n loadOptions,\n filter,\n loading,\n minChars = 1,\n debounceMs = 300,\n onSelect,\n fillOnSelect = true,\n label,\n icon,\n invalid,\n error,\n hint,\n status,\n emptyLabel,\n loadErrorLabel,\n open: openProp,\n onOpenChange,\n size,\n className,\n inputClassName,\n id,\n disabled,\n disabledReason,\n commit,\n onKeyDown,\n onFocus,\n onBlur,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n } = props;\n const generated = useId();\n const fieldId = id ?? generated;\n // Off the GENERATED id: a caller's `id` is theirs to collide with.\n const listboxId = `${generated}-listbox`;\n const labelId = `${generated}-label`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const fieldRef = useRef<HTMLDivElement>(null);\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n noResults: emptyLabel,\n loadError: loadErrorLabel,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useComboboxFieldError(error, invalid, hintParts.describedBy);\n\n /** The user's intent: the field is focused and the list has not been dismissed.\n * Whether anything SHOWS is decided below, from what there is to show. */\n const [ownOpen, setOwnOpen] = useState(false);\n // Controlled when the caller passes a boolean. The own state is kept up to date\n // either way, so a caller that controls only SOMETIMES (`open={confirming ? false :\n // undefined}`) hands back a field that is exactly as open as focus says it is.\n const open = openProp ?? ownOpen;\n const setOpen = (next: boolean) => {\n setOwnOpen(next);\n if (next !== open) onOpenChange?.(next);\n };\n const [active, setActive] = useState(-1);\n // The lock: focusable, `readOnly`, no list — see `disabledReason`. Folded into `live`,\n // so a locked field asks nothing and shows nothing even with `open` held true.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const live = open && !disabled && !locked;\n const { results, busy, failed, tooShort } = useOptionSource<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n query: value,\n active: live,\n });\n\n const hasStatus = status !== undefined && status !== null && status !== false && status !== \"\";\n // The visible line under the rows. Nothing below `minChars` unless the caller has\n // something to say — an empty field opening a box that says \"type more\" is noise.\n const statusLine: ReactNode = hasStatus\n ? status\n : tooShort\n ? null\n : busy && results.length === 0\n ? labels.loading\n : failed\n ? labels.loadError\n : !busy && results.length === 0\n ? labels.noResults\n : null;\n const expanded = live && (results.length > 0 || statusLine !== null);\n // A disabled row is never the keyboard's — not even when a list that changed under\n // the highlight put one there.\n const activeId = expanded && isOptionEnabled(results[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n const { rect, top, maxHeight } = useAnchoredPanel(fieldRef, expanded, { preferredHeight: 256 });\n const dir = useAnchorDir(fieldRef, expanded);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n };\n const take = (o: ComboOption<V>) => {\n if (o.disabled || locked) return;\n if (fillOnSelect) onChange(o.label);\n onSelect?.(o);\n close();\n };\n\n const hasLabel = label !== undefined;\n // `size=\"sm\"` is for an UNLABELLED field, as on Select: the floating label needs the\n // tall box. A number is the native attribute.\n const small = size === \"sm\" && !hasLabel;\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // The caller's name wins; else the floating label, by reference so a non-string\n // label still names the field.\n const labelledBy =\n ariaLabel === undefined ? (ariaLabelledBy ?? (hasLabel ? labelId : undefined)) : undefined;\n // While locked the field sits in the reason's Tooltip — in a fragment with a hidden\n // copy of the reason, Button's anatomy; `block` so a full-width field stays full width.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `relative`: the floating label and the live region's `sr-only` both need a\n // local containing block.\n <div className={cn(\"relative\", className)}>\n {/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the\n name — and the listbox's, which borrows it — is exactly what it was. */}\n {hasLabel && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId}>\n {label}\n </ComboboxFieldLabel>\n )}\n {/* `\"hint\" in props`: the family's 0.22 rule (see `endHintRowProps`) — passed at\n all, even as `undefined`, a hint keeps the unlabelled field's end-hint row, so\n a \"?\" coming and going never re-parents the <input> under the caret (0.24, the\n last of the family to get it; the siblings had it in 0.23). */}\n <EndHintRow {...endHintRowProps(\"hint\" in props, hasLabel, hintParts.labelHint)}>\n {withLock(\n <div ref={fieldRef} className={cn(\"relative\", locked && \"cursor-not-allowed\")}>\n {icon && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none absolute flex text-[var(--text-muted)]\",\n small ? \"start-2 [&>svg]:size-3.5\" : \"start-2.5 [&>svg]:size-4\",\n // Labelled, the icon sits on the VALUE's line — the one line box below\n // FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of\n // the box. Centred on the box it rose into the top strip and sat on the\n // label's first letters, which float at the same start inset. On the\n // value line it is beside what it describes, and the label keeps the\n // start edge every other label in the form's column shares.\n hasLabel\n ? \"top-[calc(1rem+1px)] h-5 items-center\"\n : \"top-1/2 -translate-y-1/2\",\n )}\n >\n {icon}\n </span>\n )}\n <input\n {...rest}\n ref={ref}\n id={fieldId}\n size={nativeSize}\n type=\"text\"\n value={value}\n // Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || rest.readOnly}\n aria-disabled={locked || rest[\"aria-disabled\"]}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={expanded}\n // Required by the role, and set while closed too, as on the siblings.\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n autoComplete=\"off\"\n onFocus={(e) => {\n onFocus?.(e);\n // A locked field takes focus to say why, and opens nothing.\n if (locked) return;\n // A seeded field (keksdose's `initialQuery`) is looked up as it is\n // focused; an empty one asks nothing, because of `minChars`.\n setOpen(true);\n }}\n onBlur={(e) => {\n onBlur?.(e);\n close();\n }}\n onChange={(e) => {\n if (locked) return;\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n // Locked: Enter is swallowed (no form submit, no caller's \"search now\");\n // every other key is the caller's and the caret's. No list to drive.\n if (locked) {\n if (e.key === \"Enter\") e.preventDefault();\n else onKeyDown?.(e);\n return;\n }\n // An Escape that closes the OPEN list is the list's, and is consumed before\n // the caller sees it. A caller whose Escape means \"close the panel\" (keksdose's\n // address search) otherwise closed the whole panel when the user only meant\n // to dismiss the suggestions — the opposite of what the docs promised.\n if (e.key === \"Escape\" && expanded) {\n e.preventDefault();\n e.stopPropagation();\n close();\n return;\n }\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n // Disabled rows are passed over; at either end the highlight stays put.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) setOpen(true);\n else setActive((i) => stepEnabled(results, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) setOpen(true);\n // From \"nothing highlighted\", Up goes to the bottom, as the APG has it.\n else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));\n } else if (e.key === \"Enter\") {\n // No row highlighted: the text is the answer, and a form's own submit\n // is left alone.\n if (expanded && activeId) {\n e.preventDefault();\n take(results[active]);\n }\n } else if (e.key === \"Tab\") {\n close();\n }\n }}\n className={cn(\n FIELD_BASE,\n hasLabel && FIELD_FLOATING_PAD,\n // Select's `SELECT_SM` box: a fixed 28px, so the caller's line height\n // cannot grow it.\n small && \"h-7 py-0 pe-2 ps-2 text-xs\",\n icon ? (small ? \"ps-7\" : \"ps-8\") : undefined,\n busy && (small ? \"pe-7\" : \"pe-9\"),\n field.isInvalid && FIELD_INVALID,\n inputClassName,\n )}\n />\n {busy && live && (\n // Decorative: the live region below already says \"Loading…\".\n <span\n className={cn(\n \"pointer-events-none absolute top-1/2 flex -translate-y-1/2\",\n small ? \"end-2\" : \"end-2.5\",\n )}\n >\n <Spinner label={null} className={small ? \"h-3.5 w-3.5\" : \"h-4 w-4\"} />\n </span>\n )}\n </div>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it, after the field so the \"?\"\n follows the control in the tab order (as on Input and Select). */}\n {hasLabel && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId} className={LABEL_IN_ROW}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {/* The live region, kept mounted so a change IS an announcement: focus stays\n in the field, and without it a reader typed into a geocoder and heard only\n their own letters. The panel's visible line is aria-hidden in favour of\n this, which also carries the row count the eye reads off the list. */}\n <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {!live\n ? null\n : statusLine !== null\n ? statusLine\n : results.length > 0\n ? labels.resultCount(results.length)\n : null}\n </div>\n {expanded &&\n rect &&\n typeof document !== \"undefined\" &&\n createPortal(\n <div\n // Every press inside the list keeps focus in the field — rows, the status\n // line, the scrollbar — or the input's blur would close the list first.\n role=\"presentation\"\n // Portalled out of the form's `dir`; the field's is put back. The list is\n // exactly the field's width, so `left` places it in either direction.\n dir={dir}\n onMouseDown={(e) => e.preventDefault()}\n className=\"fixed z-50 flex flex-col overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg\"\n style={{ top, left: rect.left, width: rect.width, maxHeight }}\n >\n {results.length > 0 && (\n <ul\n id={listboxId}\n role=\"listbox\"\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n className=\"min-h-0 flex-1 overflow-y-auto py-1\"\n >\n {results.map((o, i) => {\n const startsGroup = o.group != null && o.group !== results[i - 1]?.group;\n return (\n // Keyed by group AND value, as in the core panel: an option may\n // appear once per group.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {startsGroup && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-[var(--text-muted)] first:pt-1\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n // CHOSEN, never \"highlighted\" — the family's rule. This\n // field holds no value, so the only row that can be chosen\n // is the one whose label the text already is.\n aria-selected={fillOnSelect && o.label === value}\n // Listed, announced as unavailable, never taken: `take`\n // refuses it and the arrows pass it over.\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onClick={() => take(o)}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n \"flex w-full items-center gap-2 px-3 text-start\",\n small ? \"py-1 text-xs\" : \"py-1.5 text-sm\",\n o.disabled\n ? DISABLED_ROW_CLASS\n : i === active\n ? \"bg-[var(--bg-active)]\"\n : \"hover:bg-[var(--bg-hover)]\",\n o.group != null && \"ps-6\",\n )}\n >\n {o.icon && (\n <span aria-hidden className=\"shrink-0 text-[var(--text-muted)]\">\n {o.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-[var(--text-primary)]\">\n {o.label}\n </span>\n {o.sublabel && (\n <span\n className={cn(\n \"block truncate text-[var(--text-placeholder)]\",\n small ? \"text-caption\" : \"text-xs\",\n )}\n >\n {o.sublabel}\n </span>\n )}\n </span>\n </button>\n </li>\n </Fragment>\n );\n })}\n </ul>\n )}\n {statusLine !== null && (\n // Visible, not live — the region above speaks for it.\n <div\n aria-hidden\n className={cn(\n small ? \"px-3 py-1.5 text-xs\" : \"px-3 py-2 text-sm\",\n failed && !hasStatus ? \"text-[var(--danger)]\" : \"text-[var(--text-muted)]\",\n results.length > 0 && \"border-t border-[var(--border)] text-xs\",\n )}\n >\n {statusLine}\n </div>\n )}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n\nconst AutocompleteBase = forwardRef(AutocompleteInner);\nAutocompleteBase.displayName = \"Autocomplete\";\n\n/**\n * An inline, async-capable text field with a suggestion list under it — the\n * WAI-ARIA APG \"editable combobox with list autocomplete\".\n *\n * Two jobs, one control:\n * - **free text** (kastlan's address field): the text is the value, the list only\n * offers completions, and taking one fills the field (`fillOnSelect`, default);\n * - **search-then-act** (keksdose's address search): the text is a query, taking a\n * row calls `onSelect` and nothing is held (`fillOnSelect={false}`).\n *\n * How it differs from its siblings: {@link Combobox} is free text over a STATIC\n * string pool and opens on focus; {@link EntityCombobox} is a trigger holding an id.\n * This one keeps no value of its own, never resets the text, asks nothing below\n * `minChars`, and reports a failed lookup instead of showing stale rows.\n *\n * Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted\n * row, `aria-controls` the list, and a polite live region says what the list now\n * holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter\n * takes the highlighted row (with none,\n * Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay\n * with the caret. An Escape that closes an open list is consumed and never reaches the\n * caller; with the list closed it does. For every other key a caller's `onKeyDown`\n * runs first and may `preventDefault()` to\n * keep a key for itself.\n *\n * The list is portalled and anchored under the field on every screen size: the\n * field already carries the keyboard, and `useAnchoredPanel` keeps the list clear\n * of it on a phone. There is no full-screen sheet as {@link Combobox} has.\n\n * Generic over the option value (`ComboOption<V>`); the ref is the `<input>`'s.\n */\nexport const Autocomplete = AutocompleteBase as <V extends string | number = string>(\n props: AutocompleteProps<V> & RefAttributes<HTMLInputElement>,\n) => ReactElement | null;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/autocomplete.tsx"],"sourcesContent":["import { Fragment, forwardRef, useId, useRef, useState } from \"react\";\nimport type {\n ComponentPropsWithoutRef,\n ForwardedRef,\n ReactElement,\n ReactNode,\n RefAttributes,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, Spinner } from \"./ui\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n isOptionEnabled,\n stepEnabled,\n useActiveOptionScroll,\n useComboboxFieldError,\n useOptionSource,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n StaticLabelRow,\n useFieldHint,\n useLockReason,\n} from \"./field-parts\";\nimport { mergeDescribedBy } from \"./choice-parts\";\nimport type { CommitScope } from \"./write-lock\";\n\n/**\n * `value`/`onChange` are the TEXT's, and `onSelect` is \"a suggestion was taken\" —\n * not the DOM's text-selection event — so the input's own spellings of those three\n * are omitted. Everything else reaches the `<input>`, the way it does on {@link Input}.\n */\nexport interface AutocompleteProps<V extends string | number = string>\n extends Omit<\n ComponentPropsWithoutRef<\"input\">,\n \"value\" | \"defaultValue\" | \"onChange\" | \"onSelect\" | \"children\" | \"type\" | \"size\"\n > {\n /** What is in the field. Controlled, and never reset by the component: opening,\n * closing and a failed lookup all leave it exactly as typed. */\n value: string;\n /** Every keystroke — and, with `fillOnSelect`, the label of a taken suggestion. */\n onChange: (text: string) => void;\n /** Suggestions the caller already has (e.g. from its own query hook). Narrowed by\n * the text unless `filter={false}`. An option with `disabled: true` is listed but\n * cannot be taken — see {@link ComboOption.disabled}. */\n options?: ComboOption<V>[];\n /** Or: fetch them. Debounced (`debounceMs`), race-safe, only at `minChars` and up,\n * never while the field is idle; a rejection shows `loadErrorLabel` and empties\n * the list rather than leaving the previous text's rows under it. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** Narrow `options` by label/sublabel substring. Default `true`. Pass `false` for\n * a list a server has already searched and ranked — it is shown as given. */\n filter?: boolean;\n /** External loading flag (a caller-side fetch), OR-ed with `loadOptions`'. */\n loading?: boolean;\n /** Below this many characters nothing is offered or fetched. Default 1: an empty\n * field asks nothing — focusing it is not a search. */\n minChars?: number;\n /** Quiet time before `loadOptions` runs. Default 300 ms — an ordinary typist's\n * inter-key interval is 150–250 ms, so the pickers' 150 fires on nearly every\n * letter, which a rate-limited geocoder cannot afford. */\n debounceMs?: number;\n /**\n * Whether the list MAY show — controlled. Omit (or pass `undefined`) and the field\n * decides: open while focused, until Escape, Tab or a taken row. `false` keeps it\n * shut whatever the user does, and stops `loadOptions` asking — keksdose's address\n * search, with its confirm map up, where it used to pass `minChars={Infinity}`.\n * `true` shows it even unfocused.\n *\n * Even open, the list only APPEARS when it has something to show (rows, or a\n * status line); `aria-expanded` follows what appears, not this flag.\n */\n open?: boolean;\n /** The field wants the list open or shut — focus, typing, ↓/↑ open it; blur,\n * Escape, Tab and taking a row shut it. Called on every change, controlled or not. */\n onOpenChange?: (open: boolean) => void;\n /**\n * `\"sm\"`: the 28px, 12px-type field of {@link Select}'s `size=\"sm\"`, with the icon,\n * spinner and list rows scaled to match — for a card or toolbar whose buttons are\n * small. `\"md\"` (default) is the form field. Unlabelled only, as on `Select`: a\n * floating label needs the tall box, so a labelled field ignores `\"sm\"`.\n *\n * A NUMBER is still the native `size` attribute (the width in characters), passed\n * straight through.\n */\n size?: \"sm\" | \"md\" | number;\n /** A suggestion was taken (click, or Enter on the highlighted row). */\n onSelect?: (option: ComboOption<V>) => void;\n /**\n * Put the taken suggestion's label into the field (`onChange(label)`). Default\n * `true` — the free-text case, where the text IS the value and a suggestion only\n * completes it (an address field). Pass `false` where taking a row is an ACTION\n * and the field is only the search that found it; the text then stays as typed.\n */\n fillOnSelect?: boolean;\n /** A floating label, as on {@link Combobox} — a real `<label for>` on the input.\n * Names the field (`aria-labelledby`) unless an `aria-label` is given. */\n label?: ReactNode;\n /** Leading decoration inside the field (a pin, a magnifier). Decorative: the\n * label names the field. An `<svg>` child is sized by the field — 16px, or 14px\n * at `size=\"sm\"` — so a lucide icon needs no class of its own. */\n icon?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}. */\n invalid?: boolean;\n /** What is wrong with the VALUE, as {@link Input}'s `error`: under the field, on\n * its `aria-describedby`, implies `invalid`. Not a failed lookup — that is\n * `loadErrorLabel`, shown in the list. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so this field reads like every other\n * (keksdose K4): plain TEXT is a caption under the field on the input's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the field's end edge, outside the box. Not the list's\n * `status` line, which talks about the lookup, not the field.\n *\n * Pass it — even as `undefined` — on a field whose hint comes and goes (0.24): the\n * unlabelled field then keeps its end-hint row whether or not a \"?\" is in it, so the\n * `<input>` is never re-parented, and focus and caret survive the toggle. The rule\n * the rest of the family has had since 0.23 (and the fields since 0.22).\n */\n hint?: ReactNode;\n /**\n * The caller's own line in the list, replacing the automatic one (loading / failed\n * / no results). For what only the caller knows: \"offline\", \"the address service\n * is switched off\", a privacy note, the hint a caller-fetched list needs below its\n * own minimum. While set, the list opens even with no rows. Announced politely.\n */\n status?: ReactNode;\n /** Default: `combobox.noResults` from the {@link UiKitProvider}, else English. */\n emptyLabel?: string;\n /** Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n /** Classes for the `<input>` itself; `className` styles the wrapper. */\n inputClassName?: string;\n /**\n * Why the field cannot be changed — {@link Button}'s `disabledReason`, the Combobox\n * family's write lock (keksdose G2, 0.23), for a field whose pick SAVES: an address\n * search whose taken row writes the address, a lookup that links a record.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey FIELD_BASE gives a `[readonly]` field), opens no list and asks no\n * `loadOptions` — whatever `open` says — and nothing reaches `onChange`, `onSelect` or\n * `onOpenChange(true)`. Enter is swallowed, so a locked field does not submit the form\n * around it, and a caller's `onKeyDown` does not hear it; every other key still\n * reaches the caller (an Escape that closes its panel) and the caret. The reason is in\n * the kit {@link Tooltip} and on the input's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /**\n * This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the\n * `disabledReason` way with the lock's reason (which wins over its own). A search\n * whose result is only draft state — the form's Save writes it — leaves this off and\n * stays live under the lock. No provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n}\n\nfunction AutocompleteInner<V extends string | number = string>(\n props: AutocompleteProps<V>,\n ref: ForwardedRef<HTMLInputElement>,\n) {\n const {\n value,\n onChange,\n options,\n loadOptions,\n filter,\n loading,\n minChars = 1,\n debounceMs = 300,\n onSelect,\n fillOnSelect = true,\n label,\n icon,\n invalid,\n error,\n hint,\n status,\n emptyLabel,\n loadErrorLabel,\n open: openProp,\n onOpenChange,\n size,\n className,\n inputClassName,\n id,\n disabled,\n disabledReason,\n commit,\n onKeyDown,\n onFocus,\n onBlur,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n } = props;\n const generated = useId();\n const fieldId = id ?? generated;\n // Off the GENERATED id: a caller's `id` is theirs to collide with.\n const listboxId = `${generated}-listbox`;\n const labelId = `${generated}-label`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const fieldRef = useRef<HTMLDivElement>(null);\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n noResults: emptyLabel,\n loadError: loadErrorLabel,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useComboboxFieldError(error, invalid, hintParts.describedBy);\n\n /** The user's intent: the field is focused and the list has not been dismissed.\n * Whether anything SHOWS is decided below, from what there is to show. */\n const [ownOpen, setOwnOpen] = useState(false);\n // Controlled when the caller passes a boolean. The own state is kept up to date\n // either way, so a caller that controls only SOMETIMES (`open={confirming ? false :\n // undefined}`) hands back a field that is exactly as open as focus says it is.\n const open = openProp ?? ownOpen;\n const setOpen = (next: boolean) => {\n setOwnOpen(next);\n if (next !== open) onOpenChange?.(next);\n };\n const [active, setActive] = useState(-1);\n // The lock: focusable, `readOnly`, no list — see `disabledReason`. Folded into `live`,\n // so a locked field asks nothing and shows nothing even with `open` held true.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const live = open && !disabled && !locked;\n const { results, busy, failed, tooShort } = useOptionSource<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n query: value,\n active: live,\n });\n\n const hasStatus = status !== undefined && status !== null && status !== false && status !== \"\";\n // The visible line under the rows. Nothing below `minChars` unless the caller has\n // something to say — an empty field opening a box that says \"type more\" is noise.\n const statusLine: ReactNode = hasStatus\n ? status\n : tooShort\n ? null\n : busy && results.length === 0\n ? labels.loading\n : failed\n ? labels.loadError\n : !busy && results.length === 0\n ? labels.noResults\n : null;\n const expanded = live && (results.length > 0 || statusLine !== null);\n // A disabled row is never the keyboard's — not even when a list that changed under\n // the highlight put one there.\n const activeId = expanded && isOptionEnabled(results[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n const { rect, top, maxHeight } = useAnchoredPanel(fieldRef, expanded, { preferredHeight: 256 });\n const dir = useAnchorDir(fieldRef, expanded);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n };\n const take = (o: ComboOption<V>) => {\n if (o.disabled || locked) return;\n if (fillOnSelect) onChange(o.label);\n onSelect?.(o);\n close();\n };\n\n const hasLabel = label !== undefined;\n // `size=\"sm\"` is for an UNLABELLED field, as on Select: the floating label needs the\n // tall box. A number is the native attribute.\n const small = size === \"sm\" && !hasLabel;\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // The caller's name wins; else the floating label, by reference so a non-string\n // label still names the field.\n const labelledBy =\n ariaLabel === undefined ? (ariaLabelledBy ?? (hasLabel ? labelId : undefined)) : undefined;\n // While locked the field sits in the reason's Tooltip — in a fragment with a hidden\n // copy of the reason, Button's anatomy; `block` so a full-width field stays full width.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `relative`: the floating label and the live region's `sr-only` both need a\n // local containing block.\n <div className={cn(\"relative\", className)}>\n {/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the\n name — and the listbox's, which borrows it — is exactly what it was. */}\n {hasLabel && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId}>\n {label}\n </ComboboxFieldLabel>\n )}\n {/* `\"hint\" in props`: the family's 0.22 rule (see `endHintRowProps`) — passed at\n all, even as `undefined`, a hint keeps the unlabelled field's end-hint row, so\n a \"?\" coming and going never re-parents the <input> under the caret (0.24, the\n last of the family to get it; the siblings had it in 0.23). */}\n <EndHintRow {...endHintRowProps(\"hint\" in props, hasLabel, hintParts.labelHint)}>\n {withLock(\n <div ref={fieldRef} className={cn(\"relative\", locked && \"cursor-not-allowed\")}>\n {icon && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none absolute flex text-[var(--text-muted)]\",\n small ? \"start-2 [&>svg]:size-3.5\" : \"start-2.5 [&>svg]:size-4\",\n // Labelled, the icon sits on the VALUE's line — the one line box below\n // FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of\n // the box. Centred on the box it rose into the top strip and sat on the\n // label's first letters, which float at the same start inset. On the\n // value line it is beside what it describes, and the label keeps the\n // start edge every other label in the form's column shares.\n // At Large the label stands above the field and the strip is gone\n // (0.33, §10.17): the value's line is under FIELD_BASE's `py-2`.\n hasLabel\n ? \"top-[calc(1rem+1px)] h-5 items-center large:top-[calc(0.5rem+1px)]\"\n : \"top-1/2 -translate-y-1/2\",\n )}\n >\n {icon}\n </span>\n )}\n <input\n {...rest}\n ref={ref}\n id={fieldId}\n size={nativeSize}\n type=\"text\"\n value={value}\n // Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || rest.readOnly}\n aria-disabled={locked || rest[\"aria-disabled\"]}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={expanded}\n // Required by the role, and set while closed too, as on the siblings.\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n autoComplete=\"off\"\n onFocus={(e) => {\n onFocus?.(e);\n // A locked field takes focus to say why, and opens nothing.\n if (locked) return;\n // A seeded field (keksdose's `initialQuery`) is looked up as it is\n // focused; an empty one asks nothing, because of `minChars`.\n setOpen(true);\n }}\n onBlur={(e) => {\n onBlur?.(e);\n close();\n }}\n onChange={(e) => {\n if (locked) return;\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n // Locked: Enter is swallowed (no form submit, no caller's \"search now\");\n // every other key is the caller's and the caret's. No list to drive.\n if (locked) {\n if (e.key === \"Enter\") e.preventDefault();\n else onKeyDown?.(e);\n return;\n }\n // An Escape that closes the OPEN list is the list's, and is consumed before\n // the caller sees it. A caller whose Escape means \"close the panel\" (keksdose's\n // address search) otherwise closed the whole panel when the user only meant\n // to dismiss the suggestions — the opposite of what the docs promised.\n if (e.key === \"Escape\" && expanded) {\n e.preventDefault();\n e.stopPropagation();\n close();\n return;\n }\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n // Disabled rows are passed over; at either end the highlight stays put.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) setOpen(true);\n else setActive((i) => stepEnabled(results, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) setOpen(true);\n // From \"nothing highlighted\", Up goes to the bottom, as the APG has it.\n else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));\n } else if (e.key === \"Enter\") {\n // No row highlighted: the text is the answer, and a form's own submit\n // is left alone.\n if (expanded && activeId) {\n e.preventDefault();\n take(results[active]);\n }\n } else if (e.key === \"Tab\") {\n close();\n }\n }}\n className={cn(\n FIELD_BASE,\n hasLabel && FIELD_FLOATING_PAD,\n // Select's `SELECT_SM` box: a fixed 28px, so the caller's line height\n // cannot grow it.\n small && \"h-7 py-0 pe-2 ps-2 text-xs\",\n icon ? (small ? \"ps-7\" : \"ps-8\") : undefined,\n busy && (small ? \"pe-7\" : \"pe-9\"),\n field.isInvalid && FIELD_INVALID,\n inputClassName,\n )}\n />\n {busy && live && (\n // Decorative: the live region below already says \"Loading…\".\n <span\n className={cn(\n \"pointer-events-none absolute top-1/2 flex -translate-y-1/2\",\n small ? \"end-2\" : \"end-2.5\",\n )}\n >\n <Spinner label={null} className={small ? \"h-3.5 w-3.5\" : \"h-4 w-4\"} />\n </span>\n )}\n </div>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it, after the field so the \"?\"\n follows the control in the tab order (as on Input and Select). */}\n {hasLabel && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId} className={LABEL_IN_ROW}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {/* The live region, kept mounted so a change IS an announcement: focus stays\n in the field, and without it a reader typed into a geocoder and heard only\n their own letters. The panel's visible line is aria-hidden in favour of\n this, which also carries the row count the eye reads off the list. */}\n <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {!live\n ? null\n : statusLine !== null\n ? statusLine\n : results.length > 0\n ? labels.resultCount(results.length)\n : null}\n </div>\n {expanded &&\n rect &&\n typeof document !== \"undefined\" &&\n createPortal(\n <div\n // Every press inside the list keeps focus in the field — rows, the status\n // line, the scrollbar — or the input's blur would close the list first.\n role=\"presentation\"\n // Portalled out of the form's `dir`; the field's is put back. The list is\n // exactly the field's width, so `left` places it in either direction.\n dir={dir}\n onMouseDown={(e) => e.preventDefault()}\n className=\"fixed z-50 flex flex-col overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg\"\n style={{ top, left: rect.left, width: rect.width, maxHeight }}\n >\n {results.length > 0 && (\n <ul\n id={listboxId}\n role=\"listbox\"\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n className=\"min-h-0 flex-1 overflow-y-auto py-1\"\n >\n {results.map((o, i) => {\n const startsGroup = o.group != null && o.group !== results[i - 1]?.group;\n return (\n // Keyed by group AND value, as in the core panel: an option may\n // appear once per group.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {startsGroup && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-[var(--text-muted)] first:pt-1\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n // CHOSEN, never \"highlighted\" — the family's rule. This\n // field holds no value, so the only row that can be chosen\n // is the one whose label the text already is.\n aria-selected={fillOnSelect && o.label === value}\n // Listed, announced as unavailable, never taken: `take`\n // refuses it and the arrows pass it over.\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onClick={() => take(o)}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n \"flex w-full items-center gap-2 px-3 text-start\",\n small ? \"py-1 text-xs\" : \"py-1.5 text-sm\",\n o.disabled\n ? DISABLED_ROW_CLASS\n : i === active\n ? \"bg-[var(--bg-active)]\"\n : \"hover:bg-[var(--bg-hover)]\",\n o.group != null && \"ps-6\",\n )}\n >\n {o.icon && (\n <span aria-hidden className=\"shrink-0 text-[var(--text-muted)]\">\n {o.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-[var(--text-primary)]\">\n {o.label}\n </span>\n {o.sublabel && (\n <span\n className={cn(\n \"block truncate text-[var(--text-placeholder)]\",\n small ? \"text-caption\" : \"text-xs\",\n )}\n >\n {o.sublabel}\n </span>\n )}\n </span>\n </button>\n </li>\n </Fragment>\n );\n })}\n </ul>\n )}\n {statusLine !== null && (\n // Visible, not live — the region above speaks for it.\n <div\n aria-hidden\n className={cn(\n small ? \"px-3 py-1.5 text-xs\" : \"px-3 py-2 text-sm\",\n failed && !hasStatus ? \"text-[var(--danger)]\" : \"text-[var(--text-muted)]\",\n results.length > 0 && \"border-t border-[var(--border)] text-xs\",\n )}\n >\n {statusLine}\n </div>\n )}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n\nconst AutocompleteBase = forwardRef(AutocompleteInner);\nAutocompleteBase.displayName = \"Autocomplete\";\n\n/**\n * An inline, async-capable text field with a suggestion list under it — the\n * WAI-ARIA APG \"editable combobox with list autocomplete\".\n *\n * Two jobs, one control:\n * - **free text** (kastlan's address field): the text is the value, the list only\n * offers completions, and taking one fills the field (`fillOnSelect`, default);\n * - **search-then-act** (keksdose's address search): the text is a query, taking a\n * row calls `onSelect` and nothing is held (`fillOnSelect={false}`).\n *\n * How it differs from its siblings: {@link Combobox} is free text over a STATIC\n * string pool and opens on focus; {@link EntityCombobox} is a trigger holding an id.\n * This one keeps no value of its own, never resets the text, asks nothing below\n * `minChars`, and reports a failed lookup instead of showing stale rows.\n *\n * Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted\n * row, `aria-controls` the list, and a polite live region says what the list now\n * holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter\n * takes the highlighted row (with none,\n * Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay\n * with the caret. An Escape that closes an open list is consumed and never reaches the\n * caller; with the list closed it does. For every other key a caller's `onKeyDown`\n * runs first and may `preventDefault()` to\n * keep a key for itself.\n *\n * The list is portalled and anchored under the field on every screen size: the\n * field already carries the keyboard, and `useAnchoredPanel` keeps the list clear\n * of it on a phone. There is no full-screen sheet as {@link Combobox} has.\n\n * Generic over the option value (`ComboOption<V>`); the ref is the `<input>`'s.\n */\nexport const Autocomplete = AutocompleteBase as <V extends string | number = string>(\n props: AutocompleteProps<V> & RefAttributes<HTMLInputElement>,\n) => ReactElement | null;\n"],"mappings":"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","names":[]}
@@ -1,19 +1,24 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentPropsWithoutRef, ReactNode, Ref, MouseEvent, ReactElement } from 'react';
3
- import { bO as ButtonVariant, bM as ButtonSize } from '../delete-account-setting-FLyJf-kZ.js';
3
+ import { bO as ButtonVariant, bM as ButtonSize } from '../delete-account-setting-DiPX4Vue.js';
4
4
  import './tooltip.js';
5
5
  import '../hooks/use-anchored-rect.js';
6
6
  import '../lib/clipping.js';
7
- import '../data-table-labels-kpAAVi6g.js';
7
+ import '../data-table-labels-BJ5JE_X2.js';
8
8
  import './data-table-sort.js';
9
9
  import './swipeable-row.js';
10
+ import './status-dot.js';
11
+ import './progress-bar.js';
12
+ import './signed-amount.js';
13
+ import './stat-tile.js';
14
+ import './chip.js';
15
+ import 'lucide-react';
16
+ import './field-parts.js';
17
+ import './write-lock.js';
10
18
  import './mini-calendar.js';
11
19
  import './calendar-heatmap.js';
12
20
  import './popover.js';
13
21
  import '../hooks/use-breakpoint.js';
14
- import './chip.js';
15
- import 'lucide-react';
16
- import './field-parts.js';
17
22
  import './field-sync.js';
18
23
  import '../wizard/types.js';
19
24
  import '../tour/tour.js';
@@ -25,7 +30,6 @@ import './page-contents.js';
25
30
  import './series-chart-labels.js';
26
31
  import './pie-chart-labels.js';
27
32
  import './sparkline.js';
28
- import './stat-tile.js';
29
33
  import './signature-pad.js';
30
34
  import './password-strength.js';
31
35
  import './swatch-picker.js';
@@ -37,9 +41,6 @@ import '../hooks/use-search-param-state.js';
37
41
  import './measured-grid.js';
38
42
  import './user-avatar.js';
39
43
  import '../lib/person-name.js';
40
- import './status-dot.js';
41
- import './progress-bar.js';
42
- import './signed-amount.js';
43
44
  import '../shell/topbar-action-menu.js';
44
45
  import './menu-item.js';
45
46
  import './floating-panel.js';
@@ -55,7 +56,6 @@ import './error-boundary.js';
55
56
  import './authed-image.js';
56
57
  import './image-grid.js';
57
58
  import './lightbox.js';
58
- import './write-lock.js';
59
59
  import '../auth/sign-in-form.js';
60
60
  import '../landing/access.js';
61
61
  import '../landing/landing-labels.js';
@@ -73,7 +73,7 @@ function ButtonGroup({
73
73
  }
74
74
  );
75
75
  }
76
- const LINK_CURRENT = "bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]";
76
+ const LINK_CURRENT = "bg-[var(--brand-bg)] text-brand-muted hover:bg-[var(--brand-bg-hover)]";
77
77
  function ButtonGroupLink({
78
78
  href,
79
79
  variant = "secondary",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/button-group.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { buttonClasses, type ButtonSize, type ButtonVariant } from \"./ui\";\nimport { useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\n\n// Every class below is spelled out in full rather than assembled from parts: the\n// consumer's Tailwind build finds classes by scanning this package's source as text,\n// and a class that exists only after a template literal runs is never generated.\n//\n// Each rule is written twice, once for a direct child and once for a `<button>` one\n// level down. The second is for a Tooltip-wrapped IconButton — the map zoom's + and −\n// both carry a tooltip — where the direct child is the tooltip's wrapper span and the\n// button that draws the border and the corners sits inside it. The same again for an\n// `<a>` one level down: a router link member (see {@link ButtonGroupLink}) under a\n// tooltip. A bare link is a direct child and needs nothing extra.\nconst JOINED_COMMON = cn(\n \"inline-flex rounded-md border border-[var(--border)]\",\n // The group draws the frame; each member gives up its own border, radius and shadow.\n \"[&>*]:rounded-none [&>*]:border-0 [&>*]:shadow-none\",\n \"[&>*>button]:rounded-none [&>*>button]:border-0 [&>*>button]:shadow-none\",\n \"[&>*>a]:rounded-none [&>*>a]:border-0 [&>*>a]:shadow-none\",\n \"[&>*]:border-[var(--border)]\",\n // A focused member rises above its neighbours so its ring is not painted over by\n // the next one's background.\n \"[&>*]:relative [&>*:focus-within]:z-10\",\n);\n\n/**\n * A joined row at Large and Extra large wraps rather than run past its container (0.32.1,\n * the 360 px Extra-large sweep; docs/text-size-harmonization.md §4 \"nothing\n * overflows\"): \"Buttons | Chips | Feedback\" was 329 px at 150 %, 67 px past a 360 px\n * phone's card. A wrapped row cannot keep one frame with dividers between members — the\n * second line's first member would carry a divider against the frame and no line would\n * part the rows — so at Large the frame is drawn by the members: each its own 1px\n * border, overlapping its neighbours by a pixel on both axes (the container's `ps-px\n * pt-px` takes the overlap back at the edges). On one row that draws the same frame and\n * dividers as Normal; wrapped, every line and every member is parted by one hairline,\n * and the members share out each line (`grow`), so the lines make one block. A group\n * that fits is only as wide as its members, so nothing grows there. A `large:` class,\n * so Normal keeps the frame it has, and it costs no render.\n */\nconst JOINED_WRAP_AT_LARGE = cn(\n \"large:max-w-full large:flex-wrap large:border-0 large:ps-px large:pt-px\",\n \"large:[&>*]:border large:[&>*]:-ms-px large:[&>*]:-mt-px\",\n \"large:[&>*]:grow large:[&>*>button]:grow large:[&>*>a]:grow\",\n);\n\nconst JOINED: Record<\"horizontal\" | \"vertical\", string> = {\n // Logical edges and corners throughout (`border-s`, `rounded-s-*`), so in RTL the\n // first member sits on the right with the right-hand corners rounded — the physical\n // pair would round the outside of the last button and square off the first.\n horizontal: cn(\n JOINED_COMMON,\n \"flex-row items-stretch\",\n \"[&>*:not(:first-child)]:border-s\",\n \"[&>:first-child]:rounded-s-md [&>:last-child]:rounded-e-md\",\n \"[&>:first-child>button]:rounded-s-md [&>:last-child>button]:rounded-e-md\",\n \"[&>:first-child>a]:rounded-s-md [&>:last-child>a]:rounded-e-md\",\n JOINED_WRAP_AT_LARGE,\n ),\n // The block axis is top-to-bottom in both reading directions, so top/bottom are\n // already the logical edges here.\n vertical: cn(\n JOINED_COMMON,\n \"flex-col items-stretch\",\n \"[&>*:not(:first-child)]:border-t\",\n \"[&>:first-child]:rounded-t-md [&>:last-child]:rounded-b-md\",\n \"[&>:first-child>button]:rounded-t-md [&>:last-child>button]:rounded-b-md\",\n \"[&>:first-child>a]:rounded-t-md [&>:last-child>a]:rounded-b-md\",\n ),\n};\n\n// `gapped`: the members keep their own shape — round overlay discs, each its own\n// border and radius — and sit apart. Nothing is stripped, so there is nothing to\n// restore per edge, and the gap is logical by nature.\nconst GAPPED: Record<\"horizontal\" | \"vertical\", string> = {\n horizontal: \"inline-flex flex-row items-center gap-1\",\n vertical: \"inline-flex flex-col items-center gap-1\",\n};\n\n// `elevated`, joined: the frame itself floats — a filled surface under the members (a\n// ghost member would otherwise show the content through it) and the shadow of a\n// control lifted over the page.\nconst ELEVATED_JOINED = \"bg-[var(--bg-surface)] shadow-lg\";\n// `elevated`, gapped: there is no frame, so each disc carries the shadow. Aimed at the\n// control, not the tooltip's square wrapper span, whose shadow would be a box round a\n// round disc.\nconst ELEVATED_GAPPED =\n \"[&>button]:shadow-md [&>a]:shadow-md [&>*>button]:shadow-md [&>*>a]:shadow-md\";\n\nexport interface ButtonGroupProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"role\"> {\n /** `horizontal` (default): side by side, in reading order. `vertical`: stacked —\n * the map's zoom + over −. */\n orientation?: \"horizontal\" | \"vertical\";\n /**\n * `joined` (default): one frame, hairline dividers, outer corners rounded. `gapped`:\n * separate controls with a small gap, each keeping its own shape — the overlay\n * discs of keksdose's receipt preview zoom (scan-file-preview.tsx:362, the − and +\n * `IconButton variant=\"overlay\"` in a hand-made `flex gap-1`), where a joined frame\n * would be a box drawn over the photo.\n */\n variant?: \"joined\" | \"gapped\";\n /**\n * Float it: a surface with a shadow, for a toolbar that sits OVER content (a map's\n * zoom, a photo's controls) rather than in the page's flow — keksdose adds\n * `shadow-sm` to each disc on the receipt preview by hand. On a joined group the\n * frame is filled and shadowed; on a gapped one, each member.\n */\n elevated?: boolean;\n}\n\n/**\n * Adjacent `Button`s or `IconButton`s drawn as one joined control: a single frame,\n * hairline dividers between the members, rounded only at the outer corners.\n *\n * keksdose asked for it twice — \"collapse all / expand all\" above its category\n * tree, and the + and − of its map zoom — both hand-built with negative margins and\n * physical `rounded-l` / `rounded-r`, which came out inside-out under `dir=\"rtl\"`.\n *\n * `role=\"group\"` with a name: a reader then introduces the pair as \"Zoom, group\"\n * before reading \"Zoom in\", which is what tells a listener that the next button\n * belongs with this one. Pass `aria-label` (or `aria-labelledby`); a group with no\n * name is one more unlabelled wrapper. It is NOT a toolbar — it adds no arrow-key\n * roving, and every member keeps its own tab stop, as plain adjacent buttons do.\n * For one-of-many selection use `ToggleGroup`, which has the radio semantics.\n */\nexport function ButtonGroup({\n orientation = \"horizontal\",\n variant = \"joined\",\n elevated = false,\n className,\n ...rest\n}: ButtonGroupProps) {\n const joined = variant === \"joined\";\n return (\n <div\n {...rest}\n role=\"group\"\n data-orientation={orientation}\n data-variant={variant}\n className={cn(\n joined ? JOINED[orientation] : GAPPED[orientation],\n elevated && (joined ? ELEVATED_JOINED : ELEVATED_GAPPED),\n className,\n )}\n />\n );\n}\n\n/** What {@link ButtonGroupLinkProps.renderLink} is handed. Spread it onto your router's\n * link, mapping `href` — `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface ButtonGroupLinkRenderProps {\n href: string;\n /** The button look — keep it, or the member is a bare link in the frame. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n \"aria-current\"?: \"page\";\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nexport interface ButtonGroupLinkProps extends Omit<ComponentPropsWithoutRef<\"a\">, \"href\" | \"className\" | \"children\"> {\n href: string;\n /** The {@link Button} variant it looks like. Default `secondary`, the usual member. */\n variant?: ButtonVariant;\n size?: ButtonSize;\n /** The member for the page you are on: `aria-current=\"page\"` and the \"on\" look of a\n * pressed member, so a link row reads as a segmented switch between pages. */\n current?: boolean;\n /** Render through your router's `Link` — see {@link ButtonGroupLinkRenderProps}.\n * Default: the `<UiKitProvider linkComponent>`, then `<a>`. */\n renderLink?: (props: ButtonGroupLinkRenderProps) => ReactElement;\n className?: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n}\n\nconst LINK_CURRENT = \"bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]\";\n\n/**\n * A {@link ButtonGroup} member that navigates: a link drawn as a {@link Button}, so it\n * joins the frame like one. A group mixing actions and pages — \"Export\" beside \"Open\n * report\" — had to either make the page a button with an `onClick` navigate (no\n * middle-click, no \"open in new tab\", no link semantics) or style an `<a>` whose\n * corners the group did not know about. Plain `<a>` by default; `renderLink` for the\n * router's `Link`, the kit's usual shape (`Chip`, `MenuItem`, `StatTile`).\n */\nexport function ButtonGroupLink({\n href,\n variant = \"secondary\",\n size,\n current = false,\n renderLink,\n className,\n children,\n ...rest\n}: ButtonGroupLinkProps) {\n const kitLink = useKitLink();\n const props: ButtonGroupLinkRenderProps = {\n ...rest,\n href,\n \"aria-current\": current ? \"page\" : undefined,\n className: buttonClasses(variant, { size, className: cn(current && LINK_CURRENT, className) }),\n children,\n };\n const render = pickLinkRenderer(renderLink, kitLink, href);\n if (render) return <RenderedGroupLink render={render} {...props} />;\n const { children: content, ...anchor } = props;\n return <a {...anchor}>{content}</a>;\n}\n\n/** Calls `renderLink` as a component, so the router link's hooks are its own. */\nfunction RenderedGroupLink({\n render,\n ...props\n}: ButtonGroupLinkRenderProps & { render: (props: ButtonGroupLinkRenderProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AAwII;AAvIJ,SAAS,UAAU;AACnB,SAAS,qBAA0D;AACnE,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AAYjC,MAAM,gBAAgB;AAAA,EACpB;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AACF;AAgBA,MAAM,uBAAuB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,SAAoD;AAAA;AAAA;AAAA;AAAA,EAIxD,YAAY;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA;AAAA;AAAA,EAGA,UAAU;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAKA,MAAM,SAAoD;AAAA,EACxD,YAAY;AAAA,EACZ,UAAU;AACZ;AAKA,MAAM,kBAAkB;AAIxB,MAAM,kBACJ;AAsCK,SAAS,YAAY;AAAA,EAC1B,cAAc;AAAA,EACd,UAAU;AAAA,EACV,WAAW;AAAA,EACX;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,SAAS,YAAY;AAC3B,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,oBAAkB;AAAA,MAClB,gBAAc;AAAA,MACd,WAAW;AAAA,QACT,SAAS,OAAO,WAAW,IAAI,OAAO,WAAW;AAAA,QACjD,aAAa,SAAS,kBAAkB;AAAA,QACxC;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;AAgCA,MAAM,eAAe;AAUd,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,QAAM,UAAU,WAAW;AAC3B,QAAM,QAAoC;AAAA,IACxC,GAAG;AAAA,IACH;AAAA,IACA,gBAAgB,UAAU,SAAS;AAAA,IACnC,WAAW,cAAc,SAAS,EAAE,MAAM,WAAW,GAAG,WAAW,cAAc,SAAS,EAAE,CAAC;AAAA,IAC7F;AAAA,EACF;AACA,QAAM,SAAS,iBAAiB,YAAY,SAAS,IAAI;AACzD,MAAI,OAAQ,QAAO,oBAAC,qBAAkB,QAAiB,GAAG,OAAO;AACjE,QAAM,EAAE,UAAU,SAAS,GAAG,OAAO,IAAI;AACzC,SAAO,oBAAC,OAAG,GAAG,QAAS,mBAAQ;AACjC;AAGA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA,GAAG;AACL,GAAiG;AAC/F,SAAO,OAAO,KAAK;AACrB;","names":[]}
1
+ {"version":3,"sources":["../../src/components/button-group.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { buttonClasses, type ButtonSize, type ButtonVariant } from \"./ui\";\nimport { useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\n\n// Every class below is spelled out in full rather than assembled from parts: the\n// consumer's Tailwind build finds classes by scanning this package's source as text,\n// and a class that exists only after a template literal runs is never generated.\n//\n// Each rule is written twice, once for a direct child and once for a `<button>` one\n// level down. The second is for a Tooltip-wrapped IconButton — the map zoom's + and −\n// both carry a tooltip — where the direct child is the tooltip's wrapper span and the\n// button that draws the border and the corners sits inside it. The same again for an\n// `<a>` one level down: a router link member (see {@link ButtonGroupLink}) under a\n// tooltip. A bare link is a direct child and needs nothing extra.\nconst JOINED_COMMON = cn(\n \"inline-flex rounded-md border border-[var(--border)]\",\n // The group draws the frame; each member gives up its own border, radius and shadow.\n \"[&>*]:rounded-none [&>*]:border-0 [&>*]:shadow-none\",\n \"[&>*>button]:rounded-none [&>*>button]:border-0 [&>*>button]:shadow-none\",\n \"[&>*>a]:rounded-none [&>*>a]:border-0 [&>*>a]:shadow-none\",\n \"[&>*]:border-[var(--border)]\",\n // A focused member rises above its neighbours so its ring is not painted over by\n // the next one's background.\n \"[&>*]:relative [&>*:focus-within]:z-10\",\n);\n\n/**\n * A joined row at Large and Extra large wraps rather than run past its container (0.32.1,\n * the 360 px Extra-large sweep; docs/text-size-harmonization.md §4 \"nothing\n * overflows\"): \"Buttons | Chips | Feedback\" was 329 px at 150 %, 67 px past a 360 px\n * phone's card. A wrapped row cannot keep one frame with dividers between members — the\n * second line's first member would carry a divider against the frame and no line would\n * part the rows — so at Large the frame is drawn by the members: each its own 1px\n * border, overlapping its neighbours by a pixel on both axes (the container's `ps-px\n * pt-px` takes the overlap back at the edges). On one row that draws the same frame and\n * dividers as Normal; wrapped, every line and every member is parted by one hairline,\n * and the members share out each line (`grow`), so the lines make one block. A group\n * that fits is only as wide as its members, so nothing grows there. A `large:` class,\n * so Normal keeps the frame it has, and it costs no render.\n */\nconst JOINED_WRAP_AT_LARGE = cn(\n \"large:max-w-full large:flex-wrap large:border-0 large:ps-px large:pt-px\",\n \"large:[&>*]:border large:[&>*]:-ms-px large:[&>*]:-mt-px\",\n \"large:[&>*]:grow large:[&>*>button]:grow large:[&>*>a]:grow\",\n);\n\nconst JOINED: Record<\"horizontal\" | \"vertical\", string> = {\n // Logical edges and corners throughout (`border-s`, `rounded-s-*`), so in RTL the\n // first member sits on the right with the right-hand corners rounded — the physical\n // pair would round the outside of the last button and square off the first.\n horizontal: cn(\n JOINED_COMMON,\n \"flex-row items-stretch\",\n \"[&>*:not(:first-child)]:border-s\",\n \"[&>:first-child]:rounded-s-md [&>:last-child]:rounded-e-md\",\n \"[&>:first-child>button]:rounded-s-md [&>:last-child>button]:rounded-e-md\",\n \"[&>:first-child>a]:rounded-s-md [&>:last-child>a]:rounded-e-md\",\n JOINED_WRAP_AT_LARGE,\n ),\n // The block axis is top-to-bottom in both reading directions, so top/bottom are\n // already the logical edges here.\n vertical: cn(\n JOINED_COMMON,\n \"flex-col items-stretch\",\n \"[&>*:not(:first-child)]:border-t\",\n \"[&>:first-child]:rounded-t-md [&>:last-child]:rounded-b-md\",\n \"[&>:first-child>button]:rounded-t-md [&>:last-child>button]:rounded-b-md\",\n \"[&>:first-child>a]:rounded-t-md [&>:last-child>a]:rounded-b-md\",\n ),\n};\n\n// `gapped`: the members keep their own shape — round overlay discs, each its own\n// border and radius — and sit apart. Nothing is stripped, so there is nothing to\n// restore per edge, and the gap is logical by nature.\nconst GAPPED: Record<\"horizontal\" | \"vertical\", string> = {\n horizontal: \"inline-flex flex-row items-center gap-1\",\n vertical: \"inline-flex flex-col items-center gap-1\",\n};\n\n// `elevated`, joined: the frame itself floats — a filled surface under the members (a\n// ghost member would otherwise show the content through it) and the shadow of a\n// control lifted over the page.\nconst ELEVATED_JOINED = \"bg-[var(--bg-surface)] shadow-lg\";\n// `elevated`, gapped: there is no frame, so each disc carries the shadow. Aimed at the\n// control, not the tooltip's square wrapper span, whose shadow would be a box round a\n// round disc.\nconst ELEVATED_GAPPED =\n \"[&>button]:shadow-md [&>a]:shadow-md [&>*>button]:shadow-md [&>*>a]:shadow-md\";\n\nexport interface ButtonGroupProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"role\"> {\n /** `horizontal` (default): side by side, in reading order. `vertical`: stacked —\n * the map's zoom + over −. */\n orientation?: \"horizontal\" | \"vertical\";\n /**\n * `joined` (default): one frame, hairline dividers, outer corners rounded. `gapped`:\n * separate controls with a small gap, each keeping its own shape — the overlay\n * discs of keksdose's receipt preview zoom (scan-file-preview.tsx:362, the − and +\n * `IconButton variant=\"overlay\"` in a hand-made `flex gap-1`), where a joined frame\n * would be a box drawn over the photo.\n */\n variant?: \"joined\" | \"gapped\";\n /**\n * Float it: a surface with a shadow, for a toolbar that sits OVER content (a map's\n * zoom, a photo's controls) rather than in the page's flow — keksdose adds\n * `shadow-sm` to each disc on the receipt preview by hand. On a joined group the\n * frame is filled and shadowed; on a gapped one, each member.\n */\n elevated?: boolean;\n}\n\n/**\n * Adjacent `Button`s or `IconButton`s drawn as one joined control: a single frame,\n * hairline dividers between the members, rounded only at the outer corners.\n *\n * keksdose asked for it twice — \"collapse all / expand all\" above its category\n * tree, and the + and − of its map zoom — both hand-built with negative margins and\n * physical `rounded-l` / `rounded-r`, which came out inside-out under `dir=\"rtl\"`.\n *\n * `role=\"group\"` with a name: a reader then introduces the pair as \"Zoom, group\"\n * before reading \"Zoom in\", which is what tells a listener that the next button\n * belongs with this one. Pass `aria-label` (or `aria-labelledby`); a group with no\n * name is one more unlabelled wrapper. It is NOT a toolbar — it adds no arrow-key\n * roving, and every member keeps its own tab stop, as plain adjacent buttons do.\n * For one-of-many selection use `ToggleGroup`, which has the radio semantics.\n */\nexport function ButtonGroup({\n orientation = \"horizontal\",\n variant = \"joined\",\n elevated = false,\n className,\n ...rest\n}: ButtonGroupProps) {\n const joined = variant === \"joined\";\n return (\n <div\n {...rest}\n role=\"group\"\n data-orientation={orientation}\n data-variant={variant}\n className={cn(\n joined ? JOINED[orientation] : GAPPED[orientation],\n elevated && (joined ? ELEVATED_JOINED : ELEVATED_GAPPED),\n className,\n )}\n />\n );\n}\n\n/** What {@link ButtonGroupLinkProps.renderLink} is handed. Spread it onto your router's\n * link, mapping `href` — `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface ButtonGroupLinkRenderProps {\n href: string;\n /** The button look — keep it, or the member is a bare link in the frame. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n \"aria-current\"?: \"page\";\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nexport interface ButtonGroupLinkProps extends Omit<ComponentPropsWithoutRef<\"a\">, \"href\" | \"className\" | \"children\"> {\n href: string;\n /** The {@link Button} variant it looks like. Default `secondary`, the usual member. */\n variant?: ButtonVariant;\n size?: ButtonSize;\n /** The member for the page you are on: `aria-current=\"page\"` and the \"on\" look of a\n * pressed member, so a link row reads as a segmented switch between pages. */\n current?: boolean;\n /** Render through your router's `Link` — see {@link ButtonGroupLinkRenderProps}.\n * Default: the `<UiKitProvider linkComponent>`, then `<a>`. */\n renderLink?: (props: ButtonGroupLinkRenderProps) => ReactElement;\n className?: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n}\n\n// The brand AS TEXT is `--brand-muted` (0.33, docs/colour-roles-harmonization.md §5.5):\n// `--brand` itself is solved to 3:1 and read 2.97:1 on its own wash in the derived dark\n// presets; the muted form holds 4.5:1 on it in every preset.\nconst LINK_CURRENT = \"bg-[var(--brand-bg)] text-brand-muted hover:bg-[var(--brand-bg-hover)]\";\n\n/**\n * A {@link ButtonGroup} member that navigates: a link drawn as a {@link Button}, so it\n * joins the frame like one. A group mixing actions and pages — \"Export\" beside \"Open\n * report\" — had to either make the page a button with an `onClick` navigate (no\n * middle-click, no \"open in new tab\", no link semantics) or style an `<a>` whose\n * corners the group did not know about. Plain `<a>` by default; `renderLink` for the\n * router's `Link`, the kit's usual shape (`Chip`, `MenuItem`, `StatTile`).\n */\nexport function ButtonGroupLink({\n href,\n variant = \"secondary\",\n size,\n current = false,\n renderLink,\n className,\n children,\n ...rest\n}: ButtonGroupLinkProps) {\n const kitLink = useKitLink();\n const props: ButtonGroupLinkRenderProps = {\n ...rest,\n href,\n \"aria-current\": current ? \"page\" : undefined,\n className: buttonClasses(variant, { size, className: cn(current && LINK_CURRENT, className) }),\n children,\n };\n const render = pickLinkRenderer(renderLink, kitLink, href);\n if (render) return <RenderedGroupLink render={render} {...props} />;\n const { children: content, ...anchor } = props;\n return <a {...anchor}>{content}</a>;\n}\n\n/** Calls `renderLink` as a component, so the router link's hooks are its own. */\nfunction RenderedGroupLink({\n render,\n ...props\n}: ButtonGroupLinkRenderProps & { render: (props: ButtonGroupLinkRenderProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AAwII;AAvIJ,SAAS,UAAU;AACnB,SAAS,qBAA0D;AACnE,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AAYjC,MAAM,gBAAgB;AAAA,EACpB;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AACF;AAgBA,MAAM,uBAAuB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,SAAoD;AAAA;AAAA;AAAA;AAAA,EAIxD,YAAY;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA;AAAA;AAAA,EAGA,UAAU;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAKA,MAAM,SAAoD;AAAA,EACxD,YAAY;AAAA,EACZ,UAAU;AACZ;AAKA,MAAM,kBAAkB;AAIxB,MAAM,kBACJ;AAsCK,SAAS,YAAY;AAAA,EAC1B,cAAc;AAAA,EACd,UAAU;AAAA,EACV,WAAW;AAAA,EACX;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,SAAS,YAAY;AAC3B,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,oBAAkB;AAAA,MAClB,gBAAc;AAAA,MACd,WAAW;AAAA,QACT,SAAS,OAAO,WAAW,IAAI,OAAO,WAAW;AAAA,QACjD,aAAa,SAAS,kBAAkB;AAAA,QACxC;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;AAmCA,MAAM,eAAe;AAUd,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,QAAM,UAAU,WAAW;AAC3B,QAAM,QAAoC;AAAA,IACxC,GAAG;AAAA,IACH;AAAA,IACA,gBAAgB,UAAU,SAAS;AAAA,IACnC,WAAW,cAAc,SAAS,EAAE,MAAM,WAAW,GAAG,WAAW,cAAc,SAAS,EAAE,CAAC;AAAA,IAC7F;AAAA,EACF;AACA,QAAM,SAAS,iBAAiB,YAAY,SAAS,IAAI;AACzD,MAAI,OAAQ,QAAO,oBAAC,qBAAkB,QAAiB,GAAG,OAAO;AACjE,QAAM,EAAE,UAAU,SAAS,GAAG,OAAO,IAAI;AACzC,SAAO,oBAAC,OAAG,GAAG,QAAS,mBAAQ;AACjC;AAGA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA,GAAG;AACL,GAAiG;AAC/F,SAAO,OAAO,KAAK;AACrB;","names":[]}