@eifi1/ui-kit 0.11.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 (342) hide show
  1. package/README.md +57 -32
  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/amount-input.d.ts +24 -4
  12. package/dist/components/amount-input.js +5 -2
  13. package/dist/components/amount-input.js.map +1 -1
  14. package/dist/components/authed-image.d.ts +68 -0
  15. package/dist/components/authed-image.js +117 -0
  16. package/dist/components/authed-image.js.map +1 -0
  17. package/dist/components/breadcrumbs.d.ts +2 -1
  18. package/dist/components/breadcrumbs.js +5 -2
  19. package/dist/components/breadcrumbs.js.map +1 -1
  20. package/dist/components/button-group.d.ts +51 -2
  21. package/dist/components/button-group.js +5 -1
  22. package/dist/components/button-group.js.map +1 -1
  23. package/dist/components/calculator.d.ts +15 -4
  24. package/dist/components/calendar-heatmap.d.ts +41 -3
  25. package/dist/components/calendar-heatmap.js +48 -7
  26. package/dist/components/calendar-heatmap.js.map +1 -1
  27. package/dist/components/chip.d.ts +2 -1
  28. package/dist/components/chip.js +6 -3
  29. package/dist/components/chip.js.map +1 -1
  30. package/dist/components/choice-card.d.ts +35 -3
  31. package/dist/components/choice-card.js +83 -25
  32. package/dist/components/choice-card.js.map +1 -1
  33. package/dist/components/combobox-core.js +2 -0
  34. package/dist/components/combobox-core.js.map +1 -1
  35. package/dist/components/combobox.d.ts +2 -2
  36. package/dist/components/combobox.js +31 -9
  37. package/dist/components/combobox.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +51 -70
  39. package/dist/components/copy-button.js +1 -1
  40. package/dist/components/copy-button.js.map +1 -1
  41. package/dist/components/data-table-cells.d.ts +63 -0
  42. package/dist/components/data-table-cells.js +64 -0
  43. package/dist/components/data-table-cells.js.map +1 -0
  44. package/dist/components/data-table-filter-popover.d.ts +1 -1
  45. package/dist/components/data-table-filters.d.ts +1 -1
  46. package/dist/components/data-table-filters.js +31 -1
  47. package/dist/components/data-table-filters.js.map +1 -1
  48. package/dist/components/data-table-labels.d.ts +13 -0
  49. package/dist/components/data-table-labels.js +8 -1
  50. package/dist/components/data-table-labels.js.map +1 -1
  51. package/dist/components/data-table.d.ts +1 -1
  52. package/dist/components/data-table.js +102 -12
  53. package/dist/components/data-table.js.map +1 -1
  54. package/dist/components/date-picker.d.ts +3 -1
  55. package/dist/components/date-picker.js +15 -2
  56. package/dist/components/date-picker.js.map +1 -1
  57. package/dist/components/description-list.d.ts +23 -3
  58. package/dist/components/description-list.js +23 -3
  59. package/dist/components/description-list.js.map +1 -1
  60. package/dist/components/dialog-frame.d.ts +1 -0
  61. package/dist/components/entity-combobox.d.ts +1 -1
  62. package/dist/components/entity-combobox.js +18 -3
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/error-boundary.d.ts +92 -0
  65. package/dist/components/error-boundary.js +111 -0
  66. package/dist/components/error-boundary.js.map +1 -0
  67. package/dist/components/field.d.ts +30 -4
  68. package/dist/components/field.js +40 -17
  69. package/dist/components/field.js.map +1 -1
  70. package/dist/components/file-button.d.ts +50 -208
  71. package/dist/components/file-dropzone.d.ts +49 -2
  72. package/dist/components/floating-panel.d.ts +13 -4
  73. package/dist/components/floating-panel.js +7 -3
  74. package/dist/components/floating-panel.js.map +1 -1
  75. package/dist/components/form-actions.d.ts +53 -0
  76. package/dist/components/form-actions.js +98 -0
  77. package/dist/components/form-actions.js.map +1 -0
  78. package/dist/components/image-grid.d.ts +67 -0
  79. package/dist/components/image-grid.js +112 -0
  80. package/dist/components/image-grid.js.map +1 -0
  81. package/dist/components/lightbox.d.ts +104 -0
  82. package/dist/components/lightbox.js +229 -0
  83. package/dist/components/lightbox.js.map +1 -0
  84. package/dist/components/line-items.d.ts +114 -0
  85. package/dist/components/line-items.js +238 -0
  86. package/dist/components/line-items.js.map +1 -0
  87. package/dist/components/list.d.ts +23 -2
  88. package/dist/components/list.js +32 -13
  89. package/dist/components/list.js.map +1 -1
  90. package/dist/components/loading-state.d.ts +24 -0
  91. package/dist/components/loading-state.js +32 -0
  92. package/dist/components/loading-state.js.map +1 -0
  93. package/dist/components/menu-item.d.ts +2 -1
  94. package/dist/components/menu-item.js +5 -1
  95. package/dist/components/menu-item.js.map +1 -1
  96. package/dist/components/mini-calendar.d.ts +13 -1
  97. package/dist/components/mini-calendar.js +46 -2
  98. package/dist/components/mini-calendar.js.map +1 -1
  99. package/dist/components/modal.d.ts +20 -1
  100. package/dist/components/modal.js +26 -2
  101. package/dist/components/modal.js.map +1 -1
  102. package/dist/components/month-picker.d.ts +21 -1
  103. package/dist/components/month-picker.js +139 -26
  104. package/dist/components/month-picker.js.map +1 -1
  105. package/dist/components/multi-entity-combobox.d.ts +1 -1
  106. package/dist/components/multi-entity-combobox.js +19 -3
  107. package/dist/components/multi-entity-combobox.js.map +1 -1
  108. package/dist/components/nav-pills.d.ts +2 -1
  109. package/dist/components/nav-pills.js +5 -1
  110. package/dist/components/nav-pills.js.map +1 -1
  111. package/dist/components/number-field.d.ts +24 -5
  112. package/dist/components/number-field.js +5 -7
  113. package/dist/components/number-field.js.map +1 -1
  114. package/dist/components/number-input.d.ts +25 -5
  115. package/dist/components/number-input.js +7 -1
  116. package/dist/components/number-input.js.map +1 -1
  117. package/dist/components/numpad-sheet.d.ts +15 -4
  118. package/dist/components/passkeys-setting.d.ts +67 -0
  119. package/dist/components/passkeys-setting.js +214 -0
  120. package/dist/components/passkeys-setting.js.map +1 -0
  121. package/dist/components/picker-sheet.js +2 -1
  122. package/dist/components/picker-sheet.js.map +1 -1
  123. package/dist/components/pie-chart-labels.d.ts +20 -0
  124. package/dist/components/pie-chart-labels.js +12 -0
  125. package/dist/components/pie-chart-labels.js.map +1 -0
  126. package/dist/components/pie-chart.d.ts +101 -0
  127. package/dist/components/pie-chart.js +283 -0
  128. package/dist/components/pie-chart.js.map +1 -0
  129. package/dist/components/progress-bar.d.ts +44 -3
  130. package/dist/components/progress-bar.js +58 -22
  131. package/dist/components/progress-bar.js.map +1 -1
  132. package/dist/components/qr-code.d.ts +34 -0
  133. package/dist/components/qr-code.js +57 -0
  134. package/dist/components/qr-code.js.map +1 -0
  135. package/dist/components/search-field.d.ts +15 -3
  136. package/dist/components/search-field.js +5 -1
  137. package/dist/components/search-field.js.map +1 -1
  138. package/dist/components/settings-fields.d.ts +49 -1
  139. package/dist/components/signed-amount.d.ts +121 -0
  140. package/dist/components/signed-amount.js +152 -0
  141. package/dist/components/signed-amount.js.map +1 -0
  142. package/dist/components/skeleton.d.ts +10 -3
  143. package/dist/components/skeleton.js +12 -2
  144. package/dist/components/skeleton.js.map +1 -1
  145. package/dist/components/stat-tile.d.ts +2 -1
  146. package/dist/components/stat-tile.js +5 -2
  147. package/dist/components/stat-tile.js.map +1 -1
  148. package/dist/components/table.d.ts +39 -5
  149. package/dist/components/table.js +45 -11
  150. package/dist/components/table.js.map +1 -1
  151. package/dist/components/text-link.d.ts +176 -0
  152. package/dist/components/text-link.js +109 -0
  153. package/dist/components/text-link.js.map +1 -0
  154. package/dist/components/time-input.d.ts +49 -1
  155. package/dist/components/toggle-group.d.ts +13 -0
  156. package/dist/components/toggle-group.js +35 -6
  157. package/dist/components/toggle-group.js.map +1 -1
  158. package/dist/components/toggle-legend.d.ts +36 -3
  159. package/dist/components/toggle-legend.js +49 -0
  160. package/dist/components/toggle-legend.js.map +1 -1
  161. package/dist/components/ui.d.ts +51 -779
  162. package/dist/components/ui.js +215 -23
  163. package/dist/components/ui.js.map +1 -1
  164. package/dist/components/use-table-state.d.ts +3 -52
  165. package/dist/components/use-table-state.js +128 -30
  166. package/dist/components/use-table-state.js.map +1 -1
  167. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  168. package/dist/data-table.d.ts +2 -1
  169. package/dist/data-table.js +5 -1
  170. package/dist/data-table.js.map +1 -1
  171. package/dist/feedback/feedback-dialog.d.ts +26 -3
  172. package/dist/feedback/feedback-dialog.js +16 -2
  173. package/dist/feedback/feedback-dialog.js.map +1 -1
  174. package/dist/feedback/feedback-thread.d.ts +133 -0
  175. package/dist/feedback/feedback-thread.js +237 -0
  176. package/dist/feedback/feedback-thread.js.map +1 -0
  177. package/dist/feedback.d.ts +2 -1
  178. package/dist/feedback.js +1 -0
  179. package/dist/feedback.js.map +1 -1
  180. package/dist/hooks/use-authed-src.d.ts +71 -0
  181. package/dist/hooks/use-authed-src.js +63 -0
  182. package/dist/hooks/use-authed-src.js.map +1 -0
  183. package/dist/hooks/use-file-drop.d.ts +49 -2
  184. package/dist/hooks/use-hotkey.d.ts +59 -0
  185. package/dist/hooks/use-hotkey.js +105 -0
  186. package/dist/hooks/use-hotkey.js.map +1 -0
  187. package/dist/hooks/use-search-param-state.d.ts +83 -0
  188. package/dist/hooks/use-search-param-state.js +94 -0
  189. package/dist/hooks/use-search-param-state.js.map +1 -0
  190. package/dist/i18n/defaults.d.ts +15 -4
  191. package/dist/i18n/defaults.js +31 -1
  192. package/dist/i18n/defaults.js.map +1 -1
  193. package/dist/i18n/kit-labels.d.ts +46 -312
  194. package/dist/i18n/kit-labels.js +20 -5
  195. package/dist/i18n/kit-labels.js.map +1 -1
  196. package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
  197. package/dist/i18n/locales/de-CH.d.ts +15 -4
  198. package/dist/i18n/locales/de-informal.d.ts +15 -4
  199. package/dist/i18n/locales/de.d.ts +15 -4
  200. package/dist/i18n/locales/de.js +156 -3
  201. package/dist/i18n/locales/de.js.map +1 -1
  202. package/dist/i18n/locales/es.d.ts +15 -4
  203. package/dist/i18n/locales/es.js +153 -3
  204. package/dist/i18n/locales/es.js.map +1 -1
  205. package/dist/i18n/locales/fr.d.ts +15 -4
  206. package/dist/i18n/locales/fr.js +153 -3
  207. package/dist/i18n/locales/fr.js.map +1 -1
  208. package/dist/i18n/locales/hu.d.ts +15 -4
  209. package/dist/i18n/locales/hu.js +153 -3
  210. package/dist/i18n/locales/hu.js.map +1 -1
  211. package/dist/i18n/locales/it.d.ts +15 -4
  212. package/dist/i18n/locales/it.js +153 -3
  213. package/dist/i18n/locales/it.js.map +1 -1
  214. package/dist/i18n/locales/zh.d.ts +15 -4
  215. package/dist/i18n/locales/zh.js +153 -3
  216. package/dist/i18n/locales/zh.js.map +1 -1
  217. package/dist/index.d.ts +37 -17
  218. package/dist/index.js +40 -1
  219. package/dist/index.js.map +1 -1
  220. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  221. package/dist/lib/format.d.ts +144 -0
  222. package/dist/lib/format.js +149 -0
  223. package/dist/lib/format.js.map +1 -0
  224. package/dist/lib/qr-encode.d.ts +44 -0
  225. package/dist/lib/qr-encode.js +338 -0
  226. package/dist/lib/qr-encode.js.map +1 -0
  227. package/dist/rhf/fields.d.ts +241 -0
  228. package/dist/rhf/fields.js +601 -0
  229. package/dist/rhf/fields.js.map +1 -0
  230. package/dist/rhf/form.d.ts +49 -1
  231. package/dist/rhf/line-items.d.ts +31 -0
  232. package/dist/rhf/line-items.js +59 -0
  233. package/dist/rhf/line-items.js.map +1 -0
  234. package/dist/rhf.d.ts +61 -1
  235. package/dist/rhf.js +2 -0
  236. package/dist/rhf.js.map +1 -1
  237. package/dist/shell/app-shell.d.ts +99 -2
  238. package/dist/shell/app-shell.js +58 -3
  239. package/dist/shell/app-shell.js.map +1 -1
  240. package/dist/shell/auth-layout.d.ts +74 -0
  241. package/dist/shell/auth-layout.js +97 -0
  242. package/dist/shell/auth-layout.js.map +1 -0
  243. package/dist/shell/top-bar-brand.d.ts +81 -0
  244. package/dist/shell/top-bar-brand.js +47 -0
  245. package/dist/shell/top-bar-brand.js.map +1 -0
  246. package/dist/shell/topbar-action-menu.d.ts +12 -1
  247. package/dist/shell/topbar-action-menu.js +16 -1
  248. package/dist/shell/topbar-action-menu.js.map +1 -1
  249. package/dist/shell.d.ts +56 -1
  250. package/dist/shell.js +2 -0
  251. package/dist/shell.js.map +1 -1
  252. package/dist/wizard/stepper-nav.d.ts +48 -1
  253. package/dist/wizard/wizard-context.d.ts +24 -1
  254. package/dist/wizard/wizard-context.js +19 -2
  255. package/dist/wizard/wizard-context.js.map +1 -1
  256. package/dist/wizard/wizard-step.d.ts +20 -5
  257. package/dist/wizard/wizard-step.js +16 -3
  258. package/dist/wizard/wizard-step.js.map +1 -1
  259. package/dist/wizard.d.ts +50 -3
  260. package/package.json +1 -1
  261. package/src/chart.ts +4 -0
  262. package/src/components/account-settings-labels.ts +190 -0
  263. package/src/components/account-settings.tsx +110 -63
  264. package/src/components/amount-input.tsx +13 -1
  265. package/src/components/authed-image.tsx +190 -0
  266. package/src/components/breadcrumbs.tsx +8 -2
  267. package/src/components/button-group.tsx +7 -2
  268. package/src/components/calendar-heatmap.tsx +105 -10
  269. package/src/components/chip.tsx +8 -4
  270. package/src/components/choice-card.tsx +136 -25
  271. package/src/components/combobox-core.tsx +5 -0
  272. package/src/components/combobox.tsx +31 -9
  273. package/src/components/copy-button.tsx +3 -2
  274. package/src/components/data-table-cells.tsx +119 -0
  275. package/src/components/data-table-filters.ts +77 -0
  276. package/src/components/data-table-labels.ts +23 -0
  277. package/src/components/data-table.tsx +224 -13
  278. package/src/components/date-picker.tsx +19 -3
  279. package/src/components/description-list.tsx +52 -3
  280. package/src/components/entity-combobox.tsx +18 -3
  281. package/src/components/error-boundary.tsx +216 -0
  282. package/src/components/field.tsx +56 -5
  283. package/src/components/floating-panel.tsx +20 -7
  284. package/src/components/form-actions.tsx +191 -0
  285. package/src/components/image-grid.tsx +181 -0
  286. package/src/components/lightbox.tsx +374 -0
  287. package/src/components/line-items.tsx +385 -0
  288. package/src/components/list.tsx +65 -16
  289. package/src/components/loading-state.tsx +47 -0
  290. package/src/components/menu-item.tsx +7 -2
  291. package/src/components/mini-calendar.tsx +72 -3
  292. package/src/components/modal.tsx +61 -3
  293. package/src/components/month-picker.tsx +181 -26
  294. package/src/components/multi-entity-combobox.tsx +19 -3
  295. package/src/components/nav-pills.tsx +8 -1
  296. package/src/components/number-field.tsx +15 -10
  297. package/src/components/number-input.tsx +16 -1
  298. package/src/components/passkeys-setting.tsx +331 -0
  299. package/src/components/picker-sheet.tsx +7 -1
  300. package/src/components/pie-chart-labels.ts +32 -0
  301. package/src/components/pie-chart.tsx +479 -0
  302. package/src/components/progress-bar.tsx +129 -19
  303. package/src/components/qr-code.tsx +83 -0
  304. package/src/components/search-field.tsx +20 -4
  305. package/src/components/signed-amount.tsx +286 -0
  306. package/src/components/skeleton.tsx +21 -3
  307. package/src/components/stat-tile.tsx +8 -2
  308. package/src/components/table.tsx +102 -13
  309. package/src/components/text-link.tsx +248 -0
  310. package/src/components/toggle-group.tsx +58 -10
  311. package/src/components/toggle-legend.tsx +87 -2
  312. package/src/components/ui.tsx +426 -36
  313. package/src/components/use-table-state.ts +265 -33
  314. package/src/data-table.ts +10 -0
  315. package/src/feedback/feedback-dialog.tsx +42 -3
  316. package/src/feedback/feedback-thread.tsx +434 -0
  317. package/src/feedback.ts +1 -0
  318. package/src/hooks/use-authed-src.ts +161 -0
  319. package/src/hooks/use-hotkey.ts +172 -0
  320. package/src/hooks/use-search-param-state.ts +187 -0
  321. package/src/i18n/defaults.ts +30 -0
  322. package/src/i18n/kit-labels.tsx +102 -8
  323. package/src/i18n/locales/de.ts +153 -0
  324. package/src/i18n/locales/es.ts +150 -0
  325. package/src/i18n/locales/fr.ts +153 -0
  326. package/src/i18n/locales/hu.ts +151 -0
  327. package/src/i18n/locales/it.ts +151 -0
  328. package/src/i18n/locales/zh.ts +150 -0
  329. package/src/index.ts +59 -0
  330. package/src/lib/format.ts +325 -0
  331. package/src/lib/qr-encode.ts +420 -0
  332. package/src/rhf/fields.tsx +1054 -0
  333. package/src/rhf/line-items.tsx +130 -0
  334. package/src/rhf.ts +6 -0
  335. package/src/shell/app-shell.tsx +147 -2
  336. package/src/shell/auth-layout.tsx +190 -0
  337. package/src/shell/top-bar-brand.tsx +76 -0
  338. package/src/shell/topbar-action-menu.tsx +21 -0
  339. package/src/shell.ts +2 -0
  340. package/src/wizard/wizard-context.tsx +41 -1
  341. package/src/wizard/wizard-step.tsx +43 -6
  342. package/tokens.css +12 -0
@@ -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\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\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 * 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\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 segments,\n legend = false,\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 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 const fills = stacked ? segmentFills(segments) : [];\n const partTotal = stacked ? min + segments.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 ? segments.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 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={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 <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 </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 {format(min + partValue(seg.value))}\n </span>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":";AA+MQ,SAEI,KAFJ;AA/MR,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;AAIvG,MAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMA,SAAS,aAAa,UAA2D;AAC/E,MAAI,OAAO;AACX,SAAO,SAAS,IAAI,CAAC,QAAQ;AAC3B,QAAI,IAAI,UAAW,QAAO,IAAI;AAC9B,QAAI,IAAI,KAAM,QAAO,KAAK,IAAI,IAAI;AAClC,WAAO,IAAI,QAAQ,SAAY,YAAY,SAAS,YAAY,MAAM;AAAA,EACxE,CAAC;AACH;AA+EO,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,SAAS;AAAA,EACT;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,UAAU,aAAa;AAC7B,QAAM,QAAQ,WAAW,YAAY;AACrC,QAAM,OAAO,MAAM;AACnB,QAAM,YAAY,CAAC,MAAe,OAAO,SAAS,CAAC,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,IAAI;AAC9E,QAAM,QAAQ,UAAU,aAAa,QAAQ,IAAI,CAAC;AAClD,QAAM,YAAY,UAAU,MAAM,SAAS,OAAO,CAAC,KAAK,QAAQ,MAAM,UAAU,IAAI,KAAK,GAAG,CAAC,IAAI;AACjG,QAAM,SAAS,CAAC,MACd,cACI,YAAY,GAAG,KAAK,GAAG,IACvB,IAAI,KAAK,aAAa,WAAW,EAAE,OAAO,WAAW,uBAAuB,EAAE,CAAC,EAAE;AAAA,IAC/E,QAAQ,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,IAAI,OAAO;AAAA,EAC5D;AACN,QAAM,YAAY,UACd,SAAS,IAAI,CAAC,QAAQ;AACpB,UAAM,OAAO,OAAO,MAAM,UAAU,IAAI,KAAK,CAAC;AAC9C,WAAO,OAAO,IAAI,UAAU,YAAY,OAAO,IAAI,UAAU,WACzD,OAAO,WAAW,OAAO,IAAI,KAAK,GAAG,IAAI,IACzC;AAAA,EACN,CAAC,IACD,CAAC;AACL,QAAM,UACJ,cAAc,UAAU,UAAa,CAAC,OAAO,SAAS,KAAK,IAAK,QAAQ,MAAM,SAAa;AAC7F,QAAM,gBAAgB,YAAY;AAClC,QAAM,UAAU,gBAAgB,SAAY,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,OAAO,CAAC;AAChF,QAAM,WAAW,YAAY,UAAa,QAAQ,IAAI,KAAK,UAAU,OAAO;AAE5E,QAAM,YAAY,YAAY,SAAY,SAAY,OAAO,OAAO;AACpE,QAAM,gBACJ,WAAW,UAAU,SAAS,IAC1B,IAAI,KAAK,WAAW,WAAW,EAAE,OAAO,SAAS,MAAM,OAAO,CAAC,EAAE,OAAO,SAAS,IACjF;AAEN,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;AAAA;AAAA;AAAA,UAGC,oBAAC,SAAI,WAAU,8BACZ,mBAAS,IAAI,CAAC,KAAK,MAClB;AAAA,YAAC;AAAA;AAAA,cAEC,aAAU;AAAA,cACV,WAAW;AAAA,gBACT;AAAA,gBACA,MAAM,CAAC;AAAA,cACT;AAAA,cACA,OAAO;AAAA,gBACL,OAAO,GAAG,QAAQ,IAAI,IAAK,UAAU,IAAI,KAAK,IAAI,OAAQ,GAAG;AAAA,gBAC7D,iBAAiB,IAAI,YAAY,SAAY,IAAI;AAAA,cACnD;AAAA;AAAA,YATK,IAAI,OAAO;AAAA,UAUlB,CACD,GACH;AAAA,YACE,gBACF;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,IACC,WAAW,UAAU,SAAS,SAAS,KACtC,oBAAC,QAAG,WAAU,0BAAyB,aAAU,UAC9C,mBAAS,IAAI,CAAC,KAAK,MAClB,qBAAC,QAAsB,WAAU,2BAC/B;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,eAAW;AAAA,UACX,WAAW;AAAA,YACT;AAAA,YACA,MAAM,CAAC;AAAA,UACT;AAAA,UACA,OAAO,EAAE,iBAAiB,IAAI,YAAY,SAAY,IAAI,MAAM;AAAA;AAAA,MAClE;AAAA,MACC,IAAI,SAAS,QACZ,oBAAC,UAAK,WAAU,wDAAwD,cAAI,OAAM;AAAA,MAEpF,oBAAC,UAAK,WAAU,kEACb,iBAAO,MAAM,UAAU,IAAI,KAAK,CAAC,GACpC;AAAA,SAdO,IAAI,OAAO,CAepB,CACD,GACH;AAAA,KAEJ;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"]}
@@ -1,10 +1,58 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { SelectProps } from './ui.js';
3
+ import { aH as SelectProps } from '../kit-labels-v3biUF1L.js';
4
4
  import { ThemePreference } from '../theme/theme-store.js';
5
+ import './data-table-labels.js';
6
+ import './mini-calendar.js';
7
+ import './calendar-heatmap.js';
8
+ import './popover.js';
9
+ import './chip.js';
10
+ import 'lucide-react';
11
+ import './field-sync.js';
5
12
  import './tooltip.js';
6
13
  import '../hooks/use-anchored-rect.js';
7
14
  import '../lib/clipping.js';
15
+ import '../wizard/types.js';
16
+ import '../tour/tour.js';
17
+ import '../search/command-palette.js';
18
+ import '../search/global-search.js';
19
+ import '../search/search-index.js';
20
+ import './month-picker.js';
21
+ import './page-contents.js';
22
+ import './series-chart-labels.js';
23
+ import './pie-chart-labels.js';
24
+ import './sparkline.js';
25
+ import './stat-tile.js';
26
+ import './signature-pad.js';
27
+ import './password-strength.js';
28
+ import './danger-confirm.js';
29
+ import './swatch-picker.js';
30
+ import './tile-radio.js';
31
+ import './icon-picker.js';
32
+ import './dialog-frame.js';
33
+ import './modal.js';
34
+ import '../hooks/use-search-param-state.js';
35
+ import './measured-grid.js';
36
+ import '../feedback/feedback-attachment.js';
37
+ import '../feedback/feedback-dialog.js';
38
+ import '../feedback/feedback-thread.js';
39
+ import '../feedback/feedback-inbox.js';
40
+ import './account-settings-labels.js';
41
+ import './confirm-dialog.js';
42
+ import './floating-panel.js';
43
+ import './bulk-action-bar.js';
44
+ import './list.js';
45
+ import './breadcrumbs.js';
46
+ import './toast.js';
47
+ import './description-list.js';
48
+ import './line-items.js';
49
+ import './progress-bar.js';
50
+ import './signed-amount.js';
51
+ import './error-boundary.js';
52
+ import './authed-image.js';
53
+ import '../hooks/use-authed-src.js';
54
+ import './image-grid.js';
55
+ import './lightbox.js';
8
56
  import 'zustand/middleware';
9
57
  import 'zustand';
10
58
 
@@ -0,0 +1,121 @@
1
+ import * as react from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
+ import { StatTileLabels } from './stat-tile.js';
4
+
5
+ /**
6
+ * The text colours a figure can carry, token-backed — `StatTile`'s tones, as a class
7
+ * any span can take.
8
+ *
9
+ * - `default` — the primary text colour. `muted` — the secondary, quiet one.
10
+ * - `income` / `expense` / `net` — the money trio (CVD-safe teal / amber / violet).
11
+ * - `success` / `warning` / `danger` / `info` — a verdict: a rent rise is the tenant's
12
+ * bad news (kastlan lease-columns), a vote "for" is green (meeting-agenda-tab).
13
+ * - `brand` — the app's accent.
14
+ *
15
+ * Colour is never the only carrier: pair a verdict with an arrow, a word, or a sign.
16
+ */
17
+ type TextTone = "default" | "muted" | "brand" | "income" | "expense" | "net" | "success" | "warning" | "danger" | "info";
18
+ /**
19
+ * The class that colours text in `tone` — for the spans kastlan colours by hand with
20
+ * `text-[var(--danger)]` / `text-emerald-600` (settlement-table, changelog-page,
21
+ * meeting-agenda-tab), so the palette they draw from is the kit's tokens.
22
+ */
23
+ declare function toneTextClass(tone: TextTone): string;
24
+ interface ToneProps extends ComponentPropsWithoutRef<"span"> {
25
+ tone: TextTone;
26
+ }
27
+ /** A `<span>` in a {@link TextTone} — `<Tone tone="success">{votesFor}</Tone>`. */
28
+ declare function Tone({ tone, className, ...rest }: ToneProps): react.JSX.Element;
29
+ /** Which way is +1: `up` for a figure where more is good news (income, savings),
30
+ * `down` where less is (a rent the tenant pays, a cost). */
31
+ type GoodDirection = "up" | "down";
32
+ /**
33
+ * How a signed figure is coloured:
34
+ * - `signed` (default) — money read by its own sign: above zero `income`, below zero
35
+ * `expense`, zero muted. A balance, a net.
36
+ * - `income` / `expense` / `neutral` — one colour whatever the sign (`neutral` is the
37
+ * text's own colour).
38
+ * - `verdict` — the change judged by {@link SignedAmountProps.goodDirection}: good
39
+ * news `success`, bad news `danger`, zero muted. Setting `goodDirection` implies it.
40
+ */
41
+ type SignedAmountTone = "signed" | "income" | "expense" | "neutral" | "verdict";
42
+ /** What a signed figure says to a screen reader, whose readers disagree about a
43
+ * leading "+" (most skip it) and a "−" (some read "dash", some nothing). */
44
+ interface SignedAmountLabels {
45
+ /** A figure above zero. `amount` is formatted and unsigned. */
46
+ positive: (amount: string) => string;
47
+ /** A figure below zero. `amount` is formatted and unsigned. */
48
+ negative: (amount: string) => string;
49
+ }
50
+ declare const DEFAULT_SIGNED_AMOUNT_LABELS: SignedAmountLabels;
51
+ interface SignedFormatProps {
52
+ /** ISO 4217 code: format as money. */
53
+ currency?: string;
54
+ /** `"percent"` reads `value` as a RATIO (`0.12` → 12 %), as `StatTile`'s delta does. */
55
+ unit?: "value" | "percent";
56
+ /** Compact notation: 12 400 → "12K". */
57
+ compact?: boolean;
58
+ /** Fraction digits, as `formatNumber`'s `digits`. */
59
+ digits?: number | {
60
+ min?: number;
61
+ max?: number;
62
+ };
63
+ /** Formats the UNSIGNED magnitude, replacing the `Intl` default outright. The sign
64
+ * is the component's. */
65
+ format?: (magnitude: number) => string;
66
+ locale?: string;
67
+ }
68
+ interface SignedAmountProps extends SignedFormatProps, Omit<ComponentPropsWithoutRef<"span">, "children"> {
69
+ value: number;
70
+ /** See {@link SignedAmountTone}. Default `signed`, or `verdict` with `goodDirection`. */
71
+ tone?: SignedAmountTone;
72
+ /** Which way is good news — colours by verdict (`success` / `danger`). */
73
+ goodDirection?: GoodDirection;
74
+ /** Print "+" before a positive figure (default `true`). A negative always shows its
75
+ * minus; zero shows neither. */
76
+ showPlus?: boolean;
77
+ /** An arrow before the figure (up / down / a dash at zero) — so the direction does not
78
+ * ride on colour alone. Decorative; the words say the same. */
79
+ arrow?: boolean;
80
+ /** Tag the figure `data-private`, for the host's demo-mode blur. */
81
+ sensitive?: boolean;
82
+ labels?: Partial<SignedAmountLabels>;
83
+ }
84
+ /**
85
+ * A figure with a sign that matters: a balance, a rent change, a net.
86
+ *
87
+ * kastlan writes it by hand in five places — `{change > 0 ? "+" : ""}{formatCurrency(
88
+ * change)}` in a span coloured by a ternary (lease-columns:190, settlement-table:58,
89
+ * the tenancy `formatPct`) — and each picks its own colours for the same verdict.
90
+ *
91
+ * The visible figure (sign, digits, arrow) is hidden from assistive tech and spoken as
92
+ * words instead ("plus CHF 50.00" / "minus CHF 50.00", `signedAmount.*`): a "+" is
93
+ * skipped by most screen readers, and `Intl`'s "−" is read as "dash" or not at all.
94
+ */
95
+ declare function SignedAmount({ value, currency, unit, compact, digits, format, locale, tone, goodDirection, showPlus, arrow, sensitive, labels, className, ...rest }: SignedAmountProps): react.JSX.Element;
96
+ interface DeltaProps extends SignedFormatProps, Omit<ComponentPropsWithoutRef<"span">, "children"> {
97
+ /** The change. Above zero is "up". */
98
+ value: number;
99
+ /** Which way is good news. Unset, the change is stated and not judged (muted); set,
100
+ * it is coloured `success` / `danger` and its sentence says "(better)" / "(worse)". */
101
+ goodDirection?: GoodDirection;
102
+ /** What it is measured against, after it: "vs last month". */
103
+ label?: ReactNode;
104
+ /** Hide the arrow (shown by default, as on a StatTile). */
105
+ arrow?: boolean;
106
+ sensitive?: boolean;
107
+ /** The `statTile` namespace's direction words — a delta says the same as a tile's. */
108
+ labels?: Partial<Pick<StatTileLabels, "increase" | "decrease" | "unchanged" | "better" | "worse">>;
109
+ }
110
+ /**
111
+ * `StatTile`'s delta on its own: an arrow, the unsigned magnitude, and a sentence for
112
+ * screen readers ("Up 12% (worse)"), coloured only when `goodDirection` judges it —
113
+ * users up and cost up are the same arrow until someone says which is good.
114
+ *
115
+ * For a change shown outside a tile: kastlan's rent-change column
116
+ * (lease-columns:219, a "+1.25%" chip), the breakdown rows of the rent calculator
117
+ * (breakdown-row:5, a trend icon coloured by hand).
118
+ */
119
+ declare function Delta({ value, currency, unit, compact, digits, format, locale, goodDirection, label, arrow, sensitive, labels, className, ...rest }: DeltaProps): react.JSX.Element | null;
120
+
121
+ export { DEFAULT_SIGNED_AMOUNT_LABELS, Delta, type DeltaProps, type GoodDirection, SignedAmount, type SignedAmountLabels, type SignedAmountProps, type SignedAmountTone, type TextTone, Tone, type ToneProps, toneTextClass };