@eifi1/ui-kit 0.17.0 → 0.19.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 (210) hide show
  1. package/README.md +83 -4
  2. package/dist/chart.d.ts +13 -1
  3. package/dist/components/account-chips.d.ts +153 -0
  4. package/dist/components/account-chips.js +135 -0
  5. package/dist/components/account-chips.js.map +1 -0
  6. package/dist/components/account-settings-labels.d.ts +10 -2
  7. package/dist/components/account-settings-labels.js +3 -0
  8. package/dist/components/account-settings-labels.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +13 -1
  10. package/dist/components/button-group.d.ts +13 -1
  11. package/dist/components/calculator.d.ts +13 -1
  12. package/dist/components/chip.d.ts +25 -1
  13. package/dist/components/chip.js +143 -6
  14. package/dist/components/chip.js.map +1 -1
  15. package/dist/components/confirm-dialog.d.ts +42 -1
  16. package/dist/components/confirm-dialog.js +36 -5
  17. package/dist/components/confirm-dialog.js.map +1 -1
  18. package/dist/components/copy-button.d.ts +13 -1
  19. package/dist/components/danger-confirm.d.ts +55 -4
  20. package/dist/components/danger-confirm.js +67 -30
  21. package/dist/components/danger-confirm.js.map +1 -1
  22. package/dist/components/data-table-cells.d.ts +1 -1
  23. package/dist/components/data-table-filter-popover.d.ts +1 -1
  24. package/dist/components/data-table-filters.d.ts +1 -1
  25. package/dist/components/data-table.d.ts +1 -1
  26. package/dist/components/data-table.js +2 -0
  27. package/dist/components/data-table.js.map +1 -1
  28. package/dist/components/facing-pair.d.ts +13 -1
  29. package/dist/components/file-button.d.ts +13 -1
  30. package/dist/components/file-dropzone.d.ts +13 -1
  31. package/dist/components/floating-panel.d.ts +4 -1
  32. package/dist/components/floating-panel.js +15 -1
  33. package/dist/components/floating-panel.js.map +1 -1
  34. package/dist/components/form-actions.d.ts +13 -1
  35. package/dist/components/form-actions.js +3 -0
  36. package/dist/components/form-actions.js.map +1 -1
  37. package/dist/components/legal.d.ts +84 -0
  38. package/dist/components/legal.js +55 -0
  39. package/dist/components/legal.js.map +1 -0
  40. package/dist/components/money-field.d.ts +13 -1
  41. package/dist/components/number-field.d.ts +14 -2
  42. package/dist/components/number-field.js +23 -22
  43. package/dist/components/number-field.js.map +1 -1
  44. package/dist/components/number-input.d.ts +13 -1
  45. package/dist/components/numpad-sheet.d.ts +13 -1
  46. package/dist/components/passkeys-setting.d.ts +27 -1
  47. package/dist/components/passkeys-setting.js +31 -5
  48. package/dist/components/passkeys-setting.js.map +1 -1
  49. package/dist/components/progress-bar.d.ts +15 -1
  50. package/dist/components/progress-bar.js +8 -1
  51. package/dist/components/progress-bar.js.map +1 -1
  52. package/dist/components/reauth-dialog.d.ts +90 -0
  53. package/dist/components/reauth-dialog.js +158 -0
  54. package/dist/components/reauth-dialog.js.map +1 -0
  55. package/dist/components/series-chart.d.ts +13 -1
  56. package/dist/components/server-wake.d.ts +60 -0
  57. package/dist/components/server-wake.js +79 -0
  58. package/dist/components/server-wake.js.map +1 -0
  59. package/dist/components/settings-fields.d.ts +13 -1
  60. package/dist/components/share-card.d.ts +64 -0
  61. package/dist/components/share-card.js +350 -0
  62. package/dist/components/share-card.js.map +1 -0
  63. package/dist/components/status-dot.d.ts +2 -0
  64. package/dist/components/text-link.d.ts +13 -1
  65. package/dist/components/time-input.d.ts +13 -1
  66. package/dist/components/toast.d.ts +10 -1
  67. package/dist/components/toast.js +42 -2
  68. package/dist/components/toast.js.map +1 -1
  69. package/dist/components/toggle-group.js +5 -2
  70. package/dist/components/toggle-group.js.map +1 -1
  71. package/dist/components/translation-review-editor.d.ts +56 -0
  72. package/dist/components/translation-review-editor.js +159 -0
  73. package/dist/components/translation-review-editor.js.map +1 -0
  74. package/dist/components/translation-review-labels.d.ts +92 -0
  75. package/dist/components/translation-review-labels.js +67 -0
  76. package/dist/components/translation-review-labels.js.map +1 -0
  77. package/dist/components/translation-review-parts.d.ts +97 -0
  78. package/dist/components/translation-review-parts.js +163 -0
  79. package/dist/components/translation-review-parts.js.map +1 -0
  80. package/dist/components/translation-review.d.ts +94 -0
  81. package/dist/components/translation-review.js +344 -0
  82. package/dist/components/translation-review.js.map +1 -0
  83. package/dist/components/ui.d.ts +13 -1
  84. package/dist/components/ui.js +43 -28
  85. package/dist/components/ui.js.map +1 -1
  86. package/dist/components/use-table-state.d.ts +1 -1
  87. package/dist/components/user-avatar.d.ts +2 -0
  88. package/dist/components/write-lock.d.ts +68 -0
  89. package/dist/components/write-lock.js +33 -0
  90. package/dist/components/write-lock.js.map +1 -0
  91. package/dist/{data-table-CpkWUJBY.d.ts → data-table-3mi2R3so.d.ts} +19 -1
  92. package/dist/data-table.d.ts +1 -1
  93. package/dist/hooks/use-file-drop.d.ts +13 -1
  94. package/dist/hooks/use-overlay-history.js +16 -0
  95. package/dist/hooks/use-overlay-history.js.map +1 -1
  96. package/dist/i18n/defaults.d.ts +13 -1
  97. package/dist/i18n/defaults.js +15 -1
  98. package/dist/i18n/defaults.js.map +1 -1
  99. package/dist/i18n/german.d.ts +81 -0
  100. package/dist/i18n/{locales/de.js → german.js} +110 -9
  101. package/dist/i18n/german.js.map +1 -0
  102. package/dist/i18n/kit-labels.d.ts +13 -1
  103. package/dist/i18n/kit-labels.js +5 -0
  104. package/dist/i18n/kit-labels.js.map +1 -1
  105. package/dist/i18n/languages.d.ts +243 -0
  106. package/dist/i18n/languages.js +70 -0
  107. package/dist/i18n/languages.js.map +1 -0
  108. package/dist/i18n/locales/de-CH.d.ts +26 -8
  109. package/dist/i18n/locales/de-CH.js +7 -3
  110. package/dist/i18n/locales/de-CH.js.map +1 -1
  111. package/dist/i18n/locales/en.d.ts +13 -1
  112. package/dist/i18n/locales/en.js +8 -0
  113. package/dist/i18n/locales/en.js.map +1 -1
  114. package/dist/i18n/locales/es.d.ts +13 -1
  115. package/dist/i18n/locales/es.js +127 -27
  116. package/dist/i18n/locales/es.js.map +1 -1
  117. package/dist/i18n/locales/fr.d.ts +13 -1
  118. package/dist/i18n/locales/fr.js +101 -1
  119. package/dist/i18n/locales/fr.js.map +1 -1
  120. package/dist/i18n/locales/hu.d.ts +13 -1
  121. package/dist/i18n/locales/hu.js +105 -1
  122. package/dist/i18n/locales/hu.js.map +1 -1
  123. package/dist/i18n/locales/it.d.ts +13 -1
  124. package/dist/i18n/locales/it.js +134 -34
  125. package/dist/i18n/locales/it.js.map +1 -1
  126. package/dist/i18n/locales/zh.d.ts +13 -1
  127. package/dist/i18n/locales/zh.js +101 -1
  128. package/dist/i18n/locales/zh.js.map +1 -1
  129. package/dist/i18n/review.d.ts +102 -0
  130. package/dist/i18n/review.js +158 -0
  131. package/dist/i18n/review.js.map +1 -0
  132. package/dist/i18n/swiss.d.ts +3 -3
  133. package/dist/i18n/swiss.js.map +1 -1
  134. package/dist/index.d.ts +17 -4
  135. package/dist/index.js +97 -1
  136. package/dist/index.js.map +1 -1
  137. package/dist/{kit-labels-C4uB_zmg.d.ts → kit-labels-D1J2_jyD.d.ts} +236 -9
  138. package/dist/lib/floating-stack.d.ts +31 -0
  139. package/dist/lib/floating-stack.js +42 -0
  140. package/dist/lib/floating-stack.js.map +1 -0
  141. package/dist/lib/server-wake.d.ts +146 -0
  142. package/dist/lib/server-wake.js +122 -0
  143. package/dist/lib/server-wake.js.map +1 -0
  144. package/dist/lib/translation-review.d.ts +232 -0
  145. package/dist/lib/translation-review.js +194 -0
  146. package/dist/lib/translation-review.js.map +1 -0
  147. package/dist/rhf/fields.d.ts +13 -1
  148. package/dist/rhf/form.d.ts +13 -1
  149. package/dist/rhf.d.ts +13 -1
  150. package/dist/search/global-search.d.ts +3 -1
  151. package/dist/search/global-search.js.map +1 -1
  152. package/dist/shell/app-shell.d.ts +13 -1
  153. package/dist/shell/top-bar-brand.d.ts +13 -1
  154. package/dist/shell/topbar-action-menu.d.ts +2 -0
  155. package/dist/shell.d.ts +13 -1
  156. package/dist/wizard/stepper-nav.d.ts +13 -1
  157. package/dist/wizard.d.ts +13 -1
  158. package/package.json +1 -1
  159. package/src/components/account-chips.tsx +302 -0
  160. package/src/components/account-settings-labels.ts +16 -2
  161. package/src/components/chip.tsx +239 -6
  162. package/src/components/confirm-dialog.tsx +82 -7
  163. package/src/components/danger-confirm.tsx +118 -33
  164. package/src/components/data-table.tsx +20 -0
  165. package/src/components/floating-panel.tsx +19 -1
  166. package/src/components/form-actions.tsx +22 -3
  167. package/src/components/legal.tsx +159 -0
  168. package/src/components/number-field.tsx +32 -26
  169. package/src/components/passkeys-setting.tsx +68 -7
  170. package/src/components/progress-bar.tsx +21 -1
  171. package/src/components/reauth-dialog.tsx +273 -0
  172. package/src/components/server-wake.tsx +156 -0
  173. package/src/components/share-card.tsx +615 -0
  174. package/src/components/toast.tsx +72 -2
  175. package/src/components/toggle-group.tsx +5 -2
  176. package/src/components/translation-review-editor.tsx +261 -0
  177. package/src/components/translation-review-labels.ts +152 -0
  178. package/src/components/translation-review-parts.tsx +269 -0
  179. package/src/components/translation-review.tsx +502 -0
  180. package/src/components/ui.tsx +89 -32
  181. package/src/components/write-lock.tsx +96 -0
  182. package/src/hooks/use-overlay-history.ts +51 -0
  183. package/src/i18n/defaults.ts +14 -0
  184. package/src/i18n/{locales/de.ts → german.ts} +120 -13
  185. package/src/i18n/kit-labels.tsx +29 -2
  186. package/src/i18n/languages.ts +285 -0
  187. package/src/i18n/locales/de-CH.ts +17 -8
  188. package/src/i18n/locales/en.ts +8 -0
  189. package/src/i18n/locales/es.ts +128 -26
  190. package/src/i18n/locales/fr.ts +103 -0
  191. package/src/i18n/locales/hu.ts +107 -0
  192. package/src/i18n/locales/it.ts +135 -33
  193. package/src/i18n/locales/zh.ts +102 -0
  194. package/src/i18n/review.ts +193 -0
  195. package/src/i18n/swiss.ts +3 -3
  196. package/src/index.ts +86 -1
  197. package/src/lib/floating-stack.ts +66 -0
  198. package/src/lib/server-wake.ts +291 -0
  199. package/src/lib/translation-review.ts +465 -0
  200. package/src/search/global-search.tsx +3 -1
  201. package/dist/i18n/locales/de-CH-informal.d.ts +0 -61
  202. package/dist/i18n/locales/de-CH-informal.js +0 -8
  203. package/dist/i18n/locales/de-CH-informal.js.map +0 -1
  204. package/dist/i18n/locales/de-informal.d.ts +0 -82
  205. package/dist/i18n/locales/de-informal.js +0 -37
  206. package/dist/i18n/locales/de-informal.js.map +0 -1
  207. package/dist/i18n/locales/de.d.ts +0 -66
  208. package/dist/i18n/locales/de.js.map +0 -1
  209. package/src/i18n/locales/de-CH-informal.ts +0 -10
  210. package/src/i18n/locales/de-informal.ts +0 -61
@@ -3,6 +3,7 @@ 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
+ import { toneTextClass } from "./signed-amount.js";
6
7
  const DEFAULT_PROGRESS_BAR_LABELS = {
7
8
  unlimited: "Unlimited",
8
9
  overLimit: (amount) => `${amount} over the limit`
@@ -51,6 +52,7 @@ function ProgressBar({
51
52
  segments,
52
53
  legend = false,
53
54
  legendValue,
55
+ legendTone = "muted",
54
56
  hint,
55
57
  overage,
56
58
  sensitive = false,
@@ -202,7 +204,12 @@ function ProgressBar({
202
204
  {
203
205
  "data-part": "legend-value",
204
206
  "data-private": priv,
205
- className: "ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]",
207
+ className: cn(
208
+ "ms-auto shrink-0 text-xs tabular-nums",
209
+ toneTextClass(
210
+ (typeof legendTone === "function" ? legendTone(seg, i) : legendTone) ?? "muted"
211
+ )
212
+ ),
206
213
  children: legendValue ? legendValue(seg, formatPart(seg.value), i) : formatPart(seg.value)
207
214
  }
208
215
  )
@@ -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/** 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 /**\n * `\"span\"` builds the bar from spans, for a place only phrasing content may go: inside\n * a button or a link (ListItem's `content`). The `legend` stays a list, so leave it off\n * there. Default `\"div\"`.\n */\n as?: \"div\" | \"span\";\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. That value is the part's OWN, signed\n * and unclamped (`formatValue`, or a percentage of the scale): a part of −40 on a\n * 0–100 scale says \"-40%\" though the bar draws nothing for it. keksdose's cut card\n * shows the share AND a private outflow amount per bucket (cut-card:150), which one\n * 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 /**\n * Tag every figure the bar PRINTS `data-private`, so the host's demo-mode rule blurs\n * it — the attribute `StatTile sensitive` and `Tooltip redact` carry: the `showValue`\n * text, each `legend` row's value (yours from `legendValue` too), the unlimited\n * figure and the `overage` line. The track, the swatches and the part names stay\n * readable — a blurred bar is no picture at all. Before this an app with a demo-mode\n * blur had to pass `legendValue` only to wrap the figure in a tagged span (keksdose).\n * The `aria-valuetext` is not touched: a screen reader is not a screen-share.\n */\n sensitive?: boolean;\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 sensitive = false,\n labels,\n className,\n as = \"div\",\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: ProgressBarProps) {\n // `as=\"span\"`: the same bar from phrasing elements only, so it is valid inside a\n // <button> or <a> — ListItem's `content` puts a budget bar inside the row's target\n // (keksdose P2). Every span is made a block except those already laid out as flex.\n const Tag = as;\n const inlineClass = as === \"span\" ? \"block [&_span:not(.flex):not(.inline-flex)]:block\" : undefined;\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 priv = sensitive ? \"\" : undefined;\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 <Tag {...rest} className={cn(\"min-w-0\", inlineClass, className)} data-state=\"unlimited\">\n <Tag 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\" data-private={priv} className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {shown !== undefined && `${shown} · `}\n {text.unlimited}\n </span>\n </Tag>\n {hintNode}\n </Tag>\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 // A PART is stated as its own value, sign and all — not as the slice of track it\n // draws. The bar cannot draw a negative part (a refund inside a month's spending) or\n // one past `max`, but the legend and the reader can still say it: before 0.16 a\n // −CHF 40 part read \"0 %\" in both, and keksdose had to pass `legendValue` just to get\n // the minus back. For a part inside `min`–`max` this is the same text as before.\n const formatPart = (v: number) => {\n if (!Number.isFinite(v)) return format(min);\n if (formatValue) return formatValue(v, max, min);\n return new Intl.NumberFormat(kitLocale, { style: \"percent\", maximumFractionDigits: 0 }).format(\n span <= 0 ? 0 : (v - min) / span,\n );\n };\n const partTexts = stacked\n ? barParts.map((seg) => {\n const text = formatPart(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\" data-private={priv} 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 <Tag className={cn(\"min-w-0\", inlineClass, className)} data-state={indeterminate ? \"indeterminate\" : \"determinate\"}>\n {(label != null || (showValue && valueText !== undefined)) && (\n <Tag 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 data-part=\"value\" data-private={priv} className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {valueText}\n </span>\n )}\n </Tag>\n )}\n <Tag\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 <Tag className=\"flex h-full w-full gap-0.5\">\n {segments.map((seg, i) =>\n seg.legendOnly ? null : (\n <Tag\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 </Tag>\n ) : indeterminate ? (\n <Tag\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 <Tag\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 </Tag>\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\n data-part=\"legend-value\"\n data-private={priv}\n className=\"ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]\"\n >\n {legendValue ? legendValue(seg, formatPart(seg.value), i) : formatPart(seg.value)}\n </span>\n </li>\n ))}\n </ul>\n )}\n {overageNode}\n {hintNode}\n </Tag>\n );\n}\n"],"mappings":"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","names":["text"]}
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\";\nimport { toneTextClass } from \"./signed-amount\";\nimport type { TextTone } from \"./signed-amount\";\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 /**\n * `\"span\"` builds the bar from spans, for a place only phrasing content may go: inside\n * a button or a link (ListItem's `content`). The `legend` stays a list, so leave it off\n * there. Default `\"div\"`.\n */\n as?: \"div\" | \"span\";\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. That value is the part's OWN, signed\n * and unclamped (`formatValue`, or a percentage of the scale): a part of −40 on a\n * 0–100 scale says \"-40%\" though the bar draws nothing for it. keksdose's cut card\n * shows the share AND a private outflow amount per bucket (cut-card:150), which one\n * figure could not:\n * `legendValue={(seg, share) => <>{share} <Amount … /></>}`.\n */\n legendValue?: (segment: ProgressBarSegment, formatted: string, index: number) => ReactNode;\n /**\n * The colour of each legend row's figure, as a {@link TextTone} — `toneTextClass`'s\n * palette, the money pair included. Default `\"muted\"`: the legend explains the bar and\n * should not outshout the headline above it. One tone for every row, or a function\n * per row. A tone and not the segment's own `tone`: keksdose's tax card fills its\n * three parts with one bucket's certainty ramp (`className`), while the FIGURES carry\n * the bucket's money direction — `legendTone=\"expense\"` for the tax owed — which it\n * could only say before through `legendValue` and an app-side `Amount`. A function\n * returning `undefined` keeps that row muted. Applies to `legendValue`'s node too\n * (it inherits the colour unless it sets its own).\n */\n legendTone?: TextTone | ((segment: ProgressBarSegment, index: number) => TextTone | undefined);\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 /**\n * Tag every figure the bar PRINTS `data-private`, so the host's demo-mode rule blurs\n * it — the attribute `StatTile sensitive` and `Tooltip redact` carry: the `showValue`\n * text, each `legend` row's value (yours from `legendValue` too), the unlimited\n * figure and the `overage` line. The track, the swatches and the part names stay\n * readable — a blurred bar is no picture at all. Before this an app with a demo-mode\n * blur had to pass `legendValue` only to wrap the figure in a tagged span (keksdose).\n * The `aria-valuetext` is not touched: a screen reader is not a screen-share.\n */\n sensitive?: boolean;\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 legendTone = \"muted\",\n hint,\n overage,\n sensitive = false,\n labels,\n className,\n as = \"div\",\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: ProgressBarProps) {\n // `as=\"span\"`: the same bar from phrasing elements only, so it is valid inside a\n // <button> or <a> — ListItem's `content` puts a budget bar inside the row's target\n // (keksdose P2). Every span is made a block except those already laid out as flex.\n const Tag = as;\n const inlineClass = as === \"span\" ? \"block [&_span:not(.flex):not(.inline-flex)]:block\" : undefined;\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 priv = sensitive ? \"\" : undefined;\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 <Tag {...rest} className={cn(\"min-w-0\", inlineClass, className)} data-state=\"unlimited\">\n <Tag 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\" data-private={priv} className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {shown !== undefined && `${shown} · `}\n {text.unlimited}\n </span>\n </Tag>\n {hintNode}\n </Tag>\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 // A PART is stated as its own value, sign and all — not as the slice of track it\n // draws. The bar cannot draw a negative part (a refund inside a month's spending) or\n // one past `max`, but the legend and the reader can still say it: before 0.16 a\n // −CHF 40 part read \"0 %\" in both, and keksdose had to pass `legendValue` just to get\n // the minus back. For a part inside `min`–`max` this is the same text as before.\n const formatPart = (v: number) => {\n if (!Number.isFinite(v)) return format(min);\n if (formatValue) return formatValue(v, max, min);\n return new Intl.NumberFormat(kitLocale, { style: \"percent\", maximumFractionDigits: 0 }).format(\n span <= 0 ? 0 : (v - min) / span,\n );\n };\n const partTexts = stacked\n ? barParts.map((seg) => {\n const text = formatPart(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\" data-private={priv} 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 <Tag className={cn(\"min-w-0\", inlineClass, className)} data-state={indeterminate ? \"indeterminate\" : \"determinate\"}>\n {(label != null || (showValue && valueText !== undefined)) && (\n <Tag 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 data-part=\"value\" data-private={priv} className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {valueText}\n </span>\n )}\n </Tag>\n )}\n <Tag\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 <Tag className=\"flex h-full w-full gap-0.5\">\n {segments.map((seg, i) =>\n seg.legendOnly ? null : (\n <Tag\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 </Tag>\n ) : indeterminate ? (\n <Tag\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 <Tag\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 </Tag>\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\n data-part=\"legend-value\"\n data-private={priv}\n className={cn(\n \"ms-auto shrink-0 text-xs tabular-nums\",\n toneTextClass(\n (typeof legendTone === \"function\" ? legendTone(seg, i) : legendTone) ?? \"muted\",\n ),\n )}\n >\n {legendValue ? legendValue(seg, formatPart(seg.value), i) : formatPart(seg.value)}\n </span>\n </li>\n ))}\n </ul>\n )}\n {overageNode}\n {hintNode}\n </Tag>\n );\n}\n"],"mappings":"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","names":["text"]}
@@ -0,0 +1,90 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * Every string the dialog renders — the `reauthDialog` namespace of
6
+ * `<UiKitProvider labels>`, and overridable per instance through `labels`. The title,
7
+ * description and submit button are usually worded per action ("Add a passkey"),
8
+ * which is what the `title` / `description` / `submitLabel` props are for; these are
9
+ * the fallbacks.
10
+ */
11
+ interface ReauthDialogLabels {
12
+ title: string;
13
+ description: string;
14
+ /** Label of the password field. */
15
+ password: string;
16
+ submit: string;
17
+ cancel: string;
18
+ }
19
+ declare const DEFAULT_REAUTH_DIALOG_LABELS: ReauthDialogLabels;
20
+ interface ReauthDialogProps {
21
+ /**
22
+ * Verifies the password — the app's call, e.g. the request the password guards.
23
+ *
24
+ * Return a promise and the dialog manages itself: busy until it settles, closes
25
+ * (`onClose(true)`) when it resolves, STAYS OPEN with the password kept and selected
26
+ * when it rejects — a wrong password is a typo to correct, not a sign-out. Pass the
27
+ * reason as `error`. Return nothing and the app owns the rest: `busy` for the
28
+ * pending state, unmounting the dialog once it is done.
29
+ */
30
+ onSubmit: (password: string) => void | Promise<unknown>;
31
+ /**
32
+ * The dialog has closed: `true` after a resolved `onSubmit`, `false` for Cancel,
33
+ * Escape, the backdrop and Back. A dismissal while `onSubmit` is still running also
34
+ * answers `false`, and that call's late result is then ignored.
35
+ */
36
+ onClose: (confirmed: boolean) => void;
37
+ /**
38
+ * Why the last attempt failed ("Wrong password"), shown under the field, which turns
39
+ * invalid (`aria-invalid`, `--danger-border-strong`). The app decides the wording —
40
+ * a 400 is a wrong password, a 500 is not. Hidden again as soon as the user edits
41
+ * the field (the message is about the password they had typed), and shown again by
42
+ * the next failed submit.
43
+ */
44
+ error?: ReactNode;
45
+ /** The check is running, for a caller that tracks it itself (a mutation's
46
+ * `isPending`); a promise returned from `onSubmit` does the same on its own. */
47
+ busy?: boolean;
48
+ /** Default: `labels.title` — "Confirm it’s you". */
49
+ title?: ReactNode;
50
+ /** The line under the title. Default: `labels.description`. */
51
+ description?: ReactNode;
52
+ /** Text of the submit button. Default: `labels.submit` — "Continue". */
53
+ submitLabel?: ReactNode;
54
+ /** `"danger"` when what the password unlocks cannot be undone: the submit is then the
55
+ * destructive button. Default `"neutral"`. */
56
+ tone?: "neutral" | "danger";
57
+ /** Extra body above the field — what is about to happen, a warning. */
58
+ children?: ReactNode;
59
+ /** Kept-mounted mode, as on `Modal`: left out, the dialog is open while mounted. A
60
+ * flip to `false` animates it out and wipes the password. */
61
+ open?: boolean;
62
+ /** User-facing strings; see {@link ReauthDialogLabels}. */
63
+ labels?: Partial<ReauthDialogLabels>;
64
+ }
65
+ /**
66
+ * "Enter your current password to continue" — the step-up check in front of an
67
+ * action that changes how an account signs in (adding a passkey) or what it can
68
+ * reach. Kurvenschmiede asked for the password before registering a passkey in a
69
+ * dialog of its own; keksdose asks for it inline in `DangerConfirm`'s
70
+ * `requirePassword` tile. Both are the same three facts — the field is the CURRENT
71
+ * password (the password manager fills it, never generates one), a wrong one stays in
72
+ * the dialog with its error, and the APP verifies it — so the promise handling is
73
+ * `DangerConfirm`'s own, shared, the field is its `CurrentPasswordInput` spelled out
74
+ * with an `error`, and this is the modal form of them.
75
+ *
76
+ * A component rather than a `useReauth()` promise: the step that follows the check
77
+ * (Kurvenschmiede's WebAuthn ceremony) needs what the server answered, and the error
78
+ * wording needs the app's own status codes — both already live in the app's mutation,
79
+ * which this dialog only has to be handed (`onSubmit={(pw) => begin.mutateAsync(pw)}`,
80
+ * `error={…}`).
81
+ *
82
+ * The field is a `<form>`, so Enter submits once something is typed. Focus starts in
83
+ * the field and returns to the trigger through `Modal`'s trap. While a check runs,
84
+ * the field is read-only, Cancel is disabled and Escape is held: a request that has
85
+ * left cannot be cancelled by closing the dialog, and the dialog should not pretend it
86
+ * can.
87
+ */
88
+ declare function ReauthDialog({ onSubmit, onClose, error, busy: busyProp, title, description, submitLabel, tone, children, open, labels: labelsProp, }: ReauthDialogProps): react.JSX.Element;
89
+
90
+ export { DEFAULT_REAUTH_DIALOG_LABELS, ReauthDialog, type ReauthDialogLabels, type ReauthDialogProps };
@@ -0,0 +1,158 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useContext, useEffect, useId, useRef, useState } from "react";
4
+ import { useKitLabels } from "../i18n/kit-labels.js";
5
+ import { usePromisePending } from "./danger-confirm.js";
6
+ import { DialogFrame } from "./dialog-frame.js";
7
+ import { ModalCloseContext } from "./modal.js";
8
+ import { Button, Input, Spinner } from "./ui.js";
9
+ const DEFAULT_REAUTH_DIALOG_LABELS = {
10
+ title: "Confirm it\u2019s you",
11
+ description: "Enter your current password to continue.",
12
+ password: "Current password",
13
+ submit: "Continue",
14
+ cancel: "Cancel"
15
+ };
16
+ function ReauthDialog({
17
+ onSubmit,
18
+ onClose,
19
+ error,
20
+ busy: busyProp,
21
+ title,
22
+ description,
23
+ submitLabel,
24
+ tone = "neutral",
25
+ children,
26
+ open,
27
+ labels: labelsProp
28
+ }) {
29
+ const labels = useKitLabels("reauthDialog", DEFAULT_REAUTH_DIALOG_LABELS, labelsProp);
30
+ const [password, setPassword] = useState("");
31
+ const [edited, setEdited] = useState(false);
32
+ const { pending, run } = usePromisePending();
33
+ const busy = Boolean(busyProp) || pending;
34
+ const inputRef = useRef(null);
35
+ const [done, setDone] = useState(false);
36
+ const confirmed = useRef(false);
37
+ const formId = useId();
38
+ const isOpen = open ?? true;
39
+ const [wasOpen, setWasOpen] = useState(isOpen);
40
+ if (wasOpen !== isOpen) {
41
+ setWasOpen(isOpen);
42
+ if (!isOpen) {
43
+ setPassword("");
44
+ setEdited(false);
45
+ setDone(false);
46
+ }
47
+ }
48
+ useEffect(() => {
49
+ if (!isOpen) return;
50
+ confirmed.current = false;
51
+ inputRef.current?.focus();
52
+ }, [isOpen]);
53
+ const submit = () => {
54
+ if (busy || password === "") return;
55
+ setEdited(false);
56
+ run(onSubmit(password), () => {
57
+ confirmed.current = true;
58
+ setDone(true);
59
+ });
60
+ };
61
+ const wasPending = useRef(false);
62
+ useEffect(() => {
63
+ if (wasPending.current && !pending && !done) inputRef.current?.select();
64
+ wasPending.current = pending;
65
+ }, [pending, done]);
66
+ const shownError = edited ? void 0 : error;
67
+ return /* @__PURE__ */ jsxs(
68
+ DialogFrame,
69
+ {
70
+ open,
71
+ title: title ?? labels.title,
72
+ description: description ?? labels.description,
73
+ onClose: () => onClose(confirmed.current),
74
+ onKeyDown: (e) => {
75
+ if (busy && e.key === "Escape") e.preventDefault();
76
+ },
77
+ "aria-busy": busy || void 0,
78
+ actions: /* @__PURE__ */ jsx(
79
+ ReauthActions,
80
+ {
81
+ formId,
82
+ done,
83
+ busy,
84
+ canSubmit: password !== "",
85
+ danger: tone === "danger",
86
+ cancelLabel: labels.cancel,
87
+ submitLabel: submitLabel ?? labels.submit
88
+ }
89
+ ),
90
+ children: [
91
+ children,
92
+ /* @__PURE__ */ jsx(
93
+ "form",
94
+ {
95
+ id: formId,
96
+ noValidate: true,
97
+ onSubmit: (e) => {
98
+ e.preventDefault();
99
+ submit();
100
+ },
101
+ children: /* @__PURE__ */ jsx(
102
+ Input,
103
+ {
104
+ ref: inputRef,
105
+ type: "password",
106
+ autoComplete: "current-password",
107
+ label: labels.password,
108
+ value: password,
109
+ readOnly: busy,
110
+ error: shownError,
111
+ onChange: (e) => {
112
+ setPassword(e.target.value);
113
+ setEdited(true);
114
+ }
115
+ }
116
+ )
117
+ }
118
+ )
119
+ ]
120
+ }
121
+ );
122
+ }
123
+ function ReauthActions({
124
+ formId,
125
+ done,
126
+ busy,
127
+ canSubmit,
128
+ danger,
129
+ cancelLabel,
130
+ submitLabel
131
+ }) {
132
+ const close = useContext(ModalCloseContext);
133
+ useEffect(() => {
134
+ if (done) close?.();
135
+ }, [done, close]);
136
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
137
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", disabled: busy, onClick: () => close?.(), children: cancelLabel }),
138
+ /* @__PURE__ */ jsxs(
139
+ Button,
140
+ {
141
+ type: "submit",
142
+ form: formId,
143
+ variant: danger ? "danger" : "primary",
144
+ disabled: !canSubmit || busy,
145
+ "aria-busy": busy || void 0,
146
+ children: [
147
+ busy && /* @__PURE__ */ jsx(Spinner, { label: null, className: "h-4 w-4" }),
148
+ submitLabel
149
+ ]
150
+ }
151
+ )
152
+ ] });
153
+ }
154
+ export {
155
+ DEFAULT_REAUTH_DIALOG_LABELS,
156
+ ReauthDialog
157
+ };
158
+ //# sourceMappingURL=reauth-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/reauth-dialog.tsx"],"sourcesContent":["import { useContext, useEffect, useId, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { usePromisePending } from \"./danger-confirm\";\nimport { DialogFrame } from \"./dialog-frame\";\nimport { ModalCloseContext } from \"./modal\";\nimport { Button, Input, Spinner } from \"./ui\";\n\n/**\n * Every string the dialog renders — the `reauthDialog` namespace of\n * `<UiKitProvider labels>`, and overridable per instance through `labels`. The title,\n * description and submit button are usually worded per action (\"Add a passkey\"),\n * which is what the `title` / `description` / `submitLabel` props are for; these are\n * the fallbacks.\n */\nexport interface ReauthDialogLabels {\n title: string;\n description: string;\n /** Label of the password field. */\n password: string;\n submit: string;\n cancel: string;\n}\n\nexport const DEFAULT_REAUTH_DIALOG_LABELS: ReauthDialogLabels = {\n title: \"Confirm it’s you\",\n description: \"Enter your current password to continue.\",\n password: \"Current password\",\n submit: \"Continue\",\n cancel: \"Cancel\",\n};\n\nexport interface ReauthDialogProps {\n /**\n * Verifies the password — the app's call, e.g. the request the password guards.\n *\n * Return a promise and the dialog manages itself: busy until it settles, closes\n * (`onClose(true)`) when it resolves, STAYS OPEN with the password kept and selected\n * when it rejects — a wrong password is a typo to correct, not a sign-out. Pass the\n * reason as `error`. Return nothing and the app owns the rest: `busy` for the\n * pending state, unmounting the dialog once it is done.\n */\n onSubmit: (password: string) => void | Promise<unknown>;\n /**\n * The dialog has closed: `true` after a resolved `onSubmit`, `false` for Cancel,\n * Escape, the backdrop and Back. A dismissal while `onSubmit` is still running also\n * answers `false`, and that call's late result is then ignored.\n */\n onClose: (confirmed: boolean) => void;\n /**\n * Why the last attempt failed (\"Wrong password\"), shown under the field, which turns\n * invalid (`aria-invalid`, `--danger-border-strong`). The app decides the wording —\n * a 400 is a wrong password, a 500 is not. Hidden again as soon as the user edits\n * the field (the message is about the password they had typed), and shown again by\n * the next failed submit.\n */\n error?: ReactNode;\n /** The check is running, for a caller that tracks it itself (a mutation's\n * `isPending`); a promise returned from `onSubmit` does the same on its own. */\n busy?: boolean;\n /** Default: `labels.title` — \"Confirm it’s you\". */\n title?: ReactNode;\n /** The line under the title. Default: `labels.description`. */\n description?: ReactNode;\n /** Text of the submit button. Default: `labels.submit` — \"Continue\". */\n submitLabel?: ReactNode;\n /** `\"danger\"` when what the password unlocks cannot be undone: the submit is then the\n * destructive button. Default `\"neutral\"`. */\n tone?: \"neutral\" | \"danger\";\n /** Extra body above the field — what is about to happen, a warning. */\n children?: ReactNode;\n /** Kept-mounted mode, as on `Modal`: left out, the dialog is open while mounted. A\n * flip to `false` animates it out and wipes the password. */\n open?: boolean;\n /** User-facing strings; see {@link ReauthDialogLabels}. */\n labels?: Partial<ReauthDialogLabels>;\n}\n\n/**\n * \"Enter your current password to continue\" — the step-up check in front of an\n * action that changes how an account signs in (adding a passkey) or what it can\n * reach. Kurvenschmiede asked for the password before registering a passkey in a\n * dialog of its own; keksdose asks for it inline in `DangerConfirm`'s\n * `requirePassword` tile. Both are the same three facts — the field is the CURRENT\n * password (the password manager fills it, never generates one), a wrong one stays in\n * the dialog with its error, and the APP verifies it — so the promise handling is\n * `DangerConfirm`'s own, shared, the field is its `CurrentPasswordInput` spelled out\n * with an `error`, and this is the modal form of them.\n *\n * A component rather than a `useReauth()` promise: the step that follows the check\n * (Kurvenschmiede's WebAuthn ceremony) needs what the server answered, and the error\n * wording needs the app's own status codes — both already live in the app's mutation,\n * which this dialog only has to be handed (`onSubmit={(pw) => begin.mutateAsync(pw)}`,\n * `error={…}`).\n *\n * The field is a `<form>`, so Enter submits once something is typed. Focus starts in\n * the field and returns to the trigger through `Modal`'s trap. While a check runs,\n * the field is read-only, Cancel is disabled and Escape is held: a request that has\n * left cannot be cancelled by closing the dialog, and the dialog should not pretend it\n * can.\n */\nexport function ReauthDialog({\n onSubmit,\n onClose,\n error,\n busy: busyProp,\n title,\n description,\n submitLabel,\n tone = \"neutral\",\n children,\n open,\n labels: labelsProp,\n}: ReauthDialogProps) {\n const labels = useKitLabels(\"reauthDialog\", DEFAULT_REAUTH_DIALOG_LABELS, labelsProp);\n const [password, setPassword] = useState(\"\");\n // The error describes the password that was submitted; once the user edits it, it\n // describes nothing on screen. Cleared by the next submit, so a second failure with\n // the same message shows again.\n const [edited, setEdited] = useState(false);\n const { pending, run } = usePromisePending();\n const busy = Boolean(busyProp) || pending;\n const inputRef = useRef<HTMLInputElement>(null);\n // `onSubmit` resolved: the actions row lowers the panel (state, so it re-renders),\n // and `onClose` answers true (a ref, because `Modal` may call the `onClose` it saw\n // BEFORE that re-render — its latest-callback ref is refreshed in an effect that runs\n // after the actions row's).\n const [done, setDone] = useState(false);\n const confirmed = useRef(false);\n const formId = useId();\n const isOpen = open ?? true;\n\n // A close (kept-mounted) wipes the password: it must not sit in a hidden dialog.\n // During render, like DangerConfirm's disarm, so no frame holds it.\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (wasOpen !== isOpen) {\n setWasOpen(isOpen);\n if (!isOpen) {\n setPassword(\"\");\n setEdited(false);\n setDone(false);\n }\n }\n\n // After `Modal`'s own effect (a parent's effects run after its children's), so the\n // trap has recorded the trigger as its restore target before focus moves in.\n useEffect(() => {\n if (!isOpen) return;\n confirmed.current = false;\n inputRef.current?.focus();\n }, [isOpen]);\n\n const submit = () => {\n if (busy || password === \"\") return;\n setEdited(false);\n run(onSubmit(password), () => {\n confirmed.current = true;\n setDone(true);\n });\n };\n\n // A rejection leaves focus where it was (the field, or the submit button) — select\n // the password there so the retry replaces it rather than appending to it.\n const wasPending = useRef(false);\n useEffect(() => {\n if (wasPending.current && !pending && !done) inputRef.current?.select();\n wasPending.current = pending;\n }, [pending, done]);\n\n const shownError = edited ? undefined : error;\n\n return (\n <DialogFrame\n open={open}\n title={title ?? labels.title}\n description={description ?? labels.description}\n onClose={() => onClose(confirmed.current)}\n onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {\n if (busy && e.key === \"Escape\") e.preventDefault();\n }}\n aria-busy={busy || undefined}\n actions={\n <ReauthActions\n formId={formId}\n done={done}\n busy={busy}\n canSubmit={password !== \"\"}\n danger={tone === \"danger\"}\n cancelLabel={labels.cancel}\n submitLabel={submitLabel ?? labels.submit}\n />\n }\n >\n {children}\n <form\n id={formId}\n noValidate\n onSubmit={(e) => {\n e.preventDefault();\n submit();\n }}\n >\n {/* CurrentPasswordInput's field — the password manager fills it with the CURRENT\n password and never offers to generate one; `readOnly`, not `disabled`, while\n busy, because disabling the field that has focus drops focus to <body> —\n plus `Input`'s own `error`, which that helper does not take. The message\n paints the field, sits under it and is its `aria-describedby`; passing the\n prop on every render keeps the field's box steady, so the message going away\n on the first keystroke of a retry no longer remounts the <input> under the\n typist (it did, which is why this used to draw its own message). */}\n <Input\n ref={inputRef}\n type=\"password\"\n autoComplete=\"current-password\"\n label={labels.password}\n value={password}\n readOnly={busy}\n error={shownError}\n onChange={(e) => {\n setPassword(e.target.value);\n setEdited(true);\n }}\n />\n </form>\n </DialogFrame>\n );\n}\n\n/**\n * The actions row. A component of its own because it renders INSIDE the `Modal`, where\n * the panel's animated close is in context: a resolved `onSubmit` lowers the panel\n * through it, the way Cancel and Escape do, and `Modal` then calls `onClose`.\n */\nfunction ReauthActions({\n formId,\n done,\n busy,\n canSubmit,\n danger,\n cancelLabel,\n submitLabel,\n}: {\n formId: string;\n done: boolean;\n busy: boolean;\n canSubmit: boolean;\n danger: boolean;\n cancelLabel: ReactNode;\n submitLabel: ReactNode;\n}) {\n const close = useContext(ModalCloseContext);\n useEffect(() => {\n if (done) close?.();\n }, [done, close]);\n return (\n <>\n <Button type=\"button\" variant=\"ghost\" disabled={busy} onClick={() => close?.()}>\n {cancelLabel}\n </Button>\n <Button\n type=\"submit\"\n form={formId}\n variant={danger ? \"danger\" : \"primary\"}\n disabled={!canSubmit || busy}\n aria-busy={busy || undefined}\n >\n {busy && <Spinner label={null} className=\"h-4 w-4\" />}\n {submitLabel}\n </Button>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -5,7 +5,7 @@ import { LegendEntry } from './toggle-legend.js';
5
5
  import { SeriesChartLabels } from './series-chart-labels.js';
6
6
  import { TimeTickUnit } from './series-chart-ticks.js';
7
7
  import { SeriesChartAxisBudget } from './series-chart-budget.js';
8
- import { x as ChartTooltipPlacement } from '../kit-labels-C4uB_zmg.js';
8
+ import { x as ChartTooltipPlacement } from '../kit-labels-D1J2_jyD.js';
9
9
  import 'recharts';
10
10
  import './data-table-labels.js';
11
11
  import './mini-calendar.js';
@@ -56,6 +56,18 @@ import './authed-image.js';
56
56
  import '../hooks/use-authed-src.js';
57
57
  import './image-grid.js';
58
58
  import './lightbox.js';
59
+ import './write-lock.js';
60
+ import './legal.js';
61
+ import './account-chips.js';
62
+ import '../lib/format.js';
63
+ import '../data-table-3mi2R3so.js';
64
+ import './data-table-sort.js';
65
+ import './swipeable-row.js';
66
+ import './reauth-dialog.js';
67
+ import './server-wake.js';
68
+ import '../lib/server-wake.js';
69
+ import './translation-review-labels.js';
70
+ import '../lib/translation-review.js';
59
71
 
60
72
  interface SeriesChartSeries {
61
73
  /** The row key this line reads — and the `--color-<key>` it is painted with, so it
@@ -0,0 +1,60 @@
1
+ import * as react from 'react';
2
+ import { ServerWakeWatcher, ServerWakeStage } from '../lib/server-wake.js';
3
+
4
+ /**
5
+ * The visible half of the cold-start watchdog (`lib/server-wake.ts`), keksdose's
6
+ * `app/server-wake-notice.tsx` lifted into the kit: after a couple of seconds of an
7
+ * unanswered read it says the app is still loading, and after seven it says WHY — the
8
+ * server sleeps when nobody is using it, and the first request has to start it again.
9
+ *
10
+ * Corner-anchored and non-blocking on purpose. The pages underneath already show their
11
+ * own skeletons; what was missing is the explanation, not another overlay. Start
12
+ * corner, above the phone's bottom nav, so it collides with neither the centred toasts
13
+ * nor the nav bar — and above any FloatingActionButton it would otherwise cover
14
+ * (kastlan's offline pill shares the corner; on a phone the notice spans both).
15
+ *
16
+ * Mount it next to the router, not inside the app layout, so it also covers the login
17
+ * screen and the landing page — a cold start is at its most confusing exactly there,
18
+ * before anything of the app is on screen.
19
+ */
20
+ interface ServerWakeLabels {
21
+ /** Past `slowMs` (2 s): a warm server would have answered by now. */
22
+ slow: string;
23
+ /** Past `wakingMs` (7 s): the explanation. `appName` is the notice's prop; without
24
+ * one the sentence says "the app". */
25
+ waking: (appName?: string) => string;
26
+ }
27
+ declare const DEFAULT_SERVER_WAKE_LABELS: ServerWakeLabels;
28
+ /** The watcher's current stage, re-rendering on every change. Default: the shared
29
+ * {@link serverWake} instance. */
30
+ declare function useServerWakeStage(watcher?: ServerWakeWatcher): ServerWakeStage;
31
+ interface ServerWakeNoticeProps {
32
+ /** Default: the shared {@link serverWake} instance. Pass the app's own when it was
33
+ * made with {@link createServerWake} (e.g. to watch the login POST too). */
34
+ watcher?: ServerWakeWatcher;
35
+ /** Named in the `waking` sentence ("…when nobody is using Keksdose…"). Default: the
36
+ * label says "the app". */
37
+ appName?: string;
38
+ labels?: Partial<ServerWakeLabels>;
39
+ /**
40
+ * The height to keep clear at the bottom — AppShell's bottom nav. Default: its
41
+ * measured `--app-nav-h` (0px from `md` up and on pages without a shell, such as the
42
+ * login screen), so nothing has to be passed under an AppShell. The safe area is
43
+ * respected either way, and a 1rem gap is added. Numbers are px.
44
+ */
45
+ navOffset?: string | number;
46
+ /** Extra classes on the fixed anchor — a different width or `start-*`. The bottom
47
+ * offset is an inline style, so move it vertically with {@link navOffset}. Either
48
+ * way it rises above a visible FloatingActionButton under it. */
49
+ className?: string;
50
+ }
51
+ /**
52
+ * Renders nothing visible while idle. The LIVE REGION, though, is always mounted: a
53
+ * `role="status"` element inserted together with its text is announced unreliably
54
+ * (several screen readers only speak changes to a region they already knew about),
55
+ * so the region waits empty and the banner appears inside it. keksdose's notice
56
+ * mounted the region and the words at once.
57
+ */
58
+ declare function ServerWakeNotice({ watcher, appName, labels: labelsProp, navOffset, className }: ServerWakeNoticeProps): react.JSX.Element;
59
+
60
+ export { DEFAULT_SERVER_WAKE_LABELS, type ServerWakeLabels, ServerWakeNotice, type ServerWakeNoticeProps, useServerWakeStage };
@@ -0,0 +1,79 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { useKitLabels } from "../i18n/kit-labels.js";
5
+ import { cn } from "../lib/cn.js";
6
+ import { clearanceAbove, subscribeFloating } from "../lib/floating-stack.js";
7
+ import { serverWake } from "../lib/server-wake.js";
8
+ import { AlertBanner } from "./alert-banner.js";
9
+ import { Spinner } from "./ui.js";
10
+ const DEFAULT_SERVER_WAKE_LABELS = {
11
+ slow: "Still loading \u2014 this is taking longer than usual.",
12
+ waking: (appName) => `The server goes to sleep when nobody is using ${appName ?? "the app"}, so the first request after a break has to start it again. That can take a moment \u2014 nothing is lost, the page fills in by itself.`
13
+ };
14
+ function useServerWakeStage(watcher = serverWake) {
15
+ return useSyncExternalStore(watcher.subscribe, watcher.getStage, () => "idle");
16
+ }
17
+ const px = (v) => typeof v === "number" ? `${v}px` : v;
18
+ const STACK_GAP = "0.5rem";
19
+ function useFloatingClearance(anchor, stage) {
20
+ const [clearance, setClearance] = useState(null);
21
+ useLayoutEffect(() => {
22
+ if (stage === "idle") return;
23
+ const measure = () => setClearance(clearanceAbove(anchor.current));
24
+ measure();
25
+ const unsubscribe = subscribeFloating(measure);
26
+ window.addEventListener("resize", measure);
27
+ return () => {
28
+ unsubscribe();
29
+ window.removeEventListener("resize", measure);
30
+ };
31
+ }, [anchor, stage]);
32
+ return stage === "idle" ? null : clearance;
33
+ }
34
+ function ServerWakeNotice({ watcher, appName, labels: labelsProp, navOffset, className }) {
35
+ const labels = useKitLabels("serverWake", DEFAULT_SERVER_WAKE_LABELS, labelsProp);
36
+ const stage = useServerWakeStage(watcher);
37
+ const waking = stage === "waking";
38
+ const anchor = useRef(null);
39
+ const clearance = useFloatingClearance(anchor, stage);
40
+ const base = `calc(max(${navOffset === void 0 ? "var(--app-nav-h, 0px)" : px(navOffset)}, env(safe-area-inset-bottom, 0px)) + 1rem)`;
41
+ const style = {
42
+ bottom: clearance === null ? base : `max(${base}, calc(${clearance}px + ${STACK_GAP}))`
43
+ };
44
+ return /* @__PURE__ */ jsx(
45
+ "div",
46
+ {
47
+ ref: anchor,
48
+ role: "status",
49
+ "aria-live": "polite",
50
+ "data-stage": stage,
51
+ style,
52
+ className: cn(
53
+ "fixed start-4 z-50 max-w-[min(24rem,calc(100vw-2rem))]",
54
+ stage === "idle" && "pointer-events-none",
55
+ className
56
+ ),
57
+ children: stage !== "idle" && // `elevated`: OPAQUE in both themes (keksdose live #209) — the notice that
58
+ // explains a stall is read over whatever stalled, and a translucent dark tint
59
+ // let the page's own text show through it. A box banner is role-less, so the
60
+ // wrapper above stays the one live region.
61
+ /* @__PURE__ */ jsx(
62
+ AlertBanner,
63
+ {
64
+ tone: waking ? "warning" : "neutral",
65
+ size: "sm",
66
+ elevated: true,
67
+ icon: /* @__PURE__ */ jsx(Spinner, { label: null, className: "h-3.5 w-3.5" }),
68
+ children: waking ? labels.waking(appName) : labels.slow
69
+ }
70
+ )
71
+ }
72
+ );
73
+ }
74
+ export {
75
+ DEFAULT_SERVER_WAKE_LABELS,
76
+ ServerWakeNotice,
77
+ useServerWakeStage
78
+ };
79
+ //# sourceMappingURL=server-wake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/server-wake.tsx"],"sourcesContent":["import { useLayoutEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { CSSProperties, RefObject } from \"react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { clearanceAbove, subscribeFloating } from \"../lib/floating-stack\";\nimport { serverWake } from \"../lib/server-wake\";\nimport type { ServerWakeStage, ServerWakeWatcher } from \"../lib/server-wake\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { Spinner } from \"./ui\";\n\n/**\n * The visible half of the cold-start watchdog (`lib/server-wake.ts`), keksdose's\n * `app/server-wake-notice.tsx` lifted into the kit: after a couple of seconds of an\n * unanswered read it says the app is still loading, and after seven it says WHY — the\n * server sleeps when nobody is using it, and the first request has to start it again.\n *\n * Corner-anchored and non-blocking on purpose. The pages underneath already show their\n * own skeletons; what was missing is the explanation, not another overlay. Start\n * corner, above the phone's bottom nav, so it collides with neither the centred toasts\n * nor the nav bar — and above any FloatingActionButton it would otherwise cover\n * (kastlan's offline pill shares the corner; on a phone the notice spans both).\n *\n * Mount it next to the router, not inside the app layout, so it also covers the login\n * screen and the landing page — a cold start is at its most confusing exactly there,\n * before anything of the app is on screen.\n */\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\nexport interface ServerWakeLabels {\n /** Past `slowMs` (2 s): a warm server would have answered by now. */\n slow: string;\n /** Past `wakingMs` (7 s): the explanation. `appName` is the notice's prop; without\n * one the sentence says \"the app\". */\n waking: (appName?: string) => string;\n}\n\nexport const DEFAULT_SERVER_WAKE_LABELS: ServerWakeLabels = {\n slow: \"Still loading — this is taking longer than usual.\",\n waking: (appName) =>\n `The server goes to sleep when nobody is using ${appName ?? \"the app\"}, so the first request after a break has to start it again. That can take a moment — nothing is lost, the page fills in by itself.`,\n};\n\n// ── Hook ──────────────────────────────────────────────────────────────────────\n\n/** The watcher's current stage, re-rendering on every change. Default: the shared\n * {@link serverWake} instance. */\nexport function useServerWakeStage(watcher: ServerWakeWatcher = serverWake): ServerWakeStage {\n // The server snapshot is `idle`: nothing is in flight while the HTML is rendered.\n return useSyncExternalStore(watcher.subscribe, watcher.getStage, () => \"idle\");\n}\n\n// ── Notice ────────────────────────────────────────────────────────────────────\n\nexport interface ServerWakeNoticeProps {\n /** Default: the shared {@link serverWake} instance. Pass the app's own when it was\n * made with {@link createServerWake} (e.g. to watch the login POST too). */\n watcher?: ServerWakeWatcher;\n /** Named in the `waking` sentence (\"…when nobody is using Keksdose…\"). Default: the\n * label says \"the app\". */\n appName?: string;\n labels?: Partial<ServerWakeLabels>;\n /**\n * The height to keep clear at the bottom — AppShell's bottom nav. Default: its\n * measured `--app-nav-h` (0px from `md` up and on pages without a shell, such as the\n * login screen), so nothing has to be passed under an AppShell. The safe area is\n * respected either way, and a 1rem gap is added. Numbers are px.\n */\n navOffset?: string | number;\n /** Extra classes on the fixed anchor — a different width or `start-*`. The bottom\n * offset is an inline style, so move it vertically with {@link navOffset}. Either\n * way it rises above a visible FloatingActionButton under it. */\n className?: string;\n}\n\nconst px = (v: string | number) => (typeof v === \"number\" ? `${v}px` : v);\n\n/** Gap between the notice and a floating control it stacks above. */\nconst STACK_GAP = \"0.5rem\";\n\n/**\n * How far off the bottom the notice must sit to clear the FloatingActionButtons under\n * it (`lib/floating-stack.ts`), re-measured when one appears, goes, resizes or the\n * viewport does — or `null` while it is idle or nothing is under it. kastlan 0.18.\n */\nfunction useFloatingClearance(anchor: RefObject<HTMLDivElement | null>, stage: ServerWakeStage): number | null {\n const [clearance, setClearance] = useState<number | null>(null);\n useLayoutEffect(() => {\n if (stage === \"idle\") return;\n const measure = () => setClearance(clearanceAbove(anchor.current));\n measure();\n const unsubscribe = subscribeFloating(measure);\n window.addEventListener(\"resize\", measure);\n return () => {\n unsubscribe();\n window.removeEventListener(\"resize\", measure);\n };\n }, [anchor, stage]);\n // Idle, the last measurement is stale and nothing shows anyway.\n return stage === \"idle\" ? null : clearance;\n}\n\n/**\n * Renders nothing visible while idle. The LIVE REGION, though, is always mounted: a\n * `role=\"status\"` element inserted together with its text is announced unreliably\n * (several screen readers only speak changes to a region they already knew about),\n * so the region waits empty and the banner appears inside it. keksdose's notice\n * mounted the region and the words at once.\n */\nexport function ServerWakeNotice({ watcher, appName, labels: labelsProp, navOffset, className }: ServerWakeNoticeProps) {\n const labels = useKitLabels(\"serverWake\", DEFAULT_SERVER_WAKE_LABELS, labelsProp);\n const stage = useServerWakeStage(watcher);\n const waking = stage === \"waking\";\n const anchor = useRef<HTMLDivElement>(null);\n const clearance = useFloatingClearance(anchor, stage);\n const base = `calc(max(${navOffset === undefined ? \"var(--app-nav-h, 0px)\" : px(navOffset)}, env(safe-area-inset-bottom, 0px)) + 1rem)`;\n const style: CSSProperties = {\n bottom: clearance === null ? base : `max(${base}, calc(${clearance}px + ${STACK_GAP}))`,\n };\n\n return (\n <div\n ref={anchor}\n role=\"status\"\n aria-live=\"polite\"\n data-stage={stage}\n style={style}\n // `pointer-events-none` on the empty anchor: a fixed box, even an empty one, is not\n // allowed to swallow a tap on what it sits over.\n className={cn(\n \"fixed start-4 z-50 max-w-[min(24rem,calc(100vw-2rem))]\",\n stage === \"idle\" && \"pointer-events-none\",\n className,\n )}\n >\n {stage !== \"idle\" && (\n // `elevated`: OPAQUE in both themes (keksdose live #209) — the notice that\n // explains a stall is read over whatever stalled, and a translucent dark tint\n // let the page's own text show through it. A box banner is role-less, so the\n // wrapper above stays the one live region.\n <AlertBanner\n tone={waking ? \"warning\" : \"neutral\"}\n size=\"sm\"\n elevated\n // `label={null}`: a Spinner is its own `role=\"status\"` saying \"Loading…\" —\n // nested here, a second live region and a second announcement ahead of the\n // words that actually explain the wait. Here it is decoration.\n icon={<Spinner label={null} className=\"h-3.5 w-3.5\" />}\n >\n {waking ? labels.waking(appName) : labels.slow}\n </AlertBanner>\n )}\n </div>\n );\n}\n"],"mappings":";AAoJgB;AApJhB,SAAS,iBAAiB,QAAQ,UAAU,4BAA4B;AAGxE,SAAS,oBAAoB;AAC7B,SAAS,UAAU;AACnB,SAAS,gBAAgB,yBAAyB;AAClD,SAAS,kBAAkB;AAE3B,SAAS,mBAAmB;AAC5B,SAAS,eAAe;AA6BjB,MAAM,6BAA+C;AAAA,EAC1D,MAAM;AAAA,EACN,QAAQ,CAAC,YACP,iDAAiD,WAAW,SAAS;AACzE;AAMO,SAAS,mBAAmB,UAA6B,YAA6B;AAE3F,SAAO,qBAAqB,QAAQ,WAAW,QAAQ,UAAU,MAAM,MAAM;AAC/E;AAyBA,MAAM,KAAK,CAAC,MAAwB,OAAO,MAAM,WAAW,GAAG,CAAC,OAAO;AAGvE,MAAM,YAAY;AAOlB,SAAS,qBAAqB,QAA0C,OAAuC;AAC7G,QAAM,CAAC,WAAW,YAAY,IAAI,SAAwB,IAAI;AAC9D,kBAAgB,MAAM;AACpB,QAAI,UAAU,OAAQ;AACtB,UAAM,UAAU,MAAM,aAAa,eAAe,OAAO,OAAO,CAAC;AACjE,YAAQ;AACR,UAAM,cAAc,kBAAkB,OAAO;AAC7C,WAAO,iBAAiB,UAAU,OAAO;AACzC,WAAO,MAAM;AACX,kBAAY;AACZ,aAAO,oBAAoB,UAAU,OAAO;AAAA,IAC9C;AAAA,EACF,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,SAAO,UAAU,SAAS,OAAO;AACnC;AASO,SAAS,iBAAiB,EAAE,SAAS,SAAS,QAAQ,YAAY,WAAW,UAAU,GAA0B;AACtH,QAAM,SAAS,aAAa,cAAc,4BAA4B,UAAU;AAChF,QAAM,QAAQ,mBAAmB,OAAO;AACxC,QAAM,SAAS,UAAU;AACzB,QAAM,SAAS,OAAuB,IAAI;AAC1C,QAAM,YAAY,qBAAqB,QAAQ,KAAK;AACpD,QAAM,OAAO,YAAY,cAAc,SAAY,0BAA0B,GAAG,SAAS,CAAC;AAC1F,QAAM,QAAuB;AAAA,IAC3B,QAAQ,cAAc,OAAO,OAAO,OAAO,IAAI,UAAU,SAAS,QAAQ,SAAS;AAAA,EACrF;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,MAAK;AAAA,MACL,aAAU;AAAA,MACV,cAAY;AAAA,MACZ;AAAA,MAGA,WAAW;AAAA,QACT;AAAA,QACA,UAAU,UAAU;AAAA,QACpB;AAAA,MACF;AAAA,MAEC,oBAAU;AAAA;AAAA;AAAA;AAAA,MAKT;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,SAAS,YAAY;AAAA,UAC3B,MAAK;AAAA,UACL,UAAQ;AAAA,UAIR,MAAM,oBAAC,WAAQ,OAAO,MAAM,WAAU,eAAc;AAAA,UAEnD,mBAAS,OAAO,OAAO,OAAO,IAAI,OAAO;AAAA;AAAA,MAC5C;AAAA;AAAA,EAEJ;AAEJ;","names":[]}