@eifi1/ui-kit 0.32.2 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. package/README.md +17 -9
  2. package/dist/account/delete-account-setting.d.ts +11 -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 +11 -10
  12. package/dist/admin/admin-action-confirm.js.map +1 -1
  13. package/dist/admin/invitations-panel.d.ts +11 -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 +101 -0
  17. package/dist/admin/plan-change.js +210 -0
  18. package/dist/admin/plan-change.js.map +1 -0
  19. package/dist/admin/reviewer-scope-editor.d.ts +11 -10
  20. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  21. package/dist/admin/role-select.d.ts +8 -7
  22. package/dist/admin/role-select.js.map +1 -1
  23. package/dist/admin/transfer-ownership-dialog.d.ts +11 -10
  24. package/dist/admin/user-roster.d.ts +8 -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 +11 -10
  34. package/dist/billing/billing-banner.d.ts +14 -11
  35. package/dist/billing/billing-banner.js.map +1 -1
  36. package/dist/billing/billing-labels.d.ts +11 -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 +40 -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 +155 -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 +11 -10
  46. package/dist/billing/checkout-processing.d.ts +218 -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 +215 -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 +11 -10
  56. package/dist/billing/plan-limit-notice.d.ts +11 -10
  57. package/dist/billing/plan-limit-toast.d.ts +144 -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 +39 -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 +10 -9
  64. package/dist/bin/eifi1-pay-page.mjs +166 -0
  65. package/dist/chart.d.ts +11 -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 +11 -10
  71. package/dist/components/autocomplete.d.ts +2 -1
  72. package/dist/components/autocomplete.js +5 -3
  73. package/dist/components/autocomplete.js.map +1 -1
  74. package/dist/components/button-group.d.ts +11 -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 +11 -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-core.js +2 -1
  91. package/dist/components/combobox-core.js.map +1 -1
  92. package/dist/components/combobox.d.ts +4 -3
  93. package/dist/components/combobox.js +2 -2
  94. package/dist/components/combobox.js.map +1 -1
  95. package/dist/components/confirm-dialog.d.ts +11 -10
  96. package/dist/components/confirm-dialog.js +6 -3
  97. package/dist/components/confirm-dialog.js.map +1 -1
  98. package/dist/components/copy-button.d.ts +12 -11
  99. package/dist/components/country-select.d.ts +2 -1
  100. package/dist/components/country-select.js.map +1 -1
  101. package/dist/components/danger-confirm.d.ts +11 -10
  102. package/dist/components/danger-confirm.js.map +1 -1
  103. package/dist/components/data-table-cells.d.ts +12 -1
  104. package/dist/components/data-table-filter-popover.d.ts +12 -1
  105. package/dist/components/data-table-filters.d.ts +12 -1
  106. package/dist/components/data-table-labels.d.ts +12 -1
  107. package/dist/components/data-table-pagination.d.ts +12 -1
  108. package/dist/components/data-table.d.ts +12 -1
  109. package/dist/components/data-table.js +3 -3
  110. package/dist/components/data-table.js.map +1 -1
  111. package/dist/components/date-picker.js +6 -4
  112. package/dist/components/date-picker.js.map +1 -1
  113. package/dist/components/dropdown.js +9 -1
  114. package/dist/components/dropdown.js.map +1 -1
  115. package/dist/components/entity-combobox.d.ts +3 -2
  116. package/dist/components/entity-combobox.js.map +1 -1
  117. package/dist/components/facing-pair.d.ts +11 -10
  118. package/dist/components/field-parts.d.ts +32 -4
  119. package/dist/components/field-parts.js +54 -19
  120. package/dist/components/field-parts.js.map +1 -1
  121. package/dist/components/file-button.d.ts +11 -10
  122. package/dist/components/file-button.js +3 -2
  123. package/dist/components/file-button.js.map +1 -1
  124. package/dist/components/file-dropzone.d.ts +12 -11
  125. package/dist/components/file-dropzone.js.map +1 -1
  126. package/dist/components/floating-panel.js +5 -5
  127. package/dist/components/floating-panel.js.map +1 -1
  128. package/dist/components/form-actions.d.ts +10 -9
  129. package/dist/components/form-actions.js +3 -3
  130. package/dist/components/form-actions.js.map +1 -1
  131. package/dist/components/full-bleed-dialog.js.map +1 -1
  132. package/dist/components/iban-input.d.ts +11 -10
  133. package/dist/components/inline-edit-field.d.ts +2 -1
  134. package/dist/components/inline-edit-field.js +6 -6
  135. package/dist/components/inline-edit-field.js.map +1 -1
  136. package/dist/components/language-select.d.ts +11 -10
  137. package/dist/components/legal-page.d.ts +11 -10
  138. package/dist/components/legal.d.ts +11 -10
  139. package/dist/components/list.d.ts +2 -1
  140. package/dist/components/list.js +4 -5
  141. package/dist/components/list.js.map +1 -1
  142. package/dist/components/modal.js.map +1 -1
  143. package/dist/components/money-field.d.ts +11 -10
  144. package/dist/components/month-picker.js +54 -44
  145. package/dist/components/month-picker.js.map +1 -1
  146. package/dist/components/multi-entity-combobox.d.ts +3 -2
  147. package/dist/components/multi-entity-combobox.js.map +1 -1
  148. package/dist/components/number-field.d.ts +18 -10
  149. package/dist/components/number-field.js.map +1 -1
  150. package/dist/components/number-input.d.ts +37 -10
  151. package/dist/components/number-input.js +27 -12
  152. package/dist/components/number-input.js.map +1 -1
  153. package/dist/components/numpad-sheet.d.ts +11 -10
  154. package/dist/components/numpad-sheet.js +2 -2
  155. package/dist/components/numpad-sheet.js.map +1 -1
  156. package/dist/components/phone-input.d.ts +11 -10
  157. package/dist/components/popover.js +6 -2
  158. package/dist/components/popover.js.map +1 -1
  159. package/dist/components/row-actions.d.ts +6 -5
  160. package/dist/components/row-actions.js +130 -32
  161. package/dist/components/row-actions.js.map +1 -1
  162. package/dist/components/series-chart.d.ts +11 -10
  163. package/dist/components/settings-fields.d.ts +11 -10
  164. package/dist/components/share-card.d.ts +8 -7
  165. package/dist/components/share-card.js +26 -10
  166. package/dist/components/share-card.js.map +1 -1
  167. package/dist/components/skeleton.d.ts +10 -2
  168. package/dist/components/skeleton.js.map +1 -1
  169. package/dist/components/status-dot.d.ts +1 -0
  170. package/dist/components/status-dot.js +2 -2
  171. package/dist/components/status-dot.js.map +1 -1
  172. package/dist/components/swatch-picker.d.ts +2 -1
  173. package/dist/components/swatch-picker.js.map +1 -1
  174. package/dist/components/swipeable-row.d.ts +88 -17
  175. package/dist/components/swipeable-row.js +46 -9
  176. package/dist/components/swipeable-row.js.map +1 -1
  177. package/dist/components/switch.d.ts +2 -1
  178. package/dist/components/switch.js.map +1 -1
  179. package/dist/components/table.d.ts +2 -1
  180. package/dist/components/table.js +6 -0
  181. package/dist/components/table.js.map +1 -1
  182. package/dist/components/text-link.d.ts +10 -9
  183. package/dist/components/time-input.d.ts +11 -10
  184. package/dist/components/toggle-group.d.ts +2 -1
  185. package/dist/components/toggle-group.js +1 -1
  186. package/dist/components/toggle-group.js.map +1 -1
  187. package/dist/components/translation-review-editor.d.ts +4 -0
  188. package/dist/components/translation-review-editor.js +4 -3
  189. package/dist/components/translation-review-editor.js.map +1 -1
  190. package/dist/components/translation-review-parts.d.ts +1 -0
  191. package/dist/components/translation-review.d.ts +1 -0
  192. package/dist/components/translation-review.js +15 -18
  193. package/dist/components/translation-review.js.map +1 -1
  194. package/dist/components/ui.d.ts +10 -9
  195. package/dist/components/ui.js +47 -27
  196. package/dist/components/ui.js.map +1 -1
  197. package/dist/components/use-table-state.d.ts +12 -1
  198. package/dist/components/user-avatar.d.ts +1 -0
  199. package/dist/components/write-lock.d.ts +101 -13
  200. package/dist/components/write-lock.js +37 -13
  201. package/dist/components/write-lock.js.map +1 -1
  202. package/dist/{data-table-labels-kpAAVi6g.d.ts → data-table-labels-BJ5JE_X2.d.ts} +17 -0
  203. package/dist/data-table.d.ts +12 -1
  204. package/dist/{delete-account-setting-Dy4kJgUm.d.ts → delete-account-setting-DAgGBrbn.d.ts} +393 -42
  205. package/dist/feedback/feedback-attachment.d.ts +11 -10
  206. package/dist/feedback/feedback-attachment.js +7 -7
  207. package/dist/feedback/feedback-attachment.js.map +1 -1
  208. package/dist/feedback/feedback-context.d.ts +11 -10
  209. package/dist/feedback/feedback-crash.d.ts +11 -10
  210. package/dist/feedback/feedback-dialog.d.ts +11 -10
  211. package/dist/feedback/feedback-inbox.d.ts +10 -9
  212. package/dist/feedback/feedback-inbox.js +7 -3
  213. package/dist/feedback/feedback-inbox.js.map +1 -1
  214. package/dist/feedback/feedback-labels.d.ts +11 -10
  215. package/dist/feedback/feedback-menu.d.ts +4 -3
  216. package/dist/feedback/feedback-record.d.ts +11 -10
  217. package/dist/feedback/feedback-row-detail.d.ts +11 -10
  218. package/dist/feedback/feedback-row-detail.js.map +1 -1
  219. package/dist/feedback/feedback-status-undo.d.ts +11 -10
  220. package/dist/feedback/feedback-submit.d.ts +11 -10
  221. package/dist/feedback/feedback-swipe.d.ts +11 -10
  222. package/dist/feedback/feedback-swipe.js +3 -6
  223. package/dist/feedback/feedback-swipe.js.map +1 -1
  224. package/dist/feedback/feedback-table.d.ts +11 -10
  225. package/dist/feedback/feedback-table.js.map +1 -1
  226. package/dist/feedback/feedback-thread.d.ts +11 -10
  227. package/dist/feedback/feedback-thread.js +5 -1
  228. package/dist/feedback/feedback-thread.js.map +1 -1
  229. package/dist/feedback.d.ts +11 -10
  230. package/dist/hooks/use-anchored-panel.d.ts +37 -7
  231. package/dist/hooks/use-anchored-panel.js +6 -7
  232. package/dist/hooks/use-anchored-panel.js.map +1 -1
  233. package/dist/hooks/use-file-drop.d.ts +11 -10
  234. package/dist/hooks/use-focus-trap.d.ts +8 -0
  235. package/dist/hooks/use-focus-trap.js +17 -11
  236. package/dist/hooks/use-focus-trap.js.map +1 -1
  237. package/dist/i18n/defaults.d.ts +11 -10
  238. package/dist/i18n/defaults.js +2 -0
  239. package/dist/i18n/defaults.js.map +1 -1
  240. package/dist/i18n/german.d.ts +11 -10
  241. package/dist/i18n/german.js +37 -1
  242. package/dist/i18n/german.js.map +1 -1
  243. package/dist/i18n/kit-labels.d.ts +5 -4
  244. package/dist/i18n/kit-labels.js.map +1 -1
  245. package/dist/i18n/languages.d.ts +11 -10
  246. package/dist/i18n/locales/de-CH.d.ts +11 -10
  247. package/dist/i18n/locales/en.d.ts +11 -10
  248. package/dist/i18n/locales/es.d.ts +11 -10
  249. package/dist/i18n/locales/es.js +35 -1
  250. package/dist/i18n/locales/es.js.map +1 -1
  251. package/dist/i18n/locales/fr.d.ts +11 -10
  252. package/dist/i18n/locales/fr.js +37 -1
  253. package/dist/i18n/locales/fr.js.map +1 -1
  254. package/dist/i18n/locales/hu.d.ts +11 -10
  255. package/dist/i18n/locales/hu.js +38 -1
  256. package/dist/i18n/locales/hu.js.map +1 -1
  257. package/dist/i18n/locales/it.d.ts +11 -10
  258. package/dist/i18n/locales/it.js +35 -1
  259. package/dist/i18n/locales/it.js.map +1 -1
  260. package/dist/i18n/locales/zh.d.ts +11 -10
  261. package/dist/i18n/locales/zh.js +35 -1
  262. package/dist/i18n/locales/zh.js.map +1 -1
  263. package/dist/i18n/plurals.d.ts +20 -1
  264. package/dist/i18n/plurals.js +11 -0
  265. package/dist/i18n/plurals.js.map +1 -1
  266. package/dist/i18n/review.d.ts +11 -10
  267. package/dist/i18n/review.js +8 -1
  268. package/dist/i18n/review.js.map +1 -1
  269. package/dist/index.d.ts +11 -6
  270. package/dist/index.js +40 -2
  271. package/dist/index.js.map +1 -1
  272. package/dist/landing/landing-actions.d.ts +11 -10
  273. package/dist/landing/landing-sections.d.ts +11 -10
  274. package/dist/landing/public-header.d.ts +11 -10
  275. package/dist/landing/routing.d.ts +11 -10
  276. package/dist/pay/.well-known/apple-developer-merchantid-domain-association +1 -0
  277. package/dist/pay/index.html +30 -0
  278. package/dist/pay/pay.css +126 -0
  279. package/dist/pay/pay.js +198 -0
  280. package/dist/rhf/fields.d.ts +11 -10
  281. package/dist/rhf/form.d.ts +11 -10
  282. package/dist/rhf.d.ts +11 -10
  283. package/dist/settings/settings-section.d.ts +11 -10
  284. package/dist/shell/app-shell.d.ts +10 -9
  285. package/dist/shell/top-bar-brand.d.ts +11 -10
  286. package/dist/shell/topbar-action-menu.d.ts +1 -0
  287. package/dist/shell.d.ts +11 -10
  288. package/dist/testing/css-variables.d.ts +74 -0
  289. package/dist/testing/css-variables.generated.d.ts +7 -0
  290. package/dist/testing/css-variables.generated.js +8 -0
  291. package/dist/testing/css-variables.generated.js.map +1 -0
  292. package/dist/testing/css-variables.js +93 -0
  293. package/dist/testing/css-variables.js.map +1 -0
  294. package/dist/testing/index.d.ts +1 -0
  295. package/dist/testing/index.js +12 -0
  296. package/dist/testing/index.js.map +1 -0
  297. package/dist/theme/contrast-tokens.d.ts +44 -1
  298. package/dist/theme/contrast-tokens.js +21 -0
  299. package/dist/theme/contrast-tokens.js.map +1 -1
  300. package/dist/theme/contrast.d.ts +2 -2
  301. package/dist/theme/palette-derive.js +60 -11
  302. package/dist/theme/palette-derive.js.map +1 -1
  303. package/dist/theme/palette-presets.js +45 -43
  304. package/dist/theme/palette-presets.js.map +1 -1
  305. package/dist/wizard/stepper-nav.d.ts +12 -11
  306. package/dist/wizard/stepper-nav.js.map +1 -1
  307. package/dist/wizard.d.ts +11 -10
  308. package/package.json +10 -2
  309. package/src/account/delete-account-setting.tsx +6 -1
  310. package/src/account/email-change-setting.tsx +9 -3
  311. package/src/account/sessions-setting.tsx +7 -2
  312. package/src/admin/admin-action-confirm.tsx +2 -1
  313. package/src/admin/invitations-panel.tsx +9 -3
  314. package/src/admin/plan-change.tsx +487 -0
  315. package/src/admin/reviewer-scope-editor.tsx +2 -1
  316. package/src/admin/role-select.tsx +2 -1
  317. package/src/admin/user-roster.tsx +5 -3
  318. package/src/auth/auth-errors.ts +25 -2
  319. package/src/auth/complete-name-dialog.tsx +6 -2
  320. package/src/billing/billing-banner.tsx +3 -1
  321. package/src/billing/billing-labels.ts +45 -0
  322. package/src/billing/billing-lock.ts +36 -3
  323. package/src/billing/billing-overview.ts +58 -0
  324. package/src/billing/checkout-processing.ts +391 -0
  325. package/src/billing/checkout-return.ts +90 -0
  326. package/src/billing/pay-page.ts +246 -0
  327. package/src/billing/plan-limit-toast.tsx +91 -0
  328. package/src/billing/subscription-actions.tsx +41 -8
  329. package/src/components/account-settings.tsx +6 -2
  330. package/src/components/autocomplete.tsx +10 -4
  331. package/src/components/button-group.tsx +4 -1
  332. package/src/components/calculator.tsx +10 -6
  333. package/src/components/checkbox-group.tsx +2 -2
  334. package/src/components/checkbox.tsx +2 -1
  335. package/src/components/chip.tsx +37 -32
  336. package/src/components/choice-card.tsx +4 -2
  337. package/src/components/combobox-core.tsx +5 -2
  338. package/src/components/combobox.tsx +8 -6
  339. package/src/components/confirm-dialog.tsx +13 -6
  340. package/src/components/country-select.tsx +2 -2
  341. package/src/components/danger-confirm.tsx +2 -2
  342. package/src/components/data-table.tsx +20 -9
  343. package/src/components/date-picker.tsx +10 -7
  344. package/src/components/dropdown.tsx +12 -2
  345. package/src/components/entity-combobox.tsx +3 -3
  346. package/src/components/field-parts.tsx +61 -14
  347. package/src/components/file-button.tsx +5 -4
  348. package/src/components/file-dropzone.tsx +2 -2
  349. package/src/components/floating-panel.tsx +7 -5
  350. package/src/components/form-actions.tsx +12 -5
  351. package/src/components/full-bleed-dialog.tsx +6 -0
  352. package/src/components/inline-edit-field.tsx +13 -13
  353. package/src/components/list.tsx +10 -9
  354. package/src/components/modal.tsx +5 -0
  355. package/src/components/month-picker.tsx +8 -2
  356. package/src/components/multi-entity-combobox.tsx +3 -3
  357. package/src/components/number-field.tsx +7 -0
  358. package/src/components/number-input.tsx +71 -11
  359. package/src/components/numpad-sheet.tsx +8 -4
  360. package/src/components/popover.tsx +6 -2
  361. package/src/components/row-actions.tsx +208 -58
  362. package/src/components/share-card.tsx +67 -13
  363. package/src/components/skeleton.tsx +10 -2
  364. package/src/components/status-dot.tsx +7 -7
  365. package/src/components/swatch-picker.tsx +2 -2
  366. package/src/components/swipeable-row.tsx +138 -27
  367. package/src/components/switch.tsx +2 -1
  368. package/src/components/table.tsx +8 -1
  369. package/src/components/toggle-group.tsx +5 -3
  370. package/src/components/translation-review-editor.tsx +8 -3
  371. package/src/components/translation-review.tsx +18 -22
  372. package/src/components/ui.tsx +92 -43
  373. package/src/components/write-lock.tsx +150 -24
  374. package/src/feedback/feedback-attachment.tsx +13 -9
  375. package/src/feedback/feedback-inbox.tsx +10 -6
  376. package/src/feedback/feedback-row-detail.tsx +3 -2
  377. package/src/feedback/feedback-swipe.tsx +6 -11
  378. package/src/feedback/feedback-table.tsx +2 -1
  379. package/src/feedback/feedback-thread.tsx +20 -1
  380. package/src/hooks/use-anchored-panel.ts +60 -14
  381. package/src/hooks/use-focus-trap.ts +55 -12
  382. package/src/i18n/defaults.ts +2 -0
  383. package/src/i18n/german.ts +41 -0
  384. package/src/i18n/kit-labels.tsx +4 -0
  385. package/src/i18n/locales/es.ts +35 -0
  386. package/src/i18n/locales/fr.ts +41 -0
  387. package/src/i18n/locales/hu.ts +39 -0
  388. package/src/i18n/locales/it.ts +36 -0
  389. package/src/i18n/locales/zh.ts +34 -0
  390. package/src/i18n/plurals.ts +29 -0
  391. package/src/i18n/review.ts +7 -0
  392. package/src/index.ts +51 -5
  393. package/src/testing/css-variables.generated.ts +112 -0
  394. package/src/testing/css-variables.ts +191 -0
  395. package/src/testing/index.ts +13 -0
  396. package/src/theme/contrast-tokens.ts +53 -2
  397. package/src/theme/palette-derive.ts +87 -10
  398. package/src/theme/palette-presets.ts +45 -43
  399. package/src/wizard/stepper-nav.tsx +2 -1
  400. package/tokens.css +324 -46
@@ -1,4 +1,4 @@
1
- export { C as ChatComposer, a as ChatComposerHandle, b as ChatComposerLabels, c as ChatComposerProps, d as ChatComposerSlotContext, D as DEFAULT_ATTACHMENT_ACCEPT, e as DEFAULT_FEEDBACK_ATTACHMENT_LABELS, f as DEFAULT_FEEDBACK_CATEGORY_LABELS, g as DEFAULT_FEEDBACK_COMPOSER_LABELS, h as DEFAULT_FEEDBACK_CONTEXT_LABELS, i as DEFAULT_FEEDBACK_DETAIL_LABELS, j as DEFAULT_FEEDBACK_DIALOG_LABELS, k as DEFAULT_FEEDBACK_MENU_LABELS, l as DEFAULT_FEEDBACK_PAGE_LABELS, m as DEFAULT_FEEDBACK_STATUS_LABELS, n as DEFAULT_FEEDBACK_THREAD_LABELS, o as DEFAULT_FEEDBACK_TOAST_LABELS, p as DEFAULT_MAX_ATTACHMENTS, q as DEFAULT_MAX_ATTACHMENT_BYTES, F as FEEDBACK_ATTACHMENT_ACCEPT, r as FEEDBACK_AUTHOR_EDITABLE_STATUSES, s as FEEDBACK_AWAITING_STATUSES, t as FEEDBACK_CATEGORY_META, u as FEEDBACK_CATEGORY_ORDER, v as FEEDBACK_PICKABLE_CATEGORIES, w as FEEDBACK_REWORKABLE_STATUSES, x as FEEDBACK_STATUS_META, y as FEEDBACK_STATUS_ORDER, z as FEEDBACK_UNDO_DURATION, A as FeedbackAttachmentError, B as FeedbackAttachmentErrorInfo, E as FeedbackAttachmentField, G as FeedbackAttachmentFieldLabels, H as FeedbackAttachmentFieldMultipleProps, I as FeedbackAttachmentFieldProps, J as FeedbackAttachmentFieldRefsProps, K as FeedbackAttachmentFieldSingleProps, L as FeedbackAttachmentLabels, M as FeedbackAttachmentRef, N as FeedbackAttachmentRefsError, O as FeedbackAwaitingToggle, P as FeedbackAwaitingToggleProps, Q as FeedbackCategory, R as FeedbackCategoryBadge, S as FeedbackCategoryLabels, T as FeedbackCategoryOption, U as FeedbackColumnsOptions, V as FeedbackComposer, W as FeedbackComposerAttachment, X as FeedbackComposerHandle, Y as FeedbackComposerLabels, Z as FeedbackComposerProps, _ as FeedbackComposerSlotContext, $ as FeedbackContext, a0 as FeedbackContextBox, a1 as FeedbackContextBoxProps, a2 as FeedbackContextInput, a3 as FeedbackContextLabels, a4 as FeedbackDetail, a5 as FeedbackDetailLabels, a6 as FeedbackDetailPatch, a7 as FeedbackDetailSection, a8 as FeedbackDetailUpdate, a9 as FeedbackDialog, aa as FeedbackDialogLabels, ab as FeedbackDialogMultipleProps, ac as FeedbackDialogProps, ad as FeedbackDialogSingleProps, ae as FeedbackDialogTextLabels, af as FeedbackEmptyState, ag as FeedbackEmptyStateProps, ah as FeedbackEnvironment, ai as FeedbackMenu, aj as FeedbackMenuLabels, ak as FeedbackMenuProps, al as FeedbackMobileCard, am as FeedbackMobileCardProps, an as FeedbackMultipleSubmission, ao as FeedbackNoteAttachment, ap as FeedbackNoteEditor, aq as FeedbackPageLabels, ar as FeedbackProse, as as FeedbackRecord, at as FeedbackRenderDate, au as FeedbackReworkSection, av as FeedbackReworkSectionProps, aw as FeedbackRowAccess, ax as FeedbackRowDetail, ay as FeedbackRowDetailProps, az as FeedbackStatus, aA as FeedbackStatusBadge, aB as FeedbackStatusChange, aC as FeedbackStatusLabels, aD as FeedbackStatusMutate, aE as FeedbackStatusPatch, aF as FeedbackStatusRow, aG as FeedbackStatusTransitions, aH as FeedbackStatusUndoOptions, aI as FeedbackSubject, aJ as FeedbackSubjectProps, aK as FeedbackSubmission, aL as FeedbackSubmitHint, aM as FeedbackSubmitter, aN as FeedbackTableRow, aO as FeedbackThread, aP as FeedbackThreadAttachment, aQ as FeedbackThreadEvent, aR as FeedbackThreadItem, aS as FeedbackThreadLabels, aT as FeedbackThreadMessage, aU as FeedbackThreadProps, aV as FeedbackToastLabels, aW as FeedbackUpload, aX as UseFeedbackColumnsOptions, aY as appendRework, aZ as attachmentName, a_ as feedbackAttachmentLine, a$ as feedbackAttachmentUrls, b0 as feedbackCategoryRank, b1 as feedbackColumns, b2 as feedbackContext, b3 as feedbackEnvironment, b4 as feedbackMobileGroupBy, b5 as feedbackPageHref, b6 as feedbackPagePath, b7 as feedbackPageUrl, b8 as feedbackRowAccess, b9 as feedbackSubmitter, ba as feedbackSubmitterEmail, bb as isFeedbackAwaiting, bc as isImageAttachment, bd as nextFeedbackStatus, be as pastedName, bf as replaceDescription, bg as reworkCount, bh as selectableFeedbackStatuses, bi as splitBodyAttachments, bj as splitDescription, bk as useFeedbackCategoryLabels, bl as useFeedbackColumns, bm as useFeedbackDetailLabels, bn as useFeedbackPageLabels, bo as useFeedbackRenderDate, bp as useFeedbackStatusLabels, bq as useFeedbackStatusUndo, br as useFeedbackToastLabels, bs as visibleFeedbackStatuses } from './delete-account-setting-Dy4kJgUm.js';
1
+ export { C as ChatComposer, a as ChatComposerHandle, b as ChatComposerLabels, c as ChatComposerProps, d as ChatComposerSlotContext, D as DEFAULT_ATTACHMENT_ACCEPT, e as DEFAULT_FEEDBACK_ATTACHMENT_LABELS, f as DEFAULT_FEEDBACK_CATEGORY_LABELS, g as DEFAULT_FEEDBACK_COMPOSER_LABELS, h as DEFAULT_FEEDBACK_CONTEXT_LABELS, i as DEFAULT_FEEDBACK_DETAIL_LABELS, j as DEFAULT_FEEDBACK_DIALOG_LABELS, k as DEFAULT_FEEDBACK_MENU_LABELS, l as DEFAULT_FEEDBACK_PAGE_LABELS, m as DEFAULT_FEEDBACK_STATUS_LABELS, n as DEFAULT_FEEDBACK_THREAD_LABELS, o as DEFAULT_FEEDBACK_TOAST_LABELS, p as DEFAULT_MAX_ATTACHMENTS, q as DEFAULT_MAX_ATTACHMENT_BYTES, F as FEEDBACK_ATTACHMENT_ACCEPT, r as FEEDBACK_AUTHOR_EDITABLE_STATUSES, s as FEEDBACK_AWAITING_STATUSES, t as FEEDBACK_CATEGORY_META, u as FEEDBACK_CATEGORY_ORDER, v as FEEDBACK_PICKABLE_CATEGORIES, w as FEEDBACK_REWORKABLE_STATUSES, x as FEEDBACK_STATUS_META, y as FEEDBACK_STATUS_ORDER, z as FEEDBACK_UNDO_DURATION, A as FeedbackAttachmentError, B as FeedbackAttachmentErrorInfo, E as FeedbackAttachmentField, G as FeedbackAttachmentFieldLabels, H as FeedbackAttachmentFieldMultipleProps, I as FeedbackAttachmentFieldProps, J as FeedbackAttachmentFieldRefsProps, K as FeedbackAttachmentFieldSingleProps, L as FeedbackAttachmentLabels, M as FeedbackAttachmentRef, N as FeedbackAttachmentRefsError, O as FeedbackAwaitingToggle, P as FeedbackAwaitingToggleProps, Q as FeedbackCategory, R as FeedbackCategoryBadge, S as FeedbackCategoryLabels, T as FeedbackCategoryOption, U as FeedbackColumnsOptions, V as FeedbackComposer, W as FeedbackComposerAttachment, X as FeedbackComposerHandle, Y as FeedbackComposerLabels, Z as FeedbackComposerProps, _ as FeedbackComposerSlotContext, $ as FeedbackContext, a0 as FeedbackContextBox, a1 as FeedbackContextBoxProps, a2 as FeedbackContextInput, a3 as FeedbackContextLabels, a4 as FeedbackDetail, a5 as FeedbackDetailLabels, a6 as FeedbackDetailPatch, a7 as FeedbackDetailSection, a8 as FeedbackDetailUpdate, a9 as FeedbackDialog, aa as FeedbackDialogLabels, ab as FeedbackDialogMultipleProps, ac as FeedbackDialogProps, ad as FeedbackDialogSingleProps, ae as FeedbackDialogTextLabels, af as FeedbackEmptyState, ag as FeedbackEmptyStateProps, ah as FeedbackEnvironment, ai as FeedbackMenu, aj as FeedbackMenuLabels, ak as FeedbackMenuProps, al as FeedbackMobileCard, am as FeedbackMobileCardProps, an as FeedbackMultipleSubmission, ao as FeedbackNoteAttachment, ap as FeedbackNoteEditor, aq as FeedbackPageLabels, ar as FeedbackProse, as as FeedbackRecord, at as FeedbackRenderDate, au as FeedbackReworkSection, av as FeedbackReworkSectionProps, aw as FeedbackRowAccess, ax as FeedbackRowDetail, ay as FeedbackRowDetailProps, az as FeedbackStatus, aA as FeedbackStatusBadge, aB as FeedbackStatusChange, aC as FeedbackStatusLabels, aD as FeedbackStatusMutate, aE as FeedbackStatusPatch, aF as FeedbackStatusRow, aG as FeedbackStatusTransitions, aH as FeedbackStatusUndoOptions, aI as FeedbackSubject, aJ as FeedbackSubjectProps, aK as FeedbackSubmission, aL as FeedbackSubmitHint, aM as FeedbackSubmitter, aN as FeedbackTableRow, aO as FeedbackThread, aP as FeedbackThreadAttachment, aQ as FeedbackThreadEvent, aR as FeedbackThreadItem, aS as FeedbackThreadLabels, aT as FeedbackThreadMessage, aU as FeedbackThreadProps, aV as FeedbackToastLabels, aW as FeedbackUpload, aX as UseFeedbackColumnsOptions, aY as appendRework, aZ as attachmentName, a_ as feedbackAttachmentLine, a$ as feedbackAttachmentUrls, b0 as feedbackCategoryRank, b1 as feedbackColumns, b2 as feedbackContext, b3 as feedbackEnvironment, b4 as feedbackMobileGroupBy, b5 as feedbackPageHref, b6 as feedbackPagePath, b7 as feedbackPageUrl, b8 as feedbackRowAccess, b9 as feedbackSubmitter, ba as feedbackSubmitterEmail, bb as isFeedbackAwaiting, bc as isImageAttachment, bd as nextFeedbackStatus, be as pastedName, bf as replaceDescription, bg as reworkCount, bh as selectableFeedbackStatuses, bi as splitBodyAttachments, bj as splitDescription, bk as useFeedbackCategoryLabels, bl as useFeedbackColumns, bm as useFeedbackDetailLabels, bn as useFeedbackPageLabels, bo as useFeedbackRenderDate, bp as useFeedbackStatusLabels, bq as useFeedbackStatusUndo, br as useFeedbackToastLabels, bs as visibleFeedbackStatuses } from './delete-account-setting-DAgGBrbn.js';
2
2
  export { DEFAULT_FEEDBACK_SWIPE, FEEDBACK_SWIPE_ACTIONS, FeedbackSwipeAction, FeedbackSwipeBinding, FeedbackSwipeOptions, feedbackSwipePlan } from './feedback/feedback-swipe.js';
3
3
  export { CaptureAppScreenshotOptions, captureAppScreenshot } from './feedback/feedback-capture.js';
4
4
  export { FeedbackCreatePayload, FeedbackSubmit, UseFeedbackSubmitOptions, useFeedbackSubmit } from './feedback/feedback-submit.js';
@@ -7,16 +7,21 @@ import 'react';
7
7
  import './components/tooltip.js';
8
8
  import './hooks/use-anchored-rect.js';
9
9
  import './lib/clipping.js';
10
- import './data-table-labels-kpAAVi6g.js';
10
+ import './data-table-labels-BJ5JE_X2.js';
11
11
  import './components/data-table-sort.js';
12
12
  import './components/swipeable-row.js';
13
+ import './components/status-dot.js';
14
+ import './components/progress-bar.js';
15
+ import './components/signed-amount.js';
16
+ import './components/stat-tile.js';
17
+ import './components/chip.js';
18
+ import 'lucide-react';
19
+ import './components/field-parts.js';
20
+ import './components/write-lock.js';
13
21
  import './components/mini-calendar.js';
14
22
  import './components/calendar-heatmap.js';
15
23
  import './components/popover.js';
16
24
  import './hooks/use-breakpoint.js';
17
- import './components/chip.js';
18
- import 'lucide-react';
19
- import './components/field-parts.js';
20
25
  import './components/field-sync.js';
21
26
  import './wizard/types.js';
22
27
  import './tour/tour.js';
@@ -28,7 +33,6 @@ import './components/page-contents.js';
28
33
  import './components/series-chart-labels.js';
29
34
  import './components/pie-chart-labels.js';
30
35
  import './components/sparkline.js';
31
- import './components/stat-tile.js';
32
36
  import './components/signature-pad.js';
33
37
  import './components/password-strength.js';
34
38
  import './components/swatch-picker.js';
@@ -40,9 +44,6 @@ import './hooks/use-search-param-state.js';
40
44
  import './components/measured-grid.js';
41
45
  import './components/user-avatar.js';
42
46
  import './lib/person-name.js';
43
- import './components/status-dot.js';
44
- import './components/progress-bar.js';
45
- import './components/signed-amount.js';
46
47
  import './shell/topbar-action-menu.js';
47
48
  import './components/menu-item.js';
48
49
  import './components/floating-panel.js';
@@ -58,7 +59,6 @@ import './components/error-boundary.js';
58
59
  import './components/authed-image.js';
59
60
  import './components/image-grid.js';
60
61
  import './components/lightbox.js';
61
- import './components/write-lock.js';
62
62
  import './auth/sign-in-form.js';
63
63
  import './landing/access.js';
64
64
  import './landing/landing-labels.js';
@@ -85,6 +85,7 @@ import './admin/admin-parts.js';
85
85
  import './admin/admin-action-log.js';
86
86
  import './settings/settings-labels.js';
87
87
  import './demo/demo-labels.js';
88
+ import './i18n/plurals.js';
88
89
  import './components/appearance-labels.js';
89
90
  import './theme/contrast.js';
90
91
  import './theme/text-size.js';
@@ -18,6 +18,13 @@ import { AnchorRect } from './use-anchored-rect.js';
18
18
  interface ViewportBox {
19
19
  top: number;
20
20
  height: number;
21
+ /**
22
+ * The height of the LAYOUT viewport itself, which `position: fixed`'s `bottom` is
23
+ * measured from. Only {@link anchoredPanelPlacement} reads it, for a flipped panel's
24
+ * `bottom`. Defaults to `top + height`: the two are the same box whenever no keyboard
25
+ * and no zoom shrink the visible region.
26
+ */
27
+ layoutHeight?: number;
21
28
  }
22
29
  interface KeyboardInsetOptions {
23
30
  /** The smallest inset that counts as a keyboard. Default 100 px. */
@@ -52,11 +59,24 @@ interface AnchoredPanelOptions {
52
59
  * In px at Normal, scaled likewise. */
53
60
  minHeight?: number;
54
61
  }
62
+ /**
63
+ * `top` and `bottom` are the two edges of a panel exactly `maxHeight` tall. A panel is
64
+ * sized by its content and `maxHeight` only caps it, so apply the edge on the trigger's
65
+ * side, the one that holds whatever the height: `top` while the panel is below,
66
+ * `bottom` (and no `top`) once it is flipped `above`.
67
+ */
55
68
  interface AnchoredPanel {
56
69
  /** The trigger's rect, or null while closed/unmeasured. */
57
70
  rect: AnchorRect | null;
58
- /** Viewport-space `top` for the panel. */
71
+ /** Viewport-space `top` for the panel — the one to apply while it is below. Flipped,
72
+ * it is where a FULL-height panel's top would be, and a three-row list placed by it
73
+ * floated far above its field (keksdose K1, live #401); place that one by `bottom`. */
59
74
  top: number;
75
+ /** CSS `bottom` for a `position: fixed` panel: the distance from the layout
76
+ * viewport's bottom edge up to the panel's. The one to apply once it is flipped
77
+ * `above`: it keeps the panel's foot `gap` above the trigger however short its
78
+ * content, and while a filtered list shrinks or grows under the user's typing. */
79
+ bottom: number;
60
80
  /** Cap that keeps the panel inside the VISIBLE viewport — apply as `maxHeight`. */
61
81
  maxHeight: number;
62
82
  /** Whether the panel was flipped above the trigger. */
@@ -76,16 +96,18 @@ interface AnchoredPanel {
76
96
  * returned `maxHeight` is what makes the flip sufficient rather than merely different:
77
97
  * without it a tall panel placed above just runs off the top instead of the bottom.
78
98
  *
99
+ * A flipped panel hangs from its BOTTOM edge, `gap` above the trigger, and is placed by
100
+ * `bottom` (keksdose K1, live #401). It used to be placed by a `top` worked out as
101
+ * though the panel were `maxHeight` tall, and a panel is only as tall as its content:
102
+ * a three-row address list under a 256 px cap sat ~176 px above its field, on a phone
103
+ * with the keyboard up. Only when even the {@link FLOOR} does not fit above does that
104
+ * edge give way, as `top` always did, so the panel's head (a search box, often) stays
105
+ * on screen rather than above it.
106
+ *
79
107
  * Split out so the geometry is testable without a DOM and a fake keyboard — @hb/ui has
80
108
  * no test runner of its own, so this is exercised from the consuming app.
81
109
  */
82
110
  declare function anchoredPanelPlacement(rect: Pick<AnchorRect, "top" | "bottom">, viewport: ViewportBox, { gap, margin, preferredHeight, minHeight }?: AnchoredPanelOptions): Omit<AnchoredPanel, "rect">;
83
- /**
84
- * Where to put a panel anchored under `ref`, tracking the visible viewport.
85
- *
86
- * Callers own horizontal placement — the pickers align to the trigger's left edge, the
87
- * popovers to its right — and must apply `maxHeight` with an internal `overflow-y`.
88
- */
89
111
  /**
90
112
  * The region actually on screen, tracked while `active` (Keksdose live #328).
91
113
  *
@@ -108,6 +130,14 @@ declare function anchoredPanelPlacement(rect: Pick<AnchorRect, "top" | "bottom">
108
130
  * sheet keeps its static full-screen box, which the reader can pan to like the page.
109
131
  */
110
132
  declare function useVisualViewport(active: boolean): ViewportBox | null;
133
+ /**
134
+ * Where to put a panel anchored under `ref`, tracking the visible viewport.
135
+ *
136
+ * Callers own horizontal placement — the pickers align to the trigger's left edge, the
137
+ * popovers to its right — and must apply `maxHeight` with an internal `overflow-y`.
138
+ * Vertically, apply `top` while the panel is below and `bottom` (with no `top`) once it
139
+ * is `above`; see {@link AnchoredPanel}.
140
+ */
111
141
  declare function useAnchoredPanel<T extends HTMLElement>(ref: RefObject<T | null>, open: boolean, options?: AnchoredPanelOptions): AnchoredPanel;
112
142
 
113
143
  export { type AnchoredPanel, type AnchoredPanelOptions, type KeyboardInsetOptions, type ViewportBox, anchoredPanelPlacement, readKeyboardInset, useAnchoredPanel, useVisualViewport };
@@ -16,7 +16,8 @@ function readKeyboardInset({ minimum = MIN_KEYBOARD_PX } = {}) {
16
16
  function readViewport() {
17
17
  if (typeof window === "undefined") return { top: 0, height: 0 };
18
18
  const vv = window.visualViewport;
19
- return vv ? { top: vv.offsetTop, height: vv.height } : { top: 0, height: window.innerHeight };
19
+ const layoutHeight = window.innerHeight;
20
+ return vv ? { top: vv.offsetTop, height: vv.height, layoutHeight } : { top: 0, height: window.innerHeight, layoutHeight };
20
21
  }
21
22
  const FLOOR = 96;
22
23
  const FLIP_GAIN = 48;
@@ -28,11 +29,9 @@ function anchoredPanelPlacement(rect, viewport, { gap = 4, margin = 8, preferred
28
29
  const above = spaceAbove > spaceBelow && (spaceBelow < minHeight || spaceBelow < preferredHeight && (spaceAbove >= preferredHeight || spaceAbove - spaceBelow >= FLIP_GAIN));
29
30
  const space = Math.max(FLOOR, above ? spaceAbove : spaceBelow);
30
31
  const maxHeight = Math.min(preferredHeight, space);
31
- return {
32
- top: above ? Math.max(viewTop, rect.top - gap - maxHeight) : rect.bottom + gap,
33
- maxHeight,
34
- above
35
- };
32
+ const top = above ? Math.max(viewTop, rect.top - gap - maxHeight) : rect.bottom + gap;
33
+ const layoutHeight = viewport.layoutHeight ?? viewport.top + viewport.height;
34
+ return { top, bottom: layoutHeight - (top + maxHeight), maxHeight, above };
36
35
  }
37
36
  function useVisualViewport(active) {
38
37
  const [box, setBox] = useState(null);
@@ -78,7 +77,7 @@ function useAnchoredPanel(ref, open, options = {}) {
78
77
  };
79
78
  }, [open]);
80
79
  if (!rect) {
81
- return { rect: null, top: 0, maxHeight: preferredHeight, above: false };
80
+ return { rect: null, top: 0, bottom: 0, maxHeight: preferredHeight, above: false };
82
81
  }
83
82
  return { rect, ...anchoredPanelPlacement(rect, viewport, { ...options, preferredHeight, minHeight }) };
84
83
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/use-anchored-panel.ts"],"sourcesContent":["import { useLayoutEffect, useState } from \"react\";\nimport type { RefObject } from \"react\";\nimport { useAnchoredRect, type AnchorRect } from \"./use-anchored-rect\";\nimport { useTextSize } from \"../theme/text-size\";\n\n/**\n * Vertical placement for a portalled, `position: fixed` panel anchored to a trigger.\n *\n * The problem it solves (feedback #135): on a phone, opening a picker focuses its\n * search box, the on-screen keyboard slides up, and a panel pinned below a trigger in\n * the lower half of the screen ends up entirely behind that keyboard — with nothing\n * the user can do about it, because `position: fixed` does not scroll.\n *\n * The keyboard is invisible to the usual measurements: on Android Chrome the LAYOUT\n * viewport (`window.innerHeight`, and the origin `getBoundingClientRect` and\n * `position: fixed` are relative to) does not shrink when it opens. Only\n * `visualViewport` reports the region actually on screen, so that is what this reads.\n */\n\n/** The on-screen region, in layout-viewport coordinates. */\nexport interface ViewportBox {\n top: number;\n height: number;\n}\n\n/**\n * Whether the page is pinch-zoomed (docs/text-size-harmonization.md §10.2). A zoom\n * shrinks `visualViewport.height` exactly like an on-screen keyboard does, so every\n * reader below must tell the two apart — and while zoomed, the answer is \"no keyboard\":\n * following a zoomed viewport would drag a pinned row on every pan, magnified, and the\n * browser already scrolls a focused field into view. The epsilon absorbs a scale of\n * 1.0000001 that some engines report at rest.\n */\nfunction isZoomed(vv: VisualViewport): boolean {\n return vv.scale > 1.001;\n}\n\n/** Below this the \"keyboard\" is rounding noise or a browser UI strip — `innerHeight` and\n * `visualViewport.height` routinely disagree by a pixel or two — and nothing should move\n * (keksdose's `MIN_KEYBOARD_PX`). */\nconst MIN_KEYBOARD_PX = 100;\n\nexport interface KeyboardInsetOptions {\n /** The smallest inset that counts as a keyboard. Default 100 px. */\n minimum?: number;\n}\n\n/**\n * How many CSS px at the bottom of the LAYOUT viewport the on-screen keyboard covers, now\n * — the one guarded reader (§10.2), so the apps' copies go (keksdose's\n * `use-keyboard-inset.ts`):\n *\n * - 0 while the page is pinch-zoomed (`visualViewport.scale > 1`), where a smaller visual\n * viewport is the zoom, not a keyboard;\n * - 0 below `minimum` (100 px), which is noise;\n * - 0 without `visualViewport` (SSR, jsdom, an old browser) and under\n * `interactive-widget=resizes-content`, where `bottom: 0` already lands above the\n * keyboard.\n *\n * Read it on `visualViewport`'s `resize` and `scroll` events (window `resize` as the\n * fallback); a bottom-anchored control lifts itself by the result.\n */\nexport function readKeyboardInset({ minimum = MIN_KEYBOARD_PX }: KeyboardInsetOptions = {}): number {\n if (typeof window === \"undefined\") return 0;\n const vv = window.visualViewport;\n if (!vv || isZoomed(vv)) return 0;\n const inset = Math.round(window.innerHeight - vv.height - vv.offsetTop);\n return inset >= minimum ? inset : 0;\n}\n\nfunction readViewport(): ViewportBox {\n if (typeof window === \"undefined\") return { top: 0, height: 0 };\n const vv = window.visualViewport;\n // offsetTop, not 0: with the keyboard up the user can also pan the visual viewport\n // within the layout viewport, which shifts where \"visible\" starts.\n return vv ? { top: vv.offsetTop, height: vv.height } : { top: 0, height: window.innerHeight };\n}\n\nexport interface AnchoredPanelOptions {\n /** Space between trigger and panel. */\n gap?: number;\n /** Space kept clear at the viewport edges. */\n margin?: number;\n /** The height the panel would like, when there is room. In px at Normal text size:\n * {@link useAnchoredPanel} multiplies it by the size's scale, since what a panel\n * holds is rows of text (0.32, docs/text-size-harmonization.md §3.2). */\n preferredHeight?: number;\n /** Below this, the space under the trigger counts as unusable and a flip is considered.\n * In px at Normal, scaled likewise. */\n minHeight?: number;\n}\n\nexport interface AnchoredPanel {\n /** The trigger's rect, or null while closed/unmeasured. */\n rect: AnchorRect | null;\n /** Viewport-space `top` for the panel. */\n top: number;\n /** Cap that keeps the panel inside the VISIBLE viewport — apply as `maxHeight`. */\n maxHeight: number;\n /** Whether the panel was flipped above the trigger. */\n above: boolean;\n}\n\n/** Absolute floor: below this a list is useless, so overflow the margin instead. */\nconst FLOOR = 96;\n\n/** How much more room above must offer, when neither side fits the preferred height,\n * before the panel changes sides — so it never jumps for a few pixels' gain. */\nconst FLIP_GAIN = 48;\n\n/**\n * The pure half of {@link useAnchoredPanel}: where a panel goes, given the trigger's\n * rect and the region actually on screen.\n *\n * Prefers below the trigger — the conventional direction, and the one that keeps the\n * trigger's own value visible — whenever the panel's preferred height fits there. When it\n * does not, the panel opens above if the full height fits there, or if above is clearly\n * roomier (by {@link FLIP_GAIN}); below an unusable `minHeight` it flips to any roomier\n * side, as before. It used to flip only below `minHeight`, so a select with 160–319 px\n * under it opened downward as a cramped list with 600 px free above (keksdose live #384,\n * the currency select of the new-budget form near the bottom of the screen). The\n * returned `maxHeight` is what makes the flip sufficient rather than merely different:\n * without it a tall panel placed above just runs off the top instead of the bottom.\n *\n * Split out so the geometry is testable without a DOM and a fake keyboard — @hb/ui has\n * no test runner of its own, so this is exercised from the consuming app.\n */\nexport function anchoredPanelPlacement(\n rect: Pick<AnchorRect, \"top\" | \"bottom\">,\n viewport: ViewportBox,\n { gap = 4, margin = 8, preferredHeight = 320, minHeight = 160 }: AnchoredPanelOptions = {},\n): Omit<AnchoredPanel, \"rect\"> {\n const viewTop = viewport.top + margin;\n const viewBottom = viewport.top + viewport.height - margin;\n const spaceBelow = viewBottom - (rect.bottom + gap);\n const spaceAbove = rect.top - gap - viewTop;\n const above =\n spaceAbove > spaceBelow &&\n (spaceBelow < minHeight ||\n (spaceBelow < preferredHeight && (spaceAbove >= preferredHeight || spaceAbove - spaceBelow >= FLIP_GAIN)));\n const space = Math.max(FLOOR, above ? spaceAbove : spaceBelow);\n const maxHeight = Math.min(preferredHeight, space);\n return {\n top: above ? Math.max(viewTop, rect.top - gap - maxHeight) : rect.bottom + gap,\n maxHeight,\n above,\n };\n}\n\n/**\n * Where to put a panel anchored under `ref`, tracking the visible viewport.\n *\n * Callers own horizontal placement — the pickers align to the trigger's left edge, the\n * popovers to its right — and must apply `maxHeight` with an internal `overflow-y`.\n */\n/**\n * The region actually on screen, tracked while `active` (Keksdose live #328).\n *\n * The same measurement {@link useAnchoredPanel} caps a dropdown with, for the callers\n * that need the box itself rather than a placement: a FULL-SCREEN sheet cannot use\n * `inset-0`, because on Android the on-screen keyboard shrinks only the VISUAL\n * viewport — `inset-0` still spans the whole screen, so the bottom of the sheet, and\n * the end of whatever list is scrolling inside it, live behind the keyboard where no\n * gesture reaches them.\n *\n * Returns `null` where there is nothing to correct: no `visualViewport` (jsdom, SSR,\n * older browsers) or no keyboard taking a bite out of it. A caller then keeps its\n * static layout, which is right at every width where this does not apply — and is why\n * the desktop and the test suite see exactly the markup they saw before.\n *\n * Also `null` while the page is pinch-zoomed (0.32, §10.2), with the guard\n * {@link readKeyboardInset} uses. A zoom shrinks the visual viewport like a keyboard,\n * and following it gave PickerSheet the zoomed region's top and height but the page's\n * full width — its close button sat off screen (kastlan, Kurvenschmiede). Zoomed, the\n * sheet keeps its static full-screen box, which the reader can pan to like the page.\n */\nexport function useVisualViewport(active: boolean): ViewportBox | null {\n const [box, setBox] = useState<ViewportBox | null>(null);\n\n useLayoutEffect(() => {\n if (!active) return;\n const update = () => {\n const vv = typeof window === \"undefined\" ? undefined : window.visualViewport;\n // The 1px slack is not superstition: `visualViewport.height` is fractional on a\n // device-pixel-ratio that is not an integer (his phone reports 1.25), so a\n // strict comparison would report a \"keyboard\" of 0.4px on every phone and pin\n // a height where none was needed.\n setBox(\n vv && !isZoomed(vv) && vv.height < window.innerHeight - 1 ? { top: vv.offsetTop, height: vv.height } : null,\n );\n };\n update();\n const vv = window.visualViewport;\n vv?.addEventListener(\"resize\", update);\n vv?.addEventListener(\"scroll\", update);\n window.addEventListener(\"resize\", update);\n return () => {\n vv?.removeEventListener(\"resize\", update);\n vv?.removeEventListener(\"scroll\", update);\n window.removeEventListener(\"resize\", update);\n };\n }, [active]);\n\n return box;\n}\n\nexport function useAnchoredPanel<T extends HTMLElement>(\n ref: RefObject<T | null>,\n open: boolean,\n options: AnchoredPanelOptions = {},\n): AnchoredPanel {\n const rect = useAnchoredRect(ref, open);\n const [viewport, setViewport] = useState<ViewportBox>(readViewport);\n // A panel of rows wants room for the same number of rows at every text size; the gap\n // and the edge margin are not text and stay px.\n const { scale } = useTextSize();\n const preferredHeight = (options.preferredHeight ?? 320) * scale;\n const minHeight = (options.minHeight ?? 160) * scale;\n\n useLayoutEffect(() => {\n if (!open) return;\n const update = () => setViewport(readViewport());\n update();\n const vv = window.visualViewport;\n // The visualViewport events are the ones that fire when the keyboard opens;\n // window.resize is the fallback for browsers without the API (and for a genuine\n // window resize on desktop).\n vv?.addEventListener(\"resize\", update);\n vv?.addEventListener(\"scroll\", update);\n window.addEventListener(\"resize\", update);\n return () => {\n vv?.removeEventListener(\"resize\", update);\n vv?.removeEventListener(\"scroll\", update);\n window.removeEventListener(\"resize\", update);\n };\n }, [open]);\n\n if (!rect) {\n return { rect: null, top: 0, maxHeight: preferredHeight, above: false };\n }\n return { rect, ...anchoredPanelPlacement(rect, viewport, { ...options, preferredHeight, minHeight }) };\n}\n"],"mappings":"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","names":["vv"]}
1
+ {"version":3,"sources":["../../src/hooks/use-anchored-panel.ts"],"sourcesContent":["import { useLayoutEffect, useState } from \"react\";\nimport type { RefObject } from \"react\";\nimport { useAnchoredRect, type AnchorRect } from \"./use-anchored-rect\";\nimport { useTextSize } from \"../theme/text-size\";\n\n/**\n * Vertical placement for a portalled, `position: fixed` panel anchored to a trigger.\n *\n * The problem it solves (feedback #135): on a phone, opening a picker focuses its\n * search box, the on-screen keyboard slides up, and a panel pinned below a trigger in\n * the lower half of the screen ends up entirely behind that keyboard — with nothing\n * the user can do about it, because `position: fixed` does not scroll.\n *\n * The keyboard is invisible to the usual measurements: on Android Chrome the LAYOUT\n * viewport (`window.innerHeight`, and the origin `getBoundingClientRect` and\n * `position: fixed` are relative to) does not shrink when it opens. Only\n * `visualViewport` reports the region actually on screen, so that is what this reads.\n */\n\n/** The on-screen region, in layout-viewport coordinates. */\nexport interface ViewportBox {\n top: number;\n height: number;\n /**\n * The height of the LAYOUT viewport itself, which `position: fixed`'s `bottom` is\n * measured from. Only {@link anchoredPanelPlacement} reads it, for a flipped panel's\n * `bottom`. Defaults to `top + height`: the two are the same box whenever no keyboard\n * and no zoom shrink the visible region.\n */\n layoutHeight?: number;\n}\n\n/**\n * Whether the page is pinch-zoomed (docs/text-size-harmonization.md §10.2). A zoom\n * shrinks `visualViewport.height` exactly like an on-screen keyboard does, so every\n * reader below must tell the two apart — and while zoomed, the answer is \"no keyboard\":\n * following a zoomed viewport would drag a pinned row on every pan, magnified, and the\n * browser already scrolls a focused field into view. The epsilon absorbs a scale of\n * 1.0000001 that some engines report at rest.\n */\nfunction isZoomed(vv: VisualViewport): boolean {\n return vv.scale > 1.001;\n}\n\n/** Below this the \"keyboard\" is rounding noise or a browser UI strip — `innerHeight` and\n * `visualViewport.height` routinely disagree by a pixel or two — and nothing should move\n * (keksdose's `MIN_KEYBOARD_PX`). */\nconst MIN_KEYBOARD_PX = 100;\n\nexport interface KeyboardInsetOptions {\n /** The smallest inset that counts as a keyboard. Default 100 px. */\n minimum?: number;\n}\n\n/**\n * How many CSS px at the bottom of the LAYOUT viewport the on-screen keyboard covers, now\n * — the one guarded reader (§10.2), so the apps' copies go (keksdose's\n * `use-keyboard-inset.ts`):\n *\n * - 0 while the page is pinch-zoomed (`visualViewport.scale > 1`), where a smaller visual\n * viewport is the zoom, not a keyboard;\n * - 0 below `minimum` (100 px), which is noise;\n * - 0 without `visualViewport` (SSR, jsdom, an old browser) and under\n * `interactive-widget=resizes-content`, where `bottom: 0` already lands above the\n * keyboard.\n *\n * Read it on `visualViewport`'s `resize` and `scroll` events (window `resize` as the\n * fallback); a bottom-anchored control lifts itself by the result.\n */\nexport function readKeyboardInset({ minimum = MIN_KEYBOARD_PX }: KeyboardInsetOptions = {}): number {\n if (typeof window === \"undefined\") return 0;\n const vv = window.visualViewport;\n if (!vv || isZoomed(vv)) return 0;\n const inset = Math.round(window.innerHeight - vv.height - vv.offsetTop);\n return inset >= minimum ? inset : 0;\n}\n\nfunction readViewport(): ViewportBox {\n if (typeof window === \"undefined\") return { top: 0, height: 0 };\n const vv = window.visualViewport;\n // The layout viewport's height is `innerHeight`, the reference `readKeyboardInset`\n // measures the keyboard against. The keyboard leaves it alone (it shrinks only the\n // VISUAL viewport; under `interactive-widget=resizes-content` it shrinks both, the\n // fixed box with them), and it follows the mobile URL bar as it slides away, as a\n // fixed box's `bottom` does. Not `documentElement.clientHeight`: on mobile Chrome and\n // Safari that stays at the small, URL-bar-shown height, so with the bar scrolled away\n // a flipped panel would sit that much too low, over its own field. `innerHeight`'s one\n // error is a desktop page with a horizontal scrollbar, which it counts and the fixed\n // box does not: the gap widens by the bar's thickness, and never closes.\n const layoutHeight = window.innerHeight;\n // offsetTop, not 0: with the keyboard up the user can also pan the visual viewport\n // within the layout viewport, which shifts where \"visible\" starts.\n return vv\n ? { top: vv.offsetTop, height: vv.height, layoutHeight }\n : { top: 0, height: window.innerHeight, layoutHeight };\n}\n\nexport interface AnchoredPanelOptions {\n /** Space between trigger and panel. */\n gap?: number;\n /** Space kept clear at the viewport edges. */\n margin?: number;\n /** The height the panel would like, when there is room. In px at Normal text size:\n * {@link useAnchoredPanel} multiplies it by the size's scale, since what a panel\n * holds is rows of text (0.32, docs/text-size-harmonization.md §3.2). */\n preferredHeight?: number;\n /** Below this, the space under the trigger counts as unusable and a flip is considered.\n * In px at Normal, scaled likewise. */\n minHeight?: number;\n}\n\n/**\n * `top` and `bottom` are the two edges of a panel exactly `maxHeight` tall. A panel is\n * sized by its content and `maxHeight` only caps it, so apply the edge on the trigger's\n * side, the one that holds whatever the height: `top` while the panel is below,\n * `bottom` (and no `top`) once it is flipped `above`.\n */\nexport interface AnchoredPanel {\n /** The trigger's rect, or null while closed/unmeasured. */\n rect: AnchorRect | null;\n /** Viewport-space `top` for the panel — the one to apply while it is below. Flipped,\n * it is where a FULL-height panel's top would be, and a three-row list placed by it\n * floated far above its field (keksdose K1, live #401); place that one by `bottom`. */\n top: number;\n /** CSS `bottom` for a `position: fixed` panel: the distance from the layout\n * viewport's bottom edge up to the panel's. The one to apply once it is flipped\n * `above`: it keeps the panel's foot `gap` above the trigger however short its\n * content, and while a filtered list shrinks or grows under the user's typing. */\n bottom: number;\n /** Cap that keeps the panel inside the VISIBLE viewport — apply as `maxHeight`. */\n maxHeight: number;\n /** Whether the panel was flipped above the trigger. */\n above: boolean;\n}\n\n/** Absolute floor: below this a list is useless, so overflow the margin instead. */\nconst FLOOR = 96;\n\n/** How much more room above must offer, when neither side fits the preferred height,\n * before the panel changes sides — so it never jumps for a few pixels' gain. */\nconst FLIP_GAIN = 48;\n\n/**\n * The pure half of {@link useAnchoredPanel}: where a panel goes, given the trigger's\n * rect and the region actually on screen.\n *\n * Prefers below the trigger — the conventional direction, and the one that keeps the\n * trigger's own value visible — whenever the panel's preferred height fits there. When it\n * does not, the panel opens above if the full height fits there, or if above is clearly\n * roomier (by {@link FLIP_GAIN}); below an unusable `minHeight` it flips to any roomier\n * side, as before. It used to flip only below `minHeight`, so a select with 160–319 px\n * under it opened downward as a cramped list with 600 px free above (keksdose live #384,\n * the currency select of the new-budget form near the bottom of the screen). The\n * returned `maxHeight` is what makes the flip sufficient rather than merely different:\n * without it a tall panel placed above just runs off the top instead of the bottom.\n *\n * A flipped panel hangs from its BOTTOM edge, `gap` above the trigger, and is placed by\n * `bottom` (keksdose K1, live #401). It used to be placed by a `top` worked out as\n * though the panel were `maxHeight` tall, and a panel is only as tall as its content:\n * a three-row address list under a 256 px cap sat ~176 px above its field, on a phone\n * with the keyboard up. Only when even the {@link FLOOR} does not fit above does that\n * edge give way, as `top` always did, so the panel's head (a search box, often) stays\n * on screen rather than above it.\n *\n * Split out so the geometry is testable without a DOM and a fake keyboard — @hb/ui has\n * no test runner of its own, so this is exercised from the consuming app.\n */\nexport function anchoredPanelPlacement(\n rect: Pick<AnchorRect, \"top\" | \"bottom\">,\n viewport: ViewportBox,\n { gap = 4, margin = 8, preferredHeight = 320, minHeight = 160 }: AnchoredPanelOptions = {},\n): Omit<AnchoredPanel, \"rect\"> {\n const viewTop = viewport.top + margin;\n const viewBottom = viewport.top + viewport.height - margin;\n const spaceBelow = viewBottom - (rect.bottom + gap);\n const spaceAbove = rect.top - gap - viewTop;\n const above =\n spaceAbove > spaceBelow &&\n (spaceBelow < minHeight ||\n (spaceBelow < preferredHeight && (spaceAbove >= preferredHeight || spaceAbove - spaceBelow >= FLIP_GAIN)));\n const space = Math.max(FLOOR, above ? spaceAbove : spaceBelow);\n const maxHeight = Math.min(preferredHeight, space);\n // Unchanged, for the apps that call the hook and apply it: the edge to use below, and\n // a full-height panel's top above. In layout-viewport space, like the rect.\n const top = above ? Math.max(viewTop, rect.top - gap - maxHeight) : rect.bottom + gap;\n // `bottom` counts up from the LAYOUT viewport's bottom edge, not the visible region's:\n // `position: fixed` is laid out in the layout viewport, which runs on behind the\n // keyboard, exactly as the rect is measured in it. The keyboard, the pan and the zoom\n // are already in `maxHeight`, which keeps the panel's top inside what is on screen.\n const layoutHeight = viewport.layoutHeight ?? viewport.top + viewport.height;\n return { top, bottom: layoutHeight - (top + maxHeight), maxHeight, above };\n}\n\n/**\n * The region actually on screen, tracked while `active` (Keksdose live #328).\n *\n * The same measurement {@link useAnchoredPanel} caps a dropdown with, for the callers\n * that need the box itself rather than a placement: a FULL-SCREEN sheet cannot use\n * `inset-0`, because on Android the on-screen keyboard shrinks only the VISUAL\n * viewport — `inset-0` still spans the whole screen, so the bottom of the sheet, and\n * the end of whatever list is scrolling inside it, live behind the keyboard where no\n * gesture reaches them.\n *\n * Returns `null` where there is nothing to correct: no `visualViewport` (jsdom, SSR,\n * older browsers) or no keyboard taking a bite out of it. A caller then keeps its\n * static layout, which is right at every width where this does not apply — and is why\n * the desktop and the test suite see exactly the markup they saw before.\n *\n * Also `null` while the page is pinch-zoomed (0.32, §10.2), with the guard\n * {@link readKeyboardInset} uses. A zoom shrinks the visual viewport like a keyboard,\n * and following it gave PickerSheet the zoomed region's top and height but the page's\n * full width — its close button sat off screen (kastlan, Kurvenschmiede). Zoomed, the\n * sheet keeps its static full-screen box, which the reader can pan to like the page.\n */\nexport function useVisualViewport(active: boolean): ViewportBox | null {\n const [box, setBox] = useState<ViewportBox | null>(null);\n\n useLayoutEffect(() => {\n if (!active) return;\n const update = () => {\n const vv = typeof window === \"undefined\" ? undefined : window.visualViewport;\n // The 1px slack is not superstition: `visualViewport.height` is fractional on a\n // device-pixel-ratio that is not an integer (his phone reports 1.25), so a\n // strict comparison would report a \"keyboard\" of 0.4px on every phone and pin\n // a height where none was needed.\n setBox(\n vv && !isZoomed(vv) && vv.height < window.innerHeight - 1 ? { top: vv.offsetTop, height: vv.height } : null,\n );\n };\n update();\n const vv = window.visualViewport;\n vv?.addEventListener(\"resize\", update);\n vv?.addEventListener(\"scroll\", update);\n window.addEventListener(\"resize\", update);\n return () => {\n vv?.removeEventListener(\"resize\", update);\n vv?.removeEventListener(\"scroll\", update);\n window.removeEventListener(\"resize\", update);\n };\n }, [active]);\n\n return box;\n}\n\n/**\n * Where to put a panel anchored under `ref`, tracking the visible viewport.\n *\n * Callers own horizontal placement — the pickers align to the trigger's left edge, the\n * popovers to its right — and must apply `maxHeight` with an internal `overflow-y`.\n * Vertically, apply `top` while the panel is below and `bottom` (with no `top`) once it\n * is `above`; see {@link AnchoredPanel}.\n */\nexport function useAnchoredPanel<T extends HTMLElement>(\n ref: RefObject<T | null>,\n open: boolean,\n options: AnchoredPanelOptions = {},\n): AnchoredPanel {\n const rect = useAnchoredRect(ref, open);\n const [viewport, setViewport] = useState<ViewportBox>(readViewport);\n // A panel of rows wants room for the same number of rows at every text size; the gap\n // and the edge margin are not text and stay px.\n const { scale } = useTextSize();\n const preferredHeight = (options.preferredHeight ?? 320) * scale;\n const minHeight = (options.minHeight ?? 160) * scale;\n\n useLayoutEffect(() => {\n if (!open) return;\n const update = () => setViewport(readViewport());\n update();\n const vv = window.visualViewport;\n // The visualViewport events are the ones that fire when the keyboard opens;\n // window.resize is the fallback for browsers without the API (and for a genuine\n // window resize on desktop).\n vv?.addEventListener(\"resize\", update);\n vv?.addEventListener(\"scroll\", update);\n window.addEventListener(\"resize\", update);\n return () => {\n vv?.removeEventListener(\"resize\", update);\n vv?.removeEventListener(\"scroll\", update);\n window.removeEventListener(\"resize\", update);\n };\n }, [open]);\n\n if (!rect) {\n return { rect: null, top: 0, bottom: 0, maxHeight: preferredHeight, above: false };\n }\n return { rect, ...anchoredPanelPlacement(rect, viewport, { ...options, preferredHeight, minHeight }) };\n}\n"],"mappings":"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","names":["vv"]}
@@ -1,18 +1,23 @@
1
1
  import { DragEvent, ReactElement } from 'react';
2
- import { fA as UseFilePickerOptions } from '../delete-account-setting-Dy4kJgUm.js';
2
+ import { fM as UseFilePickerOptions } from '../delete-account-setting-DAgGBrbn.js';
3
3
  import '../components/tooltip.js';
4
4
  import './use-anchored-rect.js';
5
5
  import '../lib/clipping.js';
6
- import '../data-table-labels-kpAAVi6g.js';
6
+ import '../data-table-labels-BJ5JE_X2.js';
7
7
  import '../components/data-table-sort.js';
8
8
  import '../components/swipeable-row.js';
9
+ import '../components/status-dot.js';
10
+ import '../components/progress-bar.js';
11
+ import '../components/signed-amount.js';
12
+ import '../components/stat-tile.js';
13
+ import '../components/chip.js';
14
+ import 'lucide-react';
15
+ import '../components/field-parts.js';
16
+ import '../components/write-lock.js';
9
17
  import '../components/mini-calendar.js';
10
18
  import '../components/calendar-heatmap.js';
11
19
  import '../components/popover.js';
12
20
  import './use-breakpoint.js';
13
- import '../components/chip.js';
14
- import 'lucide-react';
15
- import '../components/field-parts.js';
16
21
  import '../components/field-sync.js';
17
22
  import '../wizard/types.js';
18
23
  import '../tour/tour.js';
@@ -24,7 +29,6 @@ import '../components/page-contents.js';
24
29
  import '../components/series-chart-labels.js';
25
30
  import '../components/pie-chart-labels.js';
26
31
  import '../components/sparkline.js';
27
- import '../components/stat-tile.js';
28
32
  import '../components/signature-pad.js';
29
33
  import '../components/password-strength.js';
30
34
  import '../components/swatch-picker.js';
@@ -36,9 +40,6 @@ import './use-search-param-state.js';
36
40
  import '../components/measured-grid.js';
37
41
  import '../components/user-avatar.js';
38
42
  import '../lib/person-name.js';
39
- import '../components/status-dot.js';
40
- import '../components/progress-bar.js';
41
- import '../components/signed-amount.js';
42
43
  import '../shell/topbar-action-menu.js';
43
44
  import '../components/menu-item.js';
44
45
  import '../components/floating-panel.js';
@@ -54,7 +55,6 @@ import '../components/error-boundary.js';
54
55
  import '../components/authed-image.js';
55
56
  import '../components/image-grid.js';
56
57
  import '../components/lightbox.js';
57
- import '../components/write-lock.js';
58
58
  import '../auth/sign-in-form.js';
59
59
  import '../landing/access.js';
60
60
  import '../landing/landing-labels.js';
@@ -81,6 +81,7 @@ import '../admin/admin-parts.js';
81
81
  import '../admin/admin-action-log.js';
82
82
  import '../settings/settings-labels.js';
83
83
  import '../demo/demo-labels.js';
84
+ import '../i18n/plurals.js';
84
85
  import '../components/appearance-labels.js';
85
86
  import '../theme/contrast.js';
86
87
  import '../theme/text-size.js';
@@ -8,6 +8,14 @@ interface FocusTrapOptions {
8
8
  * what `Modal` does and why: focusing the first FIELD pops the software keyboard on
9
9
  * a phone the moment a dialog opens, before the user has asked to type.
10
10
  * The container needs `tabIndex={-1}` for this to work.
11
+ *
12
+ * `"container"` and `"first"` both step aside when focus is ALREADY inside the
13
+ * container, on something other than the container itself. That is a child the app
14
+ * focused on purpose — an `autoFocus`, which React honours before this hook's effect
15
+ * runs — and the keyboard it brings up is the one the user was promised. Moving focus
16
+ * off it lost keksdose's create card its amount field the instant it got it, and on a
17
+ * phone the numpad sheet with it, which shows only while the field is focused (live
18
+ * #402). A function is the caller's explicit choice and is always followed.
11
19
  */
12
20
  initialFocus?: "container" | "first" | (() => HTMLElement | null);
13
21
  }
@@ -1,18 +1,23 @@
1
1
  "use client";
2
- import { useEffect, useRef } from "react";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
  const FOCUSABLE = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
4
4
  const TRAP_ATTR = "data-focus-trap";
5
+ function focusedElement() {
6
+ return typeof document === "undefined" ? null : document.activeElement;
7
+ }
5
8
  function useFocusTrap(containerRef, { active = true, restoreFocus = true, initialFocus = "container" } = {}) {
6
- const optsRef = useRef({ restoreFocus, initialFocus });
7
- useEffect(() => {
8
- optsRef.current = { restoreFocus, initialFocus };
9
+ const optsRef = useRef({ active, restoreFocus, initialFocus });
10
+ useLayoutEffect(() => {
11
+ optsRef.current = { active, restoreFocus, initialFocus };
9
12
  });
13
+ const [opener, setOpener] = useState(void 0);
14
+ if (active && opener === void 0) setOpener(focusedElement());
15
+ else if (!active && opener !== void 0) setOpener(void 0);
10
16
  useEffect(() => {
11
17
  if (!active) return;
12
18
  const container = containerRef.current;
13
19
  if (!container) return;
14
20
  container.setAttribute(TRAP_ATTR, "");
15
- const previouslyFocused = document.activeElement;
16
21
  const inNestedTrap = (node) => {
17
22
  if (!node) return false;
18
23
  const owner = node.closest(`[${TRAP_ATTR}]`);
@@ -25,12 +30,12 @@ function useFocusTrap(containerRef, { active = true, restoreFocus = true, initia
25
30
  (el) => !inNestedTrap(el)
26
31
  );
27
32
  const { initialFocus: how } = optsRef.current;
33
+ const focused = document.activeElement;
34
+ const childHasFocus = focused !== container && container.contains(focused);
28
35
  if (typeof how === "function") {
29
36
  (how() ?? container).focus();
30
- } else if (how === "first") {
31
- (tabbables()[0] ?? container).focus();
32
- } else {
33
- container.focus();
37
+ } else if (!childHasFocus) {
38
+ (how === "first" ? tabbables()[0] ?? container : container).focus();
34
39
  }
35
40
  const onKeyDown = (e) => {
36
41
  if (e.key !== "Tab") return;
@@ -56,10 +61,11 @@ function useFocusTrap(containerRef, { active = true, restoreFocus = true, initia
56
61
  return () => {
57
62
  container.removeEventListener("keydown", onKeyDown);
58
63
  container.removeAttribute(TRAP_ATTR);
64
+ if (container.isConnected && optsRef.current.active) return;
59
65
  if (!optsRef.current.restoreFocus) return;
60
- if (previouslyFocused?.isConnected) previouslyFocused.focus?.();
66
+ if (opener?.isConnected) opener.focus?.();
61
67
  };
62
- }, [active, containerRef]);
68
+ }, [active, containerRef, opener]);
63
69
  }
64
70
  export {
65
71
  useFocusTrap
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/**\n * Keep keyboard focus inside an open overlay, and give it back when it closes.\n *\n * Extracted from `Modal`, which was the ONLY component in this package that did any of\n * this. Every other overlay — the full-bleed dialog a phone's row editor opens, the\n * picker sheet, the numpad, the tour card — declared `role=\"dialog\" aria-modal=\"true\"`\n * and then left focus on the page behind it. That is the worst of the two possible\n * bugs: assistive technology hides the page because of `aria-modal`, while the user's\n * focus is still down there in the part that is now hidden, so Tab walks through\n * controls the screen reader refuses to describe.\n *\n * What this does beyond Modal's original copy:\n *\n * - **Recomputes the tabbable list on every Tab**, not once when the overlay opens.\n * These panels have conditionally-rendered contents — a search box that appears\n * once options load, a \"clear\" button that exists only while a filter is set — and\n * a list captured at open time traps focus against elements that have since gone.\n * - **Guards the restore target.** The element focused before opening may have left\n * the DOM by the time the overlay closes (the row that owned the button was\n * filtered away). Calling `.focus()` on a detached node silently drops focus to\n * `<body>`, which for a keyboard user means losing their place in the page\n * entirely. When the target is gone, focus falls back to the nearest thing that\n * still exists rather than to nothing.\n * - **Nests.** An overlay can open another — a picker inside a dialog. Only the\n * innermost trap handles a keystroke, and closing it hands control back to the one\n * underneath. Innermost is decided by DOM containment rather than by mount order;\n * see the note on `TRAP_ATTR` for why the obvious stack is wrong.\n * `use-overlay-history.ts` reasons about the same nesting problem for the back\n * button; this is the focus half of it.\n */\n\n/**\n * Tabbable descendants, in DOM order.\n *\n * Deliberately the same selector `Modal` used, rather than a more thorough one that\n * also walks `inert`, `visibility: hidden` and open `<details>`: this is the set that\n * has been in production across three apps, and widening it here would change which\n * element receives focus in overlays nobody asked me to change the behaviour of.\n */\nconst FOCUSABLE =\n 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Marks a container whose trap is currently active.\n *\n * \"Which trap is innermost?\" is answered by DOM CONTAINMENT, not by mount order. The\n * obvious implementation — a module-level stack pushed in the effect — is wrong, and\n * wrong in a way that passes a casual reading: React runs CHILD effects before PARENT\n * effects, so a nested overlay pushes first and the outer one ends up on top of the\n * stack, claiming to be innermost. The outer trap then handles the keystroke and wraps\n * focus to its own first element, jumping the user out of the picker they had open.\n *\n * An attribute on the live DOM cannot disagree with the DOM.\n */\nconst TRAP_ATTR = \"data-focus-trap\";\n\nexport interface FocusTrapOptions {\n /** Trap only while this is true. Mount-time `true` is the common case. */\n active?: boolean;\n /** Return focus to whatever had it before the trap engaged. Default `true`. */\n restoreFocus?: boolean;\n /**\n * What to focus when the trap engages. Default is the container itself, which is\n * what `Modal` does and why: focusing the first FIELD pops the software keyboard on\n * a phone the moment a dialog opens, before the user has asked to type.\n * The container needs `tabIndex={-1}` for this to work.\n */\n initialFocus?: \"container\" | \"first\" | (() => HTMLElement | null);\n}\n\nexport function useFocusTrap(\n containerRef: React.RefObject<HTMLElement | null>,\n { active = true, restoreFocus = true, initialFocus = \"container\" }: FocusTrapOptions = {},\n): void {\n // Latest-ref so a caller passing an inline `initialFocus` thunk — which every caller\n // will — does not tear the trap down and re-engage it on each render, stealing focus\n // back to the top of the panel mid-interaction.\n const optsRef = useRef({ restoreFocus, initialFocus });\n useEffect(() => {\n optsRef.current = { restoreFocus, initialFocus };\n });\n\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n container.setAttribute(TRAP_ATTR, \"\");\n const previouslyFocused = document.activeElement as HTMLElement | null;\n\n /** Is `node` inside a trap nested within this one? */\n const inNestedTrap = (node: Element | null) => {\n if (!node) return false;\n const owner = node.closest(`[${TRAP_ATTR}]`);\n return owner !== null && owner !== container;\n };\n\n const tabbables = () =>\n Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n // A tabbable inside a NESTED open overlay belongs to that overlay's trap, not\n // to this one — otherwise the outer list spans both panels and Tab from the\n // inner panel's last control wraps to the OUTER panel's first.\n (el) => !inNestedTrap(el),\n );\n\n const { initialFocus: how } = optsRef.current;\n if (typeof how === \"function\") {\n (how() ?? container).focus();\n } else if (how === \"first\") {\n (tabbables()[0] ?? container).focus();\n } else {\n container.focus();\n }\n\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key !== \"Tab\") return;\n // Only the innermost trap acts. The event bubbles through every ancestor\n // container, so without this an outer dialog and an inner picker both handle one\n // Tab and fight over where focus lands.\n if (inNestedTrap(document.activeElement)) return;\n\n // Recomputed per keystroke — see the note at the top of this file.\n const items = tabbables();\n if (items.length === 0) {\n e.preventDefault();\n container.focus();\n return;\n }\n const first = items[0];\n const last = items[items.length - 1];\n const activeEl = document.activeElement;\n if (e.shiftKey && (activeEl === first || activeEl === container)) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && activeEl === last) {\n e.preventDefault();\n first.focus();\n }\n };\n\n // On the container, not the document: a portalled child panel (an open dropdown\n // rendered to <body>) is outside this subtree and manages its own focus, which is\n // the behaviour Modal's comment describes and consumers already depend on.\n container.addEventListener(\"keydown\", onKeyDown);\n\n return () => {\n container.removeEventListener(\"keydown\", onKeyDown);\n container.removeAttribute(TRAP_ATTR);\n\n if (!optsRef.current.restoreFocus) return;\n // `isConnected` is the whole point: restoring to a node that has been removed\n // sends focus to <body> and the user loses their place entirely. When the\n // original is gone, leave focus where the browser put it rather than moving it\n // somewhere arbitrary — a wrong guess is worse than no move.\n if (previouslyFocused?.isConnected) previouslyFocused.focus?.();\n };\n }, [active, containerRef]);\n}\n"],"mappings":";AAAA,SAAS,WAAW,cAAc;AAyClC,MAAM,YACJ;AAcF,MAAM,YAAY;AAgBX,SAAS,aACd,cACA,EAAE,SAAS,MAAM,eAAe,MAAM,eAAe,YAAY,IAAsB,CAAC,GAClF;AAIN,QAAM,UAAU,OAAO,EAAE,cAAc,aAAa,CAAC;AACrD,YAAU,MAAM;AACd,YAAQ,UAAU,EAAE,cAAc,aAAa;AAAA,EACjD,CAAC;AAED,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,UAAW;AAEhB,cAAU,aAAa,WAAW,EAAE;AACpC,UAAM,oBAAoB,SAAS;AAGnC,UAAM,eAAe,CAAC,SAAyB;AAC7C,UAAI,CAAC,KAAM,QAAO;AAClB,YAAM,QAAQ,KAAK,QAAQ,IAAI,SAAS,GAAG;AAC3C,aAAO,UAAU,QAAQ,UAAU;AAAA,IACrC;AAEA,UAAM,YAAY,MAChB,MAAM,KAAK,UAAU,iBAA8B,SAAS,CAAC,EAAE;AAAA;AAAA;AAAA;AAAA,MAI7D,CAAC,OAAO,CAAC,aAAa,EAAE;AAAA,IAC1B;AAEF,UAAM,EAAE,cAAc,IAAI,IAAI,QAAQ;AACtC,QAAI,OAAO,QAAQ,YAAY;AAC7B,OAAC,IAAI,KAAK,WAAW,MAAM;AAAA,IAC7B,WAAW,QAAQ,SAAS;AAC1B,OAAC,UAAU,EAAE,CAAC,KAAK,WAAW,MAAM;AAAA,IACtC,OAAO;AACL,gBAAU,MAAM;AAAA,IAClB;AAEA,UAAM,YAAY,CAAC,MAAqB;AACtC,UAAI,EAAE,QAAQ,MAAO;AAIrB,UAAI,aAAa,SAAS,aAAa,EAAG;AAG1C,YAAM,QAAQ,UAAU;AACxB,UAAI,MAAM,WAAW,GAAG;AACtB,UAAE,eAAe;AACjB,kBAAU,MAAM;AAChB;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,CAAC;AACrB,YAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,YAAM,WAAW,SAAS;AAC1B,UAAI,EAAE,aAAa,aAAa,SAAS,aAAa,YAAY;AAChE,UAAE,eAAe;AACjB,aAAK,MAAM;AAAA,MACb,WAAW,CAAC,EAAE,YAAY,aAAa,MAAM;AAC3C,UAAE,eAAe;AACjB,cAAM,MAAM;AAAA,MACd;AAAA,IACF;AAKA,cAAU,iBAAiB,WAAW,SAAS;AAE/C,WAAO,MAAM;AACX,gBAAU,oBAAoB,WAAW,SAAS;AAClD,gBAAU,gBAAgB,SAAS;AAEnC,UAAI,CAAC,QAAQ,QAAQ,aAAc;AAKnC,UAAI,mBAAmB,YAAa,mBAAkB,QAAQ;AAAA,IAChE;AAAA,EACF,GAAG,CAAC,QAAQ,YAAY,CAAC;AAC3B;","names":[]}
1
+ {"version":3,"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\n\n/**\n * Keep keyboard focus inside an open overlay, and give it back when it closes.\n *\n * Extracted from `Modal`, which was the ONLY component in this package that did any of\n * this. Every other overlay — the full-bleed dialog a phone's row editor opens, the\n * picker sheet, the numpad, the tour card — declared `role=\"dialog\" aria-modal=\"true\"`\n * and then left focus on the page behind it. That is the worst of the two possible\n * bugs: assistive technology hides the page because of `aria-modal`, while the user's\n * focus is still down there in the part that is now hidden, so Tab walks through\n * controls the screen reader refuses to describe.\n *\n * What this does beyond Modal's original copy:\n *\n * - **Recomputes the tabbable list on every Tab**, not once when the overlay opens.\n * These panels have conditionally-rendered contents — a search box that appears\n * once options load, a \"clear\" button that exists only while a filter is set — and\n * a list captured at open time traps focus against elements that have since gone.\n * - **Guards the restore target.** The element focused before opening may have left\n * the DOM by the time the overlay closes (the row that owned the button was\n * filtered away). Calling `.focus()` on a detached node silently drops focus to\n * `<body>`, which for a keyboard user means losing their place in the page\n * entirely. When the target is gone, focus falls back to the nearest thing that\n * still exists rather than to nothing.\n * - **Leaves an app's own autofocus alone** (0.33.1). React honours a child's\n * `autoFocus` while it commits, and this hook's effect runs AFTER that commit, so\n * focusing the container there took focus straight back off the field the app had\n * asked for. See `initialFocus`.\n * - **Nests.** An overlay can open another — a picker inside a dialog. Only the\n * innermost trap handles a keystroke, and closing it hands control back to the one\n * underneath. Innermost is decided by DOM containment rather than by mount order;\n * see the note on `TRAP_ATTR` for why the obvious stack is wrong.\n * `use-overlay-history.ts` reasons about the same nesting problem for the back\n * button; this is the focus half of it.\n */\n\n/**\n * Tabbable descendants, in DOM order.\n *\n * Deliberately the same selector `Modal` used, rather than a more thorough one that\n * also walks `inert`, `visibility: hidden` and open `<details>`: this is the set that\n * has been in production across three apps, and widening it here would change which\n * element receives focus in overlays nobody asked me to change the behaviour of.\n */\nconst FOCUSABLE =\n 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Marks a container whose trap is currently active.\n *\n * \"Which trap is innermost?\" is answered by DOM CONTAINMENT, not by mount order. The\n * obvious implementation — a module-level stack pushed in the effect — is wrong, and\n * wrong in a way that passes a casual reading: React runs CHILD effects before PARENT\n * effects, so a nested overlay pushes first and the outer one ends up on top of the\n * stack, claiming to be innermost. The outer trap then handles the keystroke and wraps\n * focus to its own first element, jumping the user out of the picker they had open.\n *\n * An attribute on the live DOM cannot disagree with the DOM.\n */\nconst TRAP_ATTR = \"data-focus-trap\";\n\nexport interface FocusTrapOptions {\n /** Trap only while this is true. Mount-time `true` is the common case. */\n active?: boolean;\n /** Return focus to whatever had it before the trap engaged. Default `true`. */\n restoreFocus?: boolean;\n /**\n * What to focus when the trap engages. Default is the container itself, which is\n * what `Modal` does and why: focusing the first FIELD pops the software keyboard on\n * a phone the moment a dialog opens, before the user has asked to type.\n * The container needs `tabIndex={-1}` for this to work.\n *\n * `\"container\"` and `\"first\"` both step aside when focus is ALREADY inside the\n * container, on something other than the container itself. That is a child the app\n * focused on purpose — an `autoFocus`, which React honours before this hook's effect\n * runs — and the keyboard it brings up is the one the user was promised. Moving focus\n * off it lost keksdose's create card its amount field the instant it got it, and on a\n * phone the numpad sheet with it, which shows only while the field is focused (live\n * #402). A function is the caller's explicit choice and is always followed.\n */\n initialFocus?: \"container\" | \"first\" | (() => HTMLElement | null);\n}\n\n/** Whatever has focus right now; nothing on the server, where there is no document. */\nfunction focusedElement(): Element | null {\n return typeof document === \"undefined\" ? null : document.activeElement;\n}\n\nexport function useFocusTrap(\n containerRef: React.RefObject<HTMLElement | null>,\n { active = true, restoreFocus = true, initialFocus = \"container\" }: FocusTrapOptions = {},\n): void {\n // Latest-ref so a caller passing an inline `initialFocus` thunk — which every caller\n // will — does not tear the trap down and re-engage it on each render, stealing focus\n // back to the top of the panel mid-interaction. Written in a LAYOUT effect so that\n // the trap's cleanup, which runs after the layout effects of the same commit, reads\n // this render's `active` rather than the last one's — see the cleanup for why.\n const optsRef = useRef({ active, restoreFocus, initialFocus });\n useLayoutEffect(() => {\n optsRef.current = { active, restoreFocus, initialFocus };\n });\n\n // Where focus was BEFORE the trap engaged: the element it is given back to on close.\n //\n // Read DURING the render that turns the trap on, and that timing is the point. React\n // honours a child's `autoFocus` while it commits that render, before any effect runs,\n // so the effect below asking `document.activeElement` would get the child, inside the\n // overlay. Restoring to that on close aimed focus at the dialog that was closing —\n // already out of the DOM, so focus fell to <body> rather than going back to the\n // button that opened it.\n //\n // State adjusted during render (the pattern `Modal` uses for `open`) rather than a\n // ref written there: set once per opening and cleared on release, so a re-render\n // while open cannot replace it with something inside, and StrictMode's second render\n // and second effect both see the same element. `undefined` means \"not captured\";\n // `null` is a capture that found nothing.\n const [opener, setOpener] = useState<Element | null | undefined>(undefined);\n if (active && opener === undefined) setOpener(focusedElement());\n else if (!active && opener !== undefined) setOpener(undefined);\n\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n container.setAttribute(TRAP_ATTR, \"\");\n\n /** Is `node` inside a trap nested within this one? */\n const inNestedTrap = (node: Element | null) => {\n if (!node) return false;\n const owner = node.closest(`[${TRAP_ATTR}]`);\n return owner !== null && owner !== container;\n };\n\n const tabbables = () =>\n Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n // A tabbable inside a NESTED open overlay belongs to that overlay's trap, not\n // to this one — otherwise the outer list spans both panels and Tab from the\n // inner panel's last control wraps to the OUTER panel's first.\n (el) => !inNestedTrap(el),\n );\n\n const { initialFocus: how } = optsRef.current;\n const focused = document.activeElement;\n // A child that already has focus was given it on purpose — see `initialFocus`.\n const childHasFocus = focused !== container && container.contains(focused);\n if (typeof how === \"function\") {\n (how() ?? container).focus();\n } else if (!childHasFocus) {\n (how === \"first\" ? (tabbables()[0] ?? container) : container).focus();\n }\n\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key !== \"Tab\") return;\n // Only the innermost trap acts. The event bubbles through every ancestor\n // container, so without this an outer dialog and an inner picker both handle one\n // Tab and fight over where focus lands.\n if (inNestedTrap(document.activeElement)) return;\n\n // Recomputed per keystroke — see the note at the top of this file.\n const items = tabbables();\n if (items.length === 0) {\n e.preventDefault();\n container.focus();\n return;\n }\n const first = items[0];\n const last = items[items.length - 1];\n const activeEl = document.activeElement;\n if (e.shiftKey && (activeEl === first || activeEl === container)) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && activeEl === last) {\n e.preventDefault();\n first.focus();\n }\n };\n\n // On the container, not the document: a portalled child panel (an open dropdown\n // rendered to <body>) is outside this subtree and manages its own focus, which is\n // the behaviour Modal's comment describes and consumers already depend on.\n container.addEventListener(\"keydown\", onKeyDown);\n\n return () => {\n container.removeEventListener(\"keydown\", onKeyDown);\n container.removeAttribute(TRAP_ATTR);\n\n // Only when the trap is really being released: the container has left the page,\n // or `active` has turned false. StrictMode runs this cleanup once more right after\n // mounting, with the overlay still up, and then the effect again — and React does\n // not re-apply `autoFocus` for that second run. Restoring here would pull focus\n // out of the autofocused field for good: live #402 again, in development.\n if (container.isConnected && optsRef.current.active) return;\n if (!optsRef.current.restoreFocus) return;\n // `isConnected` is the whole point: restoring to a node that has been removed\n // sends focus to <body> and the user loses their place entirely. When the\n // original is gone, leave focus where the browser put it rather than moving it\n // somewhere arbitrary — a wrong guess is worse than no move.\n if (opener?.isConnected) (opener as HTMLElement).focus?.();\n };\n }, [active, containerRef, opener]);\n}\n"],"mappings":";AAAA,SAAS,WAAW,iBAAiB,QAAQ,gBAAgB;AA6C7D,MAAM,YACJ;AAcF,MAAM,YAAY;AAyBlB,SAAS,iBAAiC;AACxC,SAAO,OAAO,aAAa,cAAc,OAAO,SAAS;AAC3D;AAEO,SAAS,aACd,cACA,EAAE,SAAS,MAAM,eAAe,MAAM,eAAe,YAAY,IAAsB,CAAC,GAClF;AAMN,QAAM,UAAU,OAAO,EAAE,QAAQ,cAAc,aAAa,CAAC;AAC7D,kBAAgB,MAAM;AACpB,YAAQ,UAAU,EAAE,QAAQ,cAAc,aAAa;AAAA,EACzD,CAAC;AAgBD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAqC,MAAS;AAC1E,MAAI,UAAU,WAAW,OAAW,WAAU,eAAe,CAAC;AAAA,WACrD,CAAC,UAAU,WAAW,OAAW,WAAU,MAAS;AAE7D,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,UAAW;AAEhB,cAAU,aAAa,WAAW,EAAE;AAGpC,UAAM,eAAe,CAAC,SAAyB;AAC7C,UAAI,CAAC,KAAM,QAAO;AAClB,YAAM,QAAQ,KAAK,QAAQ,IAAI,SAAS,GAAG;AAC3C,aAAO,UAAU,QAAQ,UAAU;AAAA,IACrC;AAEA,UAAM,YAAY,MAChB,MAAM,KAAK,UAAU,iBAA8B,SAAS,CAAC,EAAE;AAAA;AAAA;AAAA;AAAA,MAI7D,CAAC,OAAO,CAAC,aAAa,EAAE;AAAA,IAC1B;AAEF,UAAM,EAAE,cAAc,IAAI,IAAI,QAAQ;AACtC,UAAM,UAAU,SAAS;AAEzB,UAAM,gBAAgB,YAAY,aAAa,UAAU,SAAS,OAAO;AACzE,QAAI,OAAO,QAAQ,YAAY;AAC7B,OAAC,IAAI,KAAK,WAAW,MAAM;AAAA,IAC7B,WAAW,CAAC,eAAe;AACzB,OAAC,QAAQ,UAAW,UAAU,EAAE,CAAC,KAAK,YAAa,WAAW,MAAM;AAAA,IACtE;AAEA,UAAM,YAAY,CAAC,MAAqB;AACtC,UAAI,EAAE,QAAQ,MAAO;AAIrB,UAAI,aAAa,SAAS,aAAa,EAAG;AAG1C,YAAM,QAAQ,UAAU;AACxB,UAAI,MAAM,WAAW,GAAG;AACtB,UAAE,eAAe;AACjB,kBAAU,MAAM;AAChB;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,CAAC;AACrB,YAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,YAAM,WAAW,SAAS;AAC1B,UAAI,EAAE,aAAa,aAAa,SAAS,aAAa,YAAY;AAChE,UAAE,eAAe;AACjB,aAAK,MAAM;AAAA,MACb,WAAW,CAAC,EAAE,YAAY,aAAa,MAAM;AAC3C,UAAE,eAAe;AACjB,cAAM,MAAM;AAAA,MACd;AAAA,IACF;AAKA,cAAU,iBAAiB,WAAW,SAAS;AAE/C,WAAO,MAAM;AACX,gBAAU,oBAAoB,WAAW,SAAS;AAClD,gBAAU,gBAAgB,SAAS;AAOnC,UAAI,UAAU,eAAe,QAAQ,QAAQ,OAAQ;AACrD,UAAI,CAAC,QAAQ,QAAQ,aAAc;AAKnC,UAAI,QAAQ,YAAa,CAAC,OAAuB,QAAQ;AAAA,IAC3D;AAAA,EACF,GAAG,CAAC,QAAQ,cAAc,MAAM,CAAC;AACnC;","names":[]}
@@ -1,18 +1,23 @@
1
- import { fx as UiKitLabels } from '../delete-account-setting-Dy4kJgUm.js';
1
+ import { fJ as UiKitLabels } from '../delete-account-setting-DAgGBrbn.js';
2
2
  import 'react';
3
3
  import '../components/tooltip.js';
4
4
  import '../hooks/use-anchored-rect.js';
5
5
  import '../lib/clipping.js';
6
- import '../data-table-labels-kpAAVi6g.js';
6
+ import '../data-table-labels-BJ5JE_X2.js';
7
7
  import '../components/data-table-sort.js';
8
8
  import '../components/swipeable-row.js';
9
+ import '../components/status-dot.js';
10
+ import '../components/progress-bar.js';
11
+ import '../components/signed-amount.js';
12
+ import '../components/stat-tile.js';
13
+ import '../components/chip.js';
14
+ import 'lucide-react';
15
+ import '../components/field-parts.js';
16
+ import '../components/write-lock.js';
9
17
  import '../components/mini-calendar.js';
10
18
  import '../components/calendar-heatmap.js';
11
19
  import '../components/popover.js';
12
20
  import '../hooks/use-breakpoint.js';
13
- import '../components/chip.js';
14
- import 'lucide-react';
15
- import '../components/field-parts.js';
16
21
  import '../components/field-sync.js';
17
22
  import '../wizard/types.js';
18
23
  import '../tour/tour.js';
@@ -24,7 +29,6 @@ import '../components/page-contents.js';
24
29
  import '../components/series-chart-labels.js';
25
30
  import '../components/pie-chart-labels.js';
26
31
  import '../components/sparkline.js';
27
- import '../components/stat-tile.js';
28
32
  import '../components/signature-pad.js';
29
33
  import '../components/password-strength.js';
30
34
  import '../components/swatch-picker.js';
@@ -36,9 +40,6 @@ import '../hooks/use-search-param-state.js';
36
40
  import '../components/measured-grid.js';
37
41
  import '../components/user-avatar.js';
38
42
  import '../lib/person-name.js';
39
- import '../components/status-dot.js';
40
- import '../components/progress-bar.js';
41
- import '../components/signed-amount.js';
42
43
  import '../shell/topbar-action-menu.js';
43
44
  import '../components/menu-item.js';
44
45
  import '../components/floating-panel.js';
@@ -54,7 +55,6 @@ import '../components/error-boundary.js';
54
55
  import '../components/authed-image.js';
55
56
  import '../components/image-grid.js';
56
57
  import '../components/lightbox.js';
57
- import '../components/write-lock.js';
58
58
  import '../auth/sign-in-form.js';
59
59
  import '../landing/access.js';
60
60
  import '../landing/landing-labels.js';
@@ -81,6 +81,7 @@ import '../admin/admin-parts.js';
81
81
  import '../admin/admin-action-log.js';
82
82
  import '../settings/settings-labels.js';
83
83
  import '../demo/demo-labels.js';
84
+ import './plurals.js';
84
85
  import '../components/appearance-labels.js';
85
86
  import '../theme/contrast.js';
86
87
  import '../theme/text-size.js';
@@ -86,6 +86,7 @@ import { DEFAULT_SETTINGS_LABELS } from "../settings/settings-labels.js";
86
86
  import { DEFAULT_LANDING_LABELS } from "../landing/landing-labels.js";
87
87
  import { DEFAULT_DEMO_LABELS } from "../demo/demo-labels.js";
88
88
  import { DEFAULT_BILLING_LABELS } from "../billing/billing-labels.js";
89
+ import { DEFAULT_PLAN_CHANGE_LABELS } from "../admin/plan-change.js";
89
90
  import { DEFAULT_APPEARANCE_LABELS } from "../components/appearance-labels.js";
90
91
  import { DEFAULT_ROW_ACTIONS_LABELS } from "../components/row-actions.js";
91
92
  import { DEFAULT_APP_SHELL_MORE_LABELS } from "../shell/app-shell.js";
@@ -210,6 +211,7 @@ const DEFAULT_UI_KIT_LABELS = {
210
211
  landing: DEFAULT_LANDING_LABELS,
211
212
  demo: DEFAULT_DEMO_LABELS,
212
213
  billing: DEFAULT_BILLING_LABELS,
214
+ planChange: DEFAULT_PLAN_CHANGE_LABELS,
213
215
  appearance: DEFAULT_APPEARANCE_LABELS,
214
216
  rowActions: DEFAULT_ROW_ACTIONS_LABELS,
215
217
  appShellMore: DEFAULT_APP_SHELL_MORE_LABELS,