@eifi1/ui-kit 0.32.1 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. package/README.md +16 -8
  2. package/dist/account/delete-account-setting.d.ts +10 -10
  3. package/dist/account/delete-account-setting.js +2 -1
  4. package/dist/account/delete-account-setting.js.map +1 -1
  5. package/dist/account/email-change-setting.d.ts +5 -0
  6. package/dist/account/email-change-setting.js +4 -3
  7. package/dist/account/email-change-setting.js.map +1 -1
  8. package/dist/account/sessions-setting.d.ts +4 -0
  9. package/dist/account/sessions-setting.js +3 -2
  10. package/dist/account/sessions-setting.js.map +1 -1
  11. package/dist/admin/admin-action-confirm.d.ts +10 -10
  12. package/dist/admin/admin-action-confirm.js.map +1 -1
  13. package/dist/admin/invitations-panel.d.ts +10 -10
  14. package/dist/admin/invitations-panel.js +4 -3
  15. package/dist/admin/invitations-panel.js.map +1 -1
  16. package/dist/admin/plan-change.d.ts +100 -0
  17. package/dist/admin/plan-change.js +208 -0
  18. package/dist/admin/plan-change.js.map +1 -0
  19. package/dist/admin/reviewer-scope-editor.d.ts +10 -10
  20. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  21. package/dist/admin/role-select.d.ts +7 -7
  22. package/dist/admin/role-select.js.map +1 -1
  23. package/dist/admin/transfer-ownership-dialog.d.ts +10 -10
  24. package/dist/admin/user-roster.d.ts +7 -7
  25. package/dist/admin/user-roster.js +2 -2
  26. package/dist/admin/user-roster.js.map +1 -1
  27. package/dist/auth/auth-errors.d.ts +13 -2
  28. package/dist/auth/auth-errors.js +5 -1
  29. package/dist/auth/auth-errors.js.map +1 -1
  30. package/dist/auth/complete-name-dialog.d.ts +4 -1
  31. package/dist/auth/complete-name-dialog.js +2 -1
  32. package/dist/auth/complete-name-dialog.js.map +1 -1
  33. package/dist/auth/register-form.d.ts +10 -10
  34. package/dist/billing/billing-banner.d.ts +13 -11
  35. package/dist/billing/billing-banner.js.map +1 -1
  36. package/dist/billing/billing-labels.d.ts +10 -10
  37. package/dist/billing/billing-labels.js +10 -1
  38. package/dist/billing/billing-labels.js.map +1 -1
  39. package/dist/billing/billing-lock.d.ts +39 -12
  40. package/dist/billing/billing-lock.js +9 -3
  41. package/dist/billing/billing-lock.js.map +1 -1
  42. package/dist/billing/billing-overview.d.ts +154 -0
  43. package/dist/billing/billing-overview.js +2 -0
  44. package/dist/billing/billing-overview.js.map +1 -0
  45. package/dist/billing/billing-standing.d.ts +10 -10
  46. package/dist/billing/checkout-processing.d.ts +217 -0
  47. package/dist/billing/checkout-processing.js +198 -0
  48. package/dist/billing/checkout-processing.js.map +1 -0
  49. package/dist/billing/checkout-return.d.ts +51 -0
  50. package/dist/billing/checkout-return.js +43 -0
  51. package/dist/billing/checkout-return.js.map +1 -0
  52. package/dist/billing/pay-page.d.ts +210 -0
  53. package/dist/billing/pay-page.js +113 -0
  54. package/dist/billing/pay-page.js.map +1 -0
  55. package/dist/billing/plan-card.d.ts +10 -10
  56. package/dist/billing/plan-limit-notice.d.ts +10 -10
  57. package/dist/billing/plan-limit-toast.d.ts +143 -0
  58. package/dist/billing/plan-limit-toast.js +40 -0
  59. package/dist/billing/plan-limit-toast.js.map +1 -0
  60. package/dist/billing/subscription-actions.d.ts +38 -15
  61. package/dist/billing/subscription-actions.js +9 -2
  62. package/dist/billing/subscription-actions.js.map +1 -1
  63. package/dist/billing/subscription-status-chip.d.ts +9 -9
  64. package/dist/bin/eifi1-pay-page.mjs +166 -0
  65. package/dist/chart.d.ts +10 -10
  66. package/dist/components/account-chips.d.ts +6 -1
  67. package/dist/components/account-settings.d.ts +4 -1
  68. package/dist/components/account-settings.js +2 -1
  69. package/dist/components/account-settings.js.map +1 -1
  70. package/dist/components/amount-input.d.ts +10 -10
  71. package/dist/components/autocomplete.d.ts +2 -1
  72. package/dist/components/autocomplete.js +3 -1
  73. package/dist/components/autocomplete.js.map +1 -1
  74. package/dist/components/button-group.d.ts +10 -10
  75. package/dist/components/button-group.js +1 -1
  76. package/dist/components/button-group.js.map +1 -1
  77. package/dist/components/calculator.d.ts +10 -10
  78. package/dist/components/calculator.js +3 -3
  79. package/dist/components/calculator.js.map +1 -1
  80. package/dist/components/checkbox-group.d.ts +2 -1
  81. package/dist/components/checkbox-group.js.map +1 -1
  82. package/dist/components/checkbox.d.ts +2 -1
  83. package/dist/components/checkbox.js.map +1 -1
  84. package/dist/components/chip.d.ts +2 -1
  85. package/dist/components/chip.js +19 -17
  86. package/dist/components/chip.js.map +1 -1
  87. package/dist/components/choice-card.d.ts +3 -1
  88. package/dist/components/choice-card.js +1 -1
  89. package/dist/components/choice-card.js.map +1 -1
  90. package/dist/components/combobox.d.ts +4 -3
  91. package/dist/components/combobox.js.map +1 -1
  92. package/dist/components/confirm-dialog.d.ts +10 -10
  93. package/dist/components/confirm-dialog.js +6 -3
  94. package/dist/components/confirm-dialog.js.map +1 -1
  95. package/dist/components/copy-button.d.ts +11 -11
  96. package/dist/components/country-select.d.ts +2 -1
  97. package/dist/components/country-select.js.map +1 -1
  98. package/dist/components/danger-confirm.d.ts +10 -10
  99. package/dist/components/danger-confirm.js.map +1 -1
  100. package/dist/components/data-table-cells.d.ts +12 -1
  101. package/dist/components/data-table-filter-popover.d.ts +12 -1
  102. package/dist/components/data-table-filters.d.ts +12 -1
  103. package/dist/components/data-table-labels.d.ts +12 -1
  104. package/dist/components/data-table-pagination.d.ts +12 -1
  105. package/dist/components/data-table.d.ts +12 -1
  106. package/dist/components/data-table.js +3 -3
  107. package/dist/components/data-table.js.map +1 -1
  108. package/dist/components/date-picker.js +6 -4
  109. package/dist/components/date-picker.js.map +1 -1
  110. package/dist/components/entity-combobox.d.ts +3 -2
  111. package/dist/components/entity-combobox.js.map +1 -1
  112. package/dist/components/facing-pair.d.ts +10 -10
  113. package/dist/components/field-parts.d.ts +32 -4
  114. package/dist/components/field-parts.js +54 -19
  115. package/dist/components/field-parts.js.map +1 -1
  116. package/dist/components/file-button.d.ts +10 -10
  117. package/dist/components/file-button.js +3 -2
  118. package/dist/components/file-button.js.map +1 -1
  119. package/dist/components/file-dropzone.d.ts +11 -11
  120. package/dist/components/file-dropzone.js.map +1 -1
  121. package/dist/components/floating-panel.js +5 -5
  122. package/dist/components/floating-panel.js.map +1 -1
  123. package/dist/components/form-actions.d.ts +9 -9
  124. package/dist/components/form-actions.js +3 -3
  125. package/dist/components/form-actions.js.map +1 -1
  126. package/dist/components/iban-input.d.ts +10 -10
  127. package/dist/components/inline-edit-field.d.ts +2 -1
  128. package/dist/components/inline-edit-field.js +6 -6
  129. package/dist/components/inline-edit-field.js.map +1 -1
  130. package/dist/components/language-select.d.ts +10 -10
  131. package/dist/components/legal-page.d.ts +10 -10
  132. package/dist/components/legal.d.ts +10 -10
  133. package/dist/components/list.d.ts +2 -1
  134. package/dist/components/list.js +4 -5
  135. package/dist/components/list.js.map +1 -1
  136. package/dist/components/money-field.d.ts +10 -10
  137. package/dist/components/month-picker.js +54 -44
  138. package/dist/components/month-picker.js.map +1 -1
  139. package/dist/components/multi-entity-combobox.d.ts +3 -2
  140. package/dist/components/multi-entity-combobox.js.map +1 -1
  141. package/dist/components/number-field.d.ts +10 -10
  142. package/dist/components/number-input.d.ts +10 -10
  143. package/dist/components/numpad-sheet.d.ts +10 -10
  144. package/dist/components/numpad-sheet.js +2 -2
  145. package/dist/components/numpad-sheet.js.map +1 -1
  146. package/dist/components/page-header.js +6 -1
  147. package/dist/components/page-header.js.map +1 -1
  148. package/dist/components/phone-input.d.ts +10 -10
  149. package/dist/components/popover.d.ts +1 -1
  150. package/dist/components/popover.js +2 -0
  151. package/dist/components/popover.js.map +1 -1
  152. package/dist/components/row-actions.d.ts +5 -5
  153. package/dist/components/row-actions.js +130 -38
  154. package/dist/components/row-actions.js.map +1 -1
  155. package/dist/components/series-chart.d.ts +10 -10
  156. package/dist/components/settings-fields.d.ts +10 -10
  157. package/dist/components/share-card.d.ts +7 -7
  158. package/dist/components/share-card.js +26 -10
  159. package/dist/components/share-card.js.map +1 -1
  160. package/dist/components/status-dot.d.ts +1 -0
  161. package/dist/components/status-dot.js +2 -2
  162. package/dist/components/status-dot.js.map +1 -1
  163. package/dist/components/swatch-picker.d.ts +2 -1
  164. package/dist/components/swatch-picker.js.map +1 -1
  165. package/dist/components/swipeable-row.d.ts +88 -17
  166. package/dist/components/swipeable-row.js +46 -9
  167. package/dist/components/swipeable-row.js.map +1 -1
  168. package/dist/components/switch.d.ts +2 -1
  169. package/dist/components/switch.js.map +1 -1
  170. package/dist/components/table.d.ts +2 -1
  171. package/dist/components/table.js +6 -0
  172. package/dist/components/table.js.map +1 -1
  173. package/dist/components/text-link.d.ts +9 -9
  174. package/dist/components/time-input.d.ts +10 -10
  175. package/dist/components/toggle-group.d.ts +2 -1
  176. package/dist/components/toggle-group.js +1 -1
  177. package/dist/components/toggle-group.js.map +1 -1
  178. package/dist/components/translation-review-editor.d.ts +4 -0
  179. package/dist/components/translation-review-editor.js +4 -3
  180. package/dist/components/translation-review-editor.js.map +1 -1
  181. package/dist/components/translation-review-parts.d.ts +1 -0
  182. package/dist/components/translation-review.d.ts +1 -0
  183. package/dist/components/translation-review.js +15 -18
  184. package/dist/components/translation-review.js.map +1 -1
  185. package/dist/components/ui.d.ts +9 -9
  186. package/dist/components/ui.js +48 -28
  187. package/dist/components/ui.js.map +1 -1
  188. package/dist/components/use-table-state.d.ts +12 -1
  189. package/dist/components/user-avatar.d.ts +1 -0
  190. package/dist/components/write-lock.d.ts +101 -13
  191. package/dist/components/write-lock.js +37 -13
  192. package/dist/components/write-lock.js.map +1 -1
  193. package/dist/{data-table-labels-kpAAVi6g.d.ts → data-table-labels-BJ5JE_X2.d.ts} +17 -0
  194. package/dist/data-table.d.ts +12 -1
  195. package/dist/{delete-account-setting-FLyJf-kZ.d.ts → delete-account-setting-DiPX4Vue.d.ts} +365 -47
  196. package/dist/feedback/feedback-attachment.d.ts +10 -10
  197. package/dist/feedback/feedback-attachment.js +7 -7
  198. package/dist/feedback/feedback-attachment.js.map +1 -1
  199. package/dist/feedback/feedback-context.d.ts +10 -10
  200. package/dist/feedback/feedback-crash.d.ts +10 -10
  201. package/dist/feedback/feedback-dialog.d.ts +10 -10
  202. package/dist/feedback/feedback-inbox.d.ts +9 -9
  203. package/dist/feedback/feedback-inbox.js +7 -3
  204. package/dist/feedback/feedback-inbox.js.map +1 -1
  205. package/dist/feedback/feedback-labels.d.ts +10 -10
  206. package/dist/feedback/feedback-menu.d.ts +3 -3
  207. package/dist/feedback/feedback-record.d.ts +10 -10
  208. package/dist/feedback/feedback-row-detail.d.ts +10 -10
  209. package/dist/feedback/feedback-row-detail.js.map +1 -1
  210. package/dist/feedback/feedback-status-undo.d.ts +10 -10
  211. package/dist/feedback/feedback-submit.d.ts +10 -10
  212. package/dist/feedback/feedback-swipe.d.ts +10 -10
  213. package/dist/feedback/feedback-swipe.js +3 -6
  214. package/dist/feedback/feedback-swipe.js.map +1 -1
  215. package/dist/feedback/feedback-table.d.ts +10 -10
  216. package/dist/feedback/feedback-table.js.map +1 -1
  217. package/dist/feedback/feedback-thread.d.ts +10 -10
  218. package/dist/feedback.d.ts +10 -10
  219. package/dist/hooks/use-file-drop.d.ts +10 -10
  220. package/dist/i18n/defaults.d.ts +10 -10
  221. package/dist/i18n/defaults.js +2 -0
  222. package/dist/i18n/defaults.js.map +1 -1
  223. package/dist/i18n/german.d.ts +10 -10
  224. package/dist/i18n/german.js +34 -1
  225. package/dist/i18n/german.js.map +1 -1
  226. package/dist/i18n/kit-labels.d.ts +4 -4
  227. package/dist/i18n/kit-labels.js.map +1 -1
  228. package/dist/i18n/languages.d.ts +10 -10
  229. package/dist/i18n/locales/de-CH.d.ts +10 -10
  230. package/dist/i18n/locales/en.d.ts +10 -10
  231. package/dist/i18n/locales/es.d.ts +10 -10
  232. package/dist/i18n/locales/es.js +34 -1
  233. package/dist/i18n/locales/es.js.map +1 -1
  234. package/dist/i18n/locales/fr.d.ts +10 -10
  235. package/dist/i18n/locales/fr.js +35 -1
  236. package/dist/i18n/locales/fr.js.map +1 -1
  237. package/dist/i18n/locales/hu.d.ts +10 -10
  238. package/dist/i18n/locales/hu.js +36 -1
  239. package/dist/i18n/locales/hu.js.map +1 -1
  240. package/dist/i18n/locales/it.d.ts +10 -10
  241. package/dist/i18n/locales/it.js +34 -1
  242. package/dist/i18n/locales/it.js.map +1 -1
  243. package/dist/i18n/locales/zh.d.ts +10 -10
  244. package/dist/i18n/locales/zh.js +34 -1
  245. package/dist/i18n/locales/zh.js.map +1 -1
  246. package/dist/i18n/review.d.ts +10 -10
  247. package/dist/i18n/review.js +8 -1
  248. package/dist/i18n/review.js.map +1 -1
  249. package/dist/index.d.ts +10 -5
  250. package/dist/index.js +40 -2
  251. package/dist/index.js.map +1 -1
  252. package/dist/landing/landing-actions.d.ts +10 -10
  253. package/dist/landing/landing-sections.d.ts +10 -10
  254. package/dist/landing/public-header.d.ts +10 -10
  255. package/dist/landing/routing.d.ts +10 -10
  256. package/dist/pay/.well-known/apple-developer-merchantid-domain-association +1 -0
  257. package/dist/pay/index.html +30 -0
  258. package/dist/pay/pay.css +126 -0
  259. package/dist/pay/pay.js +198 -0
  260. package/dist/rhf/fields.d.ts +10 -10
  261. package/dist/rhf/form.d.ts +10 -10
  262. package/dist/rhf.d.ts +10 -10
  263. package/dist/settings/settings-section.d.ts +10 -10
  264. package/dist/shell/app-shell.d.ts +9 -9
  265. package/dist/shell/app-shell.js +2 -1
  266. package/dist/shell/app-shell.js.map +1 -1
  267. package/dist/shell/top-bar-brand.d.ts +10 -10
  268. package/dist/shell/topbar-action-menu.d.ts +1 -0
  269. package/dist/shell.d.ts +10 -10
  270. package/dist/testing/css-variables.d.ts +74 -0
  271. package/dist/testing/css-variables.generated.d.ts +7 -0
  272. package/dist/testing/css-variables.generated.js +8 -0
  273. package/dist/testing/css-variables.generated.js.map +1 -0
  274. package/dist/testing/css-variables.js +93 -0
  275. package/dist/testing/css-variables.js.map +1 -0
  276. package/dist/testing/index.d.ts +1 -0
  277. package/dist/testing/index.js +12 -0
  278. package/dist/testing/index.js.map +1 -0
  279. package/dist/theme/contrast-tokens.d.ts +44 -1
  280. package/dist/theme/contrast-tokens.js +21 -0
  281. package/dist/theme/contrast-tokens.js.map +1 -1
  282. package/dist/theme/contrast.d.ts +2 -2
  283. package/dist/theme/palette-derive.js +60 -11
  284. package/dist/theme/palette-derive.js.map +1 -1
  285. package/dist/theme/palette-presets.js +45 -43
  286. package/dist/theme/palette-presets.js.map +1 -1
  287. package/dist/wizard/stepper-nav.d.ts +11 -11
  288. package/dist/wizard/stepper-nav.js.map +1 -1
  289. package/dist/wizard.d.ts +10 -10
  290. package/package.json +10 -2
  291. package/src/account/delete-account-setting.tsx +6 -1
  292. package/src/account/email-change-setting.tsx +9 -3
  293. package/src/account/sessions-setting.tsx +7 -2
  294. package/src/admin/admin-action-confirm.tsx +2 -1
  295. package/src/admin/invitations-panel.tsx +9 -3
  296. package/src/admin/plan-change.tsx +459 -0
  297. package/src/admin/reviewer-scope-editor.tsx +2 -1
  298. package/src/admin/role-select.tsx +2 -1
  299. package/src/admin/user-roster.tsx +5 -3
  300. package/src/auth/auth-errors.ts +25 -2
  301. package/src/auth/complete-name-dialog.tsx +6 -2
  302. package/src/billing/billing-banner.tsx +3 -1
  303. package/src/billing/billing-labels.ts +45 -0
  304. package/src/billing/billing-lock.ts +36 -3
  305. package/src/billing/billing-overview.ts +58 -0
  306. package/src/billing/checkout-processing.ts +391 -0
  307. package/src/billing/checkout-return.ts +90 -0
  308. package/src/billing/pay-page.ts +242 -0
  309. package/src/billing/plan-limit-toast.tsx +91 -0
  310. package/src/billing/subscription-actions.tsx +41 -8
  311. package/src/components/account-settings.tsx +6 -2
  312. package/src/components/autocomplete.tsx +5 -2
  313. package/src/components/button-group.tsx +4 -1
  314. package/src/components/calculator.tsx +10 -6
  315. package/src/components/checkbox-group.tsx +2 -2
  316. package/src/components/checkbox.tsx +2 -1
  317. package/src/components/chip.tsx +37 -32
  318. package/src/components/choice-card.tsx +4 -2
  319. package/src/components/combobox.tsx +4 -4
  320. package/src/components/confirm-dialog.tsx +13 -6
  321. package/src/components/country-select.tsx +2 -2
  322. package/src/components/danger-confirm.tsx +2 -2
  323. package/src/components/data-table.tsx +20 -9
  324. package/src/components/date-picker.tsx +10 -7
  325. package/src/components/entity-combobox.tsx +3 -3
  326. package/src/components/field-parts.tsx +61 -14
  327. package/src/components/file-button.tsx +5 -4
  328. package/src/components/file-dropzone.tsx +2 -2
  329. package/src/components/floating-panel.tsx +7 -5
  330. package/src/components/form-actions.tsx +12 -5
  331. package/src/components/inline-edit-field.tsx +13 -13
  332. package/src/components/list.tsx +10 -9
  333. package/src/components/month-picker.tsx +8 -2
  334. package/src/components/multi-entity-combobox.tsx +3 -3
  335. package/src/components/numpad-sheet.tsx +8 -4
  336. package/src/components/page-header.tsx +15 -1
  337. package/src/components/popover.tsx +4 -0
  338. package/src/components/row-actions.tsx +201 -50
  339. package/src/components/share-card.tsx +67 -13
  340. package/src/components/status-dot.tsx +7 -7
  341. package/src/components/swatch-picker.tsx +2 -2
  342. package/src/components/swipeable-row.tsx +138 -27
  343. package/src/components/switch.tsx +2 -1
  344. package/src/components/table.tsx +8 -1
  345. package/src/components/toggle-group.tsx +5 -3
  346. package/src/components/translation-review-editor.tsx +8 -3
  347. package/src/components/translation-review.tsx +18 -22
  348. package/src/components/ui.tsx +98 -44
  349. package/src/components/write-lock.tsx +150 -24
  350. package/src/feedback/feedback-attachment.tsx +13 -9
  351. package/src/feedback/feedback-inbox.tsx +10 -6
  352. package/src/feedback/feedback-row-detail.tsx +3 -2
  353. package/src/feedback/feedback-swipe.tsx +6 -11
  354. package/src/feedback/feedback-table.tsx +2 -1
  355. package/src/i18n/defaults.ts +2 -0
  356. package/src/i18n/german.ts +38 -0
  357. package/src/i18n/kit-labels.tsx +4 -0
  358. package/src/i18n/locales/es.ts +34 -0
  359. package/src/i18n/locales/fr.ts +39 -0
  360. package/src/i18n/locales/hu.ts +37 -0
  361. package/src/i18n/locales/it.ts +35 -0
  362. package/src/i18n/locales/zh.ts +33 -0
  363. package/src/i18n/review.ts +7 -0
  364. package/src/index.ts +48 -4
  365. package/src/shell/app-shell.tsx +11 -6
  366. package/src/testing/css-variables.generated.ts +112 -0
  367. package/src/testing/css-variables.ts +191 -0
  368. package/src/testing/index.ts +13 -0
  369. package/src/theme/contrast-tokens.ts +53 -2
  370. package/src/theme/palette-derive.ts +87 -10
  371. package/src/theme/palette-presets.ts +45 -43
  372. package/src/wizard/stepper-nav.tsx +2 -1
  373. package/tokens.css +306 -46
@@ -2,20 +2,25 @@ import * as react from 'react';
2
2
  import { ComponentProps, ReactNode } from 'react';
3
3
  import { NumberInput } from './number-input.js';
4
4
  import './calculator.js';
5
- import '../delete-account-setting-FLyJf-kZ.js';
5
+ import '../delete-account-setting-DiPX4Vue.js';
6
6
  import './tooltip.js';
7
7
  import '../hooks/use-anchored-rect.js';
8
8
  import '../lib/clipping.js';
9
- import '../data-table-labels-kpAAVi6g.js';
9
+ import '../data-table-labels-BJ5JE_X2.js';
10
10
  import './data-table-sort.js';
11
11
  import './swipeable-row.js';
12
+ import './status-dot.js';
13
+ import './progress-bar.js';
14
+ import './signed-amount.js';
15
+ import './stat-tile.js';
16
+ import './chip.js';
17
+ import 'lucide-react';
18
+ import './field-parts.js';
19
+ import './write-lock.js';
12
20
  import './mini-calendar.js';
13
21
  import './calendar-heatmap.js';
14
22
  import './popover.js';
15
23
  import '../hooks/use-breakpoint.js';
16
- import './chip.js';
17
- import 'lucide-react';
18
- import './field-parts.js';
19
24
  import './field-sync.js';
20
25
  import '../wizard/types.js';
21
26
  import '../tour/tour.js';
@@ -27,7 +32,6 @@ import './page-contents.js';
27
32
  import './series-chart-labels.js';
28
33
  import './pie-chart-labels.js';
29
34
  import './sparkline.js';
30
- import './stat-tile.js';
31
35
  import './signature-pad.js';
32
36
  import './password-strength.js';
33
37
  import './swatch-picker.js';
@@ -39,9 +43,6 @@ import '../hooks/use-search-param-state.js';
39
43
  import './measured-grid.js';
40
44
  import './user-avatar.js';
41
45
  import '../lib/person-name.js';
42
- import './status-dot.js';
43
- import './progress-bar.js';
44
- import './signed-amount.js';
45
46
  import '../shell/topbar-action-menu.js';
46
47
  import './menu-item.js';
47
48
  import './floating-panel.js';
@@ -57,7 +58,6 @@ import './error-boundary.js';
57
58
  import './authed-image.js';
58
59
  import './image-grid.js';
59
60
  import './lightbox.js';
60
- import './write-lock.js';
61
61
  import '../auth/sign-in-form.js';
62
62
  import '../landing/access.js';
63
63
  import '../landing/landing-labels.js';
@@ -2,20 +2,25 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { CalculatorButtonLabels } from './calculator.js';
4
4
  import { NumberPadSheetLabels } from './numpad-sheet.js';
5
- import '../delete-account-setting-FLyJf-kZ.js';
5
+ import '../delete-account-setting-DiPX4Vue.js';
6
6
  import './tooltip.js';
7
7
  import '../hooks/use-anchored-rect.js';
8
8
  import '../lib/clipping.js';
9
- import '../data-table-labels-kpAAVi6g.js';
9
+ import '../data-table-labels-BJ5JE_X2.js';
10
10
  import './data-table-sort.js';
11
11
  import './swipeable-row.js';
12
+ import './status-dot.js';
13
+ import './progress-bar.js';
14
+ import './signed-amount.js';
15
+ import './stat-tile.js';
16
+ import './chip.js';
17
+ import 'lucide-react';
18
+ import './field-parts.js';
19
+ import './write-lock.js';
12
20
  import './mini-calendar.js';
13
21
  import './calendar-heatmap.js';
14
22
  import './popover.js';
15
23
  import '../hooks/use-breakpoint.js';
16
- import './chip.js';
17
- import 'lucide-react';
18
- import './field-parts.js';
19
24
  import './field-sync.js';
20
25
  import '../wizard/types.js';
21
26
  import '../tour/tour.js';
@@ -27,7 +32,6 @@ import './page-contents.js';
27
32
  import './series-chart-labels.js';
28
33
  import './pie-chart-labels.js';
29
34
  import './sparkline.js';
30
- import './stat-tile.js';
31
35
  import './signature-pad.js';
32
36
  import './password-strength.js';
33
37
  import './swatch-picker.js';
@@ -39,9 +43,6 @@ import '../hooks/use-search-param-state.js';
39
43
  import './measured-grid.js';
40
44
  import './user-avatar.js';
41
45
  import '../lib/person-name.js';
42
- import './status-dot.js';
43
- import './progress-bar.js';
44
- import './signed-amount.js';
45
46
  import '../shell/topbar-action-menu.js';
46
47
  import './menu-item.js';
47
48
  import './floating-panel.js';
@@ -57,7 +58,6 @@ import './error-boundary.js';
57
58
  import './authed-image.js';
58
59
  import './image-grid.js';
59
60
  import './lightbox.js';
60
- import './write-lock.js';
61
61
  import '../auth/sign-in-form.js';
62
62
  import '../landing/access.js';
63
63
  import '../landing/landing-labels.js';
@@ -1,19 +1,24 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { bP as CalculatorLabels } from '../delete-account-setting-FLyJf-kZ.js';
3
+ import { bP as CalculatorLabels } from '../delete-account-setting-DiPX4Vue.js';
4
4
  import './tooltip.js';
5
5
  import '../hooks/use-anchored-rect.js';
6
6
  import '../lib/clipping.js';
7
- import '../data-table-labels-kpAAVi6g.js';
7
+ import '../data-table-labels-BJ5JE_X2.js';
8
8
  import './data-table-sort.js';
9
9
  import './swipeable-row.js';
10
+ import './status-dot.js';
11
+ import './progress-bar.js';
12
+ import './signed-amount.js';
13
+ import './stat-tile.js';
14
+ import './chip.js';
15
+ import 'lucide-react';
16
+ import './field-parts.js';
17
+ import './write-lock.js';
10
18
  import './mini-calendar.js';
11
19
  import './calendar-heatmap.js';
12
20
  import './popover.js';
13
21
  import '../hooks/use-breakpoint.js';
14
- import './chip.js';
15
- import 'lucide-react';
16
- import './field-parts.js';
17
22
  import './field-sync.js';
18
23
  import '../wizard/types.js';
19
24
  import '../tour/tour.js';
@@ -25,7 +30,6 @@ import './page-contents.js';
25
30
  import './series-chart-labels.js';
26
31
  import './pie-chart-labels.js';
27
32
  import './sparkline.js';
28
- import './stat-tile.js';
29
33
  import './signature-pad.js';
30
34
  import './password-strength.js';
31
35
  import './swatch-picker.js';
@@ -37,9 +41,6 @@ import '../hooks/use-search-param-state.js';
37
41
  import './measured-grid.js';
38
42
  import './user-avatar.js';
39
43
  import '../lib/person-name.js';
40
- import './status-dot.js';
41
- import './progress-bar.js';
42
- import './signed-amount.js';
43
44
  import '../shell/topbar-action-menu.js';
44
45
  import './menu-item.js';
45
46
  import './floating-panel.js';
@@ -55,7 +56,6 @@ import './error-boundary.js';
55
56
  import './authed-image.js';
56
57
  import './image-grid.js';
57
58
  import './lightbox.js';
58
- import './write-lock.js';
59
59
  import '../auth/sign-in-form.js';
60
60
  import '../landing/access.js';
61
61
  import '../landing/landing-labels.js';
@@ -30,8 +30,8 @@ const PAD_KEYS = [
30
30
  { kind: "ins", label: "+", ins: "+", accent: true, name: "plus" }
31
31
  ];
32
32
  const PAD_BTN = `flex h-14 items-center justify-center rounded-lg text-lg font-medium transition-transform select-none focus:outline-none ${FOCUS_RING} focus-visible:ring-inset active:scale-[0.97]`;
33
- const PAD_DIGIT = "bg-[var(--bg-surface-2)] text-[var(--text-primary)] active:bg-[var(--border)]";
34
- const PAD_ACCENT = "bg-[var(--border)] text-[var(--text-primary)] active:bg-[var(--bg-surface-2)]";
33
+ const PAD_DIGIT = "bg-surface-2 text-primary active:bg-active";
34
+ const PAD_ACCENT = "bg-active text-primary active:bg-surface-2";
35
35
  function NumberPadSheet({
36
36
  value,
37
37
  onChange,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/numpad-sheet.tsx"],"sourcesContent":["import { createPortal } from \"react-dom\";\nimport { Delete } from \"lucide-react\";\nimport { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { useEscapeKey } from \"../hooks/use-dismiss\";\nimport { evaluateExpression, formatResult, sanitizeLive } from \"../lib/calc\";\nimport {\n DEFAULT_CALCULATOR_LABELS,\n useKitLabels,\n type CalculatorLabels,\n} from \"../i18n/kit-labels\";\n\n/**\n * A full calculator keypad rendered as a bottom sheet — the mobile counterpart to\n * the desktop {@link CalculatorButton} popover, and the replacement for the old\n * under-the-field operator bar (`MathKeys`, feedback #334).\n *\n * A native app (YNAB) can swap the system keyboard for its own keypad; a PWA\n * cannot — but it CAN suppress the OS keyboard with `inputMode=\"none\"` on the\n * host field and paint its own keypad in the freed space. So the host input keeps\n * real focus/caret/selection + the existing value/commit contract, while this\n * sheet drives it: digits + operators + a live `= …` preview, evaluated through\n * the shared {@link evaluateExpression} engine. Desktop is untouched (it keeps the\n * native keyboard and the calculator popover); the host gates this to mobile.\n *\n * Buttons commit through the host's `onChange`/blur, so the sheet is stateless —\n * the field text is the single source of truth. `onPointerDown` preventDefault on\n * the root keeps the host input focused (caret stays, keyboard stays hidden) as\n * the user taps keys; the buttons' `onClick` still fires.\n */\n/** Names for the pad itself and its four non-digit keys. The first four are\n * screen-reader names; `done` is the one string here that is READ OFF THE SCREEN —\n * the brand-filled primary button — and it was a hardcoded English literal while\n * its three neighbours each took an override, so a German phone showed \"Done\"\n * among German labels with no prop able to change it. */\nexport interface NumberPadSheetLabels extends Partial<CalculatorLabels> {\n /** The pad's own name — the `calculator.panel` key under its older name here. */\n pad?: string;\n backspace?: string;\n clear?: string;\n equals?: string;\n /** Visible text on the primary key, not an `aria-label`. */\n done?: string;\n}\n\ntype PadKey =\n | { kind: \"ins\"; label: string; ins: string; accent?: boolean; name?: PadKeyName }\n | { kind: \"back\" };\n\n/** The {@link CalculatorLabels} key naming a non-digit key; digits are their glyph. */\ntype PadKeyName = \"plus\" | \"minus\" | \"times\" | \"divide\" | \"decimal\";\n\n// Insert chars that survive `sanitizeLive` (it keeps `× ÷` and ASCII `+ - * /`\n// but strips the unicode minus `−`, so minus inserts an ASCII \"-\" while showing\n// the nicer glyph). The evaluator normalises `× ÷` back to `* /`.\nconst PAD_KEYS: PadKey[] = [\n { kind: \"ins\", label: \"7\", ins: \"7\" },\n { kind: \"ins\", label: \"8\", ins: \"8\" },\n { kind: \"ins\", label: \"9\", ins: \"9\" },\n { kind: \"ins\", label: \"÷\", ins: \"÷\", accent: true, name: \"divide\" },\n { kind: \"ins\", label: \"4\", ins: \"4\" },\n { kind: \"ins\", label: \"5\", ins: \"5\" },\n { kind: \"ins\", label: \"6\", ins: \"6\" },\n { kind: \"ins\", label: \"×\", ins: \"×\", accent: true, name: \"times\" },\n { kind: \"ins\", label: \"1\", ins: \"1\" },\n { kind: \"ins\", label: \"2\", ins: \"2\" },\n { kind: \"ins\", label: \"3\", ins: \"3\" },\n { kind: \"ins\", label: \"−\", ins: \"-\", accent: true, name: \"minus\" },\n { kind: \"ins\", label: \".\", ins: \".\", name: \"decimal\" },\n { kind: \"ins\", label: \"0\", ins: \"0\" },\n { kind: \"back\" },\n { kind: \"ins\", label: \"+\", ins: \"+\", accent: true, name: \"plus\" },\n];\n\n/** Every key's shape — including the thing this pad used to delete without replacing.\n *\n * `focus:outline-none` alone is not a style choice, it is the removal of the only\n * signal a keyboard user has; on a 4×4 grid of identical tiles it leaves no way at all\n * to tell which key Enter is about to press (the audit's §a11y, and the same defect as\n * the dropdown search box).\n *\n * `focus-visible` and `ring-inset`, for the two reasons {@link ToggleGroup} already\n * writes down. This is a touch control first, and a plain `focus:` ring paints itself\n * on every TAP, because a tap focuses the button. And the keys sit 6px apart, which is\n * thinner than an outward ring plus the neighbour it would spill onto — inset keeps the\n * ring inside the key it describes.\n *\n * --brand rather than --border-strong: the operator keys are filled with --border, and\n * a ring in the colour of the thing it surrounds is not an indicator. */\nconst PAD_BTN = `flex h-14 items-center justify-center rounded-lg text-lg font-medium transition-transform select-none focus:outline-none ${FOCUS_RING} focus-visible:ring-inset active:scale-[0.97]`;\nconst PAD_DIGIT = \"bg-[var(--bg-surface-2)] text-[var(--text-primary)] active:bg-[var(--border)]\";\nconst PAD_ACCENT = \"bg-[var(--border)] text-[var(--text-primary)] active:bg-[var(--bg-surface-2)]\";\n\nexport function NumberPadSheet({\n value,\n onChange,\n onDone,\n label,\n labels,\n decimalMark = \".\",\n onResult,\n}: {\n value: string;\n /** Fired with the raw (sanitised) field text on every key — same contract as\n * the host field's own `onChange`. */\n onChange: (value: string) => void;\n /** The glyph on the decimal key — \",\" where the host shows a comma (AmountInput\n * in fr-CH, kastlan 40). With \",\" the key inserts a comma and the text is handed\n * back unsanitized, for the host to read by its locale (keksdose G1). */\n decimalMark?: \".\" | \",\";\n /** \"=\": the evaluated result, dot-decimal. Default `onChange`. A host that reads\n * typed text by the locale's marks takes it apart from keystrokes, since a result\n * like \"1.234\" is a decimal and never a grouped thousand. */\n onResult?: (value: string) => void;\n /** Fired on \"Done\": the host blurs the input, which commits (evaluates) and\n * unmounts the sheet via its existing blur handler. */\n onDone: () => void;\n /** Optional field label, echoed in the sheet header so the user still knows\n * which field they're editing when the sheet covers it. */\n label?: ReactNode;\n /** Names for the pad itself and its non-digit keys — screen-reader names, except\n * `done`, which is the visible text on the primary key. Merged over `calculator`\n * from `<UiKitProvider labels>`: this is the same keypad as `CalculatorButton`'s,\n * on a phone, and one translation covers both. Digits need no name — their glyph\n * IS the name; the operators do (see {@link CalculatorLabels.plus}). */\n labels?: NumberPadSheetLabels;\n}) {\n // `pad` is this sheet's older spelling of `panel`, so it is folded in as that.\n const fromProps = useMemo(() => {\n if (!labels) return undefined;\n const { pad, ...rest } = labels;\n return pad === undefined ? rest : { ...rest, panel: pad };\n }, [labels]);\n const text = useKitLabels(\"calculator\", DEFAULT_CALCULATOR_LABELS, fromProps);\n\n // NO `useBodyScrollLock`, and that is the point of the control (Keksdose live\n // #317: *\"Background not scrollable when the amount input calculator field is\n // open\"*).\n //\n // This sheet is a KEYBOARD, not a dialog. It is opened by focusing a field and\n // exists only because a PWA cannot swap the system keyboard for its own — so it\n // suppresses the OS keyboard with `inputMode=\"none\"` and paints itself in the\n // freed space. An OS keyboard does not freeze the page behind it; it takes the\n // bottom of the screen and leaves you free to scroll what is left, which is how\n // you reach the field you are typing into when the keys cover it.\n //\n // Locking here made the page unreachable at exactly the moment it matters most:\n // correcting a receipt total against the running line sum, where the figure you\n // are comparing against sits below the keypad. The other four holders of that\n // hook are all modal — `Modal`, `PickerSheet`, `FullBleedDialog`, the DataTable\n // row dialog — and they stay locked, because for them the page behind is not\n // part of the task.\n //\n // The pad still does not scroll ITSELF away under a stray drag: the root's\n // `onPointerDown` preventDefault (below) keeps the host input focused and eats\n // the gesture on the sheet, so a touch that starts on a key is not a page scroll.\n\n // ── NO focus trap either, and for a harder reason than the scroll lock ────────\n //\n // Wave 3 put `useFocusTrap` on every overlay in this package. This one is the\n // exception, and it is not an oversight to be tidied up later: a trap here would\n // DELETE THE COMPONENT IT WAS PROTECTING. Both hosts render the pad with\n // `showNumpad = isMobile && !disabled && focused`, where `focused` is the host\n // input's own focus state. Moving focus into the pad blurs that input, the host\n // sets `focused` false, and the pad unmounts — on mount, before a single key can be\n // pressed. The blur also COMMITS, so the trap would evaluate a half-typed\n // expression on the way out.\n //\n // That is the contract, not an accident of it. This sheet exists because a PWA\n // cannot swap the system keyboard for its own: the host keeps real focus, caret and\n // selection while `inputMode=\"none\"` suppresses the OS keys, and the root's\n // `onPointerDown` preventDefault below is there for the same reason. The pad is\n // `role=\"group\"`, not a dialog. A keyboard does not trap focus; it is what you type\n // WITH.\n //\n // What was genuinely missing is a way OUT that is not a tap. `useEscapeKey` and not\n // a handler on the sheet, because the sheet never has the focus a keydown would\n // bubble from — the host does, and the host is not this component. Escape does what\n // the Done key does rather than cancelling, because committing on blur is the\n // host's contract and a cancel would have to be the host's to offer.\n //\n // The limit, since it is better written down than discovered: inside a `Modal` the\n // dialog's own Escape handler sits on the focus path and stops the event before a\n // document listener sees it, so there the press closes the dialog — which it did\n // before this line existed too, and which takes the pad with it either way.\n useEscapeKey(onDone);\n\n const result = evaluateExpression(value);\n const preview = result !== null && formatResult(result) !== value.trim() ? `= ${formatResult(result)}` : \"\";\n\n // With a \",\" mark the host keeps the typist's own text (AmountInput, keksdose G1):\n // the key inserts the comma it shows, and the host, not this pad, decides which mark\n // is the decimal one — sanitizing here would fold a grouping \".\" into a decimal.\n const insert = (ch: string) =>\n onChange(decimalMark === \",\" ? value + (ch === \".\" ? \",\" : ch) : sanitizeLive(value + ch));\n const backspace = () => onChange(value.slice(0, -1));\n const clearAll = () => onChange(\"\");\n const equals = () => {\n if (result !== null) (onResult ?? onChange)(formatResult(result));\n };\n\n const sheet = (\n <div\n role=\"group\"\n aria-label={text.panel}\n // Keep the host input focused when tapping the pad: preventDefault on\n // pointerdown blocks the focus/blur, while the buttons' click still fires.\n onPointerDown={(e) => e.preventDefault()}\n className=\"fixed inset-x-0 bottom-0 z-50 border-t border-[var(--border)] bg-[var(--bg-surface)] px-2 pt-2 shadow-[0_-4px_24px_rgba(0,0,0,0.12)]\"\n style={{ paddingBottom: \"max(0.5rem, env(safe-area-inset-bottom))\" }}\n >\n {/* Header display: which field + the running expression and live result, so\n the value stays visible even when the sheet covers the field. */}\n <div className=\"flex items-end justify-between gap-3 px-2 pb-2\">\n {label != null && (\n <span className=\"truncate pb-1 text-xs font-medium text-[var(--money-neutral)]\">{label}</span>\n )}\n {/* At the logical end, opposite the label; the figures themselves stay\n left-to-right and right-aligned, like the desktop calculator's. */}\n <div className=\"ms-auto min-w-0\">\n <div dir=\"ltr\" className=\"truncate text-right font-mono text-lg leading-tight text-[var(--text-primary)]\">{value || \"0\"}</div>\n <div dir=\"ltr\" className=\"h-4 text-right font-mono text-xs text-[var(--money-neutral)]\">{preview}</div>\n </div>\n </div>\n\n <div className=\"grid grid-cols-4 gap-1.5\">\n {PAD_KEYS.map((key, i) =>\n key.kind === \"back\" ? (\n <button\n key=\"back\"\n type=\"button\"\n aria-label={text.backspace}\n onClick={backspace}\n className={cn(PAD_BTN, PAD_ACCENT)}\n >\n <Delete className=\"size-5\" />\n </button>\n ) : (\n <button\n key={`${key.ins}-${i}`}\n type=\"button\"\n aria-label={key.name ? text[key.name] : key.label}\n onClick={() => insert(key.ins)}\n className={cn(PAD_BTN, key.accent ? PAD_ACCENT : PAD_DIGIT)}\n >\n {key.kind === \"ins\" && key.name === \"decimal\" ? decimalMark : key.label}\n </button>\n ),\n )}\n </div>\n\n <div className=\"mt-1.5 grid grid-cols-4 gap-1.5\">\n <button type=\"button\" aria-label={text.clear} onClick={clearAll} className={cn(PAD_BTN, PAD_ACCENT)}>\n C\n </button>\n <button type=\"button\" aria-label={text.equals} onClick={equals} className={cn(PAD_BTN, PAD_ACCENT)}>\n =\n </button>\n <button\n type=\"button\"\n onClick={onDone}\n // The one key whose own fill is --brand, so it rings in the colour that fill\n // was chosen to be legible against instead (tailwind-merge keeps the later\n // ring colour). A --brand ring here would be present in the DOM and invisible\n // on screen, which is the defect this change exists to fix.\n className={cn(\n PAD_BTN,\n \"col-span-2 bg-[var(--brand)] text-[var(--brand-contrast)] focus-visible:ring-[var(--brand-contrast)] active:bg-[var(--brand-hover)]\",\n )}\n >\n {text.done}\n </button>\n </div>\n </div>\n );\n\n return createPortal(sheet, document.body);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/numpad-sheet.tsx"],"sourcesContent":["import { createPortal } from \"react-dom\";\nimport { Delete } from \"lucide-react\";\nimport { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { useEscapeKey } from \"../hooks/use-dismiss\";\nimport { evaluateExpression, formatResult, sanitizeLive } from \"../lib/calc\";\nimport {\n DEFAULT_CALCULATOR_LABELS,\n useKitLabels,\n type CalculatorLabels,\n} from \"../i18n/kit-labels\";\n\n/**\n * A full calculator keypad rendered as a bottom sheet — the mobile counterpart to\n * the desktop {@link CalculatorButton} popover, and the replacement for the old\n * under-the-field operator bar (`MathKeys`, feedback #334).\n *\n * A native app (YNAB) can swap the system keyboard for its own keypad; a PWA\n * cannot — but it CAN suppress the OS keyboard with `inputMode=\"none\"` on the\n * host field and paint its own keypad in the freed space. So the host input keeps\n * real focus/caret/selection + the existing value/commit contract, while this\n * sheet drives it: digits + operators + a live `= …` preview, evaluated through\n * the shared {@link evaluateExpression} engine. Desktop is untouched (it keeps the\n * native keyboard and the calculator popover); the host gates this to mobile.\n *\n * Buttons commit through the host's `onChange`/blur, so the sheet is stateless —\n * the field text is the single source of truth. `onPointerDown` preventDefault on\n * the root keeps the host input focused (caret stays, keyboard stays hidden) as\n * the user taps keys; the buttons' `onClick` still fires.\n */\n/** Names for the pad itself and its four non-digit keys. The first four are\n * screen-reader names; `done` is the one string here that is READ OFF THE SCREEN —\n * the brand-filled primary button — and it was a hardcoded English literal while\n * its three neighbours each took an override, so a German phone showed \"Done\"\n * among German labels with no prop able to change it. */\nexport interface NumberPadSheetLabels extends Partial<CalculatorLabels> {\n /** The pad's own name — the `calculator.panel` key under its older name here. */\n pad?: string;\n backspace?: string;\n clear?: string;\n equals?: string;\n /** Visible text on the primary key, not an `aria-label`. */\n done?: string;\n}\n\ntype PadKey =\n | { kind: \"ins\"; label: string; ins: string; accent?: boolean; name?: PadKeyName }\n | { kind: \"back\" };\n\n/** The {@link CalculatorLabels} key naming a non-digit key; digits are their glyph. */\ntype PadKeyName = \"plus\" | \"minus\" | \"times\" | \"divide\" | \"decimal\";\n\n// Insert chars that survive `sanitizeLive` (it keeps `× ÷` and ASCII `+ - * /`\n// but strips the unicode minus `−`, so minus inserts an ASCII \"-\" while showing\n// the nicer glyph). The evaluator normalises `× ÷` back to `* /`.\nconst PAD_KEYS: PadKey[] = [\n { kind: \"ins\", label: \"7\", ins: \"7\" },\n { kind: \"ins\", label: \"8\", ins: \"8\" },\n { kind: \"ins\", label: \"9\", ins: \"9\" },\n { kind: \"ins\", label: \"÷\", ins: \"÷\", accent: true, name: \"divide\" },\n { kind: \"ins\", label: \"4\", ins: \"4\" },\n { kind: \"ins\", label: \"5\", ins: \"5\" },\n { kind: \"ins\", label: \"6\", ins: \"6\" },\n { kind: \"ins\", label: \"×\", ins: \"×\", accent: true, name: \"times\" },\n { kind: \"ins\", label: \"1\", ins: \"1\" },\n { kind: \"ins\", label: \"2\", ins: \"2\" },\n { kind: \"ins\", label: \"3\", ins: \"3\" },\n { kind: \"ins\", label: \"−\", ins: \"-\", accent: true, name: \"minus\" },\n { kind: \"ins\", label: \".\", ins: \".\", name: \"decimal\" },\n { kind: \"ins\", label: \"0\", ins: \"0\" },\n { kind: \"back\" },\n { kind: \"ins\", label: \"+\", ins: \"+\", accent: true, name: \"plus\" },\n];\n\n/** Every key's shape — including the thing this pad used to delete without replacing.\n *\n * `focus:outline-none` alone is not a style choice, it is the removal of the only\n * signal a keyboard user has; on a 4×4 grid of identical tiles it leaves no way at all\n * to tell which key Enter is about to press (the audit's §a11y, and the same defect as\n * the dropdown search box).\n *\n * `focus-visible` and `ring-inset`, for the two reasons {@link ToggleGroup} already\n * writes down. This is a touch control first, and a plain `focus:` ring paints itself\n * on every TAP, because a tap focuses the button. And the keys sit 6px apart, which is\n * thinner than an outward ring plus the neighbour it would spill onto — inset keeps the\n * ring inside the key it describes.\n *\n * --brand rather than --border-strong: the operator keys are filled with a step of the\n * ink, and a ring in the colour of the thing it surrounds is not an indicator. */\nconst PAD_BTN = `flex h-14 items-center justify-center rounded-lg text-lg font-medium transition-transform select-none focus:outline-none ${FOCUS_RING} focus-visible:ring-inset active:scale-[0.97]`;\n// Digits on the well, operators a step down on `--bg-active`, each pressing toward the\n// other. `--bg-active`, not `--border` (0.33, docs/colour-roles-harmonization.md §12.9):\n// More contrast steps the border until it is a 3:1 LINE, and as a fill under the label\n// that left 2.7–3.3:1; `--bg-active` carries the ink at 7:1 and up at both levels.\nconst PAD_DIGIT = \"bg-surface-2 text-primary active:bg-active\";\nconst PAD_ACCENT = \"bg-active text-primary active:bg-surface-2\";\n\nexport function NumberPadSheet({\n value,\n onChange,\n onDone,\n label,\n labels,\n decimalMark = \".\",\n onResult,\n}: {\n value: string;\n /** Fired with the raw (sanitised) field text on every key — same contract as\n * the host field's own `onChange`. */\n onChange: (value: string) => void;\n /** The glyph on the decimal key — \",\" where the host shows a comma (AmountInput\n * in fr-CH, kastlan 40). With \",\" the key inserts a comma and the text is handed\n * back unsanitized, for the host to read by its locale (keksdose G1). */\n decimalMark?: \".\" | \",\";\n /** \"=\": the evaluated result, dot-decimal. Default `onChange`. A host that reads\n * typed text by the locale's marks takes it apart from keystrokes, since a result\n * like \"1.234\" is a decimal and never a grouped thousand. */\n onResult?: (value: string) => void;\n /** Fired on \"Done\": the host blurs the input, which commits (evaluates) and\n * unmounts the sheet via its existing blur handler. */\n onDone: () => void;\n /** Optional field label, echoed in the sheet header so the user still knows\n * which field they're editing when the sheet covers it. */\n label?: ReactNode;\n /** Names for the pad itself and its non-digit keys — screen-reader names, except\n * `done`, which is the visible text on the primary key. Merged over `calculator`\n * from `<UiKitProvider labels>`: this is the same keypad as `CalculatorButton`'s,\n * on a phone, and one translation covers both. Digits need no name — their glyph\n * IS the name; the operators do (see {@link CalculatorLabels.plus}). */\n labels?: NumberPadSheetLabels;\n}) {\n // `pad` is this sheet's older spelling of `panel`, so it is folded in as that.\n const fromProps = useMemo(() => {\n if (!labels) return undefined;\n const { pad, ...rest } = labels;\n return pad === undefined ? rest : { ...rest, panel: pad };\n }, [labels]);\n const text = useKitLabels(\"calculator\", DEFAULT_CALCULATOR_LABELS, fromProps);\n\n // NO `useBodyScrollLock`, and that is the point of the control (Keksdose live\n // #317: *\"Background not scrollable when the amount input calculator field is\n // open\"*).\n //\n // This sheet is a KEYBOARD, not a dialog. It is opened by focusing a field and\n // exists only because a PWA cannot swap the system keyboard for its own — so it\n // suppresses the OS keyboard with `inputMode=\"none\"` and paints itself in the\n // freed space. An OS keyboard does not freeze the page behind it; it takes the\n // bottom of the screen and leaves you free to scroll what is left, which is how\n // you reach the field you are typing into when the keys cover it.\n //\n // Locking here made the page unreachable at exactly the moment it matters most:\n // correcting a receipt total against the running line sum, where the figure you\n // are comparing against sits below the keypad. The other four holders of that\n // hook are all modal — `Modal`, `PickerSheet`, `FullBleedDialog`, the DataTable\n // row dialog — and they stay locked, because for them the page behind is not\n // part of the task.\n //\n // The pad still does not scroll ITSELF away under a stray drag: the root's\n // `onPointerDown` preventDefault (below) keeps the host input focused and eats\n // the gesture on the sheet, so a touch that starts on a key is not a page scroll.\n\n // ── NO focus trap either, and for a harder reason than the scroll lock ────────\n //\n // Wave 3 put `useFocusTrap` on every overlay in this package. This one is the\n // exception, and it is not an oversight to be tidied up later: a trap here would\n // DELETE THE COMPONENT IT WAS PROTECTING. Both hosts render the pad with\n // `showNumpad = isMobile && !disabled && focused`, where `focused` is the host\n // input's own focus state. Moving focus into the pad blurs that input, the host\n // sets `focused` false, and the pad unmounts — on mount, before a single key can be\n // pressed. The blur also COMMITS, so the trap would evaluate a half-typed\n // expression on the way out.\n //\n // That is the contract, not an accident of it. This sheet exists because a PWA\n // cannot swap the system keyboard for its own: the host keeps real focus, caret and\n // selection while `inputMode=\"none\"` suppresses the OS keys, and the root's\n // `onPointerDown` preventDefault below is there for the same reason. The pad is\n // `role=\"group\"`, not a dialog. A keyboard does not trap focus; it is what you type\n // WITH.\n //\n // What was genuinely missing is a way OUT that is not a tap. `useEscapeKey` and not\n // a handler on the sheet, because the sheet never has the focus a keydown would\n // bubble from — the host does, and the host is not this component. Escape does what\n // the Done key does rather than cancelling, because committing on blur is the\n // host's contract and a cancel would have to be the host's to offer.\n //\n // The limit, since it is better written down than discovered: inside a `Modal` the\n // dialog's own Escape handler sits on the focus path and stops the event before a\n // document listener sees it, so there the press closes the dialog — which it did\n // before this line existed too, and which takes the pad with it either way.\n useEscapeKey(onDone);\n\n const result = evaluateExpression(value);\n const preview = result !== null && formatResult(result) !== value.trim() ? `= ${formatResult(result)}` : \"\";\n\n // With a \",\" mark the host keeps the typist's own text (AmountInput, keksdose G1):\n // the key inserts the comma it shows, and the host, not this pad, decides which mark\n // is the decimal one — sanitizing here would fold a grouping \".\" into a decimal.\n const insert = (ch: string) =>\n onChange(decimalMark === \",\" ? value + (ch === \".\" ? \",\" : ch) : sanitizeLive(value + ch));\n const backspace = () => onChange(value.slice(0, -1));\n const clearAll = () => onChange(\"\");\n const equals = () => {\n if (result !== null) (onResult ?? onChange)(formatResult(result));\n };\n\n const sheet = (\n <div\n role=\"group\"\n aria-label={text.panel}\n // Keep the host input focused when tapping the pad: preventDefault on\n // pointerdown blocks the focus/blur, while the buttons' click still fires.\n onPointerDown={(e) => e.preventDefault()}\n className=\"fixed inset-x-0 bottom-0 z-50 border-t border-[var(--border)] bg-[var(--bg-surface)] px-2 pt-2 shadow-[0_-4px_24px_rgba(0,0,0,0.12)]\"\n style={{ paddingBottom: \"max(0.5rem, env(safe-area-inset-bottom))\" }}\n >\n {/* Header display: which field + the running expression and live result, so\n the value stays visible even when the sheet covers the field. */}\n <div className=\"flex items-end justify-between gap-3 px-2 pb-2\">\n {label != null && (\n <span className=\"truncate pb-1 text-xs font-medium text-[var(--money-neutral)]\">{label}</span>\n )}\n {/* At the logical end, opposite the label; the figures themselves stay\n left-to-right and right-aligned, like the desktop calculator's. */}\n <div className=\"ms-auto min-w-0\">\n <div dir=\"ltr\" className=\"truncate text-right font-mono text-lg leading-tight text-[var(--text-primary)]\">{value || \"0\"}</div>\n <div dir=\"ltr\" className=\"h-4 text-right font-mono text-xs text-[var(--money-neutral)]\">{preview}</div>\n </div>\n </div>\n\n <div className=\"grid grid-cols-4 gap-1.5\">\n {PAD_KEYS.map((key, i) =>\n key.kind === \"back\" ? (\n <button\n key=\"back\"\n type=\"button\"\n aria-label={text.backspace}\n onClick={backspace}\n className={cn(PAD_BTN, PAD_ACCENT)}\n >\n <Delete className=\"size-5\" />\n </button>\n ) : (\n <button\n key={`${key.ins}-${i}`}\n type=\"button\"\n aria-label={key.name ? text[key.name] : key.label}\n onClick={() => insert(key.ins)}\n className={cn(PAD_BTN, key.accent ? PAD_ACCENT : PAD_DIGIT)}\n >\n {key.kind === \"ins\" && key.name === \"decimal\" ? decimalMark : key.label}\n </button>\n ),\n )}\n </div>\n\n <div className=\"mt-1.5 grid grid-cols-4 gap-1.5\">\n <button type=\"button\" aria-label={text.clear} onClick={clearAll} className={cn(PAD_BTN, PAD_ACCENT)}>\n C\n </button>\n <button type=\"button\" aria-label={text.equals} onClick={equals} className={cn(PAD_BTN, PAD_ACCENT)}>\n =\n </button>\n <button\n type=\"button\"\n onClick={onDone}\n // The one key whose own fill is --brand, so it rings in the colour that fill\n // was chosen to be legible against instead (tailwind-merge keeps the later\n // ring colour). A --brand ring here would be present in the DOM and invisible\n // on screen, which is the defect this change exists to fix.\n className={cn(\n PAD_BTN,\n \"col-span-2 bg-[var(--brand)] text-[var(--brand-contrast)] focus-visible:ring-[var(--brand-contrast)] active:bg-[var(--brand-hover)]\",\n )}\n >\n {text.done}\n </button>\n </div>\n </div>\n );\n\n return createPortal(sheet, document.body);\n}\n"],"mappings":"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","names":[]}
@@ -8,6 +8,7 @@ const TITLE = {
8
8
  compact: "text-xl font-semibold",
9
9
  md: "text-xl font-semibold tracking-tight sm:text-2xl"
10
10
  };
11
+ const STACK_ACTIONS_AT_LARGE = "large:max-sm:flex-col large:max-sm:items-stretch large:max-sm:[&>*]:w-full";
11
12
  const ROW = {
12
13
  stacked: "flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4",
13
14
  inline: "flex-row items-center justify-between gap-2 sm:gap-4"
@@ -85,7 +86,11 @@ function PageHeader({
85
86
  "div",
86
87
  {
87
88
  ref: actionsRef,
88
- className: cn("flex shrink-0 flex-wrap items-center gap-2", hasSecondary && "sm:order-2"),
89
+ className: cn(
90
+ "flex shrink-0 flex-wrap items-center gap-2",
91
+ hasSecondary && "sm:order-2",
92
+ mobileLayout === "stacked" && STACK_ACTIONS_AT_LARGE
93
+ ),
89
94
  children: actions
90
95
  }
91
96
  ),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/page-header.tsx"],"sourcesContent":["import { useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ElementType, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { SECTION_LABEL_CLASS } from \"./text\";\n\n/**\n * `md` (default) is kastlan's `text-2xl` page title from `sm` up, a step smaller on a\n * phone; `sm` is lenkbank's `text-lg` title on a dense tool page (projects-page.tsx).\n *\n * `compact` (0.13) is keksdose's page title (keksdose F4): `text-xl font-semibold` at\n * EVERY width — budgets, rules, accounts, transactions and budget pages all write\n * exactly that `<h1>` by hand. It is `md`'s phone size without `md`'s step up to\n * `text-2xl` on a wider screen, and without its `tracking-tight`, which keksdose's\n * titles never had. A word and not a letter for the reason `ProgressBar`'s `slim` is\n * one: the letters are taken in order, and there is none between `sm` and `md` — an\n * `lg` or `xl` at `text-xl` would read as LARGER than `md`, which from `sm` up it is\n * not.\n */\nexport type PageHeaderSize = \"sm\" | \"compact\" | \"md\";\n\nconst TITLE: Record<PageHeaderSize, string> = {\n sm: \"text-lg font-semibold\",\n compact: \"text-xl font-semibold\",\n md: \"text-xl font-semibold tracking-tight sm:text-2xl\",\n};\n\n/**\n * How the title block and the actions sit below the `sm` breakpoint. From `sm` up both\n * layouts are one row: title block at the start, actions at the end.\n */\nexport type PageHeaderMobileLayout = \"stacked\" | \"inline\";\n\nconst ROW: Record<PageHeaderMobileLayout, string> = {\n stacked: \"flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4\",\n inline: \"flex-row items-center justify-between gap-2 sm:gap-4\",\n};\n\n/**\n * Where the actions sit on the title block's height, in the one row the header is from\n * `sm` up (and on a phone too when {@link PageHeaderMobileLayout} is `inline`, the only\n * layout that has a row there). Unset keeps each layout's own: `start` for `stacked`,\n * `center` for `inline`.\n */\nexport type PageHeaderActionsAlign = \"start\" | \"center\" | \"end\";\n\n/** Applied to the ROW, so the title block and the actions are aligned against each\n * other on one cross axis. 0.14.0 put `self-*` on the actions box alone, which moved\n * nothing when the actions were the taller item — they already filled the row, and\n * the title stayed at the top (keksdose H2, reports-page's two-line labelled\n * CurrencySelect). Per layout, because `stacked` is a column on a phone, where\n * `items-center` would centre everything HORIZONTALLY — so it aligns from `sm` up. */\nconst ROW_ALIGN: Record<PageHeaderMobileLayout, Record<PageHeaderActionsAlign, string>> = {\n stacked: { start: \"sm:items-start\", center: \"sm:items-center\", end: \"sm:items-end\" },\n inline: { start: \"items-start\", center: \"items-center\", end: \"items-end\" },\n};\n\nexport interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\"> {\n title: ReactNode;\n /** A sentence under the title. */\n description?: ReactNode;\n /** Buttons for the page — \"New\", \"Export\". At the title's end from `sm` up; under it,\n * wrapping, on a phone — unless {@link mobileLayout} is `inline`. */\n actions?: ReactNode;\n /** A `Breadcrumbs` trail, drawn above everything else. */\n breadcrumbs?: ReactNode;\n /** A small uppercase line above the title — the section or the kind of record\n * (\"Building\", \"Settings\"). The section-label type. */\n eyebrow?: ReactNode;\n /** The title's element. Default `h1`: a page has one, and this is it. `h2` for a\n * header inside a page that already has its `h1` (a tab's own header). */\n as?: \"h1\" | \"h2\" | \"h3\";\n size?: PageHeaderSize;\n /**\n * `stacked` (default): on a phone the actions drop under the title and wrap, so a\n * row of three buttons never pushes the title off the screen.\n *\n * `inline`: the actions stay beside the title on a phone too, vertically centred on\n * it — keksdose's headers (transactions-page.tsx:515, `flex items-center\n * justify-between gap-2`), which carry one or two icon-sized controls and were tuned\n * for the phone in live #263. Adopting `PageHeader` must not undo that tuning, so the\n * row stays a row (keksdose F4). The actions do NOT wrap or shrink here: `inline` is\n * for a header whose actions are known to fit beside a phone-width title (an icon\n * button, a month picker); a long title wraps to make room for them instead.\n */\n mobileLayout?: PageHeaderMobileLayout;\n /**\n * A second group of actions that gets its OWN full-width row under the header on a\n * phone, and joins the actions' row from `sm` up, between the title and\n * {@link actions}.\n *\n * keksdose's budget page (budget-page.tsx, feedback #60) is the case: title and month\n * navigation on row 1, the fold/unfold toggles on row 2 on a phone — \"two tidy rows\n * instead of the old ragged justify-between overflow\" — and one row, title | toggles |\n * month nav, on a wider screen. It builds that by hand with a wrapping flex row and\n * swapped order utilities; adopting `PageHeader` (keksdose G6b) must not lose it.\n * There: `actions` is the month navigation, this is the toggles.\n *\n * The DOM order is title, `actions`, `secondaryActions` — the phone's visual order,\n * and the one a screen reader and the Tab key follow at every width. From `sm` up the\n * two groups swap places visually only, which keeps the page's primary control (the\n * month) at the row's end, where keksdose had it, and first in reading order. The\n * breakpoint is `sm`, the one {@link mobileLayout} switches at, not keksdose's `md`:\n * one header, one breakpoint.\n */\n secondaryActions?: ReactNode;\n /**\n * The actions' vertical alignment against the title block — see\n * {@link PageHeaderActionsAlign}. Unset keeps the layout's own.\n *\n * keksdose's reports page (reports-page.tsx, keksdose G8) carries a LABELLED\n * `CurrencySelect` as its action: a field with its label on top is taller than the\n * title, and `stacked`'s default top alignment hangs it from the title's cap height\n * with its control well below the title's line. `center` sets it on the title's\n * middle, as that page's hand-written `items-center` row did. It aligns the whole row\n * (title block and actions against each other), since 0.14.1: 0.14.0 moved only the\n * actions box, which did nothing when the actions were the taller item (keksdose H2).\n */\n actionsAlign?: PageHeaderActionsAlign;\n /**\n * One line, cut with an ellipsis, instead of the default breaking of a long word onto\n * as many lines as it needs.\n *\n * keksdose's payees page (payees-page.tsx, live #263, keksdose G6a) wraps its title in\n * its own truncating span so a long name gives way to the `inline` header's actions\n * instead of pushing them. This is that span's behaviour on the heading itself; the\n * title block is already allowed to shrink below its content, which is what lets the\n * cut happen inside a flex row.\n *\n * The full title stays in the DOM as the heading's text, so a screen reader and the\n * document outline read all of it. There is deliberately no native `title` tooltip:\n * the kit does not use them (not on touch, not on keyboard focus, not styled).\n */\n truncateTitle?: boolean;\n}\n\n/**\n * A page's head: optional breadcrumbs, an eyebrow, the title, a description, and the\n * page's actions.\n *\n * kastlan's (shared/components/display/page-header.tsx) is the model — title and\n * description on one side, actions on the other, stacked on a phone — and this adds\n * what its pages kept writing around it: a trail above, an eyebrow, a `ReactNode`\n * title (a status chip beside the name), and actions that WRAP when there are more\n * than fit a phone's width, instead of running off it. No outer margin: the page's\n * own rhythm (`space-y-*`, a grid gap) places it.\n */\nexport function PageHeader({\n title,\n description,\n actions,\n breadcrumbs,\n eyebrow,\n as = \"h1\",\n size = \"md\",\n mobileLayout = \"stacked\",\n secondaryActions,\n actionsAlign,\n truncateTitle = false,\n className,\n ...rest\n}: PageHeaderProps) {\n const Heading = as as ElementType;\n const hasSecondary = secondaryActions != null;\n const wrapsSecondary = hasSecondary && mobileLayout === \"inline\";\n const align = actionsAlign != null ? ROW_ALIGN[mobileLayout][actionsAlign] : undefined;\n // keksdose H1: in `inline` the title block could shrink below its longest word, and\n // `break-words` then split \"Monatsbudget\" into \"Monatsbudg\" / \"et\" beside the actions.\n // Unless it truncates, the title keeps its longest word (min-content) and the ROW\n // wraps instead: actions that do not fit go to a line of their own, as the app's old\n // `flex-wrap` header did.\n const keepsWords = mobileLayout === \"inline\" && !truncateTitle;\n // keksdose, after H1: once the actions have wrapped below the title, the phone's\n // header was THREE rows — title / actions / secondary — where the app's old header\n // shared the second row between the month nav and the toggles (~48px of phone\n // height). So the secondary group's full-width basis (G6b: a row of its own) is\n // dropped exactly when the actions already sit below the title; then it joins their\n // row if it fits, and wraps under them if not. While the actions share the title's\n // row, nothing changes: the secondary group never climbs onto the title's row.\n //\n // CSS cannot see \"the previous item wrapped\", so it is measured — by a\n // ResizeObserver, whose first callback runs on `observe`. It is stable: the\n // secondary group comes after the actions, so its basis cannot move them. No\n // ResizeObserver (jsdom) or no layout keeps the basis, the 0.14.0 layout.\n const measuresWrap = keepsWords && hasSecondary && actions != null;\n const titleRef = useRef<HTMLDivElement>(null);\n const actionsRef = useRef<HTMLDivElement>(null);\n const [actionsWrapped, setActionsWrapped] = useState(false);\n useLayoutEffect(() => {\n const titleBox = titleRef.current;\n const actionsBox = actionsRef.current;\n if (!measuresWrap || !titleBox || !actionsBox || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n const t = titleBox.getBoundingClientRect();\n const a = actionsBox.getBoundingClientRect();\n // Unmeasured (display: none, no layout): keep the full-width basis.\n setActionsWrapped(t.height > 0 && a.height > 0 && a.top >= t.bottom - 1);\n });\n observer.observe(titleBox);\n observer.observe(actionsBox);\n if (titleBox.parentElement) observer.observe(titleBox.parentElement);\n return () => observer.disconnect();\n }, [measuresWrap]);\n return (\n <div {...rest} className={cn(\"flex min-w-0 flex-col gap-2\", className)}>\n {breadcrumbs}\n {/* With a second group, `inline` wraps so that group's full-width basis puts it\n on a row of its own on a phone (`stacked` is a column there already, and a\n basis would size a column item's HEIGHT); from `sm` up the row stops wrapping\n and the order utilities seat it between the title and the actions. Without\n one, nothing changes. */}\n <div\n className={cn(\n \"flex min-w-0\",\n ROW[mobileLayout],\n keepsWords ? \"flex-wrap\" : wrapsSecondary && \"flex-wrap sm:flex-nowrap\",\n align,\n )}\n >\n <div ref={titleRef} className={cn(\"flex-1\", keepsWords ? \"min-w-min\" : \"min-w-0\")}>\n {eyebrow != null && <p className={cn(SECTION_LABEL_CLASS.xs, \"mb-1\")}>{eyebrow}</p>}\n <Heading\n className={cn(\n truncateTitle ? \"truncate\" : \"break-words\",\n \"text-[var(--text-primary)]\",\n TITLE[size],\n )}\n >\n {title}\n </Heading>\n {description != null && <p className=\"mt-1 text-sm text-[var(--text-muted)]\">{description}</p>}\n </div>\n {actions != null && (\n <div\n ref={actionsRef}\n className={cn(\"flex shrink-0 flex-wrap items-center gap-2\", hasSecondary && \"sm:order-2\")}\n >\n {actions}\n </div>\n )}\n {hasSecondary && (\n <div\n className={cn(\n \"flex flex-wrap items-center gap-2 sm:order-1 sm:shrink-0\",\n wrapsSecondary && (measuresWrap && actionsWrapped ? \"sm:basis-auto\" : \"basis-full sm:basis-auto\"),\n )}\n >\n {secondaryActions}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/page-header.tsx"],"sourcesContent":["import { useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ElementType, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { SECTION_LABEL_CLASS } from \"./text\";\n\n/**\n * `md` (default) is kastlan's `text-2xl` page title from `sm` up, a step smaller on a\n * phone; `sm` is lenkbank's `text-lg` title on a dense tool page (projects-page.tsx).\n *\n * `compact` (0.13) is keksdose's page title (keksdose F4): `text-xl font-semibold` at\n * EVERY width — budgets, rules, accounts, transactions and budget pages all write\n * exactly that `<h1>` by hand. It is `md`'s phone size without `md`'s step up to\n * `text-2xl` on a wider screen, and without its `tracking-tight`, which keksdose's\n * titles never had. A word and not a letter for the reason `ProgressBar`'s `slim` is\n * one: the letters are taken in order, and there is none between `sm` and `md` — an\n * `lg` or `xl` at `text-xl` would read as LARGER than `md`, which from `sm` up it is\n * not.\n */\nexport type PageHeaderSize = \"sm\" | \"compact\" | \"md\";\n\nconst TITLE: Record<PageHeaderSize, string> = {\n sm: \"text-lg font-semibold\",\n compact: \"text-xl font-semibold\",\n md: \"text-xl font-semibold tracking-tight sm:text-2xl\",\n};\n\n/**\n * How the title block and the actions sit below the `sm` breakpoint. From `sm` up both\n * layouts are one row: title block at the start, actions at the end.\n */\nexport type PageHeaderMobileLayout = \"stacked\" | \"inline\";\n\n/**\n * `stacked` at Large and Extra large on a phone (0.32.2, kastlan's 0.32.1 report;\n * docs/text-size-harmonization.md §4, as FormActions does): the actions take the full\n * width, one under the other. Side by side in 240 px of layout, two buttons each wrapped\n * their label onto three lines. `large:max-sm:`, the scaled phone width, so Normal and\n * every wider screen keep their row; `inline` is for actions known to fit, and stays.\n */\nconst STACK_ACTIONS_AT_LARGE =\n \"large:max-sm:flex-col large:max-sm:items-stretch large:max-sm:[&>*]:w-full\";\n\nconst ROW: Record<PageHeaderMobileLayout, string> = {\n stacked: \"flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4\",\n inline: \"flex-row items-center justify-between gap-2 sm:gap-4\",\n};\n\n/**\n * Where the actions sit on the title block's height, in the one row the header is from\n * `sm` up (and on a phone too when {@link PageHeaderMobileLayout} is `inline`, the only\n * layout that has a row there). Unset keeps each layout's own: `start` for `stacked`,\n * `center` for `inline`.\n */\nexport type PageHeaderActionsAlign = \"start\" | \"center\" | \"end\";\n\n/** Applied to the ROW, so the title block and the actions are aligned against each\n * other on one cross axis. 0.14.0 put `self-*` on the actions box alone, which moved\n * nothing when the actions were the taller item — they already filled the row, and\n * the title stayed at the top (keksdose H2, reports-page's two-line labelled\n * CurrencySelect). Per layout, because `stacked` is a column on a phone, where\n * `items-center` would centre everything HORIZONTALLY — so it aligns from `sm` up. */\nconst ROW_ALIGN: Record<PageHeaderMobileLayout, Record<PageHeaderActionsAlign, string>> = {\n stacked: { start: \"sm:items-start\", center: \"sm:items-center\", end: \"sm:items-end\" },\n inline: { start: \"items-start\", center: \"items-center\", end: \"items-end\" },\n};\n\nexport interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\"> {\n title: ReactNode;\n /** A sentence under the title. */\n description?: ReactNode;\n /** Buttons for the page — \"New\", \"Export\". At the title's end from `sm` up; under it,\n * wrapping, on a phone — unless {@link mobileLayout} is `inline`. */\n actions?: ReactNode;\n /** A `Breadcrumbs` trail, drawn above everything else. */\n breadcrumbs?: ReactNode;\n /** A small uppercase line above the title — the section or the kind of record\n * (\"Building\", \"Settings\"). The section-label type. */\n eyebrow?: ReactNode;\n /** The title's element. Default `h1`: a page has one, and this is it. `h2` for a\n * header inside a page that already has its `h1` (a tab's own header). */\n as?: \"h1\" | \"h2\" | \"h3\";\n size?: PageHeaderSize;\n /**\n * `stacked` (default): on a phone the actions drop under the title and wrap, so a\n * row of three buttons never pushes the title off the screen.\n *\n * `inline`: the actions stay beside the title on a phone too, vertically centred on\n * it — keksdose's headers (transactions-page.tsx:515, `flex items-center\n * justify-between gap-2`), which carry one or two icon-sized controls and were tuned\n * for the phone in live #263. Adopting `PageHeader` must not undo that tuning, so the\n * row stays a row (keksdose F4). The actions do NOT wrap or shrink here: `inline` is\n * for a header whose actions are known to fit beside a phone-width title (an icon\n * button, a month picker); a long title wraps to make room for them instead.\n */\n mobileLayout?: PageHeaderMobileLayout;\n /**\n * A second group of actions that gets its OWN full-width row under the header on a\n * phone, and joins the actions' row from `sm` up, between the title and\n * {@link actions}.\n *\n * keksdose's budget page (budget-page.tsx, feedback #60) is the case: title and month\n * navigation on row 1, the fold/unfold toggles on row 2 on a phone — \"two tidy rows\n * instead of the old ragged justify-between overflow\" — and one row, title | toggles |\n * month nav, on a wider screen. It builds that by hand with a wrapping flex row and\n * swapped order utilities; adopting `PageHeader` (keksdose G6b) must not lose it.\n * There: `actions` is the month navigation, this is the toggles.\n *\n * The DOM order is title, `actions`, `secondaryActions` — the phone's visual order,\n * and the one a screen reader and the Tab key follow at every width. From `sm` up the\n * two groups swap places visually only, which keeps the page's primary control (the\n * month) at the row's end, where keksdose had it, and first in reading order. The\n * breakpoint is `sm`, the one {@link mobileLayout} switches at, not keksdose's `md`:\n * one header, one breakpoint.\n */\n secondaryActions?: ReactNode;\n /**\n * The actions' vertical alignment against the title block — see\n * {@link PageHeaderActionsAlign}. Unset keeps the layout's own.\n *\n * keksdose's reports page (reports-page.tsx, keksdose G8) carries a LABELLED\n * `CurrencySelect` as its action: a field with its label on top is taller than the\n * title, and `stacked`'s default top alignment hangs it from the title's cap height\n * with its control well below the title's line. `center` sets it on the title's\n * middle, as that page's hand-written `items-center` row did. It aligns the whole row\n * (title block and actions against each other), since 0.14.1: 0.14.0 moved only the\n * actions box, which did nothing when the actions were the taller item (keksdose H2).\n */\n actionsAlign?: PageHeaderActionsAlign;\n /**\n * One line, cut with an ellipsis, instead of the default breaking of a long word onto\n * as many lines as it needs.\n *\n * keksdose's payees page (payees-page.tsx, live #263, keksdose G6a) wraps its title in\n * its own truncating span so a long name gives way to the `inline` header's actions\n * instead of pushing them. This is that span's behaviour on the heading itself; the\n * title block is already allowed to shrink below its content, which is what lets the\n * cut happen inside a flex row.\n *\n * The full title stays in the DOM as the heading's text, so a screen reader and the\n * document outline read all of it. There is deliberately no native `title` tooltip:\n * the kit does not use them (not on touch, not on keyboard focus, not styled).\n */\n truncateTitle?: boolean;\n}\n\n/**\n * A page's head: optional breadcrumbs, an eyebrow, the title, a description, and the\n * page's actions.\n *\n * kastlan's (shared/components/display/page-header.tsx) is the model — title and\n * description on one side, actions on the other, stacked on a phone — and this adds\n * what its pages kept writing around it: a trail above, an eyebrow, a `ReactNode`\n * title (a status chip beside the name), and actions that WRAP when there are more\n * than fit a phone's width, instead of running off it. No outer margin: the page's\n * own rhythm (`space-y-*`, a grid gap) places it.\n */\nexport function PageHeader({\n title,\n description,\n actions,\n breadcrumbs,\n eyebrow,\n as = \"h1\",\n size = \"md\",\n mobileLayout = \"stacked\",\n secondaryActions,\n actionsAlign,\n truncateTitle = false,\n className,\n ...rest\n}: PageHeaderProps) {\n const Heading = as as ElementType;\n const hasSecondary = secondaryActions != null;\n const wrapsSecondary = hasSecondary && mobileLayout === \"inline\";\n const align = actionsAlign != null ? ROW_ALIGN[mobileLayout][actionsAlign] : undefined;\n // keksdose H1: in `inline` the title block could shrink below its longest word, and\n // `break-words` then split \"Monatsbudget\" into \"Monatsbudg\" / \"et\" beside the actions.\n // Unless it truncates, the title keeps its longest word (min-content) and the ROW\n // wraps instead: actions that do not fit go to a line of their own, as the app's old\n // `flex-wrap` header did.\n const keepsWords = mobileLayout === \"inline\" && !truncateTitle;\n // keksdose, after H1: once the actions have wrapped below the title, the phone's\n // header was THREE rows — title / actions / secondary — where the app's old header\n // shared the second row between the month nav and the toggles (~48px of phone\n // height). So the secondary group's full-width basis (G6b: a row of its own) is\n // dropped exactly when the actions already sit below the title; then it joins their\n // row if it fits, and wraps under them if not. While the actions share the title's\n // row, nothing changes: the secondary group never climbs onto the title's row.\n //\n // CSS cannot see \"the previous item wrapped\", so it is measured — by a\n // ResizeObserver, whose first callback runs on `observe`. It is stable: the\n // secondary group comes after the actions, so its basis cannot move them. No\n // ResizeObserver (jsdom) or no layout keeps the basis, the 0.14.0 layout.\n const measuresWrap = keepsWords && hasSecondary && actions != null;\n const titleRef = useRef<HTMLDivElement>(null);\n const actionsRef = useRef<HTMLDivElement>(null);\n const [actionsWrapped, setActionsWrapped] = useState(false);\n useLayoutEffect(() => {\n const titleBox = titleRef.current;\n const actionsBox = actionsRef.current;\n if (!measuresWrap || !titleBox || !actionsBox || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n const t = titleBox.getBoundingClientRect();\n const a = actionsBox.getBoundingClientRect();\n // Unmeasured (display: none, no layout): keep the full-width basis.\n setActionsWrapped(t.height > 0 && a.height > 0 && a.top >= t.bottom - 1);\n });\n observer.observe(titleBox);\n observer.observe(actionsBox);\n if (titleBox.parentElement) observer.observe(titleBox.parentElement);\n return () => observer.disconnect();\n }, [measuresWrap]);\n return (\n <div {...rest} className={cn(\"flex min-w-0 flex-col gap-2\", className)}>\n {breadcrumbs}\n {/* With a second group, `inline` wraps so that group's full-width basis puts it\n on a row of its own on a phone (`stacked` is a column there already, and a\n basis would size a column item's HEIGHT); from `sm` up the row stops wrapping\n and the order utilities seat it between the title and the actions. Without\n one, nothing changes. */}\n <div\n className={cn(\n \"flex min-w-0\",\n ROW[mobileLayout],\n keepsWords ? \"flex-wrap\" : wrapsSecondary && \"flex-wrap sm:flex-nowrap\",\n align,\n )}\n >\n <div ref={titleRef} className={cn(\"flex-1\", keepsWords ? \"min-w-min\" : \"min-w-0\")}>\n {eyebrow != null && <p className={cn(SECTION_LABEL_CLASS.xs, \"mb-1\")}>{eyebrow}</p>}\n <Heading\n className={cn(\n truncateTitle ? \"truncate\" : \"break-words\",\n \"text-[var(--text-primary)]\",\n TITLE[size],\n )}\n >\n {title}\n </Heading>\n {description != null && <p className=\"mt-1 text-sm text-[var(--text-muted)]\">{description}</p>}\n </div>\n {actions != null && (\n <div\n ref={actionsRef}\n className={cn(\n \"flex shrink-0 flex-wrap items-center gap-2\",\n hasSecondary && \"sm:order-2\",\n mobileLayout === \"stacked\" && STACK_ACTIONS_AT_LARGE,\n )}\n >\n {actions}\n </div>\n )}\n {hasSecondary && (\n <div\n className={cn(\n \"flex flex-wrap items-center gap-2 sm:order-1 sm:shrink-0\",\n wrapsSecondary && (measuresWrap && actionsWrapped ? \"sm:basis-auto\" : \"basis-full sm:basis-auto\"),\n )}\n >\n {secondaryActions}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,19 +1,24 @@
1
1
  import 'react';
2
2
  import '../lib/phone.js';
3
- export { cJ as DEFAULT_PHONE_INPUT_LABELS, eq as PhoneInput, er as PhoneInputLabels, es as PhoneInputProps } from '../delete-account-setting-FLyJf-kZ.js';
3
+ export { cJ as DEFAULT_PHONE_INPUT_LABELS, er as PhoneInput, es as PhoneInputLabels, et as PhoneInputProps } from '../delete-account-setting-DiPX4Vue.js';
4
4
  import './tooltip.js';
5
5
  import '../hooks/use-anchored-rect.js';
6
6
  import '../lib/clipping.js';
7
- import '../data-table-labels-kpAAVi6g.js';
7
+ import '../data-table-labels-BJ5JE_X2.js';
8
8
  import './data-table-sort.js';
9
9
  import './swipeable-row.js';
10
+ import './status-dot.js';
11
+ import './progress-bar.js';
12
+ import './signed-amount.js';
13
+ import './stat-tile.js';
14
+ import './chip.js';
15
+ import 'lucide-react';
16
+ import './field-parts.js';
17
+ import './write-lock.js';
10
18
  import './mini-calendar.js';
11
19
  import './calendar-heatmap.js';
12
20
  import './popover.js';
13
21
  import '../hooks/use-breakpoint.js';
14
- import './chip.js';
15
- import 'lucide-react';
16
- import './field-parts.js';
17
22
  import './field-sync.js';
18
23
  import '../wizard/types.js';
19
24
  import '../tour/tour.js';
@@ -25,7 +30,6 @@ import './page-contents.js';
25
30
  import './series-chart-labels.js';
26
31
  import './pie-chart-labels.js';
27
32
  import './sparkline.js';
28
- import './stat-tile.js';
29
33
  import './signature-pad.js';
30
34
  import './password-strength.js';
31
35
  import './swatch-picker.js';
@@ -37,9 +41,6 @@ import '../hooks/use-search-param-state.js';
37
41
  import './measured-grid.js';
38
42
  import './user-avatar.js';
39
43
  import '../lib/person-name.js';
40
- import './status-dot.js';
41
- import './progress-bar.js';
42
- import './signed-amount.js';
43
44
  import '../shell/topbar-action-menu.js';
44
45
  import './menu-item.js';
45
46
  import './floating-panel.js';
@@ -55,7 +56,6 @@ import './error-boundary.js';
55
56
  import './authed-image.js';
56
57
  import './image-grid.js';
57
58
  import './lightbox.js';
58
- import './write-lock.js';
59
59
  import '../auth/sign-in-form.js';
60
60
  import '../landing/access.js';
61
61
  import '../landing/landing-labels.js';
@@ -81,6 +81,6 @@ interface PopoverProps extends Omit<ComponentPropsWithoutRef<"div">, "children">
81
81
  * Only Tab is contained. A popover that took the page away as well would be a
82
82
  * {@link Modal}, and there is one of those.
83
83
  */
84
- declare function Popover({ trigger, children, width, labels, panelId, className, style, "aria-label": ariaLabel, ...rest }: PopoverProps): react.JSX.Element;
84
+ declare function Popover({ trigger, children, width, labels, panelId, className, style, "aria-label": ariaLabel, onKeyDown: onKeyDownProp, ...rest }: PopoverProps): react.JSX.Element;
85
85
 
86
86
  export { DEFAULT_POPOVER_LABELS, Popover, type PopoverLabels, type PopoverProps };
@@ -26,6 +26,7 @@ function Popover({
26
26
  className,
27
27
  style,
28
28
  "aria-label": ariaLabel,
29
+ onKeyDown: onKeyDownProp,
29
30
  ...rest
30
31
  }) {
31
32
  const triggerRef = useRef(null);
@@ -65,6 +66,7 @@ function Popover({
65
66
  }
66
67
  });
67
68
  const handleKeyDown = (e) => {
69
+ onKeyDownProp?.(e);
68
70
  if (e.key !== "Escape") return;
69
71
  e.stopPropagation();
70
72
  close();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/popover.tsx"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useEscapeKey, useOutsideClick } from \"../hooks/use-dismiss\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { lengthPx, useRemPx, type RemLength } from \"../hooks/use-breakpoint\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf } from \"../lib/direction\";\nimport type { Direction } from \"../lib/direction\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\n/** The panel's accessible name. A `role=\"dialog\"` with no name announces as \"dialog\"\n * and nothing else, which is worse than the unlabelled div it replaced — so it is a\n * prop with an English default, like every other string this package renders. Callers\n * should pass the control's own name (\"Filter\", \"Choose a date\"); the default is only\n * there so an unmigrated call site is still named. */\nexport interface PopoverLabels {\n panel: string;\n}\n\n// Exported since the kit grew one label tree (`UiKitLabels`, src/i18n): the complete\n// English reference a translator works from has to be able to name every namespace.\nexport const DEFAULT_POPOVER_LABELS: PopoverLabels = {\n // \"Pop-up\", not \"Popover\": the panel's accessible name is read to USERS, and\n // \"popover\" is a developer's word for it (reported by keksdose, 0.5.0). Only a\n // bare Popover falls back to this — the kit's own pickers name their panels.\n panel: \"Pop-up\",\n};\n\n/**\n * Exported, and extending the `<div>` attributes, so a consumer wrapping this — which is\n * what `DateField` and `FilterPopover` in this very package do — can hand anything\n * through to the PANEL: a `data-tour` anchor for the kit's own guided tour, a test id,\n * an `aria-describedby`, or an `aria-label` naming this particular popover.\n *\n * `children` is omitted from those attributes rather than inherited: here it is a render\n * prop taking the panel's `close`, not a `ReactNode`, so the two cannot be reconciled.\n */\nexport interface PopoverProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n trigger: (state: { open: boolean; toggle: () => void; ref: RefObject<HTMLButtonElement | null> }) => ReactNode;\n children: (close: () => void) => ReactNode;\n /**\n * The panel's width: `\"18rem\"` (the default) grows with the text size — the panel\n * holds text, and a 288 px calendar at 150 % cut its own month off (0.32,\n * docs/text-size-harmonization.md §3.2). A plain number is px, fixed, as before.\n * Either way the panel is never wider than the viewport less 8 px a side: 18rem is\n * 432 px at Extra large, more than a 360 px phone has.\n */\n width?: number | RemLength;\n labels?: Partial<PopoverLabels>;\n /** Id for the panel, so a trigger wearing `role=\"combobox\"` can `aria-controls` it.\n * Required by ARIA on that role, and the panel is rendered here rather than by the\n * caller, so the id has to come in from outside. */\n panelId?: string;\n /** Extra classes for the PANEL. The trigger is the caller's own element. */\n className?: string;\n}\n\n/** 288 px at Normal, in rem so it follows the text size (§3.2). */\nconst POPOVER_WIDTH: RemLength = \"18rem\";\n\n/** The gap the panel keeps from either edge of the viewport, in px. */\nconst EDGE = 8;\n\n/**\n * Portal-rendered popover anchored under its trigger button. The panel is fixed\n * to the viewport (so table/overflow ancestors can't clip it) and re-aligns on\n * scroll/resize; it closes on outside-click and Escape.\n *\n * ## Trapped, not merely focused — and why, because it is the closer call\n *\n * The audit's finding was that this panel *\"portals out of the tab order with no role\n * and no focus move\"*: Tab from the trigger went straight past the panel into the page\n * behind it, so the calendar, the calculator keypad and every column filter in the kit\n * were mouse-only. Two shapes fix that, and a popover is not automatically a dialog, so\n * this was chosen rather than inherited:\n *\n * - **The light one** — move focus in on open, hand it back on close, and let Tab walk\n * OUT of the panel into whatever follows the trigger. That is the right shape for a\n * non-modal popover sitting inline in the document.\n * - **The trap** — Tab cycles inside the panel; Escape and a click outside are the\n * ways out. What is here.\n *\n * The deciding fact is the portal. This panel is rendered to the END of `<body>`, not\n * beside its trigger, so \"let Tab walk out\" does not mean \"carry on from the trigger\" —\n * it means land at the end of the document, several hundred controls from where the\n * user was. Tabbing out of it would be a worse place to arrive than the one the finding\n * complains about, and fixing that properly needs a sentinel-and-return dance that\n * nothing in this package has. The trap gets the user back to the trigger every time,\n * because `useFocusTrap` restores focus on close.\n *\n * And the panels this kit actually opens are self-contained: pick a date, tap out a\n * sum, narrow a column. Each of them commits and closes, and each already has two\n * documented ways out wired above — Escape and an outside press — so containment costs\n * a keyboard user nothing they had.\n *\n * **It stays non-modal in every other respect**, deliberately: no backdrop, no body\n * scroll lock, and no `aria-modal`, so the page behind is still readable by a screen\n * reader, still scrollable, and still clickable — clicking it is what dismisses this.\n * Only Tab is contained. A popover that took the page away as well would be a\n * {@link Modal}, and there is one of those.\n */\nexport function Popover({\n trigger,\n children,\n width = POPOVER_WIDTH,\n labels,\n panelId,\n className,\n style,\n \"aria-label\": ariaLabel,\n ...rest\n}: PopoverProps) {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n // The trigger's reading direction, read when it is pressed. The panel is portalled to\n // <body>, out of any `dir=\"rtl\"` subtree the trigger sits in, so without carrying it\n // across a right-to-left calendar or filter rendered left-to-right.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const close = () => setOpen(false);\n const toggle = () => {\n setDir(dirOf(triggerRef.current));\n setOpen((v) => !v);\n };\n // prop > `<UiKitProvider labels={{ popover }}>` > English. The provider's\n // `popover.panel` is only the last-resort name for a panel no caller named.\n const text = useKitLabels(\"popover\", DEFAULT_POPOVER_LABELS, labels);\n\n // Align the panel's END edge to the trigger's — right in LTR, left in RTL; it was\n // always `rect.right`, so an RTL panel hung off the wrong side — and clamp it\n // horizontally; the\n // hook owns the vertical half — below by default, flipped above (and height-capped)\n // when that is where the room is, e.g. once a mobile keyboard has eaten the bottom\n // of the screen (feedback #135). It re-measures on scroll/resize.\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n // In px for the arithmetic below, at the text size in force; capped at the viewport.\n const remPx = useRemPx();\n const panelWidth = rect\n ? Math.min(lengthPx(width, remPx), Math.max(0, window.innerWidth - 2 * EDGE))\n : 0;\n const pos = rect\n ? {\n top: placement.top,\n left: Math.min(\n Math.max(EDGE, dir === \"rtl\" ? rect.left : rect.right - panelWidth),\n window.innerWidth - panelWidth - EDGE,\n ),\n }\n : null;\n\n useOutsideClick([triggerRef, panelRef], close, open);\n useEscapeKey(close, open);\n\n useFocusTrap(panelRef, {\n active: open && pos !== null,\n // The panel keeps a focus one of its own children has already taken. `FilterPopover`\n // autofocuses its text box on mount — that is what the press on \"Filter\" was FOR,\n // and it is a prop there precisely so the popover keeps it — and a trap that\n // unconditionally focused the container would take it straight back off again,\n // one effect later and invisibly.\n initialFocus: () => {\n const panel = panelRef.current;\n const active = document.activeElement;\n return panel?.contains(active) ? (active as HTMLElement) : panel;\n },\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key !== \"Escape\") return;\n // `useEscapeKey` above listens on the document and still covers the case where\n // focus somehow is not in here. This handler is for the case that focus IS, which\n // is now the normal one — and it exists to STOP the key, because a popover opened\n // from inside a `Modal` (the calculator on an amount field is exactly that) would\n // otherwise have its Escape travel on up the React tree and close the dialog\n // underneath as well. Closing twice is harmless — `close` is a state setter, not a\n // caller's callback — so the two paths cannot double-fire anything.\n e.stopPropagation();\n close();\n };\n\n return (\n <>\n {trigger({ open, toggle, ref: triggerRef })}\n {open && pos &&\n createPortal(\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the\n dialog container, per the APG dialog pattern: `onKeyDown` is Escape-to-close for\n whatever inside it holds focus. */\n <div\n // `...rest` first: the role, the id the trigger's `aria-controls` points at,\n // the `tabIndex` the trap needs and the measured position are what make this\n // a popover, and a caller reaching for a `data-tour` anchor must not be able\n // to remove one of them by accident.\n {...rest}\n ref={panelRef}\n role=\"dialog\"\n id={panelId}\n // The caller's own `aria-label` wins over {@link PopoverLabels.panel}, which\n // is the package's English fallback for an unnamed panel. Both name the same\n // element; the DOM spelling is the one being standardised on, so where both\n // are present it is the one that survives.\n aria-label={ariaLabel ?? text.panel}\n dir={rest.dir ?? dir}\n // Without it the panel cannot take focus at all, and the trap above would\n // silently do nothing on a panel whose children are not yet tabbable.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n style={{\n // Merged over the caller's, never replaced by it: these four are measured\n // against the trigger every scroll and resize, and a panel that dropped\n // them would paint at the top-left of the viewport.\n ...style,\n position: \"fixed\",\n top: pos.top,\n left: pos.left,\n width: panelWidth,\n maxHeight: placement.maxHeight,\n }}\n className={cn(\n \"z-50 overflow-y-auto rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-3 shadow-lg outline-none\",\n className,\n )}\n >\n {children(close)}\n </div>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":";AAwLI,mBAOM,KAPN;AAxLJ,SAAS,QAAQ,gBAAgB;AAEjC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AACjC,SAAS,cAAc,uBAAuB;AAC9C,SAAS,oBAAoB;AAC7B,SAAS,UAAU,gBAAgC;AACnD,SAAS,UAAU;AACnB,SAAS,aAAa;AAEtB,SAAS,oBAAoB;AAatB,MAAM,yBAAwC;AAAA;AAAA;AAAA;AAAA,EAInD,OAAO;AACT;AAgCA,MAAM,gBAA2B;AAGjC,MAAM,OAAO;AAwCN,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAiB;AACf,QAAM,aAAa,OAA0B,IAAI;AACjD,QAAM,WAAW,OAAuB,IAAI;AAC5C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AAItC,QAAM,CAAC,KAAK,MAAM,IAAI,SAAoB,KAAK;AAC/C,QAAM,QAAQ,MAAM,QAAQ,KAAK;AACjC,QAAM,SAAS,MAAM;AACnB,WAAO,MAAM,WAAW,OAAO,CAAC;AAChC,YAAQ,CAAC,MAAM,CAAC,CAAC;AAAA,EACnB;AAGA,QAAM,OAAO,aAAa,WAAW,wBAAwB,MAAM;AAQnE,QAAM,YAAY,iBAAiB,YAAY,IAAI;AACnD,QAAM,OAAO,UAAU;AAEvB,QAAM,QAAQ,SAAS;AACvB,QAAM,aAAa,OACf,KAAK,IAAI,SAAS,OAAO,KAAK,GAAG,KAAK,IAAI,GAAG,OAAO,aAAa,IAAI,IAAI,CAAC,IAC1E;AACJ,QAAM,MAAM,OACR;AAAA,IACE,KAAK,UAAU;AAAA,IACf,MAAM,KAAK;AAAA,MACT,KAAK,IAAI,MAAM,QAAQ,QAAQ,KAAK,OAAO,KAAK,QAAQ,UAAU;AAAA,MAClE,OAAO,aAAa,aAAa;AAAA,IACnC;AAAA,EACF,IACA;AAEJ,kBAAgB,CAAC,YAAY,QAAQ,GAAG,OAAO,IAAI;AACnD,eAAa,OAAO,IAAI;AAExB,eAAa,UAAU;AAAA,IACrB,QAAQ,QAAQ,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMxB,cAAc,MAAM;AAClB,YAAM,QAAQ,SAAS;AACvB,YAAM,SAAS,SAAS;AACxB,aAAO,OAAO,SAAS,MAAM,IAAK,SAAyB;AAAA,IAC7D;AAAA,EACF,CAAC;AAED,QAAM,gBAAgB,CAAC,MAAqC;AAC1D,QAAI,EAAE,QAAQ,SAAU;AAQxB,MAAE,gBAAgB;AAClB,UAAM;AAAA,EACR;AAEA,SACE,iCACG;AAAA,YAAQ,EAAE,MAAM,QAAQ,KAAK,WAAW,CAAC;AAAA,IACzC,QAAQ,OACP;AAAA;AAAA;AAAA;AAAA,MAIE;AAAA,QAAC;AAAA;AAAA,UAKE,GAAG;AAAA,UACJ,KAAK;AAAA,UACL,MAAK;AAAA,UACL,IAAI;AAAA,UAKJ,cAAY,aAAa,KAAK;AAAA,UAC9B,KAAK,KAAK,OAAO;AAAA,UAGjB,UAAU;AAAA,UACV,WAAW;AAAA,UACX,OAAO;AAAA;AAAA;AAAA;AAAA,YAIL,GAAG;AAAA,YACH,UAAU;AAAA,YACV,KAAK,IAAI;AAAA,YACT,MAAM,IAAI;AAAA,YACV,OAAO;AAAA,YACP,WAAW,UAAU;AAAA,UACvB;AAAA,UACA,WAAW;AAAA,YACT;AAAA,YACA;AAAA,UACF;AAAA,UAEC,mBAAS,KAAK;AAAA;AAAA,MACjB;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/popover.tsx"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useEscapeKey, useOutsideClick } from \"../hooks/use-dismiss\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { lengthPx, useRemPx, type RemLength } from \"../hooks/use-breakpoint\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf } from \"../lib/direction\";\nimport type { Direction } from \"../lib/direction\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\n/** The panel's accessible name. A `role=\"dialog\"` with no name announces as \"dialog\"\n * and nothing else, which is worse than the unlabelled div it replaced — so it is a\n * prop with an English default, like every other string this package renders. Callers\n * should pass the control's own name (\"Filter\", \"Choose a date\"); the default is only\n * there so an unmigrated call site is still named. */\nexport interface PopoverLabels {\n panel: string;\n}\n\n// Exported since the kit grew one label tree (`UiKitLabels`, src/i18n): the complete\n// English reference a translator works from has to be able to name every namespace.\nexport const DEFAULT_POPOVER_LABELS: PopoverLabels = {\n // \"Pop-up\", not \"Popover\": the panel's accessible name is read to USERS, and\n // \"popover\" is a developer's word for it (reported by keksdose, 0.5.0). Only a\n // bare Popover falls back to this — the kit's own pickers name their panels.\n panel: \"Pop-up\",\n};\n\n/**\n * Exported, and extending the `<div>` attributes, so a consumer wrapping this — which is\n * what `DateField` and `FilterPopover` in this very package do — can hand anything\n * through to the PANEL: a `data-tour` anchor for the kit's own guided tour, a test id,\n * an `aria-describedby`, or an `aria-label` naming this particular popover.\n *\n * `children` is omitted from those attributes rather than inherited: here it is a render\n * prop taking the panel's `close`, not a `ReactNode`, so the two cannot be reconciled.\n */\nexport interface PopoverProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n trigger: (state: { open: boolean; toggle: () => void; ref: RefObject<HTMLButtonElement | null> }) => ReactNode;\n children: (close: () => void) => ReactNode;\n /**\n * The panel's width: `\"18rem\"` (the default) grows with the text size — the panel\n * holds text, and a 288 px calendar at 150 % cut its own month off (0.32,\n * docs/text-size-harmonization.md §3.2). A plain number is px, fixed, as before.\n * Either way the panel is never wider than the viewport less 8 px a side: 18rem is\n * 432 px at Extra large, more than a 360 px phone has.\n */\n width?: number | RemLength;\n labels?: Partial<PopoverLabels>;\n /** Id for the panel, so a trigger wearing `role=\"combobox\"` can `aria-controls` it.\n * Required by ARIA on that role, and the panel is rendered here rather than by the\n * caller, so the id has to come in from outside. */\n panelId?: string;\n /** Extra classes for the PANEL. The trigger is the caller's own element. */\n className?: string;\n}\n\n/** 288 px at Normal, in rem so it follows the text size (§3.2). */\nconst POPOVER_WIDTH: RemLength = \"18rem\";\n\n/** The gap the panel keeps from either edge of the viewport, in px. */\nconst EDGE = 8;\n\n/**\n * Portal-rendered popover anchored under its trigger button. The panel is fixed\n * to the viewport (so table/overflow ancestors can't clip it) and re-aligns on\n * scroll/resize; it closes on outside-click and Escape.\n *\n * ## Trapped, not merely focused — and why, because it is the closer call\n *\n * The audit's finding was that this panel *\"portals out of the tab order with no role\n * and no focus move\"*: Tab from the trigger went straight past the panel into the page\n * behind it, so the calendar, the calculator keypad and every column filter in the kit\n * were mouse-only. Two shapes fix that, and a popover is not automatically a dialog, so\n * this was chosen rather than inherited:\n *\n * - **The light one** — move focus in on open, hand it back on close, and let Tab walk\n * OUT of the panel into whatever follows the trigger. That is the right shape for a\n * non-modal popover sitting inline in the document.\n * - **The trap** — Tab cycles inside the panel; Escape and a click outside are the\n * ways out. What is here.\n *\n * The deciding fact is the portal. This panel is rendered to the END of `<body>`, not\n * beside its trigger, so \"let Tab walk out\" does not mean \"carry on from the trigger\" —\n * it means land at the end of the document, several hundred controls from where the\n * user was. Tabbing out of it would be a worse place to arrive than the one the finding\n * complains about, and fixing that properly needs a sentinel-and-return dance that\n * nothing in this package has. The trap gets the user back to the trigger every time,\n * because `useFocusTrap` restores focus on close.\n *\n * And the panels this kit actually opens are self-contained: pick a date, tap out a\n * sum, narrow a column. Each of them commits and closes, and each already has two\n * documented ways out wired above — Escape and an outside press — so containment costs\n * a keyboard user nothing they had.\n *\n * **It stays non-modal in every other respect**, deliberately: no backdrop, no body\n * scroll lock, and no `aria-modal`, so the page behind is still readable by a screen\n * reader, still scrollable, and still clickable — clicking it is what dismisses this.\n * Only Tab is contained. A popover that took the page away as well would be a\n * {@link Modal}, and there is one of those.\n */\nexport function Popover({\n trigger,\n children,\n width = POPOVER_WIDTH,\n labels,\n panelId,\n className,\n style,\n \"aria-label\": ariaLabel,\n onKeyDown: onKeyDownProp,\n ...rest\n}: PopoverProps) {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n // The trigger's reading direction, read when it is pressed. The panel is portalled to\n // <body>, out of any `dir=\"rtl\"` subtree the trigger sits in, so without carrying it\n // across a right-to-left calendar or filter rendered left-to-right.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const close = () => setOpen(false);\n const toggle = () => {\n setDir(dirOf(triggerRef.current));\n setOpen((v) => !v);\n };\n // prop > `<UiKitProvider labels={{ popover }}>` > English. The provider's\n // `popover.panel` is only the last-resort name for a panel no caller named.\n const text = useKitLabels(\"popover\", DEFAULT_POPOVER_LABELS, labels);\n\n // Align the panel's END edge to the trigger's — right in LTR, left in RTL; it was\n // always `rect.right`, so an RTL panel hung off the wrong side — and clamp it\n // horizontally; the\n // hook owns the vertical half — below by default, flipped above (and height-capped)\n // when that is where the room is, e.g. once a mobile keyboard has eaten the bottom\n // of the screen (feedback #135). It re-measures on scroll/resize.\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n // In px for the arithmetic below, at the text size in force; capped at the viewport.\n const remPx = useRemPx();\n const panelWidth = rect\n ? Math.min(lengthPx(width, remPx), Math.max(0, window.innerWidth - 2 * EDGE))\n : 0;\n const pos = rect\n ? {\n top: placement.top,\n left: Math.min(\n Math.max(EDGE, dir === \"rtl\" ? rect.left : rect.right - panelWidth),\n window.innerWidth - panelWidth - EDGE,\n ),\n }\n : null;\n\n useOutsideClick([triggerRef, panelRef], close, open);\n useEscapeKey(close, open);\n\n useFocusTrap(panelRef, {\n active: open && pos !== null,\n // The panel keeps a focus one of its own children has already taken. `FilterPopover`\n // autofocuses its text box on mount — that is what the press on \"Filter\" was FOR,\n // and it is a prop there precisely so the popover keeps it — and a trap that\n // unconditionally focused the container would take it straight back off again,\n // one effect later and invisibly.\n initialFocus: () => {\n const panel = panelRef.current;\n const active = document.activeElement;\n return panel?.contains(active) ? (active as HTMLElement) : panel;\n },\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // A caller's own handler first (0.32.2: RowActions stops Enter / Space at the panel);\n // it used to be dropped, overwritten by this one.\n onKeyDownProp?.(e);\n if (e.key !== \"Escape\") return;\n // `useEscapeKey` above listens on the document and still covers the case where\n // focus somehow is not in here. This handler is for the case that focus IS, which\n // is now the normal one — and it exists to STOP the key, because a popover opened\n // from inside a `Modal` (the calculator on an amount field is exactly that) would\n // otherwise have its Escape travel on up the React tree and close the dialog\n // underneath as well. Closing twice is harmless — `close` is a state setter, not a\n // caller's callback — so the two paths cannot double-fire anything.\n e.stopPropagation();\n close();\n };\n\n return (\n <>\n {trigger({ open, toggle, ref: triggerRef })}\n {open && pos &&\n createPortal(\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the\n dialog container, per the APG dialog pattern: `onKeyDown` is Escape-to-close for\n whatever inside it holds focus. */\n <div\n // `...rest` first: the role, the id the trigger's `aria-controls` points at,\n // the `tabIndex` the trap needs and the measured position are what make this\n // a popover, and a caller reaching for a `data-tour` anchor must not be able\n // to remove one of them by accident.\n {...rest}\n ref={panelRef}\n role=\"dialog\"\n id={panelId}\n // The caller's own `aria-label` wins over {@link PopoverLabels.panel}, which\n // is the package's English fallback for an unnamed panel. Both name the same\n // element; the DOM spelling is the one being standardised on, so where both\n // are present it is the one that survives.\n aria-label={ariaLabel ?? text.panel}\n dir={rest.dir ?? dir}\n // Without it the panel cannot take focus at all, and the trap above would\n // silently do nothing on a panel whose children are not yet tabbable.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n style={{\n // Merged over the caller's, never replaced by it: these four are measured\n // against the trigger every scroll and resize, and a panel that dropped\n // them would paint at the top-left of the viewport.\n ...style,\n position: \"fixed\",\n top: pos.top,\n left: pos.left,\n width: panelWidth,\n maxHeight: placement.maxHeight,\n }}\n className={cn(\n \"z-50 overflow-y-auto rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-3 shadow-lg outline-none\",\n className,\n )}\n >\n {children(close)}\n </div>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":";AA4LI,mBAOM,KAPN;AA5LJ,SAAS,QAAQ,gBAAgB;AAEjC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AACjC,SAAS,cAAc,uBAAuB;AAC9C,SAAS,oBAAoB;AAC7B,SAAS,UAAU,gBAAgC;AACnD,SAAS,UAAU;AACnB,SAAS,aAAa;AAEtB,SAAS,oBAAoB;AAatB,MAAM,yBAAwC;AAAA;AAAA;AAAA;AAAA,EAInD,OAAO;AACT;AAgCA,MAAM,gBAA2B;AAGjC,MAAM,OAAO;AAwCN,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,WAAW;AAAA,EACX,GAAG;AACL,GAAiB;AACf,QAAM,aAAa,OAA0B,IAAI;AACjD,QAAM,WAAW,OAAuB,IAAI;AAC5C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AAItC,QAAM,CAAC,KAAK,MAAM,IAAI,SAAoB,KAAK;AAC/C,QAAM,QAAQ,MAAM,QAAQ,KAAK;AACjC,QAAM,SAAS,MAAM;AACnB,WAAO,MAAM,WAAW,OAAO,CAAC;AAChC,YAAQ,CAAC,MAAM,CAAC,CAAC;AAAA,EACnB;AAGA,QAAM,OAAO,aAAa,WAAW,wBAAwB,MAAM;AAQnE,QAAM,YAAY,iBAAiB,YAAY,IAAI;AACnD,QAAM,OAAO,UAAU;AAEvB,QAAM,QAAQ,SAAS;AACvB,QAAM,aAAa,OACf,KAAK,IAAI,SAAS,OAAO,KAAK,GAAG,KAAK,IAAI,GAAG,OAAO,aAAa,IAAI,IAAI,CAAC,IAC1E;AACJ,QAAM,MAAM,OACR;AAAA,IACE,KAAK,UAAU;AAAA,IACf,MAAM,KAAK;AAAA,MACT,KAAK,IAAI,MAAM,QAAQ,QAAQ,KAAK,OAAO,KAAK,QAAQ,UAAU;AAAA,MAClE,OAAO,aAAa,aAAa;AAAA,IACnC;AAAA,EACF,IACA;AAEJ,kBAAgB,CAAC,YAAY,QAAQ,GAAG,OAAO,IAAI;AACnD,eAAa,OAAO,IAAI;AAExB,eAAa,UAAU;AAAA,IACrB,QAAQ,QAAQ,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMxB,cAAc,MAAM;AAClB,YAAM,QAAQ,SAAS;AACvB,YAAM,SAAS,SAAS;AACxB,aAAO,OAAO,SAAS,MAAM,IAAK,SAAyB;AAAA,IAC7D;AAAA,EACF,CAAC;AAED,QAAM,gBAAgB,CAAC,MAAqC;AAG1D,oBAAgB,CAAC;AACjB,QAAI,EAAE,QAAQ,SAAU;AAQxB,MAAE,gBAAgB;AAClB,UAAM;AAAA,EACR;AAEA,SACE,iCACG;AAAA,YAAQ,EAAE,MAAM,QAAQ,KAAK,WAAW,CAAC;AAAA,IACzC,QAAQ,OACP;AAAA;AAAA;AAAA;AAAA,MAIE;AAAA,QAAC;AAAA;AAAA,UAKE,GAAG;AAAA,UACJ,KAAK;AAAA,UACL,MAAK;AAAA,UACL,IAAI;AAAA,UAKJ,cAAY,aAAa,KAAK;AAAA,UAC9B,KAAK,KAAK,OAAO;AAAA,UAGjB,UAAU;AAAA,UACV,WAAW;AAAA,UACX,OAAO;AAAA;AAAA;AAAA;AAAA,YAIL,GAAG;AAAA,YACH,UAAU;AAAA,YACV,KAAK,IAAI;AAAA,YACT,MAAM,IAAI;AAAA,YACV,OAAO;AAAA,YACP,WAAW,UAAU;AAAA,UACvB;AAAA,UACA,WAAW;AAAA,YACT;AAAA,YACA;AAAA,UACF;AAAA,UAEC,mBAAS,KAAK;AAAA;AAAA,MACjB;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;","names":[]}
@@ -1,16 +1,17 @@
1
1
  import 'react';
2
2
  import 'lucide-react';
3
- export { cO as DEFAULT_ROW_ACTIONS_LABELS, eO as RowAction, eP as RowActionList, eQ as RowActions, eR as RowActionsCollapse, eS as RowActionsColumnOptions, eT as RowActionsLabels, eU as RowActionsProps, eV as RowActionsSize, fY as rowActionsColumn } from '../delete-account-setting-FLyJf-kZ.js';
4
- import '../data-table-labels-kpAAVi6g.js';
3
+ import './write-lock.js';
4
+ export { cP as DEFAULT_ROW_ACTIONS_LABELS, eZ as RowAction, e_ as RowActionList, e$ as RowActionTone, f0 as RowActions, f1 as RowActionsCollapse, f2 as RowActionsColumnOptions, f3 as RowActionsLabels, f4 as RowActionsProps, f5 as RowActionsSize, g9 as rowActionsColumn } from '../delete-account-setting-DiPX4Vue.js';
5
5
  import './tooltip.js';
6
- import '../hooks/use-anchored-rect.js';
7
- import '../lib/clipping.js';
6
+ import '../data-table-labels-BJ5JE_X2.js';
8
7
  import './mini-calendar.js';
9
8
  import './calendar-heatmap.js';
10
9
  import './popover.js';
11
10
  import '../hooks/use-breakpoint.js';
12
11
  import './chip.js';
13
12
  import './field-parts.js';
13
+ import '../hooks/use-anchored-rect.js';
14
+ import '../lib/clipping.js';
14
15
  import './field-sync.js';
15
16
  import '../wizard/types.js';
16
17
  import '../tour/tour.js';
@@ -52,7 +53,6 @@ import './error-boundary.js';
52
53
  import './authed-image.js';
53
54
  import './image-grid.js';
54
55
  import './lightbox.js';
55
- import './write-lock.js';
56
56
  import '../auth/sign-in-form.js';
57
57
  import '../landing/access.js';
58
58
  import '../landing/landing-labels.js';