@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
@@ -1,6 +1,14 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
 
4
+ /** The words a progress bar adds of its own. */
5
+ interface ProgressBarLabels {
6
+ /** Shown after the value when `max={null}`: a quota with no ceiling. */
7
+ unlimited: string;
8
+ /** The `overage` line: how far past `max` the value is. `amount` is formatted. */
9
+ overLimit: (amount: string) => string;
10
+ }
11
+ declare const DEFAULT_PROGRESS_BAR_LABELS: ProgressBarLabels;
4
12
  /**
5
13
  * `income` / `expense` are the money pair (`--money-income` / `--money-expense`), the
6
14
  * names `Chip`, `StatTile` and `Sparkline` already use for them: a share bar of what
@@ -17,12 +25,41 @@ type ProgressBarTone = "brand" | "neutral" | "success" | "warning" | "danger" |
17
25
  * line of text where 8px outweighs the text.
18
26
  */
19
27
  type ProgressBarSize = "sm" | "slim" | "md" | "lg";
28
+ /** One part of a stacked bar. See {@link ProgressBarProps.segments}. */
29
+ interface ProgressBarSegment {
30
+ /** Its size, on the bar's own `min`–`max` scale. */
31
+ value: number;
32
+ /** A kit tone. Left out (and no `color`/`className`), the next `--chart-N` colour. */
33
+ tone?: ProgressBarTone;
34
+ /** Any CSS colour (`var(--token)`), for a palette the tones do not cover. Wins over `tone`. */
35
+ color?: string;
36
+ /** Classes for the segment's fill — keksdose's `STEP_CLASS` ramp. Wins over `tone`. */
37
+ className?: string;
38
+ /** The part's name: said in the bar's `aria-valuetext` and shown in the legend. */
39
+ label?: ReactNode;
40
+ /** A stable React key, when `label` is not a string. */
41
+ key?: string;
42
+ /**
43
+ * List it in the `legend` but leave it out of the bar — its value, its share of the
44
+ * `aria-valuetext`, and the sum. keksdose's cut card (reports/cut-card:132) names
45
+ * EVERY bucket under its bar, the empty ones included, while the bar draws only the
46
+ * ones with a share: pass the empty buckets as `legendOnly` entries.
47
+ */
48
+ legendOnly?: boolean;
49
+ }
20
50
  interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
21
51
  /** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way
22
52
  * and nobody knows how much is left (an import that reports no count). */
23
53
  value?: number;
24
54
  min?: number;
25
- max?: number;
55
+ /**
56
+ * The top of the scale (default 100). `null` is a quota with NO ceiling — kastlan's
57
+ * billing usage on an unlimited plan (usage-progress-bar:25): nothing to fill, so no
58
+ * bar is drawn; the label row shows the value and "Unlimited" (`progressBar.unlimited`)
59
+ * as plain text. `formatValue` is then called with `max` = `Infinity`; without it,
60
+ * the value is a plain number in the kit's locale.
61
+ */
62
+ max?: number | null;
26
63
  /**
27
64
  * `progress` (default): a task moving toward completion — `role="progressbar"`.
28
65
  * `meter`: a share of a known whole that is not going anywhere (budget used, disk
@@ -45,6 +82,39 @@ interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
45
82
  tone?: ProgressBarTone;
46
83
  size?: ProgressBarSize;
47
84
  locale?: string;
85
+ /**
86
+ * Draw a METER OF PARTS: one bar, several fills side by side in order, a 2px gap
87
+ * between them — keksdose's discretionary-spending bar (cut-card:133), a whole
88
+ * split into the steps of a scale. Forces `variant="meter"`: parts of a whole are
89
+ * never "busy". `aria-valuenow` is the sum; `aria-valuetext` names every part
90
+ * ("Rent: 40%, Food: 25%"), each part's value through `formatValue` and joined in the
91
+ * kit's locale — the words keksdose's hand-drawn bar had to hide from a reader
92
+ * altogether, leaving the legend as the only way in. The sum is clamped to `max`,
93
+ * and parts past it are cut off at the track's end.
94
+ */
95
+ segments?: ProgressBarSegment[];
96
+ /** With `segments`: a list under the bar naming each part with its swatch and value
97
+ * — the table view of the bar, in text tokens rather than the segment colour. */
98
+ legend?: boolean;
99
+ /**
100
+ * What a legend row shows at its end, in place of the part's formatted value — which
101
+ * it is handed, with the segment and its index. keksdose's cut card shows the share
102
+ * AND a private outflow amount per bucket (cut-card:150), which one figure could not:
103
+ * `legendValue={(seg, share) => <>{share} <Amount … /></>}`.
104
+ */
105
+ legendValue?: (segment: ProgressBarSegment, formatted: string, index: number) => ReactNode;
106
+ /** A muted line under the bar — the plan's reset date, "3 of 5 seats in use". It
107
+ * describes the bar (`aria-describedby`). */
108
+ hint?: ReactNode;
109
+ /**
110
+ * When `value` is past `max`, say by how much, in a danger-coloured line under the bar
111
+ * (kastlan usage-progress-bar:46): `true` prints `progressBar.overLimit` with the
112
+ * excess as a plain number; a function renders the line itself from the excess (a
113
+ * currency, "2 seats"). The bar stays full at `max` either way — colour it with
114
+ * `tone="danger"` if it should read as over. Ignored for `segments` and `max={null}`.
115
+ */
116
+ overage?: boolean | ((over: number) => ReactNode);
117
+ labels?: Partial<ProgressBarLabels>;
48
118
  }
49
119
  /**
50
120
  * A horizontal bar that fills toward `max`.
@@ -63,6 +133,6 @@ interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
63
133
  * tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an
64
134
  * unlayered rule beats every layered utility, `motion-reduce:animate-none` included.
65
135
  */
66
- declare function ProgressBar({ value, min, max, variant, label, showValue, formatValue, tone, size, locale, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...rest }: ProgressBarProps): react.JSX.Element;
136
+ declare function ProgressBar({ value, min, max: maxProp, variant, label, showValue, formatValue, tone, size, locale, segments, legend, legendValue, hint, overage, labels, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...rest }: ProgressBarProps): react.JSX.Element;
67
137
 
68
- export { ProgressBar, type ProgressBarProps, type ProgressBarSize, type ProgressBarTone };
138
+ export { DEFAULT_PROGRESS_BAR_LABELS, ProgressBar, type ProgressBarLabels, type ProgressBarProps, type ProgressBarSegment, type ProgressBarSize, type ProgressBarTone };
@@ -3,6 +3,10 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useId } from "react";
4
4
  import { cn } from "../lib/cn.js";
5
5
  import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
6
+ const DEFAULT_PROGRESS_BAR_LABELS = {
7
+ unlimited: "Unlimited",
8
+ overLimit: (amount) => `${amount} over the limit`
9
+ };
6
10
  const FILL = {
7
11
  brand: "bg-[var(--brand)]",
8
12
  neutral: "bg-[var(--text-muted)]",
@@ -14,10 +18,29 @@ const FILL = {
14
18
  expense: "bg-[var(--money-expense)]"
15
19
  };
16
20
  const TRACK_HEIGHT = { sm: "h-1", slim: "h-1.5", md: "h-2", lg: "h-3" };
21
+ const SERIES_FILL = [
22
+ "bg-[var(--chart-1)]",
23
+ "bg-[var(--chart-2)]",
24
+ "bg-[var(--chart-3)]",
25
+ "bg-[var(--chart-4)]",
26
+ "bg-[var(--chart-5)]",
27
+ "bg-[var(--chart-6)]",
28
+ "bg-[var(--chart-7)]",
29
+ "bg-[var(--chart-8)]",
30
+ "bg-[var(--chart-9)]"
31
+ ];
32
+ function segmentFills(segments) {
33
+ let next = 0;
34
+ return segments.map((seg) => {
35
+ if (seg.className) return seg.className;
36
+ if (seg.tone) return FILL[seg.tone];
37
+ return seg.color ? void 0 : SERIES_FILL[next++ % SERIES_FILL.length];
38
+ });
39
+ }
17
40
  function ProgressBar({
18
41
  value,
19
42
  min = 0,
20
- max = 100,
43
+ max: maxProp = 100,
21
44
  variant = "progress",
22
45
  label,
23
46
  showValue = false,
@@ -25,21 +48,61 @@ function ProgressBar({
25
48
  tone = "brand",
26
49
  size = "md",
27
50
  locale,
51
+ segments,
52
+ legend = false,
53
+ legendValue,
54
+ hint,
55
+ overage,
56
+ labels,
28
57
  className,
29
58
  "aria-label": ariaLabel,
30
59
  "aria-labelledby": ariaLabelledBy,
60
+ "aria-describedby": ariaDescribedBy,
31
61
  ...rest
32
62
  }) {
33
63
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
64
+ const text = useKitLabels("progressBar", DEFAULT_PROGRESS_BAR_LABELS, labels);
34
65
  const kitLocale = useKitLocale(locale);
35
66
  const labelId = useId();
36
- const meter = variant === "meter";
37
- const current = value === void 0 || !Number.isFinite(value) ? meter ? min : void 0 : value;
38
- const indeterminate = current === void 0;
67
+ const hintId = useId();
68
+ const hintNode = hint != null ? /* @__PURE__ */ jsx("p", { id: hintId, "data-part": "hint", className: "mt-1 text-xs text-[var(--text-muted)]", children: hint }) : null;
69
+ if (maxProp === null) {
70
+ const shown = value === void 0 || !Number.isFinite(value) ? void 0 : formatValue ? formatValue(value, Infinity, min) : new Intl.NumberFormat(kitLocale).format(value);
71
+ return /* @__PURE__ */ jsxs("div", { ...rest, className: cn("min-w-0", className), "data-state": "unlimited", children: [
72
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-2 text-sm", children: [
73
+ label != null && /* @__PURE__ */ jsx("span", { id: labelId, className: "min-w-0 text-[var(--text-secondary)]", children: label }),
74
+ /* @__PURE__ */ jsxs("span", { "data-part": "unlimited", className: "ms-auto shrink-0 tabular-nums text-[var(--text-muted)]", children: [
75
+ shown !== void 0 && `${shown} \xB7 `,
76
+ text.unlimited
77
+ ] })
78
+ ] }),
79
+ hintNode
80
+ ] });
81
+ }
82
+ const max = maxProp;
83
+ const stacked = segments !== void 0;
84
+ const meter = stacked || variant === "meter";
39
85
  const span = max - min;
86
+ const partValue = (v) => Number.isFinite(v) ? Math.max(0, v - min) : 0;
87
+ const fills = stacked ? segmentFills(segments) : [];
88
+ const barParts = stacked ? segments.filter((seg) => !seg.legendOnly) : [];
89
+ const partTotal = stacked ? min + barParts.reduce((sum, seg) => sum + partValue(seg.value), 0) : void 0;
90
+ const format = (v) => formatValue ? formatValue(v, max, min) : new Intl.NumberFormat(kitLocale, { style: "percent", maximumFractionDigits: 0 }).format(
91
+ span <= 0 ? 0 : (Math.min(max, Math.max(min, v)) - min) / span
92
+ );
93
+ const partTexts = stacked ? barParts.map((seg) => {
94
+ const text2 = format(min + partValue(seg.value));
95
+ return typeof seg.label === "string" || typeof seg.label === "number" ? common.fieldValue(String(seg.label), text2) : text2;
96
+ }) : [];
97
+ const current = partTotal ?? (value === void 0 || !Number.isFinite(value) ? meter ? min : void 0 : value);
98
+ const indeterminate = current === void 0;
40
99
  const clamped = indeterminate ? void 0 : Math.min(max, Math.max(min, current));
41
100
  const fraction = clamped === void 0 || span <= 0 ? 0 : (clamped - min) / span;
42
- const valueText = clamped === void 0 ? void 0 : formatValue ? formatValue(clamped, max, min) : new Intl.NumberFormat(kitLocale, { style: "percent", maximumFractionDigits: 0 }).format(fraction);
101
+ const valueText = clamped === void 0 ? void 0 : format(clamped);
102
+ const ariaValueText = stacked && partTexts.length > 0 ? new Intl.ListFormat(kitLocale, { style: "short", type: "unit" }).format(partTexts) : valueText;
103
+ const over = overage && !stacked && value !== void 0 && Number.isFinite(value) && value > max ? value - max : 0;
104
+ const overageNode = over > 0 ? /* @__PURE__ */ jsx("p", { id: `${hintId}-over`, "data-part": "overage", className: "mt-1 text-xs text-[var(--danger)]", children: typeof overage === "function" ? overage(over) : text.overLimit(new Intl.NumberFormat(kitLocale).format(over)) }) : null;
105
+ const describedBy = [ariaDescribedBy, hintNode && hintId, overageNode && `${hintId}-over`].filter(Boolean).join(" ") || void 0;
43
106
  const labelledBy = ariaLabelledBy ?? (label != null ? labelId : void 0);
44
107
  const name = labelledBy ? void 0 : ariaLabel ?? (indeterminate ? common.loading : void 0);
45
108
  return /* @__PURE__ */ jsxs("div", { className: cn("min-w-0", className), "data-state": indeterminate ? "indeterminate" : "determinate", children: [
@@ -56,13 +119,34 @@ function ProgressBar({
56
119
  role: meter ? "meter" : "progressbar",
57
120
  "aria-label": name,
58
121
  "aria-labelledby": labelledBy,
122
+ "aria-describedby": describedBy,
59
123
  "aria-valuemin": indeterminate ? void 0 : min,
60
124
  "aria-valuemax": indeterminate ? void 0 : max,
61
125
  "aria-valuenow": clamped,
62
- "aria-valuetext": valueText,
126
+ "aria-valuetext": ariaValueText,
63
127
  "aria-busy": !meter && indeterminate ? true : void 0,
64
128
  className: cn("relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]", TRACK_HEIGHT[size]),
65
- children: indeterminate ? /* @__PURE__ */ jsx(
129
+ children: stacked ? (
130
+ // The parts in a flex row with a 2px gap, so two neighbouring steps of one
131
+ // hue are parted by the track rather than by their difference in lightness.
132
+ /* @__PURE__ */ jsx("div", { className: "flex h-full w-full gap-0.5", children: segments.map(
133
+ (seg, i) => seg.legendOnly ? null : /* @__PURE__ */ jsx(
134
+ "div",
135
+ {
136
+ "data-part": "segment",
137
+ className: cn(
138
+ "h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none",
139
+ fills[i]
140
+ ),
141
+ style: {
142
+ width: `${span <= 0 ? 0 : partValue(seg.value) / span * 100}%`,
143
+ backgroundColor: seg.className ? void 0 : seg.color
144
+ }
145
+ },
146
+ seg.key ?? i
147
+ )
148
+ ) })
149
+ ) : indeterminate ? /* @__PURE__ */ jsx(
66
150
  "div",
67
151
  {
68
152
  "data-part": "fill",
@@ -87,10 +171,28 @@ function ProgressBar({
87
171
  }
88
172
  )
89
173
  }
90
- )
174
+ ),
175
+ stacked && legend && segments.length > 0 && /* @__PURE__ */ jsx("ul", { className: "mt-2 space-y-1 text-sm", "data-part": "legend", children: segments.map((seg, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2", children: [
176
+ /* @__PURE__ */ jsx(
177
+ "span",
178
+ {
179
+ "aria-hidden": true,
180
+ className: cn(
181
+ "size-2.5 shrink-0 rounded-sm",
182
+ fills[i]
183
+ ),
184
+ style: { backgroundColor: seg.className ? void 0 : seg.color }
185
+ }
186
+ ),
187
+ seg.label != null && /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[var(--text-secondary)]", children: seg.label }),
188
+ /* @__PURE__ */ jsx("span", { className: "ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]", children: legendValue ? legendValue(seg, format(min + partValue(seg.value)), i) : format(min + partValue(seg.value)) })
189
+ ] }, seg.key ?? i)) }),
190
+ overageNode,
191
+ hintNode
91
192
  ] });
92
193
  }
93
194
  export {
195
+ DEFAULT_PROGRESS_BAR_LABELS,
94
196
  ProgressBar
95
197
  };
96
198
  //# sourceMappingURL=progress-bar.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/progress-bar.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\n/**\n * `income` / `expense` are the money pair (`--money-income` / `--money-expense`), the\n * names `Chip`, `StatTile` and `Sparkline` already use for them: a share bar of what\n * came in or went out is money, and painting it `success` / `warning` said \"good\" and\n * \"careful\" about a figure that is neither. keksdose's report meters (food-group-card,\n * drilldown-modal) sit beside amounts that are already in the money colours.\n */\nexport type ProgressBarTone =\n | \"brand\"\n | \"neutral\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\"\n | \"income\"\n | \"expense\";\n/**\n * Track heights: `sm` 4px, `slim` 6px, `md` 8px, `lg` 12px. `slim` is a word and not a\n * letter because the letters are taken in order — an `xs` at 6px would be THICKER than\n * `sm` — and renaming `sm` would move every bar already on it. 6px is the Slider's\n * track and keksdose's hand-drawn meters (`h-1.5`, landing-visuals), for a bar under a\n * line of text where 8px outweighs the text.\n */\nexport type ProgressBarSize = \"sm\" | \"slim\" | \"md\" | \"lg\";\n\nconst FILL: Record<ProgressBarTone, string> = {\n brand: \"bg-[var(--brand)]\",\n neutral: \"bg-[var(--text-muted)]\",\n success: \"bg-[var(--success)]\",\n warning: \"bg-[var(--warning)]\",\n danger: \"bg-[var(--danger)]\",\n info: \"bg-[var(--info)]\",\n income: \"bg-[var(--money-income)]\",\n expense: \"bg-[var(--money-expense)]\",\n};\n\nconst TRACK_HEIGHT: Record<ProgressBarSize, string> = { sm: \"h-1\", slim: \"h-1.5\", md: \"h-2\", lg: \"h-3\" };\n\nexport interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n /** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way\n * and nobody knows how much is left (an import that reports no count). */\n value?: number;\n min?: number;\n max?: number;\n /**\n * `progress` (default): a task moving toward completion — `role=\"progressbar\"`.\n * `meter`: a share of a known whole that is not going anywhere (budget used, disk\n * space, one category's slice of spending) — `role=\"meter\"`. keksdose's share bars\n * were progress bars to a screen reader, which reads \"busy\" into a figure that is\n * simply a figure. A meter is never indeterminate; a missing value reads as `min`.\n */\n variant?: \"progress\" | \"meter\";\n /** Visible label above the bar, and its accessible name. Without it, pass\n * `aria-label` (an indeterminate bar with neither is named `common.loading`). */\n label?: ReactNode;\n /** Show the value text at the end of the label row. */\n showValue?: boolean;\n /**\n * The value as words — both the visible `showValue` text and `aria-valuetext`.\n * Default: the fraction as a percentage in the kit's locale (\"42 %\" in French).\n * Pass one for anything that is not a percentage: `(v, max) => \\`${v} of ${max} files\\``.\n */\n formatValue?: (value: number, max: number, min: number) => string;\n tone?: ProgressBarTone;\n size?: ProgressBarSize;\n locale?: string;\n}\n\n/**\n * A horizontal bar that fills toward `max`.\n *\n * keksdose (import progress, budget share bars) and kastlan (upload and job\n * progress) each drew their own div-in-a-div. None had a role, so a reader heard\n * nothing at all; the share bars that did have one claimed to be progress.\n *\n * `aria-valuetext` is always set when there is a value: a bare `aria-valuenow=\"37\"`\n * is read as \"37\" by some readers and \"37 percent\" by others, and neither is right\n * when `max` is 212 files.\n *\n * The indeterminate sweep stops under `prefers-reduced-motion`. It becomes a still,\n * half-strength bar across the whole track — still visibly \"something is happening\",\n * with nothing moving. The sweep is a Tailwind arbitrary `animate-[…]` rather than\n * tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an\n * unlayered rule beats every layered utility, `motion-reduce:animate-none` included.\n */\nexport function ProgressBar({\n value,\n min = 0,\n max = 100,\n variant = \"progress\",\n label,\n showValue = false,\n formatValue,\n tone = \"brand\",\n size = \"md\",\n locale,\n className,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n ...rest\n}: ProgressBarProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const kitLocale = useKitLocale(locale);\n const labelId = useId();\n\n const meter = variant === \"meter\";\n const current = value === undefined || !Number.isFinite(value) ? (meter ? min : undefined) : value;\n const indeterminate = current === undefined;\n const span = max - min;\n const clamped = indeterminate ? undefined : Math.min(max, Math.max(min, current));\n const fraction = clamped === undefined || span <= 0 ? 0 : (clamped - min) / span;\n\n const valueText =\n clamped === undefined\n ? undefined\n : formatValue\n ? formatValue(clamped, max, min)\n : new Intl.NumberFormat(kitLocale, { style: \"percent\", maximumFractionDigits: 0 }).format(fraction);\n\n const labelledBy = ariaLabelledBy ?? (label != null ? labelId : undefined);\n const name = labelledBy ? undefined : (ariaLabel ?? (indeterminate ? common.loading : undefined));\n\n return (\n <div className={cn(\"min-w-0\", className)} data-state={indeterminate ? \"indeterminate\" : \"determinate\"}>\n {(label != null || (showValue && valueText !== undefined)) && (\n <div className=\"mb-1 flex items-baseline gap-2 text-sm\">\n {label != null && (\n <span id={labelId} className=\"min-w-0 text-[var(--text-secondary)]\">\n {label}\n </span>\n )}\n {showValue && valueText !== undefined && (\n // Hidden: the same words are the bar's aria-valuetext, and reading them\n // twice in a row is noise.\n <span aria-hidden className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {valueText}\n </span>\n )}\n </div>\n )}\n <div\n {...rest}\n role={meter ? \"meter\" : \"progressbar\"}\n aria-label={name}\n aria-labelledby={labelledBy}\n aria-valuemin={indeterminate ? undefined : min}\n aria-valuemax={indeterminate ? undefined : max}\n aria-valuenow={clamped}\n aria-valuetext={valueText}\n aria-busy={!meter && indeterminate ? true : undefined}\n // `relative` + `overflow-hidden`: the indeterminate segment is positioned in\n // here and must not paint past the rounded ends.\n className={cn(\"relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]\", TRACK_HEIGHT[size])}\n >\n {indeterminate ? (\n <div\n data-part=\"fill\"\n className={cn(\n // `left-0`, deliberately physical: the keyframes (tokens.css\n // `indeterminate-sweep`) translate along the physical x axis, and under\n // RTL the same keyframes are run in REVERSE — from the right edge back\n // to the left. A logical `start-0` would park the segment at the right\n // in RTL and the reversed sweep would then start off-screen.\n \"absolute inset-y-0 left-0 w-1/3 rounded-full\",\n \"animate-[indeterminate-sweep_1.4s_ease-in-out_infinite] rtl:[animation-direction:reverse]\",\n \"motion-reduce:w-full motion-reduce:animate-none motion-reduce:opacity-50\",\n FILL[tone],\n )}\n />\n ) : (\n <div\n data-part=\"fill\"\n // Block flow starts at the inline start, so a plain width fills from the\n // right under `dir=\"rtl\"` with nothing extra.\n className={cn(\"h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none\", FILL[tone])}\n style={{ width: `${fraction * 100}%` }}\n />\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AAkIQ,SAEI,KAFJ;AAlIR,SAAS,aAAa;AAEtB,SAAS,UAAU;AACnB,SAAS,uBAAuB,cAAc,oBAAoB;AA2BlE,MAAM,OAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AACX;AAEA,MAAM,eAAgD,EAAE,IAAI,OAAO,MAAM,SAAS,IAAI,OAAO,IAAI,MAAM;AAiDhG,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AAAA,EACN,UAAU;AAAA,EACV;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,mBAAmB;AAAA,EACnB,GAAG;AACL,GAAqB;AACnB,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,YAAY,aAAa,MAAM;AACrC,QAAM,UAAU,MAAM;AAEtB,QAAM,QAAQ,YAAY;AAC1B,QAAM,UAAU,UAAU,UAAa,CAAC,OAAO,SAAS,KAAK,IAAK,QAAQ,MAAM,SAAa;AAC7F,QAAM,gBAAgB,YAAY;AAClC,QAAM,OAAO,MAAM;AACnB,QAAM,UAAU,gBAAgB,SAAY,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,OAAO,CAAC;AAChF,QAAM,WAAW,YAAY,UAAa,QAAQ,IAAI,KAAK,UAAU,OAAO;AAE5E,QAAM,YACJ,YAAY,SACR,SACA,cACE,YAAY,SAAS,KAAK,GAAG,IAC7B,IAAI,KAAK,aAAa,WAAW,EAAE,OAAO,WAAW,uBAAuB,EAAE,CAAC,EAAE,OAAO,QAAQ;AAExG,QAAM,aAAa,mBAAmB,SAAS,OAAO,UAAU;AAChE,QAAM,OAAO,aAAa,SAAa,cAAc,gBAAgB,OAAO,UAAU;AAEtF,SACE,qBAAC,SAAI,WAAW,GAAG,WAAW,SAAS,GAAG,cAAY,gBAAgB,kBAAkB,eACpF;AAAA,cAAS,QAAS,aAAa,cAAc,WAC7C,qBAAC,SAAI,WAAU,0CACZ;AAAA,eAAS,QACR,oBAAC,UAAK,IAAI,SAAS,WAAU,wCAC1B,iBACH;AAAA,MAED,aAAa,cAAc;AAAA;AAAA,MAG1B,oBAAC,UAAK,eAAW,MAAC,WAAU,0DACzB,qBACH;AAAA,OAEJ;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ,MAAM,QAAQ,UAAU;AAAA,QACxB,cAAY;AAAA,QACZ,mBAAiB;AAAA,QACjB,iBAAe,gBAAgB,SAAY;AAAA,QAC3C,iBAAe,gBAAgB,SAAY;AAAA,QAC3C,iBAAe;AAAA,QACf,kBAAgB;AAAA,QAChB,aAAW,CAAC,SAAS,gBAAgB,OAAO;AAAA,QAG5C,WAAW,GAAG,sEAAsE,aAAa,IAAI,CAAC;AAAA,QAErG,0BACC;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YACV,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAMT;AAAA,cACA;AAAA,cACA;AAAA,cACA,KAAK,IAAI;AAAA,YACX;AAAA;AAAA,QACF,IAEA;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YAGV,WAAW,GAAG,qFAAqF,KAAK,IAAI,CAAC;AAAA,YAC7G,OAAO,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI;AAAA;AAAA,QACvC;AAAA;AAAA,IAEJ;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/progress-bar.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\n/** The words a progress bar adds of its own. */\nexport interface ProgressBarLabels {\n /** Shown after the value when `max={null}`: a quota with no ceiling. */\n unlimited: string;\n /** The `overage` line: how far past `max` the value is. `amount` is formatted. */\n overLimit: (amount: string) => string;\n}\n\nexport const DEFAULT_PROGRESS_BAR_LABELS: ProgressBarLabels = {\n unlimited: \"Unlimited\",\n overLimit: (amount) => `${amount} over the limit`,\n};\n\n/**\n * `income` / `expense` are the money pair (`--money-income` / `--money-expense`), the\n * names `Chip`, `StatTile` and `Sparkline` already use for them: a share bar of what\n * came in or went out is money, and painting it `success` / `warning` said \"good\" and\n * \"careful\" about a figure that is neither. keksdose's report meters (food-group-card,\n * drilldown-modal) sit beside amounts that are already in the money colours.\n */\nexport type ProgressBarTone =\n | \"brand\"\n | \"neutral\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\"\n | \"income\"\n | \"expense\";\n/**\n * Track heights: `sm` 4px, `slim` 6px, `md` 8px, `lg` 12px. `slim` is a word and not a\n * letter because the letters are taken in order — an `xs` at 6px would be THICKER than\n * `sm` — and renaming `sm` would move every bar already on it. 6px is the Slider's\n * track and keksdose's hand-drawn meters (`h-1.5`, landing-visuals), for a bar under a\n * line of text where 8px outweighs the text.\n */\nexport type ProgressBarSize = \"sm\" | \"slim\" | \"md\" | \"lg\";\n\nconst FILL: Record<ProgressBarTone, string> = {\n brand: \"bg-[var(--brand)]\",\n neutral: \"bg-[var(--text-muted)]\",\n success: \"bg-[var(--success)]\",\n warning: \"bg-[var(--warning)]\",\n danger: \"bg-[var(--danger)]\",\n info: \"bg-[var(--info)]\",\n income: \"bg-[var(--money-income)]\",\n expense: \"bg-[var(--money-expense)]\",\n};\n\nconst TRACK_HEIGHT: Record<ProgressBarSize, string> = { sm: \"h-1\", slim: \"h-1.5\", md: \"h-2\", lg: \"h-3\" };\n\n// A segment with no tone of its own takes the next categorical chart colour, so a\n// stacked bar of unnamed parts is still a set of tellable parts rather than one fill.\nconst SERIES_FILL = [\n \"bg-[var(--chart-1)]\",\n \"bg-[var(--chart-2)]\",\n \"bg-[var(--chart-3)]\",\n \"bg-[var(--chart-4)]\",\n \"bg-[var(--chart-5)]\",\n \"bg-[var(--chart-6)]\",\n \"bg-[var(--chart-7)]\",\n \"bg-[var(--chart-8)]\",\n \"bg-[var(--chart-9)]\",\n];\n\n/** Each segment's fill classes: its own classes, else its tone, else (with no `color`)\n * the next chart colour. \"Next\" counts the UNCOLOURED segments only — a segment with\n * its own `className`/`tone`/`color` does not use up `--chart-1`, so an \"Other\" in grey\n * ahead of two plain parts leaves them `--chart-1` and `--chart-2`. */\nfunction segmentFills(segments: ProgressBarSegment[]): Array<string | undefined> {\n let next = 0;\n return segments.map((seg) => {\n if (seg.className) return seg.className;\n if (seg.tone) return FILL[seg.tone];\n return seg.color ? undefined : SERIES_FILL[next++ % SERIES_FILL.length];\n });\n}\n\n/** One part of a stacked bar. See {@link ProgressBarProps.segments}. */\nexport interface ProgressBarSegment {\n /** Its size, on the bar's own `min`–`max` scale. */\n value: number;\n /** A kit tone. Left out (and no `color`/`className`), the next `--chart-N` colour. */\n tone?: ProgressBarTone;\n /** Any CSS colour (`var(--token)`), for a palette the tones do not cover. Wins over `tone`. */\n color?: string;\n /** Classes for the segment's fill — keksdose's `STEP_CLASS` ramp. Wins over `tone`. */\n className?: string;\n /** The part's name: said in the bar's `aria-valuetext` and shown in the legend. */\n label?: ReactNode;\n /** A stable React key, when `label` is not a string. */\n key?: string;\n /**\n * List it in the `legend` but leave it out of the bar — its value, its share of the\n * `aria-valuetext`, and the sum. keksdose's cut card (reports/cut-card:132) names\n * EVERY bucket under its bar, the empty ones included, while the bar draws only the\n * ones with a share: pass the empty buckets as `legendOnly` entries.\n */\n legendOnly?: boolean;\n}\n\nexport interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n /** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way\n * and nobody knows how much is left (an import that reports no count). */\n value?: number;\n min?: number;\n /**\n * The top of the scale (default 100). `null` is a quota with NO ceiling — kastlan's\n * billing usage on an unlimited plan (usage-progress-bar:25): nothing to fill, so no\n * bar is drawn; the label row shows the value and \"Unlimited\" (`progressBar.unlimited`)\n * as plain text. `formatValue` is then called with `max` = `Infinity`; without it,\n * the value is a plain number in the kit's locale.\n */\n max?: number | null;\n /**\n * `progress` (default): a task moving toward completion — `role=\"progressbar\"`.\n * `meter`: a share of a known whole that is not going anywhere (budget used, disk\n * space, one category's slice of spending) — `role=\"meter\"`. keksdose's share bars\n * were progress bars to a screen reader, which reads \"busy\" into a figure that is\n * simply a figure. A meter is never indeterminate; a missing value reads as `min`.\n */\n variant?: \"progress\" | \"meter\";\n /** Visible label above the bar, and its accessible name. Without it, pass\n * `aria-label` (an indeterminate bar with neither is named `common.loading`). */\n label?: ReactNode;\n /** Show the value text at the end of the label row. */\n showValue?: boolean;\n /**\n * The value as words — both the visible `showValue` text and `aria-valuetext`.\n * Default: the fraction as a percentage in the kit's locale (\"42 %\" in French).\n * Pass one for anything that is not a percentage: `(v, max) => \\`${v} of ${max} files\\``.\n */\n formatValue?: (value: number, max: number, min: number) => string;\n tone?: ProgressBarTone;\n size?: ProgressBarSize;\n locale?: string;\n /**\n * Draw a METER OF PARTS: one bar, several fills side by side in order, a 2px gap\n * between them — keksdose's discretionary-spending bar (cut-card:133), a whole\n * split into the steps of a scale. Forces `variant=\"meter\"`: parts of a whole are\n * never \"busy\". `aria-valuenow` is the sum; `aria-valuetext` names every part\n * (\"Rent: 40%, Food: 25%\"), each part's value through `formatValue` and joined in the\n * kit's locale — the words keksdose's hand-drawn bar had to hide from a reader\n * altogether, leaving the legend as the only way in. The sum is clamped to `max`,\n * and parts past it are cut off at the track's end.\n */\n segments?: ProgressBarSegment[];\n /** With `segments`: a list under the bar naming each part with its swatch and value\n * — the table view of the bar, in text tokens rather than the segment colour. */\n legend?: boolean;\n /**\n * What a legend row shows at its end, in place of the part's formatted value — which\n * it is handed, with the segment and its index. keksdose's cut card shows the share\n * AND a private outflow amount per bucket (cut-card:150), which one figure could not:\n * `legendValue={(seg, share) => <>{share} <Amount … /></>}`.\n */\n legendValue?: (segment: ProgressBarSegment, formatted: string, index: number) => ReactNode;\n /** A muted line under the bar — the plan's reset date, \"3 of 5 seats in use\". It\n * describes the bar (`aria-describedby`). */\n hint?: ReactNode;\n /**\n * When `value` is past `max`, say by how much, in a danger-coloured line under the bar\n * (kastlan usage-progress-bar:46): `true` prints `progressBar.overLimit` with the\n * excess as a plain number; a function renders the line itself from the excess (a\n * currency, \"2 seats\"). The bar stays full at `max` either way — colour it with\n * `tone=\"danger\"` if it should read as over. Ignored for `segments` and `max={null}`.\n */\n overage?: boolean | ((over: number) => ReactNode);\n labels?: Partial<ProgressBarLabels>;\n}\n\n/**\n * A horizontal bar that fills toward `max`.\n *\n * keksdose (import progress, budget share bars) and kastlan (upload and job\n * progress) each drew their own div-in-a-div. None had a role, so a reader heard\n * nothing at all; the share bars that did have one claimed to be progress.\n *\n * `aria-valuetext` is always set when there is a value: a bare `aria-valuenow=\"37\"`\n * is read as \"37\" by some readers and \"37 percent\" by others, and neither is right\n * when `max` is 212 files.\n *\n * The indeterminate sweep stops under `prefers-reduced-motion`. It becomes a still,\n * half-strength bar across the whole track — still visibly \"something is happening\",\n * with nothing moving. The sweep is a Tailwind arbitrary `animate-[…]` rather than\n * tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an\n * unlayered rule beats every layered utility, `motion-reduce:animate-none` included.\n */\nexport function ProgressBar({\n value,\n min = 0,\n max: maxProp = 100,\n variant = \"progress\",\n label,\n showValue = false,\n formatValue,\n tone = \"brand\",\n size = \"md\",\n locale,\n segments,\n legend = false,\n legendValue,\n hint,\n overage,\n labels,\n className,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: ProgressBarProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const text = useKitLabels(\"progressBar\", DEFAULT_PROGRESS_BAR_LABELS, labels);\n const kitLocale = useKitLocale(locale);\n const labelId = useId();\n const hintId = useId();\n const hintNode =\n hint != null ? (\n <p id={hintId} data-part=\"hint\" className=\"mt-1 text-xs text-[var(--text-muted)]\">\n {hint}\n </p>\n ) : null;\n\n if (maxProp === null) {\n // No ceiling: no bar, no role — a meter with no maximum is not one. The value and\n // \"Unlimited\" are the content, so they are plain visible text.\n const shown =\n value === undefined || !Number.isFinite(value)\n ? undefined\n : formatValue\n ? formatValue(value, Infinity, min)\n : new Intl.NumberFormat(kitLocale).format(value);\n return (\n <div {...rest} className={cn(\"min-w-0\", className)} data-state=\"unlimited\">\n <div className=\"flex items-baseline gap-2 text-sm\">\n {label != null && (\n <span id={labelId} className=\"min-w-0 text-[var(--text-secondary)]\">\n {label}\n </span>\n )}\n <span data-part=\"unlimited\" className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {shown !== undefined && `${shown} · `}\n {text.unlimited}\n </span>\n </div>\n {hintNode}\n </div>\n );\n }\n const max = maxProp;\n\n const stacked = segments !== undefined;\n const meter = stacked || variant === \"meter\";\n const span = max - min;\n const partValue = (v: number) => (Number.isFinite(v) ? Math.max(0, v - min) : 0);\n // Colours are dealt over EVERY segment, legend-only ones included, so an entry keeps\n // its swatch whether or not it is in the bar this time.\n const fills = stacked ? segmentFills(segments) : [];\n const barParts = stacked ? segments.filter((seg) => !seg.legendOnly) : [];\n const partTotal = stacked ? min + barParts.reduce((sum, seg) => sum + partValue(seg.value), 0) : undefined;\n const format = (v: number) =>\n formatValue\n ? formatValue(v, max, min)\n : new Intl.NumberFormat(kitLocale, { style: \"percent\", maximumFractionDigits: 0 }).format(\n span <= 0 ? 0 : (Math.min(max, Math.max(min, v)) - min) / span,\n );\n const partTexts = stacked\n ? barParts.map((seg) => {\n const text = format(min + partValue(seg.value));\n return typeof seg.label === \"string\" || typeof seg.label === \"number\"\n ? common.fieldValue(String(seg.label), text)\n : text;\n })\n : [];\n const current =\n partTotal ?? (value === undefined || !Number.isFinite(value) ? (meter ? min : undefined) : value);\n const indeterminate = current === undefined;\n const clamped = indeterminate ? undefined : Math.min(max, Math.max(min, current));\n const fraction = clamped === undefined || span <= 0 ? 0 : (clamped - min) / span;\n\n const valueText = clamped === undefined ? undefined : format(clamped);\n const ariaValueText =\n stacked && partTexts.length > 0\n ? new Intl.ListFormat(kitLocale, { style: \"short\", type: \"unit\" }).format(partTexts)\n : valueText;\n\n const over =\n overage && !stacked && value !== undefined && Number.isFinite(value) && value > max ? value - max : 0;\n const overageNode =\n over > 0 ? (\n <p id={`${hintId}-over`} data-part=\"overage\" className=\"mt-1 text-xs text-[var(--danger)]\">\n {typeof overage === \"function\"\n ? overage(over)\n : text.overLimit(new Intl.NumberFormat(kitLocale).format(over))}\n </p>\n ) : null;\n const describedBy =\n [ariaDescribedBy, hintNode && hintId, overageNode && `${hintId}-over`].filter(Boolean).join(\" \") || undefined;\n\n const labelledBy = ariaLabelledBy ?? (label != null ? labelId : undefined);\n const name = labelledBy ? undefined : (ariaLabel ?? (indeterminate ? common.loading : undefined));\n\n return (\n <div className={cn(\"min-w-0\", className)} data-state={indeterminate ? \"indeterminate\" : \"determinate\"}>\n {(label != null || (showValue && valueText !== undefined)) && (\n <div className=\"mb-1 flex items-baseline gap-2 text-sm\">\n {label != null && (\n <span id={labelId} className=\"min-w-0 text-[var(--text-secondary)]\">\n {label}\n </span>\n )}\n {showValue && valueText !== undefined && (\n // Hidden: the same words are the bar's aria-valuetext, and reading them\n // twice in a row is noise.\n <span aria-hidden className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {valueText}\n </span>\n )}\n </div>\n )}\n <div\n {...rest}\n role={meter ? \"meter\" : \"progressbar\"}\n aria-label={name}\n aria-labelledby={labelledBy}\n aria-describedby={describedBy}\n aria-valuemin={indeterminate ? undefined : min}\n aria-valuemax={indeterminate ? undefined : max}\n aria-valuenow={clamped}\n aria-valuetext={ariaValueText}\n aria-busy={!meter && indeterminate ? true : undefined}\n // `relative` + `overflow-hidden`: the indeterminate segment is positioned in\n // here and must not paint past the rounded ends.\n className={cn(\"relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]\", TRACK_HEIGHT[size])}\n >\n {stacked ? (\n // The parts in a flex row with a 2px gap, so two neighbouring steps of one\n // hue are parted by the track rather than by their difference in lightness.\n <div className=\"flex h-full w-full gap-0.5\">\n {segments.map((seg, i) =>\n seg.legendOnly ? null : (\n <div\n key={seg.key ?? i}\n data-part=\"segment\"\n className={cn(\n \"h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none\",\n fills[i],\n )}\n style={{\n width: `${span <= 0 ? 0 : (partValue(seg.value) / span) * 100}%`,\n backgroundColor: seg.className ? undefined : seg.color,\n }}\n />\n ),\n )}\n </div>\n ) : indeterminate ? (\n <div\n data-part=\"fill\"\n className={cn(\n // `left-0`, deliberately physical: the keyframes (tokens.css\n // `indeterminate-sweep`) translate along the physical x axis, and under\n // RTL the same keyframes are run in REVERSE — from the right edge back\n // to the left. A logical `start-0` would park the segment at the right\n // in RTL and the reversed sweep would then start off-screen.\n \"absolute inset-y-0 left-0 w-1/3 rounded-full\",\n \"animate-[indeterminate-sweep_1.4s_ease-in-out_infinite] rtl:[animation-direction:reverse]\",\n \"motion-reduce:w-full motion-reduce:animate-none motion-reduce:opacity-50\",\n FILL[tone],\n )}\n />\n ) : (\n <div\n data-part=\"fill\"\n // Block flow starts at the inline start, so a plain width fills from the\n // right under `dir=\"rtl\"` with nothing extra.\n className={cn(\"h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none\", FILL[tone])}\n style={{ width: `${fraction * 100}%` }}\n />\n )}\n </div>\n {stacked && legend && segments.length > 0 && (\n <ul className=\"mt-2 space-y-1 text-sm\" data-part=\"legend\">\n {segments.map((seg, i) => (\n <li key={seg.key ?? i} className=\"flex items-center gap-2\">\n <span\n aria-hidden\n className={cn(\n \"size-2.5 shrink-0 rounded-sm\",\n fills[i],\n )}\n style={{ backgroundColor: seg.className ? undefined : seg.color }}\n />\n {seg.label != null && (\n <span className=\"min-w-0 flex-1 truncate text-[var(--text-secondary)]\">{seg.label}</span>\n )}\n <span className=\"ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]\">\n {legendValue\n ? legendValue(seg, format(min + partValue(seg.value)), i)\n : format(min + partValue(seg.value))}\n </span>\n </li>\n ))}\n </ul>\n )}\n {overageNode}\n {hintNode}\n </div>\n );\n}\n"],"mappings":"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","names":["text"]}
@@ -0,0 +1,34 @@
1
+ import * as react from 'react';
2
+ import { ComponentPropsWithoutRef } from 'react';
3
+ import { QrErrorCorrection } from '../lib/qr-encode.js';
4
+
5
+ interface QrCodeProps extends Omit<ComponentPropsWithoutRef<"svg">, "children" | "viewBox"> {
6
+ /** The text to encode (UTF-8) — an `otpauth://` URI, a URL, a pairing code. */
7
+ value: string;
8
+ /** The accessible name. Without it the code is decorative (`aria-hidden`): say what
9
+ * it is for, not what it encodes — nobody wants a URI read out. */
10
+ label?: string;
11
+ /** Default `"M"`, raised automatically where the size allows it. */
12
+ errorCorrection?: QrErrorCorrection;
13
+ /** The quiet zone, in modules. The spec asks for 4; less reads worse. Default 4. */
14
+ margin?: number;
15
+ }
16
+ /**
17
+ * A QR code as one inline SVG — no canvas, no data URL, no dependency (see
18
+ * `lib/qr-encode.ts` for why the encoder lives in the kit).
19
+ *
20
+ * Sized by CSS (`className="size-48"`); the SVG scales without blurring because every
21
+ * module is a whole unit of the viewBox and `shape-rendering` is `crispEdges`.
22
+ *
23
+ * **Black on white, always — the one literal colour pair in the kit.** A camera looks
24
+ * for the contrast between the modules and their quiet zone; on the dark theme a
25
+ * surface-coloured background makes a code slow to acquire, and an inverted one is not
26
+ * read at all by several authenticator apps. The quiet zone is part of the SVG, so the
27
+ * code carries its own white border wherever it is placed.
28
+ *
29
+ * A value too long for any QR version (≈2.9 kB) renders nothing rather than throwing
30
+ * in render.
31
+ */
32
+ declare function QrCode({ value, label, errorCorrection, margin, className, ...rest }: QrCodeProps): react.JSX.Element | null;
33
+
34
+ export { QrCode, type QrCodeProps };
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useMemo } from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { encodeQr } from "../lib/qr-encode.js";
6
+ function QrCode({
7
+ value,
8
+ label,
9
+ errorCorrection = "M",
10
+ margin = 4,
11
+ className,
12
+ ...rest
13
+ }) {
14
+ const path = useMemo(() => {
15
+ try {
16
+ const { modules, size } = encodeQr(value, { errorCorrection });
17
+ let d = "";
18
+ for (let y = 0; y < size; y++) {
19
+ for (let x = 0; x < size; ) {
20
+ if (!modules[y][x]) {
21
+ x++;
22
+ continue;
23
+ }
24
+ let run = 1;
25
+ while (x + run < size && modules[y][x + run]) run++;
26
+ d += `M${x + margin} ${y + margin}h${run}v1h-${run}z`;
27
+ x += run;
28
+ }
29
+ }
30
+ return { d, extent: size + margin * 2 };
31
+ } catch {
32
+ return null;
33
+ }
34
+ }, [value, errorCorrection, margin]);
35
+ if (!path) return null;
36
+ return /* @__PURE__ */ jsxs(
37
+ "svg",
38
+ {
39
+ ...rest,
40
+ xmlns: "http://www.w3.org/2000/svg",
41
+ viewBox: `0 0 ${path.extent} ${path.extent}`,
42
+ shapeRendering: "crispEdges",
43
+ role: label ? "img" : void 0,
44
+ "aria-label": label,
45
+ "aria-hidden": label ? void 0 : true,
46
+ className: cn("block", className),
47
+ children: [
48
+ /* @__PURE__ */ jsx("rect", { width: path.extent, height: path.extent, fill: "#ffffff" }),
49
+ /* @__PURE__ */ jsx("path", { d: path.d, fill: "#000000" })
50
+ ]
51
+ }
52
+ );
53
+ }
54
+ export {
55
+ QrCode
56
+ };
57
+ //# sourceMappingURL=qr-code.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/qr-code.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ComponentPropsWithoutRef } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { encodeQr } from \"../lib/qr-encode\";\nimport type { QrErrorCorrection } from \"../lib/qr-encode\";\n\nexport interface QrCodeProps extends Omit<ComponentPropsWithoutRef<\"svg\">, \"children\" | \"viewBox\"> {\n /** The text to encode (UTF-8) — an `otpauth://` URI, a URL, a pairing code. */\n value: string;\n /** The accessible name. Without it the code is decorative (`aria-hidden`): say what\n * it is for, not what it encodes — nobody wants a URI read out. */\n label?: string;\n /** Default `\"M\"`, raised automatically where the size allows it. */\n errorCorrection?: QrErrorCorrection;\n /** The quiet zone, in modules. The spec asks for 4; less reads worse. Default 4. */\n margin?: number;\n}\n\n/**\n * A QR code as one inline SVG — no canvas, no data URL, no dependency (see\n * `lib/qr-encode.ts` for why the encoder lives in the kit).\n *\n * Sized by CSS (`className=\"size-48\"`); the SVG scales without blurring because every\n * module is a whole unit of the viewBox and `shape-rendering` is `crispEdges`.\n *\n * **Black on white, always — the one literal colour pair in the kit.** A camera looks\n * for the contrast between the modules and their quiet zone; on the dark theme a\n * surface-coloured background makes a code slow to acquire, and an inverted one is not\n * read at all by several authenticator apps. The quiet zone is part of the SVG, so the\n * code carries its own white border wherever it is placed.\n *\n * A value too long for any QR version (≈2.9 kB) renders nothing rather than throwing\n * in render.\n */\nexport function QrCode({\n value,\n label,\n errorCorrection = \"M\",\n margin = 4,\n className,\n ...rest\n}: QrCodeProps) {\n const path = useMemo(() => {\n try {\n const { modules, size } = encodeQr(value, { errorCorrection });\n // One path, one sub-path per horizontal run of dark modules: a few hundred\n // commands rather than one <rect> per module.\n let d = \"\";\n for (let y = 0; y < size; y++) {\n for (let x = 0; x < size; ) {\n if (!modules[y][x]) {\n x++;\n continue;\n }\n let run = 1;\n while (x + run < size && modules[y][x + run]) run++;\n d += `M${x + margin} ${y + margin}h${run}v1h-${run}z`;\n x += run;\n }\n }\n return { d, extent: size + margin * 2 };\n } catch {\n return null;\n }\n }, [value, errorCorrection, margin]);\n\n if (!path) return null;\n return (\n <svg\n {...rest}\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox={`0 0 ${path.extent} ${path.extent}`}\n shapeRendering=\"crispEdges\"\n role={label ? \"img\" : undefined}\n aria-label={label}\n aria-hidden={label ? undefined : true}\n className={cn(\"block\", className)}\n >\n <rect width={path.extent} height={path.extent} fill=\"#ffffff\" />\n <path d={path.d} fill=\"#000000\" />\n </svg>\n );\n}\n"],"mappings":";AAoEI,SAUE,KAVF;AApEJ,SAAS,eAAe;AAExB,SAAS,UAAU;AACnB,SAAS,gBAAgB;AA+BlB,SAAS,OAAO;AAAA,EACrB;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,SAAS;AAAA,EACT;AAAA,EACA,GAAG;AACL,GAAgB;AACd,QAAM,OAAO,QAAQ,MAAM;AACzB,QAAI;AACF,YAAM,EAAE,SAAS,KAAK,IAAI,SAAS,OAAO,EAAE,gBAAgB,CAAC;AAG7D,UAAI,IAAI;AACR,eAAS,IAAI,GAAG,IAAI,MAAM,KAAK;AAC7B,iBAAS,IAAI,GAAG,IAAI,QAAQ;AAC1B,cAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG;AAClB;AACA;AAAA,UACF;AACA,cAAI,MAAM;AACV,iBAAO,IAAI,MAAM,QAAQ,QAAQ,CAAC,EAAE,IAAI,GAAG,EAAG;AAC9C,eAAK,IAAI,IAAI,MAAM,IAAI,IAAI,MAAM,IAAI,GAAG,OAAO,GAAG;AAClD,eAAK;AAAA,QACP;AAAA,MACF;AACA,aAAO,EAAE,GAAG,QAAQ,OAAO,SAAS,EAAE;AAAA,IACxC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,OAAO,iBAAiB,MAAM,CAAC;AAEnC,MAAI,CAAC,KAAM,QAAO;AAClB,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,OAAM;AAAA,MACN,SAAS,OAAO,KAAK,MAAM,IAAI,KAAK,MAAM;AAAA,MAC1C,gBAAe;AAAA,MACf,MAAM,QAAQ,QAAQ;AAAA,MACtB,cAAY;AAAA,MACZ,eAAa,QAAQ,SAAY;AAAA,MACjC,WAAW,GAAG,SAAS,SAAS;AAAA,MAEhC;AAAA,4BAAC,UAAK,OAAO,KAAK,QAAQ,QAAQ,KAAK,QAAQ,MAAK,WAAU;AAAA,QAC9D,oBAAC,UAAK,GAAG,KAAK,GAAG,MAAK,WAAU;AAAA;AAAA;AAAA,EAClC;AAEJ;","names":[]}
@@ -16,10 +16,22 @@ interface SearchFieldOwnProps extends Omit<InputHTMLAttributes<HTMLInputElement>
16
16
  * absent, so a consumer can migrate one call site at a time.
17
17
  */
18
18
  label?: string;
19
- /** Names the clear button. Omit it and no clear button is rendered — which is a
20
- * decision, not a default: a filter you can type into and not untype is the
21
- * complaint that produced the button on three of these four screens. */
19
+ /** Names the clear button. Default: the catalogue's `common.clear`, in the
20
+ * provider's language. */
22
21
  clearLabel?: string;
22
+ /**
23
+ * Draw the "×" that empties the field once there is something in it. Default
24
+ * `true`.
25
+ *
26
+ * It used to appear only when a `clearLabel` was passed, and every kastlan call
27
+ * site passed one — lease-list-page.tsx:91, trial-balance-page.tsx:~196,
28
+ * account-ledger-page.tsx:~131 and data-table/resource-list-page.tsx:256 each
29
+ * spelled `clearLabel={t("table.clear")}` — so the opt-in was boilerplate: a filter
30
+ * you can type into and not untype is the complaint that produced the button in
31
+ * the first place, and the catalogue already names it. `false` is for a field
32
+ * whose clearing lives elsewhere (a "Reset filters" beside it).
33
+ */
34
+ clearable?: boolean;
23
35
  /**
24
36
  * `"field"` (default): the bordered filter box that sits on a page.
25
37
  *
@@ -4,12 +4,14 @@ import { forwardRef, useRef } from "react";
4
4
  import { Search, X } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
6
  import { FIELD_BASE } from "./ui.js";
7
+ import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels.js";
7
8
  const SEARCH_INLINE = "block w-full min-w-0 border-0 bg-transparent py-1 text-[var(--text-primary)] shadow-none placeholder:text-[var(--text-placeholder)] focus:outline-none focus:ring-0";
8
9
  const SearchField = forwardRef(function SearchField2({
9
10
  value,
10
11
  onChange,
11
12
  label,
12
- clearLabel,
13
+ clearLabel: clearLabelProp,
14
+ clearable = true,
13
15
  className,
14
16
  inputClassName,
15
17
  placeholder,
@@ -18,6 +20,8 @@ const SearchField = forwardRef(function SearchField2({
18
20
  ...rest
19
21
  }, ref) {
20
22
  const innerRef = useRef(null);
23
+ const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
24
+ const clearLabel = clearable ? clearLabelProp ?? common.clear : void 0;
21
25
  const name = ariaLabel ?? label;
22
26
  const inline = variant === "inline";
23
27
  return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/search-field.tsx"],"sourcesContent":["import { forwardRef, useRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE } from \"./ui\";\n\ninterface SearchFieldOwnProps\n extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n \"value\" | \"onChange\" | \"type\" | \"aria-label\"\n > {\n value: string;\n /** The typed text. Emitted as a plain string, not an event: every caller of the\n * four this replaced wanted the string, and one of them held it in a URL param\n * rather than in state. */\n onChange: (value: string) => void;\n /**\n * The field's accessible name.\n *\n * @deprecated Pass `aria-label` instead — the DOM spelling this kit standardised on\n * when it found three names for one idea (`ariaLabel`, `aria-label`, and this).\n * Unchanged and still supported; it names the field only when `aria-label` is\n * absent, so a consumer can migrate one call site at a time.\n */\n label?: string;\n /** Names the clear button. Omit it and no clear button is rendered — which is a\n * decision, not a default: a filter you can type into and not untype is the\n * complaint that produced the button on three of these four screens. */\n clearLabel?: string;\n /**\n * `\"field\"` (default): the bordered filter box that sits on a page.\n *\n * `\"inline\"`: no box at all — no border, no fill, no padding of its own — and no\n * type size of its own either, so it takes the size of the header it sits in.\n * For a search that IS the header: a command-palette sheet, a picker's top line\n * (Keksdose's transaction search, which lost its `text-base` headline to a\n * bordered `text-sm` box when it moved onto this component). The container is\n * what frames it, so give the container the focus cue if it needs one\n * (`focus-within:`); the caret is the field's own.\n */\n variant?: \"field\" | \"inline\";\n /** Classes for the `<input>` itself. `className` styles the wrapper — the box the\n * icon and the clear button are positioned against — so it could set a width\n * and nothing else. See {@link Input}'s `inputClassName`. */\n inputClassName?: string;\n}\n\n// The inline look: FIELD_DISPLAY's idea (the chrome gone, the value the thing\n// itself) without even its baseline, because the header it sits in already draws\n// the line. No `text-*` size, on purpose — the input inherits the caller's.\nconst SEARCH_INLINE =\n \"block w-full min-w-0 border-0 bg-transparent py-1 text-[var(--text-primary)] shadow-none placeholder:text-[var(--text-placeholder)] focus:outline-none focus:ring-0\";\n\n/**\n * One of the two spellings is REQUIRED, and the union is how that survives the\n * deprecation. Making `label` merely optional would have been the easy change and the\n * wrong one: the whole reason this component exists is that two of the four boxes it\n * replaced were named by nothing but their placeholder — which disappears the moment\n * someone types, i.e. exactly when a screen-reader user is working in the field. A\n * caller now has a choice of spelling; it still has no option to pass neither.\n */\nexport type SearchFieldProps = SearchFieldOwnProps &\n ({ \"aria-label\": string; label?: string } | { label: string; \"aria-label\"?: string });\n\n/**\n * The page-level filter box: a search icon, a `type=\"search\"` input, and a clear\n * \"×\" that appears once there is something to clear.\n *\n * ## Why it is a component\n *\n * Keksdose had four of these and no two agreed. Two were character-identical\n * twenty-line copies (the settings catalogue filter and the tour list filter) —\n * the same absolute icon at `left-3`, the same `pl-9 pr-9`, the same\n * `[&::-webkit-search-cancel-button]:appearance-none`, the same conditional clear\n * button at `right-2` — differing only in which state setter they called. The\n * third (the admin support queue) was the same idea at `left-2`/`size-3.5`/`pl-7`\n * with no clear button and no `type=\"search\"`, so it had neither the `searchbox`\n * role nor a way to empty itself. The fourth (the bank picker) was a bare `<Input\n * placeholder=\"Search\">`: no icon, no clear, and no accessible name at all once\n * the placeholder went away.\n *\n * None of that is a design decision anyone made; it is four people solving the\n * same problem on four days. A shared component is how the fourth screen gets the\n * clear button the first one earned.\n *\n * ## The two X's\n *\n * `type=\"search\"` is what gives the field its `searchbox` role — and, in Chrome, a\n * NATIVE clear cross that paints right next to ours, so a non-empty box shows two\n * X's an inch apart. Ours is the one that survives: it carries an accessible name\n * and matches the app's other icon buttons, and it returns focus to the input so\n * the next query can be typed straight away.\n */\nexport const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(function SearchField(\n {\n value,\n onChange,\n label,\n clearLabel,\n className,\n inputClassName,\n placeholder,\n variant = \"field\",\n \"aria-label\": ariaLabel,\n ...rest\n },\n ref,\n) {\n const innerRef = useRef<HTMLInputElement>(null);\n // Whichever spelling the caller used. It is the accessible name AND the placeholder\n // fallback, so a box named only by `aria-label` still says what it is before anyone\n // has typed in it — the same deal `label` has always had.\n const name = ariaLabel ?? label;\n const inline = variant === \"inline\";\n return (\n <div className={cn(\"relative\", className)}>\n <Search\n aria-hidden\n className={cn(\n \"pointer-events-none absolute top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\",\n // Logical sides throughout, so a right-to-left page gets the icon at the\n // start of the line and the clear button at its end.\n inline ? \"start-0\" : \"start-3\",\n )}\n />\n <input\n ref={(node) => {\n innerRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n type=\"search\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n aria-label={name}\n placeholder={placeholder ?? name}\n // Keksdose live #333: *\"Entering the search here — is that set as password?\n // … I got prompted for remembering my name or mail address\"*, typed into the\n // settings filter on a phone.\n //\n // A `type=\"search\"` input with no `name` and no `autocomplete` is a field\n // Chrome's autofill classifies by GUESSWORK, from the labels around it and\n // the other fields on the page — and a settings page has an email and a\n // password on it. Naming the field and declaring the intent is what takes it\n // out of that guess: `autocomplete=\"off\"` says it is not part of any profile,\n // and a `name` of \"search\" is the strongest hint the heuristics take.\n //\n // The three that follow are not about autofill but about the same keyboard:\n // a filter box that capitalises the first letter and autocorrects \"2fa\" to\n // \"2FA\" is fighting the person typing into it. Declared BEFORE `{...rest}` so\n // a caller with a reason can still override any of them.\n name=\"search\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n autoCorrect=\"off\"\n spellCheck={false}\n {...rest}\n className={cn(\n inline ? SEARCH_INLINE : FIELD_BASE,\n inline ? \"ps-7\" : \"ps-9\",\n // Room for our own \"×\" only when there is one; a field that can't be\n // cleared has no reason to reserve the space.\n clearLabel === undefined ? (inline ? \"pe-0\" : \"pe-3\") : inline ? \"pe-8\" : \"pe-9\",\n \"[&::-webkit-search-cancel-button]:appearance-none\",\n inputClassName,\n )}\n />\n {clearLabel !== undefined && value !== \"\" && (\n <button\n type=\"button\"\n onClick={() => {\n onChange(\"\");\n // Clearing is the start of the next query far more often than it is the\n // end of this one, so the caret stays where it can be typed into.\n innerRef.current?.focus();\n }}\n aria-label={clearLabel}\n className={cn(\n \"absolute top-1/2 -translate-y-1/2 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\",\n inline ? \"end-0\" : \"end-2\",\n )}\n >\n <X className=\"size-4\" />\n </button>\n )}\n </div>\n );\n});\nSearchField.displayName = \"SearchField\";\n"],"mappings":";AAmHI,SACE,KADF;AAnHJ,SAAS,YAAY,cAAc;AAEnC,SAAS,QAAQ,SAAS;AAC1B,SAAS,UAAU;AACnB,SAAS,kBAAkB;AA8C3B,MAAM,gBACJ;AA0CK,MAAM,cAAc,WAA+C,SAASA,aACjF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,cAAc;AAAA,EACd,GAAG;AACL,GACA,KACA;AACA,QAAM,WAAW,OAAyB,IAAI;AAI9C,QAAM,OAAO,aAAa;AAC1B,QAAM,SAAS,YAAY;AAC3B,SACE,qBAAC,SAAI,WAAW,GAAG,YAAY,SAAS,GACtC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,WAAW;AAAA,UACT;AAAA;AAAA;AAAA,UAGA,SAAS,YAAY;AAAA,QACvB;AAAA;AAAA,IACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,CAAC,SAAS;AACb,mBAAS,UAAU;AACnB,cAAI,OAAO,QAAQ,WAAY,KAAI,IAAI;AAAA,mBAC9B,IAAK,KAAI,UAAU;AAAA,QAC9B;AAAA,QACA,MAAK;AAAA,QACL;AAAA,QACA,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,QACxC,cAAY;AAAA,QACZ,aAAa,eAAe;AAAA,QAgB5B,MAAK;AAAA,QACL,cAAa;AAAA,QACb,gBAAe;AAAA,QACf,aAAY;AAAA,QACZ,YAAY;AAAA,QACX,GAAG;AAAA,QACJ,WAAW;AAAA,UACT,SAAS,gBAAgB;AAAA,UACzB,SAAS,SAAS;AAAA;AAAA;AAAA,UAGlB,eAAe,SAAa,SAAS,SAAS,SAAU,SAAS,SAAS;AAAA,UAC1E;AAAA,UACA;AAAA,QACF;AAAA;AAAA,IACF;AAAA,IACC,eAAe,UAAa,UAAU,MACrC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM;AACb,mBAAS,EAAE;AAGX,mBAAS,SAAS,MAAM;AAAA,QAC1B;AAAA,QACA,cAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA,SAAS,UAAU;AAAA,QACrB;AAAA,QAEA,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,IACxB;AAAA,KAEJ;AAEJ,CAAC;AACD,YAAY,cAAc;","names":["SearchField"]}
1
+ {"version":3,"sources":["../../src/components/search-field.tsx"],"sourcesContent":["import { forwardRef, useRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE } from \"./ui\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\ninterface SearchFieldOwnProps\n extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n \"value\" | \"onChange\" | \"type\" | \"aria-label\"\n > {\n value: string;\n /** The typed text. Emitted as a plain string, not an event: every caller of the\n * four this replaced wanted the string, and one of them held it in a URL param\n * rather than in state. */\n onChange: (value: string) => void;\n /**\n * The field's accessible name.\n *\n * @deprecated Pass `aria-label` instead — the DOM spelling this kit standardised on\n * when it found three names for one idea (`ariaLabel`, `aria-label`, and this).\n * Unchanged and still supported; it names the field only when `aria-label` is\n * absent, so a consumer can migrate one call site at a time.\n */\n label?: string;\n /** Names the clear button. Default: the catalogue's `common.clear`, in the\n * provider's language. */\n clearLabel?: string;\n /**\n * Draw the \"×\" that empties the field once there is something in it. Default\n * `true`.\n *\n * It used to appear only when a `clearLabel` was passed, and every kastlan call\n * site passed one — lease-list-page.tsx:91, trial-balance-page.tsx:~196,\n * account-ledger-page.tsx:~131 and data-table/resource-list-page.tsx:256 each\n * spelled `clearLabel={t(\"table.clear\")}` — so the opt-in was boilerplate: a filter\n * you can type into and not untype is the complaint that produced the button in\n * the first place, and the catalogue already names it. `false` is for a field\n * whose clearing lives elsewhere (a \"Reset filters\" beside it).\n */\n clearable?: boolean;\n /**\n * `\"field\"` (default): the bordered filter box that sits on a page.\n *\n * `\"inline\"`: no box at all — no border, no fill, no padding of its own — and no\n * type size of its own either, so it takes the size of the header it sits in.\n * For a search that IS the header: a command-palette sheet, a picker's top line\n * (Keksdose's transaction search, which lost its `text-base` headline to a\n * bordered `text-sm` box when it moved onto this component). The container is\n * what frames it, so give the container the focus cue if it needs one\n * (`focus-within:`); the caret is the field's own.\n */\n variant?: \"field\" | \"inline\";\n /** Classes for the `<input>` itself. `className` styles the wrapper — the box the\n * icon and the clear button are positioned against — so it could set a width\n * and nothing else. See {@link Input}'s `inputClassName`. */\n inputClassName?: string;\n}\n\n// The inline look: FIELD_DISPLAY's idea (the chrome gone, the value the thing\n// itself) without even its baseline, because the header it sits in already draws\n// the line. No `text-*` size, on purpose — the input inherits the caller's.\nconst SEARCH_INLINE =\n \"block w-full min-w-0 border-0 bg-transparent py-1 text-[var(--text-primary)] shadow-none placeholder:text-[var(--text-placeholder)] focus:outline-none focus:ring-0\";\n\n/**\n * One of the two spellings is REQUIRED, and the union is how that survives the\n * deprecation. Making `label` merely optional would have been the easy change and the\n * wrong one: the whole reason this component exists is that two of the four boxes it\n * replaced were named by nothing but their placeholder — which disappears the moment\n * someone types, i.e. exactly when a screen-reader user is working in the field. A\n * caller now has a choice of spelling; it still has no option to pass neither.\n */\nexport type SearchFieldProps = SearchFieldOwnProps &\n ({ \"aria-label\": string; label?: string } | { label: string; \"aria-label\"?: string });\n\n/**\n * The page-level filter box: a search icon, a `type=\"search\"` input, and a clear\n * \"×\" that appears once there is something to clear.\n *\n * ## Why it is a component\n *\n * Keksdose had four of these and no two agreed. Two were character-identical\n * twenty-line copies (the settings catalogue filter and the tour list filter) —\n * the same absolute icon at `left-3`, the same `pl-9 pr-9`, the same\n * `[&::-webkit-search-cancel-button]:appearance-none`, the same conditional clear\n * button at `right-2` — differing only in which state setter they called. The\n * third (the admin support queue) was the same idea at `left-2`/`size-3.5`/`pl-7`\n * with no clear button and no `type=\"search\"`, so it had neither the `searchbox`\n * role nor a way to empty itself. The fourth (the bank picker) was a bare `<Input\n * placeholder=\"Search\">`: no icon, no clear, and no accessible name at all once\n * the placeholder went away.\n *\n * None of that is a design decision anyone made; it is four people solving the\n * same problem on four days. A shared component is how the fourth screen gets the\n * clear button the first one earned.\n *\n * ## The two X's\n *\n * `type=\"search\"` is what gives the field its `searchbox` role — and, in Chrome, a\n * NATIVE clear cross that paints right next to ours, so a non-empty box shows two\n * X's an inch apart. Ours is the one that survives: it carries an accessible name\n * and matches the app's other icon buttons, and it returns focus to the input so\n * the next query can be typed straight away.\n */\nexport const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(function SearchField(\n {\n value,\n onChange,\n label,\n clearLabel: clearLabelProp,\n clearable = true,\n className,\n inputClassName,\n placeholder,\n variant = \"field\",\n \"aria-label\": ariaLabel,\n ...rest\n },\n ref,\n) {\n const innerRef = useRef<HTMLInputElement>(null);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const clearLabel = clearable ? (clearLabelProp ?? common.clear) : undefined;\n // Whichever spelling the caller used. It is the accessible name AND the placeholder\n // fallback, so a box named only by `aria-label` still says what it is before anyone\n // has typed in it — the same deal `label` has always had.\n const name = ariaLabel ?? label;\n const inline = variant === \"inline\";\n return (\n <div className={cn(\"relative\", className)}>\n <Search\n aria-hidden\n className={cn(\n \"pointer-events-none absolute top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\",\n // Logical sides throughout, so a right-to-left page gets the icon at the\n // start of the line and the clear button at its end.\n inline ? \"start-0\" : \"start-3\",\n )}\n />\n <input\n ref={(node) => {\n innerRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n type=\"search\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n aria-label={name}\n placeholder={placeholder ?? name}\n // Keksdose live #333: *\"Entering the search here — is that set as password?\n // … I got prompted for remembering my name or mail address\"*, typed into the\n // settings filter on a phone.\n //\n // A `type=\"search\"` input with no `name` and no `autocomplete` is a field\n // Chrome's autofill classifies by GUESSWORK, from the labels around it and\n // the other fields on the page — and a settings page has an email and a\n // password on it. Naming the field and declaring the intent is what takes it\n // out of that guess: `autocomplete=\"off\"` says it is not part of any profile,\n // and a `name` of \"search\" is the strongest hint the heuristics take.\n //\n // The three that follow are not about autofill but about the same keyboard:\n // a filter box that capitalises the first letter and autocorrects \"2fa\" to\n // \"2FA\" is fighting the person typing into it. Declared BEFORE `{...rest}` so\n // a caller with a reason can still override any of them.\n name=\"search\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n autoCorrect=\"off\"\n spellCheck={false}\n {...rest}\n className={cn(\n inline ? SEARCH_INLINE : FIELD_BASE,\n inline ? \"ps-7\" : \"ps-9\",\n // Room for our own \"×\" only when there is one; a field that can't be\n // cleared has no reason to reserve the space.\n clearLabel === undefined ? (inline ? \"pe-0\" : \"pe-3\") : inline ? \"pe-8\" : \"pe-9\",\n \"[&::-webkit-search-cancel-button]:appearance-none\",\n inputClassName,\n )}\n />\n {clearLabel !== undefined && value !== \"\" && (\n <button\n type=\"button\"\n onClick={() => {\n onChange(\"\");\n // Clearing is the start of the next query far more often than it is the\n // end of this one, so the caret stays where it can be typed into.\n innerRef.current?.focus();\n }}\n aria-label={clearLabel}\n className={cn(\n \"absolute top-1/2 -translate-y-1/2 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\",\n inline ? \"end-0\" : \"end-2\",\n )}\n >\n <X className=\"size-4\" />\n </button>\n )}\n </div>\n );\n});\nSearchField.displayName = \"SearchField\";\n"],"mappings":";AAmII,SACE,KADF;AAnIJ,SAAS,YAAY,cAAc;AAEnC,SAAS,QAAQ,SAAS;AAC1B,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB,oBAAoB;AA0DpD,MAAM,gBACJ;AA0CK,MAAM,cAAc,WAA+C,SAASA,aACjF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,cAAc;AAAA,EACd,GAAG;AACL,GACA,KACA;AACA,QAAM,WAAW,OAAyB,IAAI;AAC9C,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,aAAa,YAAa,kBAAkB,OAAO,QAAS;AAIlE,QAAM,OAAO,aAAa;AAC1B,QAAM,SAAS,YAAY;AAC3B,SACE,qBAAC,SAAI,WAAW,GAAG,YAAY,SAAS,GACtC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,WAAW;AAAA,UACT;AAAA;AAAA;AAAA,UAGA,SAAS,YAAY;AAAA,QACvB;AAAA;AAAA,IACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,CAAC,SAAS;AACb,mBAAS,UAAU;AACnB,cAAI,OAAO,QAAQ,WAAY,KAAI,IAAI;AAAA,mBAC9B,IAAK,KAAI,UAAU;AAAA,QAC9B;AAAA,QACA,MAAK;AAAA,QACL;AAAA,QACA,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,QACxC,cAAY;AAAA,QACZ,aAAa,eAAe;AAAA,QAgB5B,MAAK;AAAA,QACL,cAAa;AAAA,QACb,gBAAe;AAAA,QACf,aAAY;AAAA,QACZ,YAAY;AAAA,QACX,GAAG;AAAA,QACJ,WAAW;AAAA,UACT,SAAS,gBAAgB;AAAA,UACzB,SAAS,SAAS;AAAA;AAAA;AAAA,UAGlB,eAAe,SAAa,SAAS,SAAS,SAAU,SAAS,SAAS;AAAA,UAC1E;AAAA,UACA;AAAA,QACF;AAAA;AAAA,IACF;AAAA,IACC,eAAe,UAAa,UAAU,MACrC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM;AACb,mBAAS,EAAE;AAGX,mBAAS,SAAS,MAAM;AAAA,QAC1B;AAAA,QACA,cAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA,SAAS,UAAU;AAAA,QACrB;AAAA,QAEA,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,IACxB;AAAA,KAEJ;AAEJ,CAAC;AACD,YAAY,cAAc;","names":["SearchField"]}
@@ -75,6 +75,18 @@ interface SeriesChartSeries {
75
75
  activeDot?: boolean | {
76
76
  r?: number;
77
77
  };
78
+ /**
79
+ * The shortest a bar of this series is drawn, in px — recharts' `minPointSize`. A
80
+ * zero or a value too small to see keeps a visible stub, so an empty slot still reads
81
+ * as a slot with a value rather than a gap: keksdose's weekday pattern, where a
82
+ * Sunday with no spending drew a 2% stub by hand. Only the DRAWING changes: the axis
83
+ * is fitted to the values, and the tooltip, the keyboard stops and every accessible
84
+ * name still say 0. A hole (no value in the row) stays a hole. Overrides the chart's
85
+ * {@link SeriesChartProps.minBarLength}; `0` turns it off for this series. Meant for
86
+ * unstacked bars — in a `stack` the stub is drawn over the layer above it. Lines and
87
+ * areas ignore it.
88
+ */
89
+ minBarLength?: number;
78
90
  }
79
91
  /** What a series draws. See {@link SeriesChartSeries.type}. */
80
92
  type SeriesChartType = "line" | "bar" | "area";
@@ -422,6 +434,10 @@ interface SeriesChartProps {
422
434
  locale?: string;
423
435
  /** Onto the chart's root (`ChartContainer`) — e.g. an app's own grid ink. */
424
436
  className?: string;
437
+ /** The shortest bar drawn, in px, for every bar series that does not set its own
438
+ * {@link SeriesChartSeries.minBarLength}. Drawing only: domain, tooltip and names
439
+ * keep the real value. Default: none — a zero draws nothing. */
440
+ minBarLength?: number;
425
441
  }
426
442
  /** How much of an axis band the rotated title takes, in px. Reserved rather than
427
443
  * shared with the ticks: a title merely offset inside the band prints on top of a