@eifi1/ui-kit 0.10.0 → 0.12.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 (382) hide show
  1. package/README.md +66 -37
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/alert-banner.d.ts +26 -1
  12. package/dist/components/alert-banner.js +10 -7
  13. package/dist/components/alert-banner.js.map +1 -1
  14. package/dist/components/amount-input.d.ts +26 -4
  15. package/dist/components/amount-input.js +5 -2
  16. package/dist/components/amount-input.js.map +1 -1
  17. package/dist/components/authed-image.d.ts +68 -0
  18. package/dist/components/authed-image.js +117 -0
  19. package/dist/components/authed-image.js.map +1 -0
  20. package/dist/components/breadcrumbs.d.ts +2 -1
  21. package/dist/components/breadcrumbs.js +5 -2
  22. package/dist/components/breadcrumbs.js.map +1 -1
  23. package/dist/components/bulk-action-bar.d.ts +51 -5
  24. package/dist/components/bulk-action-bar.js +88 -41
  25. package/dist/components/bulk-action-bar.js.map +1 -1
  26. package/dist/components/button-group.d.ts +107 -3
  27. package/dist/components/button-group.js +60 -5
  28. package/dist/components/button-group.js.map +1 -1
  29. package/dist/components/calculator.d.ts +17 -4
  30. package/dist/components/calendar-heatmap.d.ts +162 -0
  31. package/dist/components/calendar-heatmap.js +336 -0
  32. package/dist/components/calendar-heatmap.js.map +1 -0
  33. package/dist/components/chip.d.ts +2 -1
  34. package/dist/components/chip.js +6 -3
  35. package/dist/components/chip.js.map +1 -1
  36. package/dist/components/choice-card.d.ts +66 -2
  37. package/dist/components/choice-card.js +108 -1
  38. package/dist/components/choice-card.js.map +1 -1
  39. package/dist/components/combobox-core.js +2 -0
  40. package/dist/components/combobox-core.js.map +1 -1
  41. package/dist/components/combobox.d.ts +2 -2
  42. package/dist/components/combobox.js +31 -9
  43. package/dist/components/combobox.js.map +1 -1
  44. package/dist/components/copy-button.d.ts +52 -70
  45. package/dist/components/copy-button.js +1 -1
  46. package/dist/components/copy-button.js.map +1 -1
  47. package/dist/components/data-table-cells.d.ts +63 -0
  48. package/dist/components/data-table-cells.js +64 -0
  49. package/dist/components/data-table-cells.js.map +1 -0
  50. package/dist/components/data-table-filter-popover.d.ts +1 -1
  51. package/dist/components/data-table-filters.d.ts +1 -1
  52. package/dist/components/data-table-filters.js +31 -1
  53. package/dist/components/data-table-filters.js.map +1 -1
  54. package/dist/components/data-table-labels.d.ts +13 -0
  55. package/dist/components/data-table-labels.js +8 -1
  56. package/dist/components/data-table-labels.js.map +1 -1
  57. package/dist/components/data-table.d.ts +1 -1
  58. package/dist/components/data-table.js +104 -12
  59. package/dist/components/data-table.js.map +1 -1
  60. package/dist/components/date-picker.d.ts +3 -1
  61. package/dist/components/date-picker.js +15 -2
  62. package/dist/components/date-picker.js.map +1 -1
  63. package/dist/components/description-list.d.ts +23 -3
  64. package/dist/components/description-list.js +23 -3
  65. package/dist/components/description-list.js.map +1 -1
  66. package/dist/components/dialog-frame.d.ts +1 -0
  67. package/dist/components/entity-combobox.d.ts +1 -1
  68. package/dist/components/entity-combobox.js +18 -3
  69. package/dist/components/entity-combobox.js.map +1 -1
  70. package/dist/components/error-boundary.d.ts +92 -0
  71. package/dist/components/error-boundary.js +111 -0
  72. package/dist/components/error-boundary.js.map +1 -0
  73. package/dist/components/field.d.ts +69 -0
  74. package/dist/components/field.js +75 -0
  75. package/dist/components/field.js.map +1 -0
  76. package/dist/components/file-button.d.ts +51 -208
  77. package/dist/components/file-dropzone.d.ts +50 -2
  78. package/dist/components/floating-panel.d.ts +156 -4
  79. package/dist/components/floating-panel.js +193 -29
  80. package/dist/components/floating-panel.js.map +1 -1
  81. package/dist/components/form-actions.d.ts +53 -0
  82. package/dist/components/form-actions.js +98 -0
  83. package/dist/components/form-actions.js.map +1 -0
  84. package/dist/components/image-grid.d.ts +67 -0
  85. package/dist/components/image-grid.js +112 -0
  86. package/dist/components/image-grid.js.map +1 -0
  87. package/dist/components/lightbox.d.ts +104 -0
  88. package/dist/components/lightbox.js +229 -0
  89. package/dist/components/lightbox.js.map +1 -0
  90. package/dist/components/line-items.d.ts +114 -0
  91. package/dist/components/line-items.js +238 -0
  92. package/dist/components/line-items.js.map +1 -0
  93. package/dist/components/list.d.ts +37 -3
  94. package/dist/components/list.js +39 -14
  95. package/dist/components/list.js.map +1 -1
  96. package/dist/components/loading-state.d.ts +24 -0
  97. package/dist/components/loading-state.js +32 -0
  98. package/dist/components/loading-state.js.map +1 -0
  99. package/dist/components/menu-item.d.ts +13 -2
  100. package/dist/components/menu-item.js +11 -2
  101. package/dist/components/menu-item.js.map +1 -1
  102. package/dist/components/mini-calendar.d.ts +82 -5
  103. package/dist/components/mini-calendar.js +212 -64
  104. package/dist/components/mini-calendar.js.map +1 -1
  105. package/dist/components/modal.d.ts +20 -1
  106. package/dist/components/modal.js +26 -2
  107. package/dist/components/modal.js.map +1 -1
  108. package/dist/components/month-picker.d.ts +21 -1
  109. package/dist/components/month-picker.js +139 -26
  110. package/dist/components/month-picker.js.map +1 -1
  111. package/dist/components/multi-entity-combobox.d.ts +1 -1
  112. package/dist/components/multi-entity-combobox.js +19 -3
  113. package/dist/components/multi-entity-combobox.js.map +1 -1
  114. package/dist/components/nav-pills.d.ts +67 -0
  115. package/dist/components/nav-pills.js +69 -0
  116. package/dist/components/nav-pills.js.map +1 -0
  117. package/dist/components/number-field.d.ts +26 -5
  118. package/dist/components/number-field.js +5 -7
  119. package/dist/components/number-field.js.map +1 -1
  120. package/dist/components/number-input.d.ts +27 -5
  121. package/dist/components/number-input.js +7 -1
  122. package/dist/components/number-input.js.map +1 -1
  123. package/dist/components/numpad-sheet.d.ts +17 -4
  124. package/dist/components/passkeys-setting.d.ts +67 -0
  125. package/dist/components/passkeys-setting.js +214 -0
  126. package/dist/components/passkeys-setting.js.map +1 -0
  127. package/dist/components/picker-sheet.js +2 -1
  128. package/dist/components/picker-sheet.js.map +1 -1
  129. package/dist/components/pie-chart-labels.d.ts +20 -0
  130. package/dist/components/pie-chart-labels.js +12 -0
  131. package/dist/components/pie-chart-labels.js.map +1 -0
  132. package/dist/components/pie-chart.d.ts +101 -0
  133. package/dist/components/pie-chart.js +283 -0
  134. package/dist/components/pie-chart.js.map +1 -0
  135. package/dist/components/progress-bar.d.ts +73 -3
  136. package/dist/components/progress-bar.js +110 -8
  137. package/dist/components/progress-bar.js.map +1 -1
  138. package/dist/components/qr-code.d.ts +34 -0
  139. package/dist/components/qr-code.js +57 -0
  140. package/dist/components/qr-code.js.map +1 -0
  141. package/dist/components/search-field.d.ts +15 -3
  142. package/dist/components/search-field.js +5 -1
  143. package/dist/components/search-field.js.map +1 -1
  144. package/dist/components/series-chart.d.ts +16 -0
  145. package/dist/components/series-chart.js +4 -1
  146. package/dist/components/series-chart.js.map +1 -1
  147. package/dist/components/settings-fields.d.ts +50 -1
  148. package/dist/components/signed-amount.d.ts +121 -0
  149. package/dist/components/signed-amount.js +152 -0
  150. package/dist/components/signed-amount.js.map +1 -0
  151. package/dist/components/skeleton.d.ts +10 -3
  152. package/dist/components/skeleton.js +12 -2
  153. package/dist/components/skeleton.js.map +1 -1
  154. package/dist/components/stat-tile.d.ts +2 -1
  155. package/dist/components/stat-tile.js +5 -2
  156. package/dist/components/stat-tile.js.map +1 -1
  157. package/dist/components/status-dot.d.ts +6 -2
  158. package/dist/components/status-dot.js +6 -1
  159. package/dist/components/status-dot.js.map +1 -1
  160. package/dist/components/table.d.ts +77 -11
  161. package/dist/components/table.js +85 -17
  162. package/dist/components/table.js.map +1 -1
  163. package/dist/components/text-link.d.ts +176 -0
  164. package/dist/components/text-link.js +109 -0
  165. package/dist/components/text-link.js.map +1 -0
  166. package/dist/components/text.d.ts +8 -1
  167. package/dist/components/text.js +1 -0
  168. package/dist/components/text.js.map +1 -1
  169. package/dist/components/time-input.d.ts +50 -1
  170. package/dist/components/toggle-group.d.ts +28 -0
  171. package/dist/components/toggle-group.js +58 -9
  172. package/dist/components/toggle-group.js.map +1 -1
  173. package/dist/components/toggle-legend.d.ts +36 -3
  174. package/dist/components/toggle-legend.js +49 -0
  175. package/dist/components/toggle-legend.js.map +1 -1
  176. package/dist/components/tooltip.d.ts +11 -3
  177. package/dist/components/tooltip.js +2 -0
  178. package/dist/components/tooltip.js.map +1 -1
  179. package/dist/components/ui.d.ts +52 -729
  180. package/dist/components/ui.js +296 -27
  181. package/dist/components/ui.js.map +1 -1
  182. package/dist/components/use-table-state.d.ts +3 -52
  183. package/dist/components/use-table-state.js +128 -30
  184. package/dist/components/use-table-state.js.map +1 -1
  185. package/dist/components/user-avatar.d.ts +25 -3
  186. package/dist/components/user-avatar.js +32 -3
  187. package/dist/components/user-avatar.js.map +1 -1
  188. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  189. package/dist/data-table.d.ts +2 -1
  190. package/dist/data-table.js +5 -1
  191. package/dist/data-table.js.map +1 -1
  192. package/dist/feedback/feedback-dialog.d.ts +26 -3
  193. package/dist/feedback/feedback-dialog.js +16 -2
  194. package/dist/feedback/feedback-dialog.js.map +1 -1
  195. package/dist/feedback/feedback-thread.d.ts +133 -0
  196. package/dist/feedback/feedback-thread.js +237 -0
  197. package/dist/feedback/feedback-thread.js.map +1 -0
  198. package/dist/feedback.d.ts +2 -1
  199. package/dist/feedback.js +1 -0
  200. package/dist/feedback.js.map +1 -1
  201. package/dist/hooks/use-authed-src.d.ts +71 -0
  202. package/dist/hooks/use-authed-src.js +63 -0
  203. package/dist/hooks/use-authed-src.js.map +1 -0
  204. package/dist/hooks/use-file-drop.d.ts +50 -2
  205. package/dist/hooks/use-hotkey.d.ts +59 -0
  206. package/dist/hooks/use-hotkey.js +105 -0
  207. package/dist/hooks/use-hotkey.js.map +1 -0
  208. package/dist/hooks/use-search-param-state.d.ts +83 -0
  209. package/dist/hooks/use-search-param-state.js +94 -0
  210. package/dist/hooks/use-search-param-state.js.map +1 -0
  211. package/dist/i18n/defaults.d.ts +17 -4
  212. package/dist/i18n/defaults.js +33 -1
  213. package/dist/i18n/defaults.js.map +1 -1
  214. package/dist/i18n/kit-labels.d.ts +47 -310
  215. package/dist/i18n/kit-labels.js +20 -5
  216. package/dist/i18n/kit-labels.js.map +1 -1
  217. package/dist/i18n/locales/de-CH-informal.d.ts +17 -4
  218. package/dist/i18n/locales/de-CH.d.ts +17 -4
  219. package/dist/i18n/locales/de-informal.d.ts +17 -4
  220. package/dist/i18n/locales/de.d.ts +17 -4
  221. package/dist/i18n/locales/de.js +165 -4
  222. package/dist/i18n/locales/de.js.map +1 -1
  223. package/dist/i18n/locales/es.d.ts +17 -4
  224. package/dist/i18n/locales/es.js +162 -4
  225. package/dist/i18n/locales/es.js.map +1 -1
  226. package/dist/i18n/locales/fr.d.ts +17 -4
  227. package/dist/i18n/locales/fr.js +162 -4
  228. package/dist/i18n/locales/fr.js.map +1 -1
  229. package/dist/i18n/locales/hu.d.ts +17 -4
  230. package/dist/i18n/locales/hu.js +162 -4
  231. package/dist/i18n/locales/hu.js.map +1 -1
  232. package/dist/i18n/locales/it.d.ts +17 -4
  233. package/dist/i18n/locales/it.js +162 -4
  234. package/dist/i18n/locales/it.js.map +1 -1
  235. package/dist/i18n/locales/zh.d.ts +17 -4
  236. package/dist/i18n/locales/zh.js +162 -4
  237. package/dist/i18n/locales/zh.js.map +1 -1
  238. package/dist/index.d.ts +45 -21
  239. package/dist/index.js +43 -1
  240. package/dist/index.js.map +1 -1
  241. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  242. package/dist/lib/clipping.d.ts +21 -5
  243. package/dist/lib/clipping.js +3 -0
  244. package/dist/lib/clipping.js.map +1 -1
  245. package/dist/lib/dates.d.ts +17 -1
  246. package/dist/lib/dates.js +19 -0
  247. package/dist/lib/dates.js.map +1 -1
  248. package/dist/lib/format.d.ts +144 -0
  249. package/dist/lib/format.js +149 -0
  250. package/dist/lib/format.js.map +1 -0
  251. package/dist/lib/qr-encode.d.ts +44 -0
  252. package/dist/lib/qr-encode.js +338 -0
  253. package/dist/lib/qr-encode.js.map +1 -0
  254. package/dist/rhf/fields.d.ts +241 -0
  255. package/dist/rhf/fields.js +601 -0
  256. package/dist/rhf/fields.js.map +1 -0
  257. package/dist/rhf/form.d.ts +50 -1
  258. package/dist/rhf/line-items.d.ts +31 -0
  259. package/dist/rhf/line-items.js +59 -0
  260. package/dist/rhf/line-items.js.map +1 -0
  261. package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
  262. package/dist/rhf/use-rhf-wizard-step.js +38 -0
  263. package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
  264. package/dist/rhf.d.ts +63 -1
  265. package/dist/rhf.js +3 -0
  266. package/dist/rhf.js.map +1 -1
  267. package/dist/shell/app-shell.d.ts +99 -2
  268. package/dist/shell/app-shell.js +58 -3
  269. package/dist/shell/app-shell.js.map +1 -1
  270. package/dist/shell/auth-layout.d.ts +74 -0
  271. package/dist/shell/auth-layout.js +97 -0
  272. package/dist/shell/auth-layout.js.map +1 -0
  273. package/dist/shell/top-bar-brand.d.ts +81 -0
  274. package/dist/shell/top-bar-brand.js +47 -0
  275. package/dist/shell/top-bar-brand.js.map +1 -0
  276. package/dist/shell/topbar-action-menu.d.ts +84 -3
  277. package/dist/shell/topbar-action-menu.js +115 -27
  278. package/dist/shell/topbar-action-menu.js.map +1 -1
  279. package/dist/shell.d.ts +58 -2
  280. package/dist/shell.js +2 -0
  281. package/dist/shell.js.map +1 -1
  282. package/dist/wizard/stepper-nav.d.ts +49 -1
  283. package/dist/wizard/wizard-context.d.ts +30 -1
  284. package/dist/wizard/wizard-context.js +23 -2
  285. package/dist/wizard/wizard-context.js.map +1 -1
  286. package/dist/wizard/wizard-step.d.ts +20 -5
  287. package/dist/wizard/wizard-step.js +16 -3
  288. package/dist/wizard/wizard-step.js.map +1 -1
  289. package/dist/wizard.d.ts +51 -3
  290. package/package.json +1 -1
  291. package/src/chart.ts +4 -0
  292. package/src/components/account-settings-labels.ts +190 -0
  293. package/src/components/account-settings.tsx +110 -63
  294. package/src/components/alert-banner.tsx +36 -7
  295. package/src/components/amount-input.tsx +13 -1
  296. package/src/components/authed-image.tsx +190 -0
  297. package/src/components/breadcrumbs.tsx +8 -2
  298. package/src/components/bulk-action-bar.tsx +163 -57
  299. package/src/components/button-group.tsx +129 -4
  300. package/src/components/calendar-heatmap.tsx +599 -0
  301. package/src/components/chip.tsx +8 -4
  302. package/src/components/choice-card.tsx +208 -1
  303. package/src/components/combobox-core.tsx +5 -0
  304. package/src/components/combobox.tsx +31 -9
  305. package/src/components/copy-button.tsx +3 -2
  306. package/src/components/data-table-cells.tsx +119 -0
  307. package/src/components/data-table-filters.ts +77 -0
  308. package/src/components/data-table-labels.ts +23 -0
  309. package/src/components/data-table.tsx +231 -13
  310. package/src/components/date-picker.tsx +19 -3
  311. package/src/components/description-list.tsx +52 -3
  312. package/src/components/entity-combobox.tsx +18 -3
  313. package/src/components/error-boundary.tsx +216 -0
  314. package/src/components/field.tsx +188 -0
  315. package/src/components/floating-panel.tsx +380 -16
  316. package/src/components/form-actions.tsx +191 -0
  317. package/src/components/image-grid.tsx +181 -0
  318. package/src/components/lightbox.tsx +374 -0
  319. package/src/components/line-items.tsx +385 -0
  320. package/src/components/list.tsx +93 -18
  321. package/src/components/loading-state.tsx +47 -0
  322. package/src/components/menu-item.tsx +29 -3
  323. package/src/components/mini-calendar.tsx +317 -57
  324. package/src/components/modal.tsx +61 -3
  325. package/src/components/month-picker.tsx +181 -26
  326. package/src/components/multi-entity-combobox.tsx +19 -3
  327. package/src/components/nav-pills.tsx +152 -0
  328. package/src/components/number-field.tsx +15 -10
  329. package/src/components/number-input.tsx +16 -1
  330. package/src/components/passkeys-setting.tsx +331 -0
  331. package/src/components/picker-sheet.tsx +7 -1
  332. package/src/components/pie-chart-labels.ts +32 -0
  333. package/src/components/pie-chart.tsx +479 -0
  334. package/src/components/progress-bar.tsx +241 -13
  335. package/src/components/qr-code.tsx +83 -0
  336. package/src/components/search-field.tsx +20 -4
  337. package/src/components/series-chart.tsx +21 -0
  338. package/src/components/signed-amount.tsx +286 -0
  339. package/src/components/skeleton.tsx +21 -3
  340. package/src/components/stat-tile.tsx +8 -2
  341. package/src/components/status-dot.tsx +10 -2
  342. package/src/components/table.tsx +186 -23
  343. package/src/components/text-link.tsx +248 -0
  344. package/src/components/text.tsx +9 -1
  345. package/src/components/toggle-group.tsx +106 -13
  346. package/src/components/toggle-legend.tsx +87 -2
  347. package/src/components/tooltip.tsx +17 -3
  348. package/src/components/ui.tsx +572 -44
  349. package/src/components/use-table-state.ts +265 -33
  350. package/src/components/user-avatar.tsx +57 -3
  351. package/src/data-table.ts +10 -0
  352. package/src/feedback/feedback-dialog.tsx +42 -3
  353. package/src/feedback/feedback-thread.tsx +434 -0
  354. package/src/feedback.ts +1 -0
  355. package/src/hooks/use-authed-src.ts +161 -0
  356. package/src/hooks/use-hotkey.ts +172 -0
  357. package/src/hooks/use-search-param-state.ts +187 -0
  358. package/src/i18n/defaults.ts +32 -0
  359. package/src/i18n/kit-labels.tsx +104 -8
  360. package/src/i18n/locales/de.ts +162 -0
  361. package/src/i18n/locales/es.ts +159 -0
  362. package/src/i18n/locales/fr.ts +162 -0
  363. package/src/i18n/locales/hu.ts +160 -0
  364. package/src/i18n/locales/it.ts +160 -0
  365. package/src/i18n/locales/zh.ts +159 -0
  366. package/src/index.ts +64 -0
  367. package/src/lib/clipping.ts +22 -4
  368. package/src/lib/dates.ts +37 -0
  369. package/src/lib/format.ts +325 -0
  370. package/src/lib/qr-encode.ts +420 -0
  371. package/src/rhf/fields.tsx +1054 -0
  372. package/src/rhf/line-items.tsx +130 -0
  373. package/src/rhf/use-rhf-wizard-step.ts +113 -0
  374. package/src/rhf.ts +8 -0
  375. package/src/shell/app-shell.tsx +147 -2
  376. package/src/shell/auth-layout.tsx +190 -0
  377. package/src/shell/top-bar-brand.tsx +76 -0
  378. package/src/shell/topbar-action-menu.tsx +260 -49
  379. package/src/shell.ts +2 -0
  380. package/src/wizard/wizard-context.tsx +50 -1
  381. package/src/wizard/wizard-step.tsx +43 -6
  382. package/tokens.css +12 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/passkeys-setting.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { KeyRound } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { Button, Card, EmptyState, Input } from \"./ui\";\nimport { Skeleton } from \"./skeleton\";\nimport { useAccountSettingsLabels } from \"./account-settings-labels\";\nimport type { PasskeysSettingLabels } from \"./account-settings-labels\";\n\n/** One registered passkey, as the app's API lists it. */\nexport interface PasskeyItem {\n id: string | number;\n name: string;\n /** A `Date`, an ISO string or epoch ms. */\n createdAt?: Date | string | number | null;\n /** Null (or left out) reads as \"Never used\". */\n lastUsedAt?: Date | string | number | null;\n}\n\nexport interface PasskeysSettingProps {\n /** `undefined` while the first load is in flight (or pass `loading`). */\n passkeys: readonly PasskeyItem[] | undefined;\n loading?: boolean;\n /**\n * Run the WebAuthn ceremony and register the passkey — app-side, since the options\n * and the attestation are the app's server's business. Called with the trimmed name\n * field, which may be EMPTY: pick your own default (\"Passkey\", the device name).\n * Return the promise and the field clears when it resolves; a rejection (including\n * the user cancelling the OS prompt) keeps what was typed.\n */\n onAdd: (name: string) => void | Promise<unknown>;\n /** The ceremony is running. The add button says so and does not start a second one. */\n adding?: boolean;\n /** Offer \"Rename\" on each row. Return a promise to leave edit mode only on success. */\n onRename?: (id: PasskeyItem[\"id\"], name: string) => void | Promise<unknown>;\n /** Offer \"Delete\" on each row — always behind an inline confirmation. */\n onDelete?: (id: PasskeyItem[\"id\"]) => void | Promise<unknown>;\n /** A row whose rename or delete is in flight; its buttons are disabled. */\n busyId?: PasskeyItem[\"id\"] | null;\n /** Show the name field above the add button. Default true. */\n nameField?: boolean;\n /**\n * Shown INSTEAD of the list and the add button — passkeys are not supported by this\n * browser, or the page is not a secure context. Its wording is the app's: only the\n * app knows which of those it detected.\n */\n unavailable?: ReactNode;\n /** A date for the \"Added …\" / \"Last used …\" lines. Default: the locale's medium date. */\n formatDate?: (date: Date) => string;\n /** Overrides the provider's locale for the default date format. */\n locale?: string;\n /** Prop > `<UiKitProvider labels={{ accountSettings: { passkeys } }}>` > English. */\n labels?: Partial<PasskeysSettingLabels>;\n className?: string;\n}\n\n/**\n * The passkeys section of an account page: what is registered, when it was last used,\n * rename and delete, and \"Add passkey\" — the fourth card beside `ProfileSetting`,\n * `PasswordSetting` and `TwoFactorSetting`, and built the same way (a self-contained\n * `<Card>` owning its transient form state; the app owns the data and the API).\n *\n * Lifted from kastlan's passkeys-card. **The WebAuthn ceremony stays app-side**:\n * `navigator.credentials.create` needs options minted by the app's server and hands\n * back an attestation only that server can verify, so the kit calls `onAdd` and waits.\n *\n * **Delete confirms inline**, in the row, rather than through `useConfirm`: this card\n * must work in an app without a `ConfirmProvider`, and the question belongs next to\n * the passkey it names. Focus moves to Cancel, as in every destructive confirm in the\n * kit, so the Enter that pressed Delete cannot also answer it.\n */\nexport function PasskeysSetting({\n passkeys,\n loading,\n onAdd,\n adding = false,\n onRename,\n onDelete,\n busyId,\n nameField = true,\n unavailable,\n formatDate,\n locale: localeProp,\n labels: labelsProp,\n className,\n}: PasskeysSettingProps) {\n const labels = useAccountSettingsLabels(\"passkeys\", labelsProp);\n const locale = useKitLocale(localeProp);\n const [name, setName] = useState(\"\");\n\n const date = (value: PasskeyItem[\"createdAt\"]): string | null => {\n if (value === null || value === undefined || value === \"\") return null;\n const d = value instanceof Date ? value : new Date(value);\n if (Number.isNaN(d.getTime())) return null;\n return formatDate ? formatDate(d) : new Intl.DateTimeFormat(locale, { dateStyle: \"medium\" }).format(d);\n };\n\n const add = () => {\n if (adding) return;\n const typed = name;\n Promise.resolve(onAdd(typed.trim()))\n // Clear only what was sent: the field stays editable while the OS prompt is up.\n .then(() => setName((current) => (current === typed ? \"\" : current)))\n .catch(() => {});\n };\n\n const isLoading = loading || passkeys === undefined;\n const items = passkeys ?? [];\n\n const body = (() => {\n if (unavailable != null && unavailable !== false) {\n return <EmptyState variant=\"inline\" size=\"sm\" icon={<KeyRound />} title={unavailable} />;\n }\n return (\n <>\n {isLoading ? (\n <Skeleton lines={2} label={labels.loading} />\n ) : items.length === 0 ? (\n <EmptyState variant=\"inline\" size=\"sm\" icon={<KeyRound />} title={labels.empty} />\n ) : (\n <ul aria-label={labels.list} className=\"divide-y divide-[var(--border)]\">\n {items.map((item) => (\n <PasskeyRow\n key={item.id}\n item={item}\n created={date(item.createdAt)}\n lastUsed={date(item.lastUsedAt)}\n busy={busyId !== undefined && busyId !== null && busyId === item.id}\n onRename={onRename}\n onDelete={onDelete}\n labels={labels}\n />\n ))}\n </ul>\n )}\n <div className=\"space-y-2 pt-1\">\n {nameField && (\n <Input\n label={labels.nameLabel}\n placeholder={labels.namePlaceholder}\n value={name}\n maxLength={120}\n onChange={(e) => setName(e.target.value)}\n onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n add();\n }\n }}\n />\n )}\n <Button onClick={add} disabled={adding} aria-busy={adding || undefined}>\n <KeyRound className=\"size-4\" aria-hidden />\n {adding ? labels.adding : labels.add}\n </Button>\n </div>\n </>\n );\n })();\n\n return (\n <Card className={cn(\"p-4 space-y-3\", className)}>\n <div>\n <div className=\"text-sm font-medium\">{labels.title}</div>\n <div className=\"text-xs text-[var(--text-muted)]\">\n {labels.description}\n </div>\n </div>\n {body}\n </Card>\n );\n}\n\ntype RowMode = \"view\" | \"rename\" | \"delete\";\n\nfunction PasskeyRow({\n item,\n created,\n lastUsed,\n busy,\n onRename,\n onDelete,\n labels,\n}: {\n item: PasskeyItem;\n created: string | null;\n lastUsed: string | null;\n busy: boolean;\n onRename?: PasskeysSettingProps[\"onRename\"];\n onDelete?: PasskeysSettingProps[\"onDelete\"];\n labels: PasskeysSettingLabels;\n}) {\n const [mode, setMode] = useState<RowMode>(\"view\");\n const [draft, setDraft] = useState(item.name);\n // Which button opened the sub-mode, so leaving it puts focus back there — the\n // confirm or the field unmounts under the focus, which otherwise falls to <body>.\n const [returnTo, setReturnTo] = useState<\"rename\" | \"delete\" | null>(null);\n const cancelDeleteRef = useRef<HTMLButtonElement>(null);\n const renameRef = useRef<HTMLInputElement>(null);\n const renameButtonRef = useRef<HTMLButtonElement>(null);\n const deleteButtonRef = useRef<HTMLButtonElement>(null);\n\n useEffect(() => {\n if (mode === \"delete\") cancelDeleteRef.current?.focus();\n else if (mode === \"rename\") renameRef.current?.select();\n else if (returnTo) (returnTo === \"rename\" ? renameButtonRef : deleteButtonRef).current?.focus();\n }, [mode, returnTo]);\n\n const open = (next: \"rename\" | \"delete\") => {\n setReturnTo(next);\n setDraft(item.name);\n setMode(next);\n };\n const leave = () => setMode(\"view\");\n const escapeLeaves = (e: KeyboardEvent<HTMLButtonElement>) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n leave();\n }\n };\n\n const saveRename = () => {\n const next = draft.trim();\n if (!onRename || !next || busy) return;\n if (next === item.name) {\n leave();\n return;\n }\n Promise.resolve(onRename(item.id, next))\n .then(leave)\n .catch(() => {});\n };\n\n const confirmDelete = () => {\n if (!onDelete || busy) return;\n // The row unmounts once the list refreshes; on a failure it stays, confirm open.\n Promise.resolve(onDelete(item.id))\n .then(() => setMode(\"view\"))\n .catch(() => {});\n };\n\n const meta = [created && labels.created(created), lastUsed ? labels.lastUsed(lastUsed) : labels.neverUsed]\n .filter(Boolean)\n .join(\" · \");\n\n if (mode === \"rename\") {\n return (\n <li className=\"flex flex-wrap items-center gap-2 py-2\">\n <Input\n ref={renameRef}\n aria-label={labels.renameField(item.name)}\n className=\"min-w-0 flex-1\"\n value={draft}\n maxLength={120}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n saveRename();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n leave();\n }\n }}\n />\n <Button size=\"sm\" onClick={saveRename} disabled={busy || !draft.trim()}>\n {labels.save}\n </Button>\n <Button size=\"sm\" variant=\"ghost\" onClick={leave}>\n {labels.cancel}\n </Button>\n </li>\n );\n }\n\n if (mode === \"delete\") {\n return (\n <li className=\"space-y-2 py-2\">\n <p role=\"alert\" className=\"text-sm\">\n {labels.deleteConfirm(item.name)}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button size=\"sm\" variant=\"danger\" onClick={confirmDelete} onKeyDown={escapeLeaves} disabled={busy}>\n {labels.confirmDelete}\n </Button>\n <Button ref={cancelDeleteRef} size=\"sm\" variant=\"ghost\" onClick={leave} onKeyDown={escapeLeaves}>\n {labels.cancel}\n </Button>\n </div>\n </li>\n );\n }\n\n return (\n <li className=\"flex items-center gap-3 py-2\">\n <KeyRound className=\"size-4 shrink-0 text-[var(--text-muted)]\" aria-hidden />\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm font-medium\">{item.name}</div>\n <div className=\"text-xs text-[var(--text-muted)]\">{meta}</div>\n </div>\n {onRename && (\n <Button\n ref={renameButtonRef}\n size=\"sm\"\n variant=\"ghost\"\n disabled={busy}\n onClick={() => open(\"rename\")}\n aria-label={labels.renameItem(item.name)}\n >\n {labels.rename}\n </Button>\n )}\n {onDelete && (\n <Button\n ref={deleteButtonRef}\n size=\"sm\"\n variant=\"ghost\"\n tone=\"danger\"\n disabled={busy}\n onClick={() => open(\"delete\")}\n aria-label={labels.deleteItem(item.name)}\n >\n {labels.delete}\n </Button>\n )}\n </li>\n );\n}\n"],"mappings":";AAgH0D,SAGpD,UAHoD,KAwChD,YAxCgD;AAhH1D,SAAS,WAAW,QAAQ,gBAAgB;AAE5C,SAAS,gBAAgB;AACzB,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAC7B,SAAS,QAAQ,MAAM,YAAY,aAAa;AAChD,SAAS,gBAAgB;AACzB,SAAS,gCAAgC;AAiElC,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR;AACF,GAAyB;AACvB,QAAM,SAAS,yBAAyB,YAAY,UAAU;AAC9D,QAAM,SAAS,aAAa,UAAU;AACtC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,EAAE;AAEnC,QAAM,OAAO,CAAC,UAAmD;AAC/D,QAAI,UAAU,QAAQ,UAAU,UAAa,UAAU,GAAI,QAAO;AAClE,UAAM,IAAI,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;AACxD,QAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AACtC,WAAO,aAAa,WAAW,CAAC,IAAI,IAAI,KAAK,eAAe,QAAQ,EAAE,WAAW,SAAS,CAAC,EAAE,OAAO,CAAC;AAAA,EACvG;AAEA,QAAM,MAAM,MAAM;AAChB,QAAI,OAAQ;AACZ,UAAM,QAAQ;AACd,YAAQ,QAAQ,MAAM,MAAM,KAAK,CAAC,CAAC,EAEhC,KAAK,MAAM,QAAQ,CAAC,YAAa,YAAY,QAAQ,KAAK,OAAQ,CAAC,EACnE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACnB;AAEA,QAAM,YAAY,WAAW,aAAa;AAC1C,QAAM,QAAQ,YAAY,CAAC;AAE3B,QAAM,QAAQ,MAAM;AAClB,QAAI,eAAe,QAAQ,gBAAgB,OAAO;AAChD,aAAO,oBAAC,cAAW,SAAQ,UAAS,MAAK,MAAK,MAAM,oBAAC,YAAS,GAAI,OAAO,aAAa;AAAA,IACxF;AACA,WACE,iCACG;AAAA,kBACC,oBAAC,YAAS,OAAO,GAAG,OAAO,OAAO,SAAS,IACzC,MAAM,WAAW,IACnB,oBAAC,cAAW,SAAQ,UAAS,MAAK,MAAK,MAAM,oBAAC,YAAS,GAAI,OAAO,OAAO,OAAO,IAEhF,oBAAC,QAAG,cAAY,OAAO,MAAM,WAAU,mCACpC,gBAAM,IAAI,CAAC,SACV;AAAA,QAAC;AAAA;AAAA,UAEC;AAAA,UACA,SAAS,KAAK,KAAK,SAAS;AAAA,UAC5B,UAAU,KAAK,KAAK,UAAU;AAAA,UAC9B,MAAM,WAAW,UAAa,WAAW,QAAQ,WAAW,KAAK;AAAA,UACjE;AAAA,UACA;AAAA,UACA;AAAA;AAAA,QAPK,KAAK;AAAA,MAQZ,CACD,GACH;AAAA,MAEF,qBAAC,SAAI,WAAU,kBACZ;AAAA,qBACC;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO;AAAA,YACd,aAAa,OAAO;AAAA,YACpB,OAAO;AAAA,YACP,WAAW;AAAA,YACX,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK;AAAA,YACvC,WAAW,CAAC,MAAuC;AACjD,kBAAI,EAAE,QAAQ,SAAS;AACrB,kBAAE,eAAe;AACjB,oBAAI;AAAA,cACN;AAAA,YACF;AAAA;AAAA,QACF;AAAA,QAEF,qBAAC,UAAO,SAAS,KAAK,UAAU,QAAQ,aAAW,UAAU,QAC3D;AAAA,8BAAC,YAAS,WAAU,UAAS,eAAW,MAAC;AAAA,UACxC,SAAS,OAAO,SAAS,OAAO;AAAA,WACnC;AAAA,SACF;AAAA,OACF;AAAA,EAEJ,GAAG;AAEH,SACE,qBAAC,QAAK,WAAW,GAAG,iBAAiB,SAAS,GAC5C;AAAA,yBAAC,SACC;AAAA,0BAAC,SAAI,WAAU,uBAAuB,iBAAO,OAAM;AAAA,MACnD,oBAAC,SAAI,WAAU,oCACZ,iBAAO,aACV;AAAA,OACF;AAAA,IACC;AAAA,KACH;AAEJ;AAIA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAkB,MAAM;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK,IAAI;AAG5C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAqC,IAAI;AACzE,QAAM,kBAAkB,OAA0B,IAAI;AACtD,QAAM,YAAY,OAAyB,IAAI;AAC/C,QAAM,kBAAkB,OAA0B,IAAI;AACtD,QAAM,kBAAkB,OAA0B,IAAI;AAEtD,YAAU,MAAM;AACd,QAAI,SAAS,SAAU,iBAAgB,SAAS,MAAM;AAAA,aAC7C,SAAS,SAAU,WAAU,SAAS,OAAO;AAAA,aAC7C,SAAU,EAAC,aAAa,WAAW,kBAAkB,iBAAiB,SAAS,MAAM;AAAA,EAChG,GAAG,CAAC,MAAM,QAAQ,CAAC;AAEnB,QAAM,OAAO,CAAC,SAA8B;AAC1C,gBAAY,IAAI;AAChB,aAAS,KAAK,IAAI;AAClB,YAAQ,IAAI;AAAA,EACd;AACA,QAAM,QAAQ,MAAM,QAAQ,MAAM;AAClC,QAAM,eAAe,CAAC,MAAwC;AAC5D,QAAI,EAAE,QAAQ,UAAU;AACtB,QAAE,eAAe;AACjB,QAAE,gBAAgB;AAClB,YAAM;AAAA,IACR;AAAA,EACF;AAEA,QAAM,aAAa,MAAM;AACvB,UAAM,OAAO,MAAM,KAAK;AACxB,QAAI,CAAC,YAAY,CAAC,QAAQ,KAAM;AAChC,QAAI,SAAS,KAAK,MAAM;AACtB,YAAM;AACN;AAAA,IACF;AACA,YAAQ,QAAQ,SAAS,KAAK,IAAI,IAAI,CAAC,EACpC,KAAK,KAAK,EACV,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACnB;AAEA,QAAM,gBAAgB,MAAM;AAC1B,QAAI,CAAC,YAAY,KAAM;AAEvB,YAAQ,QAAQ,SAAS,KAAK,EAAE,CAAC,EAC9B,KAAK,MAAM,QAAQ,MAAM,CAAC,EAC1B,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACnB;AAEA,QAAM,OAAO,CAAC,WAAW,OAAO,QAAQ,OAAO,GAAG,WAAW,OAAO,SAAS,QAAQ,IAAI,OAAO,SAAS,EACtG,OAAO,OAAO,EACd,KAAK,QAAK;AAEb,MAAI,SAAS,UAAU;AACrB,WACE,qBAAC,QAAG,WAAU,0CACZ;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,cAAY,OAAO,YAAY,KAAK,IAAI;AAAA,UACxC,WAAU;AAAA,UACV,OAAO;AAAA,UACP,WAAW;AAAA,UACX,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,UACxC,WAAW,CAAC,MAAuC;AACjD,gBAAI,EAAE,QAAQ,SAAS;AACrB,gBAAE,eAAe;AACjB,yBAAW;AAAA,YACb,WAAW,EAAE,QAAQ,UAAU;AAC7B,gBAAE,eAAe;AACjB,gBAAE,gBAAgB;AAClB,oBAAM;AAAA,YACR;AAAA,UACF;AAAA;AAAA,MACF;AAAA,MACA,oBAAC,UAAO,MAAK,MAAK,SAAS,YAAY,UAAU,QAAQ,CAAC,MAAM,KAAK,GAClE,iBAAO,MACV;AAAA,MACA,oBAAC,UAAO,MAAK,MAAK,SAAQ,SAAQ,SAAS,OACxC,iBAAO,QACV;AAAA,OACF;AAAA,EAEJ;AAEA,MAAI,SAAS,UAAU;AACrB,WACE,qBAAC,QAAG,WAAU,kBACZ;AAAA,0BAAC,OAAE,MAAK,SAAQ,WAAU,WACvB,iBAAO,cAAc,KAAK,IAAI,GACjC;AAAA,MACA,qBAAC,SAAI,WAAU,wBACb;AAAA,4BAAC,UAAO,MAAK,MAAK,SAAQ,UAAS,SAAS,eAAe,WAAW,cAAc,UAAU,MAC3F,iBAAO,eACV;AAAA,QACA,oBAAC,UAAO,KAAK,iBAAiB,MAAK,MAAK,SAAQ,SAAQ,SAAS,OAAO,WAAW,cAChF,iBAAO,QACV;AAAA,SACF;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,qBAAC,QAAG,WAAU,gCACZ;AAAA,wBAAC,YAAS,WAAU,4CAA2C,eAAW,MAAC;AAAA,IAC3E,qBAAC,SAAI,WAAU,kBACb;AAAA,0BAAC,SAAI,WAAU,gCAAgC,eAAK,MAAK;AAAA,MACzD,oBAAC,SAAI,WAAU,oCAAoC,gBAAK;AAAA,OAC1D;AAAA,IACC,YACC;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,UAAU;AAAA,QACV,SAAS,MAAM,KAAK,QAAQ;AAAA,QAC5B,cAAY,OAAO,WAAW,KAAK,IAAI;AAAA,QAEtC,iBAAO;AAAA;AAAA,IACV;AAAA,IAED,YACC;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,UAAU;AAAA,QACV,SAAS,MAAM,KAAK,QAAQ;AAAA,QAC5B,cAAY,OAAO,WAAW,KAAK,IAAI;AAAA,QAEtC,iBAAO;AAAA;AAAA,IACV;AAAA,KAEJ;AAEJ;","names":[]}
@@ -4,6 +4,7 @@ import { useRef } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import { X } from "lucide-react";
6
6
  import { cn } from "../lib/cn.js";
7
+ import { CLIPS_ATTRIBUTE } from "../lib/clipping.js";
7
8
  import { DropdownSearchHeader } from "./dropdown.js";
8
9
  import { useBodyScrollLock } from "../hooks/use-body-scroll-lock.js";
9
10
  import { useOverlayHistory } from "../hooks/use-overlay-history.js";
@@ -80,7 +81,7 @@ function PickerSheet({
80
81
  placeholder: searchPlaceholder
81
82
  }
82
83
  ),
83
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain", children })
84
+ /* @__PURE__ */ jsx("div", { ...{ [CLIPS_ATTRIBUTE]: "" }, className: "min-h-0 flex-1 overflow-y-auto overscroll-contain", children })
84
85
  ]
85
86
  }
86
87
  ),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/picker-sheet.tsx"],"sourcesContent":["import { useRef } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { X } from \"lucide-react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownSearchHeader } from \"./dropdown\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useVisualViewport } from \"../hooks/use-anchored-panel\";\nimport { DEFAULT_PICKER_SHEET_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/**\n * Two of the div's own attributes are omitted because this component already owns\n * the name: `title` here is the sheet's HEADING (and a ReactNode), not the browser's\n * tooltip string, and `onClose` is \"the sheet was dismissed\", not the DOM's\n * `<dialog>` close event. Everything else a caller can pass reaches the sheet.\n */\nexport interface PickerSheetProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\" | \"onClose\"> {\n open: boolean;\n onClose: () => void;\n /** The field's own label — a sheet that fills the screen has to say what it is\n * asking for, which the anchored panel got for free by sitting under it. */\n title?: ReactNode;\n query: string;\n onQueryChange: (value: string) => void;\n searchPlaceholder?: string;\n inputRef?: RefObject<HTMLInputElement | null>;\n /** Default: `pickerSheet.close` from the {@link UiKitProvider}, else \"Close\". */\n closeLabel?: string;\n children: ReactNode;\n}\n\n/**\n * The PHONE presentation of a picker: a full-screen dialog with a search box at\n * the top and the list filling everything below it (Keksdose live #200).\n *\n * *\"Paid as full screen dialog with input. Similar to the account select that\n * already appears as full screen.\"* The account field is a native `<select>`, and\n * a phone browser renders that as a full-screen list — so the app's own pickers,\n * which are anchored dropdown panels, were the odd ones out: a 320px-tall panel\n * squeezed between the field and the keyboard, showing three or four rows of a\n * list that might have two hundred entries.\n *\n * What this fixes beyond size: the panel had to be *placed* (above/below, tracking\n * the visual viewport as the keyboard opened — see `useAnchoredPanel`), and every\n * one of those decisions is a chance to be wrong on a screen this small. A sheet\n * has no placement. It also gives the search input somewhere unambiguous to live,\n * which is the other half of the request: *\"add filler possibility to the account\n * select or even better right to the hoc the account select is derived from\"* —\n * so this lives in the shared picker, not in one form's copy of it.\n *\n * Rows are `min-h-11` (44px): the touch target the rest of the app uses. The list\n * scrolls, the header does not, and the body behind it is scroll-locked so a drag\n * that overshoots the list does not move the page underneath.\n */\nexport function PickerSheet({\n open,\n onClose,\n title,\n query,\n onQueryChange,\n searchPlaceholder,\n inputRef,\n closeLabel,\n children,\n \"aria-label\": ariaLabel,\n className,\n style,\n ...rest\n}: PickerSheetProps) {\n // Every picker in the kit opens one of these on a phone, and most of them pass\n // their own `closeLabel` through as `undefined` — so this lookup is what makes the\n // one way out of a full-screen sheet speak the app's language without each of\n // them having to (see the prop's note in `EntityCombobox`).\n const labels = useKitLabels(\"pickerSheet\", DEFAULT_PICKER_SHEET_LABELS, { close: closeLabel });\n useBodyScrollLock(open);\n /**\n * Back closes THE SHEET, not the dialog it was opened from (Keksdose live #309:\n * *\"Mouse Back does not only close the select but also the whole edit or create\n * dialog which is cumbersome.\"*).\n *\n * The sheet is a full-screen overlay over whatever opened it, and on a phone that\n * is almost always the row editor — which DOES push a history entry\n * (`data-table.tsx`). So the sheet was invisible to Back: one press popped the\n * editor's entry and took the whole dialog with it, discarding an edit in progress\n * in order to dismiss a list.\n *\n * `useOverlayHistory` was built for exactly this nesting — its stack unwinds\n * last-in-first-out, and the U-12 regression test in its own suite is written\n * around \"a Modal and a PickerSheet inside it\". The sheet simply never called it.\n *\n * Nothing else has to change: the hook is a no-op while `open` is false, and it\n * pops its own entry when the sheet closes some other way (the X, a row, the\n * backdrop) so history does not accumulate husks.\n */\n useOverlayHistory(open, onClose);\n // The visible region, while the sheet is up — see the `style` below for why a\n // full-screen sheet cannot simply be `inset-0` on a phone (live #328).\n const vv = useVisualViewport(open);\n // A fallback ref so a caller that does not need the handle still gets the opening\n // focus, rather than the behaviour depending on whether a prop was passed. What that\n // focus is FOR is on the trap below.\n const ownRef = useRef<HTMLInputElement | null>(null);\n const searchRef = inputRef ?? ownRef;\n\n const sheetRef = useRef<HTMLDivElement>(null);\n /**\n * The sheet said `aria-modal=\"true\"` and then left focus on the field behind it, so\n * a screen reader was told to hide the page the user's cursor was still standing in\n * (the audit's *\"Only Modal manages focus\"*). Through the shared hook rather than\n * the bare `.focus()` that used to live here: the hook re-reads the tabbable list on\n * every Tab, which this panel needs more than most — its list is filtered as you\n * type, so the rows a trap captured at open time are gone by the second keystroke.\n *\n * **`initialFocus` is the search box**, not the container, and this is the one place\n * in the package that overrides `Modal`'s rule. The sheet exists in order to be typed\n * into (live #212: *\"make the text input filter active on select click\"*), and the\n * callers cannot do it themselves — `Combobox`'s field runs\n * `setOpen(true); sheetInputRef.current?.focus()` in a single `onFocus`, so the focus\n * call happens while this sheet is still unmounted and the ref is null. Only the\n * sheet knows when its own input exists. Popping the keyboard is the POINT here,\n * where in a form dialog it is the cost.\n *\n * **`restoreFocus` is off**, which is the uncomfortable half. Every caller in this\n * kit opens the sheet from the field's own `onFocus` handler, so handing focus back\n * to that field on close re-opens the sheet the user has just dismissed — a row tap\n * would select a value and immediately put the list back over it. The field that\n * opened it is the only thing that knows whether re-focusing it means \"open\"; a sheet\n * that cannot tell must not guess. The cost is that focus lands on `<body>` after a\n * dismissal, which on the phone this shape is for costs nothing, and on a pointer\n * device is what happened before this change too.\n */\n useFocusTrap(sheetRef, {\n active: open,\n restoreFocus: false,\n initialFocus: () => searchRef.current,\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key !== \"Escape\") return;\n // The sheet is almost always the INNERMOST overlay — it opens from a field inside a\n // `Modal` or a `FullBleedDialog`, both of which take Escape on their own panel. A\n // React portal's events still travel up the React tree, so without stopping it here\n // one press would dismiss this list and the dialog underneath with it: Keksdose\n // live #309 (*\"Mouse Back does not only close the select but also the whole edit or\n // create dialog which is cumbersome\"*), which the Back gesture already learned, on\n // the key a desktop user reaches for instead.\n e.stopPropagation();\n onClose();\n };\n\n if (!open || typeof document === \"undefined\") return null;\n return createPortal(\n <div\n // Spread FIRST, and every attribute below it deliberately wins: `style`,\n // `onMouseDown` and `onKeyDown` here are not styling choices, they are live\n // #328, dev#477 and live #309 respectively, and a caller who passed one by\n // accident would re-open a bug three apps have already paid for. `style` is\n // MERGED rather than won outright (see below), which is the one case where a\n // caller has something to add.\n {...rest}\n ref={sheetRef}\n role=\"dialog\"\n aria-modal=\"true\"\n // Without it the container cannot take focus, and `useFocusTrap` has nothing to\n // fall back to when the list is empty and the search box is the only thing in\n // here — see the hook's \"nothing tabbable\" branch.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n // The caller's own name wins over the heading: a sheet titled \"Account\" in a\n // form with two of them needs to say WHICH, and `title` is what the user reads\n // rather than what a reader announces.\n aria-label={ariaLabel ?? (typeof title === \"string\" ? title : undefined)}\n // The sheet is PORTALLED to <body>, so it is not inside the field's own\n // wrapper — and `useDropdown` closes on any document mousedown landing\n // outside that wrapper. Tapping a row therefore unmounted the row (open →\n // false) before its CLICK could fire, so the tap selected nothing at all and\n // the sheet just vanished (Keksdose dev#477).\n //\n // It bit whichever sheet commits on `onClick` — the payee field's, since live\n // #200, where it hid behind the free-text value the search box was already\n // setting; and the account field's the moment dev#477 moved it onto the inline\n // picker. `ComboboxPanel`'s rows happen to commit on `onMouseDown`, so they\n // beat the document listener and were never affected — which is luck, not a\n // design, and exactly why the fix belongs to the sheet.\n //\n // Stopping the mousedown here rather than teaching every caller about the\n // portal: the sheet is modal and full-screen, so \"a click in here is not an\n // outside click\" is a property of the sheet, not of whoever opened it.\n onMouseDown={(e) => e.stopPropagation()}\n // ⚠️ `inset-0` was the whole of Keksdose live #328: *\"When the keyboard overlays\n // the entries I cannot scroll past them … to see also the last entries. Happens\n // when browsing the account select.\"*\n //\n // This sheet FOCUSES ITS OWN SEARCH BOX on open (see above), so the keyboard is\n // up every single time it is used on a phone. On Android that shrinks the visual\n // viewport and leaves the LAYOUT viewport alone — `inset-0` is the layout\n // viewport — so the sheet kept its full height with its bottom third behind the\n // keyboard. The list below is `flex-1 overflow-y-auto`, so it sized itself to\n // that hidden height too: it scrolled to ITS end while the last rows were still\n // under the keys, and no gesture could bring them out. Nothing was clipped and\n // nothing looked broken, which is why it reads as \"I cannot scroll past them\".\n //\n // `visualViewport` is the API that answers rather than hints (the live #292\n // lesson): it reports the region actually on screen, keyboard and pinch-pan\n // included, and it fires on the keyboard opening and closing. Pinned to that box,\n // the sheet ends where the keys begin and the list's own scroll covers the rest.\n // `useVisualViewport` returns null when there is nothing to correct, and then\n // this is character-for-character the sheet that was here before.\n // A caller's `style` is kept and the correction laid OVER it: the three values\n // below are the whole of live #328, so they are not negotiable, but a z-index\n // or a transition from outside has nothing to do with them. Without a visual\n // viewport to correct for this is character-for-character what the caller passed.\n style={vv ? { ...style, top: vv.top, height: vv.height, bottom: \"auto\" } : style}\n className={cn(\"fixed inset-0 z-50 flex flex-col bg-[var(--bg-surface)] outline-none\", className)}\n >\n <div className=\"flex items-center gap-2 border-b border-[var(--border)] px-3 py-2\">\n <span className=\"min-w-0 flex-1 truncate text-sm font-semibold text-[var(--text-primary)]\">\n {title}\n </span>\n <button\n type=\"button\"\n aria-label={labels.close}\n onClick={onClose}\n className=\"rounded p-1.5 text-[var(--text-muted)] hover:bg-[var(--bg-hover)]\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n <DropdownSearchHeader\n query={query}\n onQueryChange={onQueryChange}\n inputRef={searchRef}\n placeholder={searchPlaceholder}\n />\n {/* min-h-0 so the LIST scrolls rather than the dialog growing past the\n viewport — a flex child defaults to min-height:auto. */}\n <div className=\"min-h-0 flex-1 overflow-y-auto overscroll-contain\">{children}</div>\n </div>,\n document.body,\n );\n}\n\n/** A row inside a {@link PickerSheet} — the phone-sized version of a dropdown row,\n * so a list is comfortable to hit with a thumb rather than merely legible. */\nexport const SHEET_ROW_CLASS = cn(\n \"flex min-h-11 w-full items-center gap-2 px-4 py-2 text-start text-base\",\n \"text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\",\n);\n"],"mappings":";AA0NM,SACE,KADF;AA1NN,SAAS,cAAc;AACvB,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,UAAU;AACnB,SAAS,4BAA4B;AACrC,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,6BAA6B,oBAAoB;AA+CnD,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AAKnB,QAAM,SAAS,aAAa,eAAe,6BAA6B,EAAE,OAAO,WAAW,CAAC;AAC7F,oBAAkB,IAAI;AAoBtB,oBAAkB,MAAM,OAAO;AAG/B,QAAM,KAAK,kBAAkB,IAAI;AAIjC,QAAM,SAAS,OAAgC,IAAI;AACnD,QAAM,YAAY,YAAY;AAE9B,QAAM,WAAW,OAAuB,IAAI;AA2B5C,eAAa,UAAU;AAAA,IACrB,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,cAAc,MAAM,UAAU;AAAA,EAChC,CAAC;AAED,QAAM,gBAAgB,CAAC,MAAqC;AAC1D,QAAI,EAAE,QAAQ,SAAU;AAQxB,MAAE,gBAAgB;AAClB,YAAQ;AAAA,EACV;AAEA,MAAI,CAAC,QAAQ,OAAO,aAAa,YAAa,QAAO;AACrD,SAAO;AAAA,IACL;AAAA,MAAC;AAAA;AAAA,QAOE,GAAG;AAAA,QACJ,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAW;AAAA,QAIX,UAAU;AAAA,QACV,WAAW;AAAA,QAIX,cAAY,cAAc,OAAO,UAAU,WAAW,QAAQ;AAAA,QAiB9D,aAAa,CAAC,MAAM,EAAE,gBAAgB;AAAA,QAwBtC,OAAO,KAAK,EAAE,GAAG,OAAO,KAAK,GAAG,KAAK,QAAQ,GAAG,QAAQ,QAAQ,OAAO,IAAI;AAAA,QAC3E,WAAW,GAAG,wEAAwE,SAAS;AAAA,QAE/F;AAAA,+BAAC,SAAI,WAAU,qEACb;AAAA,gCAAC,UAAK,WAAU,4EACb,iBACH;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,OAAO;AAAA,gBACnB,SAAS;AAAA,gBACT,WAAU;AAAA,gBAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,YACxB;AAAA,aACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA,UAAU;AAAA,cACV,aAAa;AAAA;AAAA,UACf;AAAA,UAGA,oBAAC,SAAI,WAAU,qDAAqD,UAAS;AAAA;AAAA;AAAA,IAC/E;AAAA,IACA,SAAS;AAAA,EACX;AACF;AAIO,MAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/components/picker-sheet.tsx"],"sourcesContent":["import { useRef } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { X } from \"lucide-react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { CLIPS_ATTRIBUTE } from \"../lib/clipping\";\nimport { DropdownSearchHeader } from \"./dropdown\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useVisualViewport } from \"../hooks/use-anchored-panel\";\nimport { DEFAULT_PICKER_SHEET_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/**\n * Two of the div's own attributes are omitted because this component already owns\n * the name: `title` here is the sheet's HEADING (and a ReactNode), not the browser's\n * tooltip string, and `onClose` is \"the sheet was dismissed\", not the DOM's\n * `<dialog>` close event. Everything else a caller can pass reaches the sheet.\n */\nexport interface PickerSheetProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\" | \"onClose\"> {\n open: boolean;\n onClose: () => void;\n /** The field's own label — a sheet that fills the screen has to say what it is\n * asking for, which the anchored panel got for free by sitting under it. */\n title?: ReactNode;\n query: string;\n onQueryChange: (value: string) => void;\n searchPlaceholder?: string;\n inputRef?: RefObject<HTMLInputElement | null>;\n /** Default: `pickerSheet.close` from the {@link UiKitProvider}, else \"Close\". */\n closeLabel?: string;\n children: ReactNode;\n}\n\n/**\n * The PHONE presentation of a picker: a full-screen dialog with a search box at\n * the top and the list filling everything below it (Keksdose live #200).\n *\n * *\"Paid as full screen dialog with input. Similar to the account select that\n * already appears as full screen.\"* The account field is a native `<select>`, and\n * a phone browser renders that as a full-screen list — so the app's own pickers,\n * which are anchored dropdown panels, were the odd ones out: a 320px-tall panel\n * squeezed between the field and the keyboard, showing three or four rows of a\n * list that might have two hundred entries.\n *\n * What this fixes beyond size: the panel had to be *placed* (above/below, tracking\n * the visual viewport as the keyboard opened — see `useAnchoredPanel`), and every\n * one of those decisions is a chance to be wrong on a screen this small. A sheet\n * has no placement. It also gives the search input somewhere unambiguous to live,\n * which is the other half of the request: *\"add filler possibility to the account\n * select or even better right to the hoc the account select is derived from\"* —\n * so this lives in the shared picker, not in one form's copy of it.\n *\n * Rows are `min-h-11` (44px): the touch target the rest of the app uses. The list\n * scrolls, the header does not, and the body behind it is scroll-locked so a drag\n * that overshoots the list does not move the page underneath.\n */\nexport function PickerSheet({\n open,\n onClose,\n title,\n query,\n onQueryChange,\n searchPlaceholder,\n inputRef,\n closeLabel,\n children,\n \"aria-label\": ariaLabel,\n className,\n style,\n ...rest\n}: PickerSheetProps) {\n // Every picker in the kit opens one of these on a phone, and most of them pass\n // their own `closeLabel` through as `undefined` — so this lookup is what makes the\n // one way out of a full-screen sheet speak the app's language without each of\n // them having to (see the prop's note in `EntityCombobox`).\n const labels = useKitLabels(\"pickerSheet\", DEFAULT_PICKER_SHEET_LABELS, { close: closeLabel });\n useBodyScrollLock(open);\n /**\n * Back closes THE SHEET, not the dialog it was opened from (Keksdose live #309:\n * *\"Mouse Back does not only close the select but also the whole edit or create\n * dialog which is cumbersome.\"*).\n *\n * The sheet is a full-screen overlay over whatever opened it, and on a phone that\n * is almost always the row editor — which DOES push a history entry\n * (`data-table.tsx`). So the sheet was invisible to Back: one press popped the\n * editor's entry and took the whole dialog with it, discarding an edit in progress\n * in order to dismiss a list.\n *\n * `useOverlayHistory` was built for exactly this nesting — its stack unwinds\n * last-in-first-out, and the U-12 regression test in its own suite is written\n * around \"a Modal and a PickerSheet inside it\". The sheet simply never called it.\n *\n * Nothing else has to change: the hook is a no-op while `open` is false, and it\n * pops its own entry when the sheet closes some other way (the X, a row, the\n * backdrop) so history does not accumulate husks.\n */\n useOverlayHistory(open, onClose);\n // The visible region, while the sheet is up — see the `style` below for why a\n // full-screen sheet cannot simply be `inset-0` on a phone (live #328).\n const vv = useVisualViewport(open);\n // A fallback ref so a caller that does not need the handle still gets the opening\n // focus, rather than the behaviour depending on whether a prop was passed. What that\n // focus is FOR is on the trap below.\n const ownRef = useRef<HTMLInputElement | null>(null);\n const searchRef = inputRef ?? ownRef;\n\n const sheetRef = useRef<HTMLDivElement>(null);\n /**\n * The sheet said `aria-modal=\"true\"` and then left focus on the field behind it, so\n * a screen reader was told to hide the page the user's cursor was still standing in\n * (the audit's *\"Only Modal manages focus\"*). Through the shared hook rather than\n * the bare `.focus()` that used to live here: the hook re-reads the tabbable list on\n * every Tab, which this panel needs more than most — its list is filtered as you\n * type, so the rows a trap captured at open time are gone by the second keystroke.\n *\n * **`initialFocus` is the search box**, not the container, and this is the one place\n * in the package that overrides `Modal`'s rule. The sheet exists in order to be typed\n * into (live #212: *\"make the text input filter active on select click\"*), and the\n * callers cannot do it themselves — `Combobox`'s field runs\n * `setOpen(true); sheetInputRef.current?.focus()` in a single `onFocus`, so the focus\n * call happens while this sheet is still unmounted and the ref is null. Only the\n * sheet knows when its own input exists. Popping the keyboard is the POINT here,\n * where in a form dialog it is the cost.\n *\n * **`restoreFocus` is off**, which is the uncomfortable half. Every caller in this\n * kit opens the sheet from the field's own `onFocus` handler, so handing focus back\n * to that field on close re-opens the sheet the user has just dismissed — a row tap\n * would select a value and immediately put the list back over it. The field that\n * opened it is the only thing that knows whether re-focusing it means \"open\"; a sheet\n * that cannot tell must not guess. The cost is that focus lands on `<body>` after a\n * dismissal, which on the phone this shape is for costs nothing, and on a pointer\n * device is what happened before this change too.\n */\n useFocusTrap(sheetRef, {\n active: open,\n restoreFocus: false,\n initialFocus: () => searchRef.current,\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key !== \"Escape\") return;\n // The sheet is almost always the INNERMOST overlay — it opens from a field inside a\n // `Modal` or a `FullBleedDialog`, both of which take Escape on their own panel. A\n // React portal's events still travel up the React tree, so without stopping it here\n // one press would dismiss this list and the dialog underneath with it: Keksdose\n // live #309 (*\"Mouse Back does not only close the select but also the whole edit or\n // create dialog which is cumbersome\"*), which the Back gesture already learned, on\n // the key a desktop user reaches for instead.\n e.stopPropagation();\n onClose();\n };\n\n if (!open || typeof document === \"undefined\") return null;\n return createPortal(\n <div\n // Spread FIRST, and every attribute below it deliberately wins: `style`,\n // `onMouseDown` and `onKeyDown` here are not styling choices, they are live\n // #328, dev#477 and live #309 respectively, and a caller who passed one by\n // accident would re-open a bug three apps have already paid for. `style` is\n // MERGED rather than won outright (see below), which is the one case where a\n // caller has something to add.\n {...rest}\n ref={sheetRef}\n role=\"dialog\"\n aria-modal=\"true\"\n // Without it the container cannot take focus, and `useFocusTrap` has nothing to\n // fall back to when the list is empty and the search box is the only thing in\n // here — see the hook's \"nothing tabbable\" branch.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n // The caller's own name wins over the heading: a sheet titled \"Account\" in a\n // form with two of them needs to say WHICH, and `title` is what the user reads\n // rather than what a reader announces.\n aria-label={ariaLabel ?? (typeof title === \"string\" ? title : undefined)}\n // The sheet is PORTALLED to <body>, so it is not inside the field's own\n // wrapper — and `useDropdown` closes on any document mousedown landing\n // outside that wrapper. Tapping a row therefore unmounted the row (open →\n // false) before its CLICK could fire, so the tap selected nothing at all and\n // the sheet just vanished (Keksdose dev#477).\n //\n // It bit whichever sheet commits on `onClick` — the payee field's, since live\n // #200, where it hid behind the free-text value the search box was already\n // setting; and the account field's the moment dev#477 moved it onto the inline\n // picker. `ComboboxPanel`'s rows happen to commit on `onMouseDown`, so they\n // beat the document listener and were never affected — which is luck, not a\n // design, and exactly why the fix belongs to the sheet.\n //\n // Stopping the mousedown here rather than teaching every caller about the\n // portal: the sheet is modal and full-screen, so \"a click in here is not an\n // outside click\" is a property of the sheet, not of whoever opened it.\n onMouseDown={(e) => e.stopPropagation()}\n // ⚠️ `inset-0` was the whole of Keksdose live #328: *\"When the keyboard overlays\n // the entries I cannot scroll past them … to see also the last entries. Happens\n // when browsing the account select.\"*\n //\n // This sheet FOCUSES ITS OWN SEARCH BOX on open (see above), so the keyboard is\n // up every single time it is used on a phone. On Android that shrinks the visual\n // viewport and leaves the LAYOUT viewport alone — `inset-0` is the layout\n // viewport — so the sheet kept its full height with its bottom third behind the\n // keyboard. The list below is `flex-1 overflow-y-auto`, so it sized itself to\n // that hidden height too: it scrolled to ITS end while the last rows were still\n // under the keys, and no gesture could bring them out. Nothing was clipped and\n // nothing looked broken, which is why it reads as \"I cannot scroll past them\".\n //\n // `visualViewport` is the API that answers rather than hints (the live #292\n // lesson): it reports the region actually on screen, keyboard and pinch-pan\n // included, and it fires on the keyboard opening and closing. Pinned to that box,\n // the sheet ends where the keys begin and the list's own scroll covers the rest.\n // `useVisualViewport` returns null when there is nothing to correct, and then\n // this is character-for-character the sheet that was here before.\n // A caller's `style` is kept and the correction laid OVER it: the three values\n // below are the whole of live #328, so they are not negotiable, but a z-index\n // or a transition from outside has nothing to do with them. Without a visual\n // viewport to correct for this is character-for-character what the caller passed.\n style={vv ? { ...style, top: vv.top, height: vv.height, bottom: \"auto\" } : style}\n className={cn(\"fixed inset-0 z-50 flex flex-col bg-[var(--bg-surface)] outline-none\", className)}\n >\n <div className=\"flex items-center gap-2 border-b border-[var(--border)] px-3 py-2\">\n <span className=\"min-w-0 flex-1 truncate text-sm font-semibold text-[var(--text-primary)]\">\n {title}\n </span>\n <button\n type=\"button\"\n aria-label={labels.close}\n onClick={onClose}\n className=\"rounded p-1.5 text-[var(--text-muted)] hover:bg-[var(--bg-hover)]\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n <DropdownSearchHeader\n query={query}\n onQueryChange={onQueryChange}\n inputRef={searchRef}\n placeholder={searchPlaceholder}\n />\n {/* min-h-0 so the LIST scrolls rather than the dialog growing past the\n viewport — a flex child defaults to min-height:auto. */}\n {/* Marked as the scroller it is (`CLIPS_ATTRIBUTE`), so a Tooltip in a row —\n `Combobox`'s `optionAdornment` — portals out under jsdom as it does in the\n browser rather than joining the option's name (keksdose E8). */}\n <div {...{ [CLIPS_ATTRIBUTE]: \"\" }} className=\"min-h-0 flex-1 overflow-y-auto overscroll-contain\">\n {children}\n </div>\n </div>,\n document.body,\n );\n}\n\n/** A row inside a {@link PickerSheet} — the phone-sized version of a dropdown row,\n * so a list is comfortable to hit with a thumb rather than merely legible. */\nexport const SHEET_ROW_CLASS = cn(\n \"flex min-h-11 w-full items-center gap-2 px-4 py-2 text-start text-base\",\n \"text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\",\n);\n"],"mappings":";AA2NM,SACE,KADF;AA3NN,SAAS,cAAc;AACvB,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,UAAU;AACnB,SAAS,uBAAuB;AAChC,SAAS,4BAA4B;AACrC,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,6BAA6B,oBAAoB;AA+CnD,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AAKnB,QAAM,SAAS,aAAa,eAAe,6BAA6B,EAAE,OAAO,WAAW,CAAC;AAC7F,oBAAkB,IAAI;AAoBtB,oBAAkB,MAAM,OAAO;AAG/B,QAAM,KAAK,kBAAkB,IAAI;AAIjC,QAAM,SAAS,OAAgC,IAAI;AACnD,QAAM,YAAY,YAAY;AAE9B,QAAM,WAAW,OAAuB,IAAI;AA2B5C,eAAa,UAAU;AAAA,IACrB,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,cAAc,MAAM,UAAU;AAAA,EAChC,CAAC;AAED,QAAM,gBAAgB,CAAC,MAAqC;AAC1D,QAAI,EAAE,QAAQ,SAAU;AAQxB,MAAE,gBAAgB;AAClB,YAAQ;AAAA,EACV;AAEA,MAAI,CAAC,QAAQ,OAAO,aAAa,YAAa,QAAO;AACrD,SAAO;AAAA,IACL;AAAA,MAAC;AAAA;AAAA,QAOE,GAAG;AAAA,QACJ,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAW;AAAA,QAIX,UAAU;AAAA,QACV,WAAW;AAAA,QAIX,cAAY,cAAc,OAAO,UAAU,WAAW,QAAQ;AAAA,QAiB9D,aAAa,CAAC,MAAM,EAAE,gBAAgB;AAAA,QAwBtC,OAAO,KAAK,EAAE,GAAG,OAAO,KAAK,GAAG,KAAK,QAAQ,GAAG,QAAQ,QAAQ,OAAO,IAAI;AAAA,QAC3E,WAAW,GAAG,wEAAwE,SAAS;AAAA,QAE/F;AAAA,+BAAC,SAAI,WAAU,qEACb;AAAA,gCAAC,UAAK,WAAU,4EACb,iBACH;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,OAAO;AAAA,gBACnB,SAAS;AAAA,gBACT,WAAU;AAAA,gBAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,YACxB;AAAA,aACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA,UAAU;AAAA,cACV,aAAa;AAAA;AAAA,UACf;AAAA,UAMA,oBAAC,SAAK,GAAG,EAAE,CAAC,eAAe,GAAG,GAAG,GAAG,WAAU,qDAC3C,UACH;AAAA;AAAA;AAAA,IACF;AAAA,IACA,SAAS;AAAA,EACX;AACF;AAIO,MAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AACF;","names":[]}
@@ -0,0 +1,20 @@
1
+ interface PieChartLabels {
2
+ /** Accessible name of the group of keyboard stops laid over the slices — one tab
3
+ * stop, arrow keys between them. */
4
+ slices: string;
5
+ /**
6
+ * One slice's accessible name. `value` arrives through `formatValue`, `percent` as
7
+ * the slice's share of what is drawn, already formatted in the locale ("42 %" in
8
+ * German, "42%" in English) — the function only arranges them.
9
+ */
10
+ slice: (label: string, value: string, percent: string) => string;
11
+ /** The caption over the donut's centre figure. */
12
+ total: string;
13
+ /** What a chart with nothing to draw says, when the caller passes no `empty`. */
14
+ empty: string;
15
+ /** Accessible name of the chart's legend (a group of switches, one per slice). */
16
+ legend: string;
17
+ }
18
+ declare const DEFAULT_PIE_CHART_LABELS: PieChartLabels;
19
+
20
+ export { DEFAULT_PIE_CHART_LABELS, type PieChartLabels };
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ const DEFAULT_PIE_CHART_LABELS = {
3
+ slices: "Chart slices",
4
+ slice: (label, value, percent) => `${label}: ${value} (${percent})`,
5
+ total: "Total",
6
+ empty: "No data",
7
+ legend: "Categories"
8
+ };
9
+ export {
10
+ DEFAULT_PIE_CHART_LABELS
11
+ };
12
+ //# sourceMappingURL=pie-chart-labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/pie-chart-labels.ts"],"sourcesContent":["// All user-facing strings for `PieChart`, as the `pieChart` namespace of\n// `<UiKitProvider labels>`, and per chart the `labels` prop over that.\n//\n// A module of its own, with no recharts in it, for the reason `series-chart-labels`\n// is one: the label registry (src/i18n) imports the defaults, and must not pull the\n// chart library in behind them.\n\nexport interface PieChartLabels {\n /** Accessible name of the group of keyboard stops laid over the slices — one tab\n * stop, arrow keys between them. */\n slices: string;\n /**\n * One slice's accessible name. `value` arrives through `formatValue`, `percent` as\n * the slice's share of what is drawn, already formatted in the locale (\"42 %\" in\n * German, \"42%\" in English) — the function only arranges them.\n */\n slice: (label: string, value: string, percent: string) => string;\n /** The caption over the donut's centre figure. */\n total: string;\n /** What a chart with nothing to draw says, when the caller passes no `empty`. */\n empty: string;\n /** Accessible name of the chart's legend (a group of switches, one per slice). */\n legend: string;\n}\n\nexport const DEFAULT_PIE_CHART_LABELS: PieChartLabels = {\n slices: \"Chart slices\",\n slice: (label, value, percent) => `${label}: ${value} (${percent})`,\n total: \"Total\",\n empty: \"No data\",\n legend: \"Categories\",\n};\n"],"mappings":";AAyBO,MAAM,2BAA2C;AAAA,EACtD,QAAQ;AAAA,EACR,OAAO,CAAC,OAAO,OAAO,YAAY,GAAG,KAAK,KAAK,KAAK,KAAK,OAAO;AAAA,EAChE,OAAO;AAAA,EACP,OAAO;AAAA,EACP,QAAQ;AACV;","names":[]}
@@ -0,0 +1,101 @@
1
+ import * as react from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
+ import { PieChartLabels } from './pie-chart-labels.js';
4
+
5
+ /** One slice. */
6
+ interface PieChartSlice {
7
+ /** Stable key, handed back by {@link PieChartProps.onSliceClick} and used by the
8
+ * legend's hidden set. Labels can collide; keys must not. */
9
+ key: string;
10
+ /** What the slice is called — in the legend, the tooltip and its accessible name. */
11
+ label: string;
12
+ /**
13
+ * The slice's size. A value that is not a positive finite number — `null` for a
14
+ * figure that is missing, a zero, a negative — is NOT drawn and not in the legend:
15
+ * an angle cannot be negative, and a zero-degree slice is one a pointer and a
16
+ * keyboard would both stop on to find nothing.
17
+ */
18
+ value: number | null | undefined;
19
+ /** Overrides the by-index palette colour (`paletteFor(i)`, `i` the slice's index in
20
+ * `data` AS GIVEN) — a status that has a colour of its own. Any CSS colour. */
21
+ color?: string;
22
+ }
23
+ /**
24
+ * Everything a `<div>` takes reaches the chart's root, so a test id or a `data-tour`
25
+ * anchor can find it. `children` is the chart's own.
26
+ */
27
+ interface PieChartProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
28
+ data: readonly PieChartSlice[];
29
+ /** `"donut"` (default) leaves a hole with the total in it; `"pie"` is the full disc. */
30
+ variant?: "donut" | "pie";
31
+ /** Height of the plot in px (the legend is under it). Default 300. */
32
+ height?: number;
33
+ /** How a value is written in the tooltip, the slice's name, a `"value"` slice label
34
+ * and the donut's total. Default: `Intl.NumberFormat` in `locale`. */
35
+ formatValue?: (value: number) => string;
36
+ /**
37
+ * What is written beside each slice: its share (`"percent"`, the default), its
38
+ * value, or nothing. Only on slices of at least {@link minLabelShare} — a label on a
39
+ * sliver sits on its neighbour's. The figure is in the tooltip and the slice's name
40
+ * either way.
41
+ */
42
+ sliceLabels?: "percent" | "value" | "none";
43
+ /** The smallest share (0–1) that gets a slice label. Default 0.04. */
44
+ minLabelShare?: number;
45
+ /** Draw the donut's centre: {@link centerLabel} over {@link centerValue}. Default
46
+ * `true`; ignored for `variant="pie"`, which has no centre to draw in. */
47
+ center?: boolean;
48
+ /** The caption in the donut's centre. Default: `labels.total`. */
49
+ centerLabel?: ReactNode;
50
+ /** The figure in the donut's centre. Default: the drawn total through `formatValue`. */
51
+ centerValue?: ReactNode;
52
+ /**
53
+ * The legend under the chart. `"toggle"` (default): each entry is a switch that
54
+ * takes its slice off the chart, and the percentages are then of what is left —
55
+ * the shares always read "of this picture". `"static"`: a key with nothing to press.
56
+ */
57
+ legend?: "toggle" | "static" | "none";
58
+ /** The slices switched off, for a caller that owns the set (a URL, a saved view).
59
+ * Left out, the chart keeps its own. */
60
+ hiddenSlices?: ReadonlySet<string>;
61
+ /** Called with the new hidden set whenever the legend switches a slice. */
62
+ onHiddenSlicesChange?: (hidden: ReadonlySet<string>) => void;
63
+ /** A slice was activated — by click, or Enter/Space on a focused slice. Makes each
64
+ * slice a `role="button"`. */
65
+ onSliceClick?: (key: string, slice: PieChartSlice) => void;
66
+ /**
67
+ * Tag every figure ON SCREEN `data-private` — the tooltip bubble, the centre and
68
+ * `"value"` slice labels — so the host's demo-mode rule blurs it. A percentage alone
69
+ * reveals no amount, so `"percent"` labels stay readable, and the slices themselves
70
+ * are not tagged: a blurred ring is no picture at all.
71
+ */
72
+ redact?: boolean;
73
+ /** What to show with nothing to draw. Default: `labels.empty`. */
74
+ empty?: ReactNode;
75
+ /** BCP 47 tag for the numbers and percentages. Default: the provider's. */
76
+ locale?: string;
77
+ labels?: Partial<PieChartLabels>;
78
+ }
79
+ /**
80
+ * Part-to-whole chart: a donut (or `variant="pie"`), one slice per
81
+ * {@link PieChartSlice}, coloured from the chart ramp by index.
82
+ *
83
+ * **Every slice is reachable from the keyboard.** The slices are ONE tab stop (a roving
84
+ * tabindex, as in the kit's grids): ←/→ and ↑/↓ step to the next and previous slice —
85
+ * clockwise from twelve o'clock, the order of the legend — with ←/→ following the
86
+ * reading direction in RTL; Home/End go to the first and last. Each slice is named
87
+ * "Label: value (xx%)" (`labels.slice`), because a colour and an angle are nothing a
88
+ * screen reader can say, and focusing one reads it out in the donut's centre. With
89
+ * `onSliceClick`, Enter and Space activate it as a click does.
90
+ *
91
+ * The legend switches slices off (`legend="toggle"`); the percentages are always of
92
+ * what is drawn. The plot is not mirrored in RTL (see `ChartContainer`); the legend
93
+ * and the centre follow the page.
94
+ *
95
+ * With nothing to draw it says so (`empty`) rather than drawing an empty box.
96
+ *
97
+ * Needs `recharts`, like everything behind `@eifi1/ui-kit/chart`.
98
+ */
99
+ declare function PieChart({ data, variant, height, formatValue: formatValueProp, sliceLabels, minLabelShare, center, centerLabel, centerValue, legend, hiddenSlices: hiddenProp, onHiddenSlicesChange, onSliceClick, redact, empty, locale: localeProp, labels: labelsProp, className, style, ...rest }: PieChartProps): react.JSX.Element;
100
+
101
+ export { PieChart, type PieChartProps, type PieChartSlice };
@@ -0,0 +1,283 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { Pie, PieChart as RechartsPieChart, Sector } from "recharts";
5
+ import { cn } from "../lib/cn.js";
6
+ import { horizontalStep } from "../lib/direction.js";
7
+ import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
8
+ import { paletteFor } from "../theme/chart-palette.js";
9
+ import { ChartContainer, ChartTooltip, ChartTooltipContent } from "./chart.js";
10
+ import { DEFAULT_PIE_CHART_LABELS } from "./pie-chart-labels.js";
11
+ import { StaticLegend, ToggleLegend, toggleHidden } from "./toggle-legend.js";
12
+ const PIE_CONFIG = {};
13
+ const RADIAN = Math.PI / 180;
14
+ function RedactedTooltipContent(props) {
15
+ return /* @__PURE__ */ jsx("div", { "data-private": true, children: /* @__PURE__ */ jsx(ChartTooltipContent, { ...props }) });
16
+ }
17
+ function PieChart({
18
+ data,
19
+ variant = "donut",
20
+ height = 300,
21
+ formatValue: formatValueProp,
22
+ sliceLabels = "percent",
23
+ minLabelShare = 0.04,
24
+ center = true,
25
+ centerLabel,
26
+ centerValue,
27
+ legend = "toggle",
28
+ hiddenSlices: hiddenProp,
29
+ onHiddenSlicesChange,
30
+ onSliceClick,
31
+ redact = false,
32
+ empty,
33
+ locale: localeProp,
34
+ labels: labelsProp,
35
+ className,
36
+ style,
37
+ ...rest
38
+ }) {
39
+ const labels = useKitLabels("pieChart", DEFAULT_PIE_CHART_LABELS, labelsProp);
40
+ const locale = useKitLocale(localeProp);
41
+ const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);
42
+ const percentFormats = useMemo(
43
+ () => ({
44
+ whole: new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 }),
45
+ fine: new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 })
46
+ }),
47
+ [locale]
48
+ );
49
+ const formatValue = useCallback(
50
+ (v) => formatValueProp ? formatValueProp(v) : numberFormat.format(v),
51
+ [formatValueProp, numberFormat]
52
+ );
53
+ const formatShare = useCallback(
54
+ (share) => (share < 0.1 ? percentFormats.fine : percentFormats.whole).format(share),
55
+ [percentFormats]
56
+ );
57
+ const [ownHidden, setOwnHidden] = useState(() => /* @__PURE__ */ new Set());
58
+ const hidden = hiddenProp ?? ownHidden;
59
+ const toggle = (key) => {
60
+ const next = toggleHidden(hidden, key);
61
+ if (hiddenProp === void 0) setOwnHidden(next);
62
+ onHiddenSlicesChange?.(next);
63
+ };
64
+ const { drawable, drawn, total, pieData } = useMemo(() => {
65
+ const drawable2 = data.map((slice, i) => ({ slice, value: slice.value, color: slice.color ?? paletteFor(i) })).filter(
66
+ (d) => typeof d.value === "number" && Number.isFinite(d.value) && d.value > 0
67
+ );
68
+ const shown = drawable2.filter((d) => !hidden.has(d.slice.key));
69
+ const total2 = shown.reduce((acc, d) => acc + d.value, 0);
70
+ const drawn2 = shown.map((d) => ({ ...d, share: total2 > 0 ? d.value / total2 : 0 }));
71
+ const pieData2 = drawn2.map((d) => ({
72
+ key: d.slice.key,
73
+ label: d.slice.label,
74
+ value: d.value,
75
+ fill: d.color
76
+ }));
77
+ return { drawable: drawable2, drawn: drawn2, total: total2, pieData: pieData2 };
78
+ }, [data, hidden]);
79
+ const [stop, setStop] = useState(0);
80
+ const [focused, setFocused] = useState(void 0);
81
+ const current = Math.min(stop, Math.max(0, drawn.length - 1));
82
+ const refs = useRef([]);
83
+ const focusedSlice = focused !== void 0 ? drawn[focused] : void 0;
84
+ const clickRef = useRef(onSliceClick);
85
+ useLayoutEffect(() => {
86
+ clickRef.current = onSliceClick;
87
+ });
88
+ const clickable = onSliceClick !== void 0;
89
+ const shape = useCallback(
90
+ (props) => {
91
+ const {
92
+ index,
93
+ isActive: _isActive,
94
+ tabIndex: _tabIndex,
95
+ animationElapsedTime: _elapsed,
96
+ isAnimating: _animating,
97
+ isEntrance: _entrance,
98
+ // recharts hands the shape its own React `key` among the props. Spread into
99
+ // JSX, a `key` inside a props object is a warning on every render (React 19),
100
+ // so it is lifted out here and passed on its own.
101
+ key,
102
+ ...sector
103
+ } = props;
104
+ const d = drawn[index];
105
+ if (!d) return /* @__PURE__ */ jsx(Sector, { ...sector }, key ?? void 0);
106
+ const onKeyDown = (event) => {
107
+ const step = horizontalStep(event.key, event.currentTarget);
108
+ let target;
109
+ if (step) target = index + step;
110
+ else if (event.key === "ArrowDown") target = index + 1;
111
+ else if (event.key === "ArrowUp") target = index - 1;
112
+ else if (event.key === "Home") target = 0;
113
+ else if (event.key === "End") target = drawn.length - 1;
114
+ if (target !== void 0) {
115
+ event.preventDefault();
116
+ refs.current[(target % drawn.length + drawn.length) % drawn.length]?.focus();
117
+ } else if (clickable && (event.key === "Enter" || event.key === " ")) {
118
+ event.preventDefault();
119
+ clickRef.current?.(d.slice.key, d.slice);
120
+ }
121
+ };
122
+ return /* @__PURE__ */ jsx(
123
+ "g",
124
+ {
125
+ ref: (node) => {
126
+ refs.current[index] = node;
127
+ },
128
+ role: clickable ? "button" : "img",
129
+ tabIndex: index === 0 ? 0 : -1,
130
+ "aria-label": labels.slice(d.slice.label, formatValue(d.value), formatShare(d.share)),
131
+ "data-slice": d.slice.key,
132
+ onKeyDown,
133
+ onFocus: () => {
134
+ setStop(index);
135
+ setFocused(index);
136
+ },
137
+ onBlur: () => setFocused(void 0),
138
+ className: "outline-none [&:focus-visible>path]:stroke-[var(--text-primary)]",
139
+ children: /* @__PURE__ */ jsx(Sector, { ...sector }, key ?? void 0)
140
+ }
141
+ );
142
+ },
143
+ [drawn, labels, formatValue, formatShare, clickable]
144
+ );
145
+ const label = useCallback(
146
+ (props) => {
147
+ const d = drawn[props.index ?? -1];
148
+ const cx = Number(props.cx);
149
+ const cy = Number(props.cy);
150
+ const radius = Number(props.outerRadius);
151
+ const mid = Number(props.midAngle);
152
+ if (!d || d.share < minLabelShare || ![cx, cy, radius, mid].every(Number.isFinite)) return null;
153
+ const r = radius + 10;
154
+ const x = cx + r * Math.cos(-mid * RADIAN);
155
+ const y = cy + r * Math.sin(-mid * RADIAN);
156
+ const isValue = sliceLabels === "value";
157
+ return /* @__PURE__ */ jsx(
158
+ "text",
159
+ {
160
+ x,
161
+ y,
162
+ textAnchor: x >= cx ? "start" : "end",
163
+ dominantBaseline: "central",
164
+ fontSize: 11,
165
+ fill: "var(--text-secondary)",
166
+ "data-private": redact && isValue ? "" : void 0,
167
+ style: { pointerEvents: "none" },
168
+ children: isValue ? formatValue(d.value) : formatShare(d.share)
169
+ }
170
+ );
171
+ },
172
+ [drawn, minLabelShare, sliceLabels, redact, formatValue, formatShare]
173
+ );
174
+ const withLabels = sliceLabels !== "none";
175
+ const outer = withLabels ? 72 : 88;
176
+ const inner = variant === "donut" ? Math.round(outer * 0.6) : 0;
177
+ const pie = useMemo(
178
+ () => /* @__PURE__ */ jsx(
179
+ Pie,
180
+ {
181
+ data: pieData,
182
+ dataKey: "value",
183
+ nameKey: "label",
184
+ cx: "50%",
185
+ cy: "50%",
186
+ outerRadius: `${outer}%`,
187
+ innerRadius: `${inner}%`,
188
+ startAngle: 90,
189
+ endAngle: -270,
190
+ stroke: "var(--bg-surface)",
191
+ strokeWidth: 2,
192
+ rootTabIndex: -1,
193
+ isAnimationActive: false,
194
+ shape,
195
+ label: withLabels ? label : false,
196
+ labelLine: false,
197
+ onClick: clickable ? (_entry, index) => {
198
+ const d = drawn[index];
199
+ if (d) clickRef.current?.(d.slice.key, d.slice);
200
+ } : void 0
201
+ }
202
+ ),
203
+ [pieData, drawn, shape, label, withLabels, outer, inner, clickable]
204
+ );
205
+ useEffect(() => {
206
+ refs.current.forEach((el, i) => el?.setAttribute("tabindex", i === current ? "0" : "-1"));
207
+ });
208
+ const legendEntries = drawable.map((d) => ({
209
+ key: d.slice.key,
210
+ label: d.slice.label,
211
+ color: d.color
212
+ }));
213
+ const emptyBox = /* @__PURE__ */ jsx(
214
+ "div",
215
+ {
216
+ className: "flex items-center justify-center text-sm text-[var(--text-muted)]",
217
+ style: { height },
218
+ children: empty ?? labels.empty
219
+ }
220
+ );
221
+ if (drawable.length === 0) {
222
+ return /* @__PURE__ */ jsx("div", { ...rest, className: cn("w-full", className), style, children: emptyBox });
223
+ }
224
+ const Content = redact ? RedactedTooltipContent : ChartTooltipContent;
225
+ return /* @__PURE__ */ jsxs("div", { ...rest, className: cn("w-full", className), style, children: [
226
+ drawn.length === 0 ? (
227
+ // Every slice switched off: the legend below is how they come back.
228
+ emptyBox
229
+ ) : /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": labels.slices, className: "relative", style: { height }, children: [
230
+ /* @__PURE__ */ jsx(
231
+ ChartContainer,
232
+ {
233
+ config: PIE_CONFIG,
234
+ className: cn("h-full", clickable && "[&_.recharts-pie-sector]:cursor-pointer"),
235
+ children: /* @__PURE__ */ jsxs(RechartsPieChart, { accessibilityLayer: false, children: [
236
+ /* @__PURE__ */ jsx(
237
+ ChartTooltip,
238
+ {
239
+ content: /* @__PURE__ */ jsx(
240
+ Content,
241
+ {
242
+ hideLabel: true,
243
+ valueFormatter: (v) => `${formatValue(v)} (${formatShare(total > 0 ? v / total : 0)})`
244
+ }
245
+ )
246
+ }
247
+ ),
248
+ pie
249
+ ] })
250
+ }
251
+ ),
252
+ variant === "donut" && center && /* @__PURE__ */ jsxs(
253
+ "div",
254
+ {
255
+ "data-private": redact ? "" : void 0,
256
+ "aria-hidden": focusedSlice ? true : void 0,
257
+ className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center",
258
+ children: [
259
+ /* @__PURE__ */ jsx("span", { className: "max-w-[40%] truncate text-xs text-[var(--text-muted)]", children: focusedSlice ? focusedSlice.slice.label : centerLabel ?? labels.total }),
260
+ /* @__PURE__ */ jsx("span", { className: "max-w-[40%] truncate text-lg font-semibold tabular-nums text-[var(--text-primary)]", children: focusedSlice ? formatValue(focusedSlice.value) : centerValue ?? formatValue(total) }),
261
+ focusedSlice && /* @__PURE__ */ jsx("span", { className: "text-xs tabular-nums text-[var(--text-muted)]", children: formatShare(focusedSlice.share) })
262
+ ]
263
+ }
264
+ )
265
+ ] }),
266
+ legend === "toggle" && /* @__PURE__ */ jsx(
267
+ ToggleLegend,
268
+ {
269
+ entries: legendEntries,
270
+ hidden,
271
+ onToggle: toggle,
272
+ showSingle: true,
273
+ className: "justify-center",
274
+ labels: { legend: labels.legend }
275
+ }
276
+ ),
277
+ legend === "static" && /* @__PURE__ */ jsx(StaticLegend, { entries: legendEntries, className: "justify-center", "aria-label": labels.legend })
278
+ ] });
279
+ }
280
+ export {
281
+ PieChart
282
+ };
283
+ //# sourceMappingURL=pie-chart.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/pie-chart.tsx"],"sourcesContent":["// A part-to-whole chart: a donut (or a full pie), one slice per category, its ANGLE\n// the category's share of what is drawn. Lifted out of kastlan, where the dashboard's\n// occupancy card and the reports' maintenance card each drew a recharts `<Pie>` by\n// hand inside the kit's `ChartContainer` — with a legend that could not switch\n// anything, slices a keyboard could not reach, and colours from a local hex list.\n//\n// It uses the kit's own chart shell (`ChartContainer`, `ChartTooltipContent`) and\n// ramp (`paletteFor`), so it re-skins with the palette like every other chart, and the\n// kit's own legends (`ToggleLegend`, `StaticLegend`).\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentProps, ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from \"react\";\nimport { Pie, PieChart as RechartsPieChart, Sector } from \"recharts\";\nimport type { PieLabelRenderProps, PieSectorShapeProps } from \"recharts\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { paletteFor } from \"../theme/chart-palette\";\nimport { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from \"./chart\";\nimport { DEFAULT_PIE_CHART_LABELS, type PieChartLabels } from \"./pie-chart-labels\";\nimport { StaticLegend, ToggleLegend, toggleHidden, type LegendEntry } from \"./toggle-legend\";\n\n/** One slice. */\nexport interface PieChartSlice {\n /** Stable key, handed back by {@link PieChartProps.onSliceClick} and used by the\n * legend's hidden set. Labels can collide; keys must not. */\n key: string;\n /** What the slice is called — in the legend, the tooltip and its accessible name. */\n label: string;\n /**\n * The slice's size. A value that is not a positive finite number — `null` for a\n * figure that is missing, a zero, a negative — is NOT drawn and not in the legend:\n * an angle cannot be negative, and a zero-degree slice is one a pointer and a\n * keyboard would both stop on to find nothing.\n */\n value: number | null | undefined;\n /** Overrides the by-index palette colour (`paletteFor(i)`, `i` the slice's index in\n * `data` AS GIVEN) — a status that has a colour of its own. Any CSS colour. */\n color?: string;\n}\n\n/**\n * Everything a `<div>` takes reaches the chart's root, so a test id or a `data-tour`\n * anchor can find it. `children` is the chart's own.\n */\nexport interface PieChartProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n data: readonly PieChartSlice[];\n /** `\"donut\"` (default) leaves a hole with the total in it; `\"pie\"` is the full disc. */\n variant?: \"donut\" | \"pie\";\n /** Height of the plot in px (the legend is under it). Default 300. */\n height?: number;\n /** How a value is written in the tooltip, the slice's name, a `\"value\"` slice label\n * and the donut's total. Default: `Intl.NumberFormat` in `locale`. */\n formatValue?: (value: number) => string;\n /**\n * What is written beside each slice: its share (`\"percent\"`, the default), its\n * value, or nothing. Only on slices of at least {@link minLabelShare} — a label on a\n * sliver sits on its neighbour's. The figure is in the tooltip and the slice's name\n * either way.\n */\n sliceLabels?: \"percent\" | \"value\" | \"none\";\n /** The smallest share (0–1) that gets a slice label. Default 0.04. */\n minLabelShare?: number;\n /** Draw the donut's centre: {@link centerLabel} over {@link centerValue}. Default\n * `true`; ignored for `variant=\"pie\"`, which has no centre to draw in. */\n center?: boolean;\n /** The caption in the donut's centre. Default: `labels.total`. */\n centerLabel?: ReactNode;\n /** The figure in the donut's centre. Default: the drawn total through `formatValue`. */\n centerValue?: ReactNode;\n /**\n * The legend under the chart. `\"toggle\"` (default): each entry is a switch that\n * takes its slice off the chart, and the percentages are then of what is left —\n * the shares always read \"of this picture\". `\"static\"`: a key with nothing to press.\n */\n legend?: \"toggle\" | \"static\" | \"none\";\n /** The slices switched off, for a caller that owns the set (a URL, a saved view).\n * Left out, the chart keeps its own. */\n hiddenSlices?: ReadonlySet<string>;\n /** Called with the new hidden set whenever the legend switches a slice. */\n onHiddenSlicesChange?: (hidden: ReadonlySet<string>) => void;\n /** A slice was activated — by click, or Enter/Space on a focused slice. Makes each\n * slice a `role=\"button\"`. */\n onSliceClick?: (key: string, slice: PieChartSlice) => void;\n /**\n * Tag every figure ON SCREEN `data-private` — the tooltip bubble, the centre and\n * `\"value\"` slice labels — so the host's demo-mode rule blurs it. A percentage alone\n * reveals no amount, so `\"percent\"` labels stay readable, and the slices themselves\n * are not tagged: a blurred ring is no picture at all.\n */\n redact?: boolean;\n /** What to show with nothing to draw. Default: `labels.empty`. */\n empty?: ReactNode;\n /** BCP 47 tag for the numbers and percentages. Default: the provider's. */\n locale?: string;\n labels?: Partial<PieChartLabels>;\n}\n\n/** A slice as drawn: only the ones with a positive value, in `data` order. */\ninterface DrawnSlice {\n slice: PieChartSlice;\n value: number;\n color: string;\n /** Of what is drawn, 0–1. */\n share: number;\n}\n\n// Slices carry their own `fill`, and `ChartTooltipContent` falls back to it for the\n// swatch, so no series config is needed — and none is wanted, because a config key is a\n// CSS identifier and a slice key is the caller's data (see `ChartStyle`).\nconst PIE_CONFIG: ChartConfig = {};\n\nconst RADIAN = Math.PI / 180;\n\n/** The shared tooltip with the bubble tagged `data-private` — see the treemap's\n * `RedactedTooltipContent` for why it has to BE the element recharts clones. */\nfunction RedactedTooltipContent(props: ComponentProps<typeof ChartTooltipContent>) {\n return (\n <div data-private>\n <ChartTooltipContent {...props} />\n </div>\n );\n}\n\n/**\n * Part-to-whole chart: a donut (or `variant=\"pie\"`), one slice per\n * {@link PieChartSlice}, coloured from the chart ramp by index.\n *\n * **Every slice is reachable from the keyboard.** The slices are ONE tab stop (a roving\n * tabindex, as in the kit's grids): ←/→ and ↑/↓ step to the next and previous slice —\n * clockwise from twelve o'clock, the order of the legend — with ←/→ following the\n * reading direction in RTL; Home/End go to the first and last. Each slice is named\n * \"Label: value (xx%)\" (`labels.slice`), because a colour and an angle are nothing a\n * screen reader can say, and focusing one reads it out in the donut's centre. With\n * `onSliceClick`, Enter and Space activate it as a click does.\n *\n * The legend switches slices off (`legend=\"toggle\"`); the percentages are always of\n * what is drawn. The plot is not mirrored in RTL (see `ChartContainer`); the legend\n * and the centre follow the page.\n *\n * With nothing to draw it says so (`empty`) rather than drawing an empty box.\n *\n * Needs `recharts`, like everything behind `@eifi1/ui-kit/chart`.\n */\nexport function PieChart({\n data,\n variant = \"donut\",\n height = 300,\n formatValue: formatValueProp,\n sliceLabels = \"percent\",\n minLabelShare = 0.04,\n center = true,\n centerLabel,\n centerValue,\n legend = \"toggle\",\n hiddenSlices: hiddenProp,\n onHiddenSlicesChange,\n onSliceClick,\n redact = false,\n empty,\n locale: localeProp,\n labels: labelsProp,\n className,\n style,\n ...rest\n}: PieChartProps) {\n const labels = useKitLabels(\"pieChart\", DEFAULT_PIE_CHART_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n const percentFormats = useMemo(\n () => ({\n whole: new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 0 }),\n fine: new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 1 }),\n }),\n [locale],\n );\n // Memoised, like everything the sectors are drawn from: they must not be rebuilt on\n // this component's own re-renders (see `pie` below).\n const formatValue = useCallback(\n (v: number) => (formatValueProp ? formatValueProp(v) : numberFormat.format(v)),\n [formatValueProp, numberFormat],\n );\n // One decimal under 10 %, where rounding to whole percents starts to lie — a slice\n // of 0.4 % labelled \"0%\" says it is not there.\n const formatShare = useCallback(\n (share: number) => (share < 0.1 ? percentFormats.fine : percentFormats.whole).format(share),\n [percentFormats],\n );\n\n const [ownHidden, setOwnHidden] = useState<ReadonlySet<string>>(() => new Set());\n const hidden = hiddenProp ?? ownHidden;\n const toggle = (key: string) => {\n const next = toggleHidden(hidden, key);\n if (hiddenProp === undefined) setOwnHidden(next);\n onHiddenSlicesChange?.(next);\n };\n\n // Coloured by index into `data` AS GIVEN, before anything is dropped, so a slice\n // keeps its colour when a neighbour is switched off or has no value.\n const { drawable, drawn, total, pieData } = useMemo(() => {\n const drawable = data\n .map((slice, i) => ({ slice, value: slice.value, color: slice.color ?? paletteFor(i) }))\n .filter((d): d is { slice: PieChartSlice; value: number; color: string } =>\n typeof d.value === \"number\" && Number.isFinite(d.value) && d.value > 0,\n );\n const shown = drawable.filter((d) => !hidden.has(d.slice.key));\n const total = shown.reduce((acc, d) => acc + d.value, 0);\n const drawn: DrawnSlice[] = shown.map((d) => ({ ...d, share: total > 0 ? d.value / total : 0 }));\n const pieData = drawn.map((d) => ({\n key: d.slice.key,\n label: d.slice.label,\n value: d.value,\n fill: d.color,\n }));\n return { drawable, drawn, total, pieData };\n }, [data, hidden]);\n\n // The roving tab stop, and the slice that has focus (which the centre reads out).\n const [stop, setStop] = useState(0);\n const [focused, setFocused] = useState<number | undefined>(undefined);\n const current = Math.min(stop, Math.max(0, drawn.length - 1));\n const refs = useRef<(SVGGElement | null)[]>([]);\n const focusedSlice = focused !== undefined ? drawn[focused] : undefined;\n\n // Read through a ref: the sectors are drawn once per `data`, and a caller's inline\n // `onSliceClick` is a new function every render.\n const clickRef = useRef(onSliceClick);\n useLayoutEffect(() => {\n clickRef.current = onSliceClick;\n });\n const clickable = onSliceClick !== undefined;\n\n /**\n * One sector, wrapped in its keyboard stop. recharts renders it inside a layer of its\n * own that carries the pointer's hover and click, so those are untouched.\n *\n * Focus does NOT drive the recharts tooltip (`defaultIndex`), as it does in\n * `SeriesChart`: an active pie sector is re-parented into an extra layer, which\n * remounts this element and drops the focus it just took. The donut's centre is the\n * keyboard's read-out instead.\n */\n const shape = useCallback(\n (props: PieSectorShapeProps) => {\n const {\n index,\n isActive: _isActive,\n tabIndex: _tabIndex,\n animationElapsedTime: _elapsed,\n isAnimating: _animating,\n isEntrance: _entrance,\n // recharts hands the shape its own React `key` among the props. Spread into\n // JSX, a `key` inside a props object is a warning on every render (React 19),\n // so it is lifted out here and passed on its own.\n key,\n ...sector\n } = props as PieSectorShapeProps & {\n animationElapsedTime?: number;\n isAnimating?: boolean;\n isEntrance?: boolean;\n key?: string | number | null;\n };\n const d = drawn[index];\n if (!d) return <Sector key={key ?? undefined} {...sector} />;\n const onKeyDown = (event: KeyboardEvent<SVGGElement>) => {\n const step = horizontalStep(event.key, event.currentTarget);\n let target: number | undefined;\n if (step) target = index + step;\n else if (event.key === \"ArrowDown\") target = index + 1;\n else if (event.key === \"ArrowUp\") target = index - 1;\n else if (event.key === \"Home\") target = 0;\n else if (event.key === \"End\") target = drawn.length - 1;\n if (target !== undefined) {\n event.preventDefault();\n // Round the circle: past the last slice is the first, as the eye goes.\n refs.current[((target % drawn.length) + drawn.length) % drawn.length]?.focus();\n } else if (clickable && (event.key === \"Enter\" || event.key === \" \")) {\n event.preventDefault();\n clickRef.current?.(d.slice.key, d.slice);\n }\n };\n return (\n <g\n ref={(node) => {\n refs.current[index] = node;\n }}\n role={clickable ? \"button\" : \"img\"}\n // The first slice until the effect below moves the stop.\n tabIndex={index === 0 ? 0 : -1}\n aria-label={labels.slice(d.slice.label, formatValue(d.value), formatShare(d.share))}\n data-slice={d.slice.key}\n onKeyDown={onKeyDown}\n onFocus={() => {\n setStop(index);\n setFocused(index);\n }}\n onBlur={() => setFocused(undefined)}\n // CSS beats a presentation attribute, so this recolours the sector's own\n // stroke for as long as it has keyboard focus.\n className=\"outline-none [&:focus-visible>path]:stroke-[var(--text-primary)]\"\n >\n <Sector key={key ?? undefined} {...sector} />\n </g>\n );\n },\n [drawn, labels, formatValue, formatShare, clickable],\n );\n\n const label = useCallback(\n (props: PieLabelRenderProps) => {\n const d = drawn[props.index ?? -1];\n const cx = Number(props.cx);\n const cy = Number(props.cy);\n const radius = Number(props.outerRadius);\n const mid = Number(props.midAngle);\n if (!d || d.share < minLabelShare || ![cx, cy, radius, mid].every(Number.isFinite)) return null;\n const r = radius + 10;\n const x = cx + r * Math.cos(-mid * RADIAN);\n const y = cy + r * Math.sin(-mid * RADIAN);\n const isValue = sliceLabels === \"value\";\n return (\n <text\n x={x}\n y={y}\n textAnchor={x >= cx ? \"start\" : \"end\"}\n dominantBaseline=\"central\"\n fontSize={11}\n fill=\"var(--text-secondary)\"\n data-private={redact && isValue ? \"\" : undefined}\n style={{ pointerEvents: \"none\" }}\n >\n {isValue ? formatValue(d.value) : formatShare(d.share)}\n </text>\n );\n },\n [drawn, minLabelShare, sliceLabels, redact, formatValue, formatShare],\n );\n\n const withLabels = sliceLabels !== \"none\";\n // Room outside the ring for the labels, or the ring fills the box.\n const outer = withLabels ? 72 : 88;\n const inner = variant === \"donut\" ? Math.round(outer * 0.6) : 0;\n\n /**\n * The `<Pie>`, as ONE element for as long as what it draws is unchanged. recharts\n * keys its sectors by an animation id that is renewed whenever the Pie's props object\n * is — i.e. on every render of the Pie — so re-rendering it remounts every sector,\n * the one with focus included, which dropped the focus to <body> on the very state\n * update that focusing a slice makes. An unchanged element is one React does not\n * re-render, so the sectors stay put, and the roving tab stop is moved on them in\n * place (the effect below).\n */\n const pie = useMemo(\n () => (\n <Pie\n data={pieData}\n dataKey=\"value\"\n nameKey=\"label\"\n cx=\"50%\"\n cy=\"50%\"\n outerRadius={`${outer}%`}\n innerRadius={`${inner}%`}\n startAngle={90}\n endAngle={-270}\n // The card showing through between the slices, as the treemap's gutter.\n stroke=\"var(--bg-surface)\"\n strokeWidth={2}\n rootTabIndex={-1}\n isAnimationActive={false}\n shape={shape}\n label={withLabels ? label : false}\n labelLine={false}\n onClick={\n clickable\n ? (_entry: unknown, index: number) => {\n const d = drawn[index];\n if (d) clickRef.current?.(d.slice.key, d.slice);\n }\n : undefined\n }\n />\n ),\n [pieData, drawn, shape, label, withLabels, outer, inner, clickable],\n );\n\n // The roving tab stop, moved on the sectors in place (they are not re-rendered).\n useEffect(() => {\n refs.current.forEach((el, i) => el?.setAttribute(\"tabindex\", i === current ? \"0\" : \"-1\"));\n });\n\n const legendEntries: LegendEntry[] = drawable.map((d) => ({\n key: d.slice.key,\n label: d.slice.label,\n color: d.color,\n }));\n\n const emptyBox = (\n <div\n className=\"flex items-center justify-center text-sm text-[var(--text-muted)]\"\n style={{ height }}\n >\n {empty ?? labels.empty}\n </div>\n );\n\n // Nothing drawable at all: no chart, and no legend of switches for nothing.\n if (drawable.length === 0) {\n return (\n <div {...rest} className={cn(\"w-full\", className)} style={style}>\n {emptyBox}\n </div>\n );\n }\n\n const Content = redact ? RedactedTooltipContent : ChartTooltipContent;\n\n return (\n <div {...rest} className={cn(\"w-full\", className)} style={style}>\n {drawn.length === 0 ? (\n // Every slice switched off: the legend below is how they come back.\n emptyBox\n ) : (\n <div role=\"group\" aria-label={labels.slices} className=\"relative\" style={{ height }}>\n <ChartContainer\n config={PIE_CONFIG}\n className={cn(\"h-full\", clickable && \"[&_.recharts-pie-sector]:cursor-pointer\")}\n >\n <RechartsPieChart accessibilityLayer={false}>\n <ChartTooltip\n content={\n <Content\n hideLabel\n valueFormatter={(v) =>\n `${formatValue(v)} (${formatShare(total > 0 ? v / total : 0)})`\n }\n />\n }\n />\n {pie}\n </RechartsPieChart>\n </ChartContainer>\n {variant === \"donut\" && center && (\n <div\n data-private={redact ? \"\" : undefined}\n // While a slice has keyboard focus the centre reads it out — the\n // keyboard's tooltip. Hidden from assistive technology then: the slice's\n // own name has just said the same.\n aria-hidden={focusedSlice ? true : undefined}\n className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center\"\n >\n <span className=\"max-w-[40%] truncate text-xs text-[var(--text-muted)]\">\n {focusedSlice ? focusedSlice.slice.label : (centerLabel ?? labels.total)}\n </span>\n <span className=\"max-w-[40%] truncate text-lg font-semibold tabular-nums text-[var(--text-primary)]\">\n {focusedSlice ? formatValue(focusedSlice.value) : (centerValue ?? formatValue(total))}\n </span>\n {focusedSlice && (\n <span className=\"text-xs tabular-nums text-[var(--text-muted)]\">\n {formatShare(focusedSlice.share)}\n </span>\n )}\n </div>\n )}\n </div>\n )}\n {legend === \"toggle\" && (\n <ToggleLegend\n entries={legendEntries}\n hidden={hidden}\n onToggle={toggle}\n showSingle\n className=\"justify-center\"\n labels={{ legend: labels.legend }}\n />\n )}\n {legend === \"static\" && (\n <StaticLegend entries={legendEntries} className=\"justify-center\" aria-label={labels.legend} />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["drawable","total","drawn","pieData"]}