@eifi1/ui-kit 0.16.0 → 0.17.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.
- package/README.md +7 -7
- package/dist/chart.d.ts +1 -1
- package/dist/components/amount-input.d.ts +9 -1
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/button-group.d.ts +1 -1
- package/dist/components/calculator.d.ts +1 -1
- package/dist/components/copy-button.d.ts +1 -1
- package/dist/components/data-table-cells.d.ts +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +91 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/facing-pair.d.ts +1 -1
- package/dist/components/file-button.d.ts +1 -1
- package/dist/components/file-button.js +7 -4
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +1 -1
- package/dist/components/form-actions.d.ts +1 -1
- package/dist/components/form-actions.js +32 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/line-items.d.ts +30 -6
- package/dist/components/line-items.js +77 -33
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +13 -1
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +1 -1
- package/dist/components/number-field.d.ts +1 -1
- package/dist/components/number-input.d.ts +1 -1
- package/dist/components/numpad-sheet.d.ts +1 -1
- package/dist/components/progress-bar.d.ts +11 -1
- package/dist/components/progress-bar.js +14 -4
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart.d.ts +1 -1
- package/dist/components/settings-fields.d.ts +1 -1
- package/dist/components/signed-amount.d.ts +12 -3
- package/dist/components/signed-amount.js +6 -4
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/text-link.d.ts +1 -1
- package/dist/components/time-input.d.ts +1 -1
- package/dist/components/ui.d.ts +1 -1
- package/dist/components/ui.js +25 -3
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/{data-table-CfVXtCeG.d.ts → data-table-CpkWUJBY.d.ts} +41 -1
- package/dist/data-table.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -1
- package/dist/i18n/defaults.d.ts +1 -1
- package/dist/i18n/kit-labels.d.ts +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +1 -1
- package/dist/i18n/locales/de-CH.d.ts +1 -1
- package/dist/i18n/locales/de-informal.d.ts +1 -1
- package/dist/i18n/locales/de.d.ts +1 -1
- package/dist/i18n/locales/en.d.ts +71 -0
- package/dist/i18n/locales/en.js +95 -0
- package/dist/i18n/locales/en.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +1 -1
- package/dist/i18n/locales/fr.d.ts +1 -1
- package/dist/i18n/locales/hu.d.ts +1 -1
- package/dist/i18n/locales/it.d.ts +1 -1
- package/dist/i18n/locales/zh.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/{kit-labels-DE--LmCk.d.ts → kit-labels-C4uB_zmg.d.ts} +57 -5
- package/dist/rhf/fields.d.ts +1 -1
- package/dist/rhf/form.d.ts +1 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf.d.ts +1 -1
- package/dist/search/global-search.d.ts +22 -4
- package/dist/search/global-search.js +31 -3
- package/dist/search/global-search.js.map +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/shell/app-shell.d.ts +1 -1
- package/dist/shell/top-bar-brand.d.ts +1 -1
- package/dist/shell.d.ts +1 -1
- package/dist/wizard/stepper-nav.d.ts +1 -1
- package/dist/wizard.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/amount-input.tsx +8 -0
- package/src/components/data-table.tsx +165 -3
- package/src/components/file-button.tsx +12 -4
- package/src/components/form-actions.tsx +73 -3
- package/src/components/line-items.tsx +116 -40
- package/src/components/list.tsx +13 -1
- package/src/components/progress-bar.tsx +20 -4
- package/src/components/signed-amount.tsx +16 -5
- package/src/components/ui.tsx +57 -3
- package/src/i18n/locales/en.ts +116 -0
- package/src/rhf/line-items.tsx +2 -1
- package/src/search/global-search.tsx +57 -6
|
@@ -53,6 +53,7 @@ function ProgressBar({
|
|
|
53
53
|
legendValue,
|
|
54
54
|
hint,
|
|
55
55
|
overage,
|
|
56
|
+
sensitive = false,
|
|
56
57
|
labels,
|
|
57
58
|
className,
|
|
58
59
|
as = "div",
|
|
@@ -67,6 +68,7 @@ function ProgressBar({
|
|
|
67
68
|
const text = useKitLabels("progressBar", DEFAULT_PROGRESS_BAR_LABELS, labels);
|
|
68
69
|
const kitLocale = useKitLocale(locale);
|
|
69
70
|
const labelId = useId();
|
|
71
|
+
const priv = sensitive ? "" : void 0;
|
|
70
72
|
const hintId = useId();
|
|
71
73
|
const hintNode = hint != null ? /* @__PURE__ */ jsx("p", { id: hintId, "data-part": "hint", className: "mt-1 text-xs text-[var(--text-muted)]", children: hint }) : null;
|
|
72
74
|
if (maxProp === null) {
|
|
@@ -74,7 +76,7 @@ function ProgressBar({
|
|
|
74
76
|
return /* @__PURE__ */ jsxs(Tag, { ...rest, className: cn("min-w-0", inlineClass, className), "data-state": "unlimited", children: [
|
|
75
77
|
/* @__PURE__ */ jsxs(Tag, { className: "flex items-baseline gap-2 text-sm", children: [
|
|
76
78
|
label != null && /* @__PURE__ */ jsx("span", { id: labelId, className: "min-w-0 text-[var(--text-secondary)]", children: label }),
|
|
77
|
-
/* @__PURE__ */ jsxs("span", { "data-part": "unlimited", className: "ms-auto shrink-0 tabular-nums text-[var(--text-muted)]", children: [
|
|
79
|
+
/* @__PURE__ */ jsxs("span", { "data-part": "unlimited", "data-private": priv, className: "ms-auto shrink-0 tabular-nums text-[var(--text-muted)]", children: [
|
|
78
80
|
shown !== void 0 && `${shown} \xB7 `,
|
|
79
81
|
text.unlimited
|
|
80
82
|
] })
|
|
@@ -111,7 +113,7 @@ function ProgressBar({
|
|
|
111
113
|
const valueText = clamped === void 0 ? void 0 : format(clamped);
|
|
112
114
|
const ariaValueText = stacked && partTexts.length > 0 ? new Intl.ListFormat(kitLocale, { style: "short", type: "unit" }).format(partTexts) : valueText;
|
|
113
115
|
const over = overage && !stacked && value !== void 0 && Number.isFinite(value) && value > max ? value - max : 0;
|
|
114
|
-
const overageNode = over > 0 ? /* @__PURE__ */ jsx("p", { id: `${hintId}-over`, "data-part": "overage", className: "mt-1 text-xs text-[var(--danger)]", children: typeof overage === "function" ? overage(over) : text.overLimit(new Intl.NumberFormat(kitLocale).format(over)) }) : null;
|
|
116
|
+
const overageNode = over > 0 ? /* @__PURE__ */ jsx("p", { id: `${hintId}-over`, "data-part": "overage", "data-private": priv, className: "mt-1 text-xs text-[var(--danger)]", children: typeof overage === "function" ? overage(over) : text.overLimit(new Intl.NumberFormat(kitLocale).format(over)) }) : null;
|
|
115
117
|
const describedBy = [ariaDescribedBy, hintNode && hintId, overageNode && `${hintId}-over`].filter(Boolean).join(" ") || void 0;
|
|
116
118
|
const labelledBy = ariaLabelledBy ?? (label != null ? labelId : void 0);
|
|
117
119
|
const name = labelledBy ? void 0 : ariaLabel ?? (indeterminate ? common.loading : void 0);
|
|
@@ -120,7 +122,7 @@ function ProgressBar({
|
|
|
120
122
|
label != null && /* @__PURE__ */ jsx("span", { id: labelId, className: "min-w-0 text-[var(--text-secondary)]", children: label }),
|
|
121
123
|
showValue && valueText !== void 0 && // Hidden: the same words are the bar's aria-valuetext, and reading them
|
|
122
124
|
// twice in a row is noise.
|
|
123
|
-
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ms-auto shrink-0 tabular-nums text-[var(--text-muted)]", children: valueText })
|
|
125
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, "data-part": "value", "data-private": priv, className: "ms-auto shrink-0 tabular-nums text-[var(--text-muted)]", children: valueText })
|
|
124
126
|
] }),
|
|
125
127
|
/* @__PURE__ */ jsx(
|
|
126
128
|
Tag,
|
|
@@ -195,7 +197,15 @@ function ProgressBar({
|
|
|
195
197
|
}
|
|
196
198
|
),
|
|
197
199
|
seg.label != null && /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[var(--text-secondary)]", children: seg.label }),
|
|
198
|
-
/* @__PURE__ */ jsx(
|
|
200
|
+
/* @__PURE__ */ jsx(
|
|
201
|
+
"span",
|
|
202
|
+
{
|
|
203
|
+
"data-part": "legend-value",
|
|
204
|
+
"data-private": priv,
|
|
205
|
+
className: "ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]",
|
|
206
|
+
children: legendValue ? legendValue(seg, formatPart(seg.value), i) : formatPart(seg.value)
|
|
207
|
+
}
|
|
208
|
+
)
|
|
199
209
|
] }, seg.key ?? i)) }),
|
|
200
210
|
overageNode,
|
|
201
211
|
hintNode
|
|
@@ -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 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 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 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\" 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\" 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 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 className=\"ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]\">\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":";AA8OM,cAsBI,YAtBJ;AA9ON,SAAS,aAAa;AAEtB,SAAS,UAAU;AACnB,SAAS,uBAAuB,cAAc,oBAAoB;AAU3D,MAAM,8BAAiD;AAAA,EAC5D,WAAW;AAAA,EACX,WAAW,CAAC,WAAW,GAAG,MAAM;AAClC;AA2BA,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;AAyHO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,KAAK,UAAU;AAAA,EACf,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;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,KAAK;AAAA,EACL,cAAc;AAAA,EACd,mBAAmB;AAAA,EACnB,oBAAoB;AAAA,EACpB,GAAG;AACL,GAAqB;AAInB,QAAM,MAAM;AACZ,QAAM,cAAc,OAAO,SAAS,sDAAsD;AAC1F,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,OAAO,aAAa,eAAe,6BAA6B,MAAM;AAC5E,QAAM,YAAY,aAAa,MAAM;AACrC,QAAM,UAAU,MAAM;AACtB,QAAM,SAAS,MAAM;AACrB,QAAM,WACJ,QAAQ,OACN,oBAAC,OAAE,IAAI,QAAQ,aAAU,QAAO,WAAU,yCACvC,gBACH,IACE;AAEN,MAAI,YAAY,MAAM;AAGpB,UAAM,QACJ,UAAU,UAAa,CAAC,OAAO,SAAS,KAAK,IACzC,SACA,cACE,YAAY,OAAO,UAAU,GAAG,IAChC,IAAI,KAAK,aAAa,SAAS,EAAE,OAAO,KAAK;AACrD,WACE,qBAAC,OAAK,GAAG,MAAM,WAAW,GAAG,WAAW,aAAa,SAAS,GAAG,cAAW,aAC1E;AAAA,2BAAC,OAAI,WAAU,qCACZ;AAAA,iBAAS,QACR,oBAAC,UAAK,IAAI,SAAS,WAAU,wCAC1B,iBACH;AAAA,QAEF,qBAAC,UAAK,aAAU,aAAY,WAAU,0DACnC;AAAA,oBAAU,UAAa,GAAG,KAAK;AAAA,UAC/B,KAAK;AAAA,WACR;AAAA,SACF;AAAA,MACC;AAAA,OACH;AAAA,EAEJ;AACA,QAAM,MAAM;AAEZ,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;AAG9E,QAAM,QAAQ,UAAU,aAAa,QAAQ,IAAI,CAAC;AAClD,QAAM,WAAW,UAAU,SAAS,OAAO,CAAC,QAAQ,CAAC,IAAI,UAAU,IAAI,CAAC;AACxE,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;AAMN,QAAM,aAAa,CAAC,MAAc;AAChC,QAAI,CAAC,OAAO,SAAS,CAAC,EAAG,QAAO,OAAO,GAAG;AAC1C,QAAI,YAAa,QAAO,YAAY,GAAG,KAAK,GAAG;AAC/C,WAAO,IAAI,KAAK,aAAa,WAAW,EAAE,OAAO,WAAW,uBAAuB,EAAE,CAAC,EAAE;AAAA,MACtF,QAAQ,IAAI,KAAK,IAAI,OAAO;AAAA,IAC9B;AAAA,EACF;AACA,QAAM,YAAY,UACd,SAAS,IAAI,CAAC,QAAQ;AACpB,UAAMA,QAAO,WAAW,IAAI,KAAK;AACjC,WAAO,OAAO,IAAI,UAAU,YAAY,OAAO,IAAI,UAAU,WACzD,OAAO,WAAW,OAAO,IAAI,KAAK,GAAGA,KAAI,IACzCA;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,OACJ,WAAW,CAAC,WAAW,UAAU,UAAa,OAAO,SAAS,KAAK,KAAK,QAAQ,MAAM,QAAQ,MAAM;AACtG,QAAM,cACJ,OAAO,IACL,oBAAC,OAAE,IAAI,GAAG,MAAM,SAAS,aAAU,WAAU,WAAU,qCACpD,iBAAO,YAAY,aAChB,QAAQ,IAAI,IACZ,KAAK,UAAU,IAAI,KAAK,aAAa,SAAS,EAAE,OAAO,IAAI,CAAC,GAClE,IACE;AACN,QAAM,cACJ,CAAC,iBAAiB,YAAY,QAAQ,eAAe,GAAG,MAAM,OAAO,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,KAAK;AAEtG,QAAM,aAAa,mBAAmB,SAAS,OAAO,UAAU;AAChE,QAAM,OAAO,aAAa,SAAa,cAAc,gBAAgB,OAAO,UAAU;AAEtF,SACE,qBAAC,OAAI,WAAW,GAAG,WAAW,aAAa,SAAS,GAAG,cAAY,gBAAgB,kBAAkB,eACjG;AAAA,cAAS,QAAS,aAAa,cAAc,WAC7C,qBAAC,OAAI,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,oBAAkB;AAAA,QAClB,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,OAAI,WAAU,8BACZ,mBAAS;AAAA,YAAI,CAAC,KAAK,MAClB,IAAI,aAAa,OACf;AAAA,cAAC;AAAA;AAAA,gBAEC,aAAU;AAAA,gBACV,WAAW;AAAA,kBACT;AAAA,kBACA,MAAM,CAAC;AAAA,gBACT;AAAA,gBACA,OAAO;AAAA,kBACL,OAAO,GAAG,QAAQ,IAAI,IAAK,UAAU,IAAI,KAAK,IAAI,OAAQ,GAAG;AAAA,kBAC7D,iBAAiB,IAAI,YAAY,SAAY,IAAI;AAAA,gBACnD;AAAA;AAAA,cATK,IAAI,OAAO;AAAA,YAUlB;AAAA,UAEJ,GACF;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,wBAAc,YAAY,KAAK,WAAW,IAAI,KAAK,GAAG,CAAC,IAAI,WAAW,IAAI,KAAK,GAClF;AAAA,SAdO,IAAI,OAAO,CAepB,CACD,GACH;AAAA,IAED;AAAA,IACA;AAAA,KACH;AAEJ;","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\";\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"]}
|
|
@@ -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 {
|
|
8
|
+
import { x as ChartTooltipPlacement } from '../kit-labels-C4uB_zmg.js';
|
|
9
9
|
import 'recharts';
|
|
10
10
|
import './data-table-labels.js';
|
|
11
11
|
import './mini-calendar.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { aP as SelectProps } from '../kit-labels-C4uB_zmg.js';
|
|
4
4
|
import { ThemePreference } from '../theme/theme-store.js';
|
|
5
5
|
import './data-table-labels.js';
|
|
6
6
|
import './mini-calendar.js';
|
|
@@ -61,8 +61,17 @@ declare const DEFAULT_SIGNED_AMOUNT_LABELS: SignedAmountLabels;
|
|
|
61
61
|
interface SignedFormatProps {
|
|
62
62
|
/** ISO 4217 code: format as money. */
|
|
63
63
|
currency?: string;
|
|
64
|
-
/** `"percent"` reads `value` as a RATIO (`0.12` → 12 %), as `StatTile`'s delta does
|
|
64
|
+
/** `"percent"` reads `value` as a RATIO (`0.12` → 12 %), as `StatTile`'s delta does
|
|
65
|
+
* — or as percent POINTS with `ratio={false}`. */
|
|
65
66
|
unit?: "value" | "percent";
|
|
67
|
+
/** With `unit="percent"`: `true` (default) — `value` is a ratio, `0.12` is 12 %.
|
|
68
|
+
* `false` — `value` is already in percent POINTS, `12` is 12 %, as `formatPercent`'s
|
|
69
|
+
* `ratio` option reads it. Backends send points (keksdose's price changes, kastlan's
|
|
70
|
+
* `change_pct`), and without this every site divided by 100 — and wrote its
|
|
71
|
+
* `flatWithin` as a ratio (`0.005`) beside a value it had to convert. With
|
|
72
|
+
* `ratio={false}` the value AND `flatWithin` are in points (`flatWithin={0.5}` is
|
|
73
|
+
* half a point). Ignored for any other unit. */
|
|
74
|
+
ratio?: boolean;
|
|
66
75
|
/** Compact notation: 12 400 → "12K". */
|
|
67
76
|
compact?: boolean;
|
|
68
77
|
/** Fraction digits, as `formatNumber`'s `digits`. */
|
|
@@ -110,7 +119,7 @@ interface SignedAmountProps extends SignedFormatProps, Omit<ComponentPropsWithou
|
|
|
110
119
|
* words instead ("plus CHF 50.00" / "minus CHF 50.00", `signedAmount.*`): a "+" is
|
|
111
120
|
* skipped by most screen readers, and `Intl`'s "−" is read as "dash" or not at all.
|
|
112
121
|
*/
|
|
113
|
-
declare function SignedAmount({ value, currency, unit, compact, digits, format, locale, tone, goodDirection, palette, flatWithin, showPlus, arrow, sensitive, labels, className, ...rest }: SignedAmountProps): react.JSX.Element;
|
|
122
|
+
declare function SignedAmount({ value, currency, unit, ratio, compact, digits, format, locale, tone, goodDirection, palette, flatWithin, showPlus, arrow, sensitive, labels, className, ...rest }: SignedAmountProps): react.JSX.Element;
|
|
114
123
|
interface DeltaProps extends SignedFormatProps, Omit<ComponentPropsWithoutRef<"span">, "children"> {
|
|
115
124
|
/** The change. Above zero is "up". */
|
|
116
125
|
value: number;
|
|
@@ -141,6 +150,6 @@ interface DeltaProps extends SignedFormatProps, Omit<ComponentPropsWithoutRef<"s
|
|
|
141
150
|
* (lease-columns:219, a "+1.25%" chip), the breakdown rows of the rent calculator
|
|
142
151
|
* (breakdown-row:5, a trend icon coloured by hand).
|
|
143
152
|
*/
|
|
144
|
-
declare function Delta({ value, currency, unit, compact, digits, format, locale, goodDirection, palette, flatWithin, label, arrow, sensitive, labels, className, ...rest }: DeltaProps): react.JSX.Element | null;
|
|
153
|
+
declare function Delta({ value, currency, unit, ratio, compact, digits, format, locale, goodDirection, palette, flatWithin, label, arrow, sensitive, labels, className, ...rest }: DeltaProps): react.JSX.Element | null;
|
|
145
154
|
|
|
146
155
|
export { DEFAULT_SIGNED_AMOUNT_LABELS, Delta, type DeltaProps, type GoodDirection, SignedAmount, type SignedAmountLabels, type SignedAmountProps, type SignedAmountTone, type TextTone, Tone, type ToneProps, type VerdictPalette, toneTextClass };
|
|
@@ -27,11 +27,11 @@ const DEFAULT_SIGNED_AMOUNT_LABELS = {
|
|
|
27
27
|
positive: (amount) => `plus ${amount}`,
|
|
28
28
|
negative: (amount) => `minus ${amount}`
|
|
29
29
|
};
|
|
30
|
-
function useMagnitudeFormatter({ currency, unit, compact, digits, format, locale }) {
|
|
30
|
+
function useMagnitudeFormatter({ currency, unit, ratio, compact, digits, format, locale }) {
|
|
31
31
|
const kitLocale = useKitLocale(locale);
|
|
32
32
|
return (magnitude) => {
|
|
33
33
|
if (format) return format(magnitude);
|
|
34
|
-
if (unit === "percent") return formatPercent(magnitude, { locale: kitLocale, compact, digits });
|
|
34
|
+
if (unit === "percent") return formatPercent(magnitude, { locale: kitLocale, compact, digits, ratio });
|
|
35
35
|
if (currency) return formatMoney(magnitude, currency, { locale: kitLocale, compact, digits });
|
|
36
36
|
return formatNumber(magnitude, { locale: kitLocale, compact, digits });
|
|
37
37
|
};
|
|
@@ -58,6 +58,7 @@ function SignedAmount({
|
|
|
58
58
|
value,
|
|
59
59
|
currency,
|
|
60
60
|
unit,
|
|
61
|
+
ratio,
|
|
61
62
|
compact,
|
|
62
63
|
digits,
|
|
63
64
|
format,
|
|
@@ -74,7 +75,7 @@ function SignedAmount({
|
|
|
74
75
|
...rest
|
|
75
76
|
}) {
|
|
76
77
|
const text = useKitLabels("signedAmount", DEFAULT_SIGNED_AMOUNT_LABELS, labels);
|
|
77
|
-
const fmt = useMagnitudeFormatter({ currency, unit, compact, digits, format, locale });
|
|
78
|
+
const fmt = useMagnitudeFormatter({ currency, unit, ratio, compact, digits, format, locale });
|
|
78
79
|
const finite = Number.isFinite(value);
|
|
79
80
|
const dir = finite ? directionOf(value, flatWithin) : "flat";
|
|
80
81
|
const amount = finite ? fmt(Math.abs(value)) : "\u2014";
|
|
@@ -107,6 +108,7 @@ function Delta({
|
|
|
107
108
|
value,
|
|
108
109
|
currency,
|
|
109
110
|
unit,
|
|
111
|
+
ratio,
|
|
110
112
|
compact,
|
|
111
113
|
digits,
|
|
112
114
|
format,
|
|
@@ -122,7 +124,7 @@ function Delta({
|
|
|
122
124
|
...rest
|
|
123
125
|
}) {
|
|
124
126
|
const text = useKitLabels("statTile", DEFAULT_STAT_TILE_LABELS, labels);
|
|
125
|
-
const fmt = useMagnitudeFormatter({ currency, unit, compact, digits, format, locale });
|
|
127
|
+
const fmt = useMagnitudeFormatter({ currency, unit, ratio, compact, digits, format, locale });
|
|
126
128
|
if (!Number.isFinite(value)) return null;
|
|
127
129
|
const amount = fmt(Math.abs(value));
|
|
128
130
|
const dir = directionOf(value, flatWithin);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/signed-amount.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { ArrowDown, ArrowUp, Minus } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { formatMoney, formatNumber, formatPercent } from \"../lib/format\";\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { DEFAULT_STAT_TILE_LABELS } from \"./stat-tile\";\nimport type { StatTileLabels } from \"./stat-tile\";\n\n/**\n * The text colours a figure can carry, token-backed — `StatTile`'s tones, as a class\n * any span can take.\n *\n * - `default` — the primary text colour. `muted` — the secondary, quiet one.\n * - `income` / `expense` / `net` — the money trio (CVD-safe teal / amber / violet).\n * - `success` / `warning` / `danger` / `info` — a verdict: a rent rise is the tenant's\n * bad news (kastlan lease-columns), a vote \"for\" is green (meeting-agenda-tab).\n * - `brand` — the app's accent.\n *\n * Colour is never the only carrier: pair a verdict with an arrow, a word, or a sign.\n */\nexport type TextTone =\n | \"default\"\n | \"muted\"\n | \"brand\"\n | \"income\"\n | \"expense\"\n | \"net\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\";\n\nconst TEXT_TONE: Record<TextTone, string> = {\n default: \"text-[var(--text-primary)]\",\n muted: \"text-[var(--text-muted)]\",\n brand: \"text-[var(--brand)]\",\n income: \"text-[var(--money-income)]\",\n expense: \"text-[var(--money-expense)]\",\n net: \"text-[var(--money-net)]\",\n success: \"text-[var(--success)]\",\n warning: \"text-[var(--warning)]\",\n danger: \"text-[var(--danger)]\",\n info: \"text-[var(--info)]\",\n};\n\n/**\n * The class that colours text in `tone` — for the spans kastlan colours by hand with\n * `text-[var(--danger)]` / `text-emerald-600` (settlement-table, changelog-page,\n * meeting-agenda-tab), so the palette they draw from is the kit's tokens.\n */\nexport function toneTextClass(tone: TextTone): string {\n return TEXT_TONE[tone];\n}\n\nexport interface ToneProps extends ComponentPropsWithoutRef<\"span\"> {\n tone: TextTone;\n}\n\n/** A `<span>` in a {@link TextTone} — `<Tone tone=\"success\">{votesFor}</Tone>`. */\nexport function Tone({ tone, className, ...rest }: ToneProps) {\n return <span {...rest} className={cn(TEXT_TONE[tone], className)} />;\n}\n\n/* ── Signed amounts ──────────────────────────────────────────────────────── */\n\n/** Which way is +1: `up` for a figure where more is good news (income, savings),\n * `down` where less is (a rent the tenant pays, a cost). */\nexport type GoodDirection = \"up\" | \"down\";\n\n/**\n * How a signed figure is coloured:\n * - `signed` (default) — money read by its own sign: above zero `income`, below zero\n * `expense`, zero muted. A balance, a net.\n * - `income` / `expense` / `neutral` — one colour whatever the sign (`neutral` is the\n * text's own colour).\n * - `verdict` — the change judged by {@link SignedAmountProps.goodDirection}: good\n * news `success`, bad news `danger`, zero muted. Setting `goodDirection` implies it.\n */\nexport type SignedAmountTone = \"signed\" | \"income\" | \"expense\" | \"neutral\" | \"verdict\";\n\n/**\n * The colours a VERDICT is painted in (`goodDirection` set, or `tone=\"verdict\"`):\n * - `status` (default) — `success` / `danger`: good news, bad news.\n * - `money` — `income` / `expense` (the `.text-money-pos` / `.text-money-neg` pair):\n * the app's ONE money palette (keksdose dev#434). A grocery price going UP is money\n * going out, so with `goodDirection=\"down\"` it wears the expense colour every other\n * outflow in the app wears — not a red that says \"error\". keksdose coloured its\n * three price-change sites by hand for want of this.\n */\nexport type VerdictPalette = \"status\" | \"money\";\n\n/** What a signed figure says to a screen reader, whose readers disagree about a\n * leading \"+\" (most skip it) and a \"−\" (some read \"dash\", some nothing). */\nexport interface SignedAmountLabels {\n /** A figure above zero. `amount` is formatted and unsigned. */\n positive: (amount: string) => string;\n /** A figure below zero. `amount` is formatted and unsigned. */\n negative: (amount: string) => string;\n}\n\nexport const DEFAULT_SIGNED_AMOUNT_LABELS: SignedAmountLabels = {\n positive: (amount) => `plus ${amount}`,\n negative: (amount) => `minus ${amount}`,\n};\n\ninterface SignedFormatProps {\n /** ISO 4217 code: format as money. */\n currency?: string;\n /** `\"percent\"` reads `value` as a RATIO (`0.12` → 12 %), as `StatTile`'s delta does. */\n unit?: \"value\" | \"percent\";\n /** Compact notation: 12 400 → \"12K\". */\n compact?: boolean;\n /** Fraction digits, as `formatNumber`'s `digits`. */\n digits?: number | { min?: number; max?: number };\n /** Formats the UNSIGNED magnitude, replacing the `Intl` default outright. The sign\n * is the component's. */\n format?: (magnitude: number) => string;\n locale?: string;\n}\n\nfunction useMagnitudeFormatter({ currency, unit, compact, digits, format, locale }: SignedFormatProps) {\n const kitLocale = useKitLocale(locale);\n return (magnitude: number): string => {\n if (format) return format(magnitude);\n if (unit === \"percent\") return formatPercent(magnitude, { locale: kitLocale, compact, digits });\n if (currency) return formatMoney(magnitude, currency, { locale: kitLocale, compact, digits });\n return formatNumber(magnitude, { locale: kitLocale, compact, digits });\n };\n}\n\n/** `flatWithin` widens \"no change\" from exactly zero to a band round it: a price that\n * moved by a fraction of a cent, a ratio that rounds to \"0.0 %\", is not a rise, and an\n * arrow and a colour for it claim one. Inclusive — `|value| ≤ flatWithin` is flat. */\nfunction directionOf(value: number, flatWithin = 0): \"up\" | \"down\" | \"flat\" {\n if (Math.abs(value) <= Math.max(0, flatWithin)) return \"flat\";\n return value > 0 ? \"up\" : \"down\";\n}\n\nfunction verdictClass(good: boolean, palette: VerdictPalette): string {\n if (palette === \"money\") return good ? TEXT_TONE.income : TEXT_TONE.expense;\n return good ? TEXT_TONE.success : TEXT_TONE.danger;\n}\n\nfunction signedToneClass(\n tone: SignedAmountTone,\n dir: \"up\" | \"down\" | \"flat\",\n goodDirection: GoodDirection | undefined,\n palette: VerdictPalette,\n): string {\n if (tone === \"verdict\" || (goodDirection && tone === \"signed\")) {\n if (dir === \"flat\" || !goodDirection) return TEXT_TONE.muted;\n return verdictClass(dir === goodDirection, palette);\n }\n if (tone === \"signed\") return dir === \"flat\" ? TEXT_TONE.muted : dir === \"up\" ? TEXT_TONE.income : TEXT_TONE.expense;\n if (tone === \"neutral\") return \"\";\n return TEXT_TONE[tone];\n}\n\nconst ARROW = { up: ArrowUp, down: ArrowDown, flat: Minus } as const;\n\nexport interface SignedAmountProps\n extends SignedFormatProps,\n Omit<ComponentPropsWithoutRef<\"span\">, \"children\"> {\n value: number;\n /** See {@link SignedAmountTone}. Default `signed`, or `verdict` with `goodDirection`. */\n tone?: SignedAmountTone;\n /** Which way is good news — colours by verdict (`success` / `danger`, or the money\n * pair with `palette=\"money\"`). */\n goodDirection?: GoodDirection;\n /** See {@link VerdictPalette}. Default `status`. Only a verdict reads it. */\n palette?: VerdictPalette;\n /** A magnitude up to which the figure counts as NO change: no sign, no arrow but the\n * flat dash, muted, spoken without \"plus\"/\"minus\" (inclusive; default 0). The digits\n * still print `|value|` — use a band below the display precision, where they round\n * to zero anyway (`flatWithin={0.005}` at two digits). */\n flatWithin?: number;\n /** Print \"+\" before a positive figure (default `true`). A negative always shows its\n * minus; zero shows neither. */\n showPlus?: boolean;\n /** An arrow before the figure (up / down / a dash at zero) — so the direction does not\n * ride on colour alone. Decorative; the words say the same. */\n arrow?: boolean;\n /** Tag the figure `data-private`, for the host's demo-mode blur. */\n sensitive?: boolean;\n labels?: Partial<SignedAmountLabels>;\n}\n\n/**\n * A figure with a sign that matters: a balance, a rent change, a net.\n *\n * kastlan writes it by hand in five places — `{change > 0 ? \"+\" : \"\"}{formatCurrency(\n * change)}` in a span coloured by a ternary (lease-columns:190, settlement-table:58,\n * the tenancy `formatPct`) — and each picks its own colours for the same verdict.\n *\n * The visible figure (sign, digits, arrow) is hidden from assistive tech and spoken as\n * words instead (\"plus CHF 50.00\" / \"minus CHF 50.00\", `signedAmount.*`): a \"+\" is\n * skipped by most screen readers, and `Intl`'s \"−\" is read as \"dash\" or not at all.\n */\nexport function SignedAmount({\n value,\n currency,\n unit,\n compact,\n digits,\n format,\n locale,\n tone = \"signed\",\n goodDirection,\n palette = \"status\",\n flatWithin,\n showPlus = true,\n arrow = false,\n sensitive = false,\n labels,\n className,\n ...rest\n}: SignedAmountProps) {\n const text = useKitLabels(\"signedAmount\", DEFAULT_SIGNED_AMOUNT_LABELS, labels);\n const fmt = useMagnitudeFormatter({ currency, unit, compact, digits, format, locale });\n const finite = Number.isFinite(value);\n const dir = finite ? directionOf(value, flatWithin) : \"flat\";\n const amount = finite ? fmt(Math.abs(value)) : \"—\";\n // U+2212, the typographic minus: the same width as \"+\", so a column of signed figures\n // lines up under `tabular-nums`.\n const sign = dir === \"up\" ? (showPlus ? \"+\" : \"\") : dir === \"down\" ? \"−\" : \"\";\n const spoken = dir === \"up\" ? text.positive(amount) : dir === \"down\" ? text.negative(amount) : amount;\n const Icon = ARROW[dir];\n return (\n <span\n {...rest}\n data-private={sensitive ? \"\" : undefined}\n data-direction={dir}\n className={cn(\n \"relative inline-flex items-center gap-0.5 whitespace-nowrap tabular-nums\",\n signedToneClass(tone, dir, goodDirection, palette),\n className,\n )}\n >\n {arrow && <Icon aria-hidden className=\"size-[1em] shrink-0\" />}\n <span aria-hidden>\n {sign}\n {amount}\n </span>\n <span className=\"sr-only\">{spoken}</span>\n </span>\n );\n}\n\nexport interface DeltaProps extends SignedFormatProps, Omit<ComponentPropsWithoutRef<\"span\">, \"children\"> {\n /** The change. Above zero is \"up\". */\n value: number;\n /** Which way is good news. Unset, the change is stated and not judged (muted); set,\n * it is coloured `success` / `danger` and its sentence says \"(better)\" / \"(worse)\". */\n goodDirection?: GoodDirection;\n /** See {@link VerdictPalette}: `money` paints good news `income` and bad news\n * `expense` instead of `success` / `danger`. Default `status`. */\n palette?: VerdictPalette;\n /** See {@link SignedAmountProps.flatWithin}: up to this magnitude the change is\n * \"unchanged\" — flat dash, muted, no verdict. Default 0. */\n flatWithin?: number;\n /** What it is measured against, after it: \"vs last month\". */\n label?: ReactNode;\n /** Hide the arrow (shown by default, as on a StatTile). At zero the arrow is a flat\n * dash BESIDE the figure (\"– 0.00 %\"), never in place of it. */\n arrow?: boolean;\n sensitive?: boolean;\n /** The `statTile` namespace's direction words — a delta says the same as a tile's. */\n labels?: Partial<Pick<StatTileLabels, \"increase\" | \"decrease\" | \"unchanged\" | \"better\" | \"worse\">>;\n}\n\n/**\n * `StatTile`'s delta on its own: an arrow, the unsigned magnitude, and a sentence for\n * screen readers (\"Up 12% (worse)\"), coloured only when `goodDirection` judges it —\n * users up and cost up are the same arrow until someone says which is good.\n *\n * For a change shown outside a tile: kastlan's rent-change column\n * (lease-columns:219, a \"+1.25%\" chip), the breakdown rows of the rent calculator\n * (breakdown-row:5, a trend icon coloured by hand).\n */\nexport function Delta({\n value,\n currency,\n unit,\n compact,\n digits,\n format,\n locale,\n goodDirection,\n palette = \"status\",\n flatWithin,\n label,\n arrow = true,\n sensitive = false,\n labels,\n className,\n ...rest\n}: DeltaProps) {\n const text = useKitLabels(\"statTile\", DEFAULT_STAT_TILE_LABELS, labels);\n const fmt = useMagnitudeFormatter({ currency, unit, compact, digits, format, locale });\n if (!Number.isFinite(value)) return null;\n const amount = fmt(Math.abs(value));\n const dir = directionOf(value, flatWithin);\n const Icon = ARROW[dir];\n let spoken = dir === \"up\" ? text.increase(amount) : dir === \"down\" ? text.decrease(amount) : text.unchanged;\n let tone = TEXT_TONE.muted;\n if (goodDirection && dir !== \"flat\") {\n const good = dir === goodDirection;\n spoken = good ? text.better(spoken) : text.worse(spoken);\n tone = verdictClass(good, palette);\n }\n return (\n <span\n {...rest}\n data-direction={dir}\n className={cn(\"relative inline-flex flex-wrap items-center gap-x-1.5 tabular-nums\", className)}\n >\n <span data-private={sensitive ? \"\" : undefined} className={cn(\"inline-flex items-center gap-0.5 font-medium\", tone)}>\n {arrow && <Icon className=\"size-[1em] shrink-0\" aria-hidden />}\n {/* The figure at zero too, beside the flat dash (kastlan feedback #48). Before\n this a flat delta with an arrow drew the dash ALONE, as a StatTile does — so\n a rent-change column that printed \"0.00 %\" for an unchanged lease showed a\n bare \"–\" in its place, and a bare dash is what a table prints for a MISSING\n value: \"unchanged\" and \"not known\" looked the same. The Minus icon is the\n neutral indicator, the muted tone the neutral colour, and the figure says\n how much (none) in the column's own digits. A value that is missing never\n reaches here — a non-finite one renders nothing, above. Visible change for\n every `<Delta value={0}>` with the default arrow: \"–\" became \"– 0 %\". */}\n <span aria-hidden>{amount}</span>\n <span className=\"sr-only\">{spoken}</span>\n </span>\n {label != null && <span className=\"text-[var(--text-muted)]\">{label}</span>}\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/signed-amount.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { ArrowDown, ArrowUp, Minus } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { formatMoney, formatNumber, formatPercent } from \"../lib/format\";\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { DEFAULT_STAT_TILE_LABELS } from \"./stat-tile\";\nimport type { StatTileLabels } from \"./stat-tile\";\n\n/**\n * The text colours a figure can carry, token-backed — `StatTile`'s tones, as a class\n * any span can take.\n *\n * - `default` — the primary text colour. `muted` — the secondary, quiet one.\n * - `income` / `expense` / `net` — the money trio (CVD-safe teal / amber / violet).\n * - `success` / `warning` / `danger` / `info` — a verdict: a rent rise is the tenant's\n * bad news (kastlan lease-columns), a vote \"for\" is green (meeting-agenda-tab).\n * - `brand` — the app's accent.\n *\n * Colour is never the only carrier: pair a verdict with an arrow, a word, or a sign.\n */\nexport type TextTone =\n | \"default\"\n | \"muted\"\n | \"brand\"\n | \"income\"\n | \"expense\"\n | \"net\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\";\n\nconst TEXT_TONE: Record<TextTone, string> = {\n default: \"text-[var(--text-primary)]\",\n muted: \"text-[var(--text-muted)]\",\n brand: \"text-[var(--brand)]\",\n income: \"text-[var(--money-income)]\",\n expense: \"text-[var(--money-expense)]\",\n net: \"text-[var(--money-net)]\",\n success: \"text-[var(--success)]\",\n warning: \"text-[var(--warning)]\",\n danger: \"text-[var(--danger)]\",\n info: \"text-[var(--info)]\",\n};\n\n/**\n * The class that colours text in `tone` — for the spans kastlan colours by hand with\n * `text-[var(--danger)]` / `text-emerald-600` (settlement-table, changelog-page,\n * meeting-agenda-tab), so the palette they draw from is the kit's tokens.\n */\nexport function toneTextClass(tone: TextTone): string {\n return TEXT_TONE[tone];\n}\n\nexport interface ToneProps extends ComponentPropsWithoutRef<\"span\"> {\n tone: TextTone;\n}\n\n/** A `<span>` in a {@link TextTone} — `<Tone tone=\"success\">{votesFor}</Tone>`. */\nexport function Tone({ tone, className, ...rest }: ToneProps) {\n return <span {...rest} className={cn(TEXT_TONE[tone], className)} />;\n}\n\n/* ── Signed amounts ──────────────────────────────────────────────────────── */\n\n/** Which way is +1: `up` for a figure where more is good news (income, savings),\n * `down` where less is (a rent the tenant pays, a cost). */\nexport type GoodDirection = \"up\" | \"down\";\n\n/**\n * How a signed figure is coloured:\n * - `signed` (default) — money read by its own sign: above zero `income`, below zero\n * `expense`, zero muted. A balance, a net.\n * - `income` / `expense` / `neutral` — one colour whatever the sign (`neutral` is the\n * text's own colour).\n * - `verdict` — the change judged by {@link SignedAmountProps.goodDirection}: good\n * news `success`, bad news `danger`, zero muted. Setting `goodDirection` implies it.\n */\nexport type SignedAmountTone = \"signed\" | \"income\" | \"expense\" | \"neutral\" | \"verdict\";\n\n/**\n * The colours a VERDICT is painted in (`goodDirection` set, or `tone=\"verdict\"`):\n * - `status` (default) — `success` / `danger`: good news, bad news.\n * - `money` — `income` / `expense` (the `.text-money-pos` / `.text-money-neg` pair):\n * the app's ONE money palette (keksdose dev#434). A grocery price going UP is money\n * going out, so with `goodDirection=\"down\"` it wears the expense colour every other\n * outflow in the app wears — not a red that says \"error\". keksdose coloured its\n * three price-change sites by hand for want of this.\n */\nexport type VerdictPalette = \"status\" | \"money\";\n\n/** What a signed figure says to a screen reader, whose readers disagree about a\n * leading \"+\" (most skip it) and a \"−\" (some read \"dash\", some nothing). */\nexport interface SignedAmountLabels {\n /** A figure above zero. `amount` is formatted and unsigned. */\n positive: (amount: string) => string;\n /** A figure below zero. `amount` is formatted and unsigned. */\n negative: (amount: string) => string;\n}\n\nexport const DEFAULT_SIGNED_AMOUNT_LABELS: SignedAmountLabels = {\n positive: (amount) => `plus ${amount}`,\n negative: (amount) => `minus ${amount}`,\n};\n\ninterface SignedFormatProps {\n /** ISO 4217 code: format as money. */\n currency?: string;\n /** `\"percent\"` reads `value` as a RATIO (`0.12` → 12 %), as `StatTile`'s delta does\n * — or as percent POINTS with `ratio={false}`. */\n unit?: \"value\" | \"percent\";\n /** With `unit=\"percent\"`: `true` (default) — `value` is a ratio, `0.12` is 12 %.\n * `false` — `value` is already in percent POINTS, `12` is 12 %, as `formatPercent`'s\n * `ratio` option reads it. Backends send points (keksdose's price changes, kastlan's\n * `change_pct`), and without this every site divided by 100 — and wrote its\n * `flatWithin` as a ratio (`0.005`) beside a value it had to convert. With\n * `ratio={false}` the value AND `flatWithin` are in points (`flatWithin={0.5}` is\n * half a point). Ignored for any other unit. */\n ratio?: boolean;\n /** Compact notation: 12 400 → \"12K\". */\n compact?: boolean;\n /** Fraction digits, as `formatNumber`'s `digits`. */\n digits?: number | { min?: number; max?: number };\n /** Formats the UNSIGNED magnitude, replacing the `Intl` default outright. The sign\n * is the component's. */\n format?: (magnitude: number) => string;\n locale?: string;\n}\n\nfunction useMagnitudeFormatter({ currency, unit, ratio, compact, digits, format, locale }: SignedFormatProps) {\n const kitLocale = useKitLocale(locale);\n return (magnitude: number): string => {\n if (format) return format(magnitude);\n if (unit === \"percent\") return formatPercent(magnitude, { locale: kitLocale, compact, digits, ratio });\n if (currency) return formatMoney(magnitude, currency, { locale: kitLocale, compact, digits });\n return formatNumber(magnitude, { locale: kitLocale, compact, digits });\n };\n}\n\n/** `flatWithin` widens \"no change\" from exactly zero to a band round it: a price that\n * moved by a fraction of a cent, a ratio that rounds to \"0.0 %\", is not a rise, and an\n * arrow and a colour for it claim one. Inclusive — `|value| ≤ flatWithin` is flat. */\nfunction directionOf(value: number, flatWithin = 0): \"up\" | \"down\" | \"flat\" {\n if (Math.abs(value) <= Math.max(0, flatWithin)) return \"flat\";\n return value > 0 ? \"up\" : \"down\";\n}\n\nfunction verdictClass(good: boolean, palette: VerdictPalette): string {\n if (palette === \"money\") return good ? TEXT_TONE.income : TEXT_TONE.expense;\n return good ? TEXT_TONE.success : TEXT_TONE.danger;\n}\n\nfunction signedToneClass(\n tone: SignedAmountTone,\n dir: \"up\" | \"down\" | \"flat\",\n goodDirection: GoodDirection | undefined,\n palette: VerdictPalette,\n): string {\n if (tone === \"verdict\" || (goodDirection && tone === \"signed\")) {\n if (dir === \"flat\" || !goodDirection) return TEXT_TONE.muted;\n return verdictClass(dir === goodDirection, palette);\n }\n if (tone === \"signed\") return dir === \"flat\" ? TEXT_TONE.muted : dir === \"up\" ? TEXT_TONE.income : TEXT_TONE.expense;\n if (tone === \"neutral\") return \"\";\n return TEXT_TONE[tone];\n}\n\nconst ARROW = { up: ArrowUp, down: ArrowDown, flat: Minus } as const;\n\nexport interface SignedAmountProps\n extends SignedFormatProps,\n Omit<ComponentPropsWithoutRef<\"span\">, \"children\"> {\n value: number;\n /** See {@link SignedAmountTone}. Default `signed`, or `verdict` with `goodDirection`. */\n tone?: SignedAmountTone;\n /** Which way is good news — colours by verdict (`success` / `danger`, or the money\n * pair with `palette=\"money\"`). */\n goodDirection?: GoodDirection;\n /** See {@link VerdictPalette}. Default `status`. Only a verdict reads it. */\n palette?: VerdictPalette;\n /** A magnitude up to which the figure counts as NO change: no sign, no arrow but the\n * flat dash, muted, spoken without \"plus\"/\"minus\" (inclusive; default 0). The digits\n * still print `|value|` — use a band below the display precision, where they round\n * to zero anyway (`flatWithin={0.005}` at two digits). */\n flatWithin?: number;\n /** Print \"+\" before a positive figure (default `true`). A negative always shows its\n * minus; zero shows neither. */\n showPlus?: boolean;\n /** An arrow before the figure (up / down / a dash at zero) — so the direction does not\n * ride on colour alone. Decorative; the words say the same. */\n arrow?: boolean;\n /** Tag the figure `data-private`, for the host's demo-mode blur. */\n sensitive?: boolean;\n labels?: Partial<SignedAmountLabels>;\n}\n\n/**\n * A figure with a sign that matters: a balance, a rent change, a net.\n *\n * kastlan writes it by hand in five places — `{change > 0 ? \"+\" : \"\"}{formatCurrency(\n * change)}` in a span coloured by a ternary (lease-columns:190, settlement-table:58,\n * the tenancy `formatPct`) — and each picks its own colours for the same verdict.\n *\n * The visible figure (sign, digits, arrow) is hidden from assistive tech and spoken as\n * words instead (\"plus CHF 50.00\" / \"minus CHF 50.00\", `signedAmount.*`): a \"+\" is\n * skipped by most screen readers, and `Intl`'s \"−\" is read as \"dash\" or not at all.\n */\nexport function SignedAmount({\n value,\n currency,\n unit,\n ratio,\n compact,\n digits,\n format,\n locale,\n tone = \"signed\",\n goodDirection,\n palette = \"status\",\n flatWithin,\n showPlus = true,\n arrow = false,\n sensitive = false,\n labels,\n className,\n ...rest\n}: SignedAmountProps) {\n const text = useKitLabels(\"signedAmount\", DEFAULT_SIGNED_AMOUNT_LABELS, labels);\n const fmt = useMagnitudeFormatter({ currency, unit, ratio, compact, digits, format, locale });\n const finite = Number.isFinite(value);\n const dir = finite ? directionOf(value, flatWithin) : \"flat\";\n const amount = finite ? fmt(Math.abs(value)) : \"—\";\n // U+2212, the typographic minus: the same width as \"+\", so a column of signed figures\n // lines up under `tabular-nums`.\n const sign = dir === \"up\" ? (showPlus ? \"+\" : \"\") : dir === \"down\" ? \"−\" : \"\";\n const spoken = dir === \"up\" ? text.positive(amount) : dir === \"down\" ? text.negative(amount) : amount;\n const Icon = ARROW[dir];\n return (\n <span\n {...rest}\n data-private={sensitive ? \"\" : undefined}\n data-direction={dir}\n className={cn(\n \"relative inline-flex items-center gap-0.5 whitespace-nowrap tabular-nums\",\n signedToneClass(tone, dir, goodDirection, palette),\n className,\n )}\n >\n {arrow && <Icon aria-hidden className=\"size-[1em] shrink-0\" />}\n <span aria-hidden>\n {sign}\n {amount}\n </span>\n <span className=\"sr-only\">{spoken}</span>\n </span>\n );\n}\n\nexport interface DeltaProps extends SignedFormatProps, Omit<ComponentPropsWithoutRef<\"span\">, \"children\"> {\n /** The change. Above zero is \"up\". */\n value: number;\n /** Which way is good news. Unset, the change is stated and not judged (muted); set,\n * it is coloured `success` / `danger` and its sentence says \"(better)\" / \"(worse)\". */\n goodDirection?: GoodDirection;\n /** See {@link VerdictPalette}: `money` paints good news `income` and bad news\n * `expense` instead of `success` / `danger`. Default `status`. */\n palette?: VerdictPalette;\n /** See {@link SignedAmountProps.flatWithin}: up to this magnitude the change is\n * \"unchanged\" — flat dash, muted, no verdict. Default 0. */\n flatWithin?: number;\n /** What it is measured against, after it: \"vs last month\". */\n label?: ReactNode;\n /** Hide the arrow (shown by default, as on a StatTile). At zero the arrow is a flat\n * dash BESIDE the figure (\"– 0.00 %\"), never in place of it. */\n arrow?: boolean;\n sensitive?: boolean;\n /** The `statTile` namespace's direction words — a delta says the same as a tile's. */\n labels?: Partial<Pick<StatTileLabels, \"increase\" | \"decrease\" | \"unchanged\" | \"better\" | \"worse\">>;\n}\n\n/**\n * `StatTile`'s delta on its own: an arrow, the unsigned magnitude, and a sentence for\n * screen readers (\"Up 12% (worse)\"), coloured only when `goodDirection` judges it —\n * users up and cost up are the same arrow until someone says which is good.\n *\n * For a change shown outside a tile: kastlan's rent-change column\n * (lease-columns:219, a \"+1.25%\" chip), the breakdown rows of the rent calculator\n * (breakdown-row:5, a trend icon coloured by hand).\n */\nexport function Delta({\n value,\n currency,\n unit,\n ratio,\n compact,\n digits,\n format,\n locale,\n goodDirection,\n palette = \"status\",\n flatWithin,\n label,\n arrow = true,\n sensitive = false,\n labels,\n className,\n ...rest\n}: DeltaProps) {\n const text = useKitLabels(\"statTile\", DEFAULT_STAT_TILE_LABELS, labels);\n const fmt = useMagnitudeFormatter({ currency, unit, ratio, compact, digits, format, locale });\n if (!Number.isFinite(value)) return null;\n const amount = fmt(Math.abs(value));\n const dir = directionOf(value, flatWithin);\n const Icon = ARROW[dir];\n let spoken = dir === \"up\" ? text.increase(amount) : dir === \"down\" ? text.decrease(amount) : text.unchanged;\n let tone = TEXT_TONE.muted;\n if (goodDirection && dir !== \"flat\") {\n const good = dir === goodDirection;\n spoken = good ? text.better(spoken) : text.worse(spoken);\n tone = verdictClass(good, palette);\n }\n return (\n <span\n {...rest}\n data-direction={dir}\n className={cn(\"relative inline-flex flex-wrap items-center gap-x-1.5 tabular-nums\", className)}\n >\n <span data-private={sensitive ? \"\" : undefined} className={cn(\"inline-flex items-center gap-0.5 font-medium\", tone)}>\n {arrow && <Icon className=\"size-[1em] shrink-0\" aria-hidden />}\n {/* The figure at zero too, beside the flat dash (kastlan feedback #48). Before\n this a flat delta with an arrow drew the dash ALONE, as a StatTile does — so\n a rent-change column that printed \"0.00 %\" for an unchanged lease showed a\n bare \"–\" in its place, and a bare dash is what a table prints for a MISSING\n value: \"unchanged\" and \"not known\" looked the same. The Minus icon is the\n neutral indicator, the muted tone the neutral colour, and the figure says\n how much (none) in the column's own digits. A value that is missing never\n reaches here — a non-finite one renders nothing, above. Visible change for\n every `<Delta value={0}>` with the default arrow: \"–\" became \"– 0 %\". */}\n <span aria-hidden>{amount}</span>\n <span className=\"sr-only\">{spoken}</span>\n </span>\n {label != null && <span className=\"text-[var(--text-muted)]\">{label}</span>}\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactElement, ComponentPropsWithoutRef, ReactNode, Ref, MouseEvent, KeyboardEvent } from 'react';
|
|
3
3
|
import { LucideIcon } from 'lucide-react';
|
|
4
|
-
import {
|
|
4
|
+
import { aE as KitLinkComponent } from '../kit-labels-C4uB_zmg.js';
|
|
5
5
|
import './data-table-labels.js';
|
|
6
6
|
import './mini-calendar.js';
|
|
7
7
|
import './calendar-heatmap.js';
|
package/dist/components/ui.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'react';
|
|
2
2
|
import './tooltip.js';
|
|
3
|
-
export { B as Button, a as ButtonClassesOptions, b as ButtonLinkProps, c as ButtonProps, d as ButtonSize, e as ButtonTone, f as ButtonVariant, g as Card, h as CardAction, i as CardActionProps, j as CardContent, k as CardContentProps, l as
|
|
3
|
+
export { B as Button, a as ButtonClassesOptions, b as ButtonLinkProps, c as ButtonProps, d as ButtonSize, e as ButtonTone, f as ButtonVariant, g as Card, h as CardAction, i as CardActionProps, j as CardContent, k as CardContentProps, l as CardDensity, m as CardDescription, n as CardDescriptionProps, o as CardFooter, p as CardFooterProps, q as CardHeader, r as CardHeaderProps, s as CardProps, t as CardTitle, u as CardTitleLevel, v as CardTitleProps, w as CardTone, S as DEFAULT_PASSWORD_REVEAL_LABELS, V as DEFAULT_TABS_LABELS, Y as EmptyState, Z as EmptyStateProps, _ as FIELD_BASE, $ as FIELD_DISPLAY, a0 as FIELD_FLOATING_PAD, a1 as FIELD_INVALID, a2 as FIELD_TRIGGER, a3 as FIELD_WRITABLE_LOOK, a4 as FLOATING_INPUT_CLASS, a5 as FLOATING_LABEL_CLASS, a6 as FLOATING_LABEL_STATIC, a7 as FieldChevron, a8 as FieldChevronProps, a9 as FieldHint, aa as FieldHintProps, ab as FieldLabel, ac as FieldLabelProps, ak as FloatingField, al as FloatingFieldProps, au as IconButton, av as IconButtonGlyphSize, aw as IconButtonLinkProps, ax as IconButtonProps, ay as IconButtonSize, az as IconButtonTone, aA as IconButtonToneColor, aB as IconButtonVariant, aC as Input, aD as InputProps, aG as Label, aI as LabelProps, aK as PHONE_QUERY, aL as PasswordRevealLabels, aO as Select, aP as SelectProps, aQ as Spinner, aR as SpinnerProps, aT as TabItem, aU as Tabs, aV as TabsLabels, aW as TabsProps, aX as Textarea, aY as TextareaProps, b4 as buttonClasses, b8 as resolvePasswordRevealLabels } from '../kit-labels-C4uB_zmg.js';
|
|
4
4
|
import '../hooks/use-anchored-rect.js';
|
|
5
5
|
import '../lib/clipping.js';
|
|
6
6
|
import './data-table-labels.js';
|
package/dist/components/ui.js
CHANGED
|
@@ -1016,6 +1016,12 @@ const CARD_PADDING_STRONG = {
|
|
|
1016
1016
|
sm: "p-[11px]",
|
|
1017
1017
|
md: "p-[15px]"
|
|
1018
1018
|
};
|
|
1019
|
+
const CARD_TONE_WASH = {
|
|
1020
|
+
warning: "bg-[image:linear-gradient(var(--warning-bg),var(--warning-bg))]",
|
|
1021
|
+
danger: "bg-[image:linear-gradient(var(--danger-bg),var(--danger-bg))]",
|
|
1022
|
+
info: "bg-[image:linear-gradient(var(--info-bg),var(--info-bg))]",
|
|
1023
|
+
success: "bg-[image:linear-gradient(var(--success-bg),var(--success-bg))]"
|
|
1024
|
+
};
|
|
1019
1025
|
const CARD_TONES_STRONG = {
|
|
1020
1026
|
warning: "border-2 border-[var(--warning)]",
|
|
1021
1027
|
danger: "border-2 border-[var(--danger-border-strong)]",
|
|
@@ -1023,6 +1029,7 @@ const CARD_TONES_STRONG = {
|
|
|
1023
1029
|
success: "border-2 border-[var(--success)]"
|
|
1024
1030
|
};
|
|
1025
1031
|
const CardPaddedContext = createContext(false);
|
|
1032
|
+
const CardDensityContext = createContext("comfortable");
|
|
1026
1033
|
function Card({
|
|
1027
1034
|
className,
|
|
1028
1035
|
children,
|
|
@@ -1030,6 +1037,8 @@ function Card({
|
|
|
1030
1037
|
variant = "default",
|
|
1031
1038
|
tone,
|
|
1032
1039
|
toneStrength = "soft",
|
|
1040
|
+
toneFill = false,
|
|
1041
|
+
density = "comfortable",
|
|
1033
1042
|
padding,
|
|
1034
1043
|
...rest
|
|
1035
1044
|
}) {
|
|
@@ -1042,6 +1051,7 @@ function Card({
|
|
|
1042
1051
|
{
|
|
1043
1052
|
"data-tone": tone,
|
|
1044
1053
|
"data-tone-strength": strong ? "strong" : void 0,
|
|
1054
|
+
"data-density": density === "compact" ? "compact" : void 0,
|
|
1045
1055
|
...rest,
|
|
1046
1056
|
className: cn(
|
|
1047
1057
|
variant === "inset" ? "rounded-md bg-[var(--bg-surface-2)] p-3" : variant === "outline" ? (
|
|
@@ -1056,16 +1066,18 @@ function Card({
|
|
|
1056
1066
|
),
|
|
1057
1067
|
toned && (bordered ? toned.border : toned.fill),
|
|
1058
1068
|
strong && tone && CARD_TONES_STRONG[tone],
|
|
1069
|
+
toneFill && bordered && tone && CARD_TONE_WASH[tone],
|
|
1059
1070
|
toned?.title,
|
|
1060
1071
|
ownPadding && (strong ? CARD_PADDING_STRONG : CARD_PADDING)[ownPadding],
|
|
1061
1072
|
className
|
|
1062
1073
|
),
|
|
1063
|
-
children: /* @__PURE__ */ jsx(CardPaddedContext.Provider, { value: ownPadding !== void 0, children })
|
|
1074
|
+
children: /* @__PURE__ */ jsx(CardPaddedContext.Provider, { value: ownPadding !== void 0, children: /* @__PURE__ */ jsx(CardDensityContext.Provider, { value: density, children }) })
|
|
1064
1075
|
}
|
|
1065
1076
|
);
|
|
1066
1077
|
}
|
|
1067
1078
|
function CardHeader({ className, stackAction = false, ...props }) {
|
|
1068
1079
|
const padded = useContext(CardPaddedContext);
|
|
1080
|
+
const compact = useContext(CardDensityContext) === "compact";
|
|
1069
1081
|
return /* @__PURE__ */ jsx(
|
|
1070
1082
|
"div",
|
|
1071
1083
|
{
|
|
@@ -1085,6 +1097,7 @@ function CardHeader({ className, stackAction = false, ...props }) {
|
|
|
1085
1097
|
// never inherits an ancestor header's two columns.
|
|
1086
1098
|
"grid auto-rows-min items-start gap-1.5 [--card-header-cols:1fr] has-data-[slot=card-action]:[--card-header-cols:1fr_auto] grid-cols-[var(--card-header-cols,1fr)]",
|
|
1087
1099
|
padded ? "px-0 pt-0" : "px-6 pt-6",
|
|
1100
|
+
compact && "gap-0.5",
|
|
1088
1101
|
// One column under `sm`, and the action back into the flow (DOM order, so
|
|
1089
1102
|
// after the description when it is written after it), at the start edge.
|
|
1090
1103
|
// The descendant selector outranks CardAction's own placement classes.
|
|
@@ -1096,14 +1109,23 @@ function CardHeader({ className, stackAction = false, ...props }) {
|
|
|
1096
1109
|
);
|
|
1097
1110
|
}
|
|
1098
1111
|
function CardTitle({ className, as: Tag = "div", ...props }) {
|
|
1099
|
-
|
|
1112
|
+
const compact = useContext(CardDensityContext) === "compact";
|
|
1113
|
+
return /* @__PURE__ */ jsx(
|
|
1114
|
+
Tag,
|
|
1115
|
+
{
|
|
1116
|
+
"data-slot": "card-title",
|
|
1117
|
+
className: cn(compact ? "text-sm font-medium" : "font-semibold", "leading-none", className),
|
|
1118
|
+
...props
|
|
1119
|
+
}
|
|
1120
|
+
);
|
|
1100
1121
|
}
|
|
1101
1122
|
function CardDescription({ className, ...props }) {
|
|
1123
|
+
const compact = useContext(CardDensityContext) === "compact";
|
|
1102
1124
|
return /* @__PURE__ */ jsx(
|
|
1103
1125
|
"div",
|
|
1104
1126
|
{
|
|
1105
1127
|
"data-slot": "card-description",
|
|
1106
|
-
className: cn("text-sm text-[var(--text-muted)]", className),
|
|
1128
|
+
className: cn(compact ? "text-xs" : "text-sm", "text-[var(--text-muted)]", className),
|
|
1107
1129
|
...props
|
|
1108
1130
|
}
|
|
1109
1131
|
);
|