@eifi1/ui-kit 0.15.5 → 0.16.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 +17 -12
- package/dist/chart.d.ts +3 -3
- package/dist/components/amount-input.d.ts +10 -3
- package/dist/components/amount-input.js +165 -156
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +14 -3
- package/dist/components/authed-image.js +32 -23
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/button-group.d.ts +3 -3
- package/dist/components/calculator.d.ts +3 -3
- package/dist/components/copy-button.d.ts +3 -3
- package/dist/components/facing-pair.d.ts +3 -3
- package/dist/components/file-button.d.ts +3 -3
- package/dist/components/file-button.js +64 -31
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +3 -3
- package/dist/components/form-actions.d.ts +4 -4
- package/dist/components/form-actions.js +18 -6
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/line-items.d.ts +53 -2
- package/dist/components/line-items.js +73 -7
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +99 -5
- package/dist/components/list.js +103 -38
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +19 -3
- package/dist/components/loading-state.js +14 -3
- package/dist/components/loading-state.js.map +1 -1
- package/dist/components/money-field.d.ts +3 -3
- package/dist/components/number-field.d.ts +3 -3
- package/dist/components/number-input.d.ts +3 -3
- package/dist/components/number-input.js +4 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +3 -3
- package/dist/components/passkeys-setting.d.ts +30 -10
- package/dist/components/passkeys-setting.js +19 -11
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/progress-bar.d.ts +12 -3
- package/dist/components/progress-bar.js +21 -11
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart.d.ts +3 -3
- package/dist/components/settings-fields.d.ts +3 -3
- package/dist/components/signed-amount.d.ts +28 -4
- package/dist/components/signed-amount.js +17 -8
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/stat-tile.d.ts +31 -2
- package/dist/components/stat-tile.js +37 -6
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +4 -1
- package/dist/components/status-dot.js +20 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +3 -1
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/table.d.ts +18 -13
- package/dist/components/table.js +20 -5
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +3 -3
- package/dist/components/time-input.d.ts +3 -3
- package/dist/components/toggle-legend.d.ts +12 -1
- package/dist/components/toggle-legend.js +6 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/ui.d.ts +3 -3
- package/dist/components/ui.js +149 -45
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +24 -11
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js +6 -5
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +1 -1
- package/dist/feedback/feedback-thread.d.ts +1 -1
- package/dist/{feedback-attachment-BYAHJMOr.d.ts → feedback-attachment-WFiXJ8We.d.ts} +21 -8
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +3 -3
- package/dist/i18n/defaults.d.ts +3 -3
- package/dist/i18n/kit-labels.d.ts +2 -2
- package/dist/i18n/locales/de-CH-informal.d.ts +3 -3
- package/dist/i18n/locales/de-CH.d.ts +3 -3
- package/dist/i18n/locales/de-informal.d.ts +3 -3
- package/dist/i18n/locales/de.d.ts +3 -3
- package/dist/i18n/locales/de.js +1 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +3 -3
- package/dist/i18n/locales/es.js +1 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +3 -3
- package/dist/i18n/locales/fr.js +1 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +3 -3
- package/dist/i18n/locales/hu.js +1 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +3 -3
- package/dist/i18n/locales/it.js +1 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +3 -3
- package/dist/i18n/locales/zh.js +1 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/{kit-labels-BD2F7sOz.d.ts → kit-labels-DE--LmCk.d.ts} +153 -12
- package/dist/lib/format.d.ts +18 -3
- package/dist/lib/format.js +2 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/rhf/fields.d.ts +3 -3
- package/dist/rhf/form.d.ts +3 -3
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf.d.ts +3 -3
- package/dist/shell/app-shell.d.ts +3 -3
- package/dist/shell/top-bar-brand.d.ts +3 -3
- package/dist/shell.d.ts +3 -3
- package/dist/wizard/stepper-nav.d.ts +3 -3
- package/dist/wizard.d.ts +3 -3
- package/package.json +1 -1
- package/src/components/amount-input.tsx +39 -15
- package/src/components/authed-image.tsx +20 -2
- package/src/components/file-button.tsx +77 -27
- package/src/components/form-actions.tsx +86 -8
- package/src/components/line-items.tsx +131 -14
- package/src/components/list.tsx +240 -50
- package/src/components/loading-state.tsx +35 -4
- package/src/components/number-input.tsx +18 -2
- package/src/components/passkeys-setting.tsx +45 -18
- package/src/components/progress-bar.tsx +46 -21
- package/src/components/signed-amount.tsx +47 -8
- package/src/components/stat-tile.tsx +76 -6
- package/src/components/status-dot.tsx +31 -0
- package/src/components/swatch-picker.tsx +3 -1
- package/src/components/table.tsx +42 -15
- package/src/components/toggle-legend.tsx +21 -4
- package/src/components/ui.tsx +273 -63
- package/src/feedback/feedback-attachment.tsx +45 -14
- package/src/feedback/feedback-dialog.tsx +31 -16
- package/src/i18n/locales/de.ts +1 -1
- package/src/i18n/locales/es.ts +1 -1
- package/src/i18n/locales/fr.ts +1 -1
- package/src/i18n/locales/hu.ts +1 -1
- package/src/i18n/locales/it.ts +1 -1
- package/src/i18n/locales/zh.ts +1 -1
- package/src/lib/format.ts +24 -3
- package/src/rhf/line-items.tsx +6 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/progress-bar.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\n/** The words a progress bar adds of its own. */\nexport interface ProgressBarLabels {\n /** Shown after the value when `max={null}`: a quota with no ceiling. */\n unlimited: string;\n /** The `overage` line: how far past `max` the value is. `amount` is formatted. */\n overLimit: (amount: string) => string;\n}\n\nexport const DEFAULT_PROGRESS_BAR_LABELS: ProgressBarLabels = {\n unlimited: \"Unlimited\",\n overLimit: (amount) => `${amount} over the limit`,\n};\n\n/**\n * `income` / `expense` are the money pair (`--money-income` / `--money-expense`), the\n * names `Chip`, `StatTile` and `Sparkline` already use for them: a share bar of what\n * came in or went out is money, and painting it `success` / `warning` said \"good\" and\n * \"careful\" about a figure that is neither. keksdose's report meters (food-group-card,\n * drilldown-modal) sit beside amounts that are already in the money colours.\n */\nexport type ProgressBarTone =\n | \"brand\"\n | \"neutral\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\"\n | \"income\"\n | \"expense\";\n/**\n * Track heights: `sm` 4px, `slim` 6px, `md` 8px, `lg` 12px. `slim` is a word and not a\n * letter because the letters are taken in order — an `xs` at 6px would be THICKER than\n * `sm` — and renaming `sm` would move every bar already on it. 6px is the Slider's\n * track and keksdose's hand-drawn meters (`h-1.5`, landing-visuals), for a bar under a\n * line of text where 8px outweighs the text.\n */\nexport type ProgressBarSize = \"sm\" | \"slim\" | \"md\" | \"lg\";\n\nconst FILL: Record<ProgressBarTone, string> = {\n brand: \"bg-[var(--brand)]\",\n neutral: \"bg-[var(--text-muted)]\",\n success: \"bg-[var(--success)]\",\n warning: \"bg-[var(--warning)]\",\n danger: \"bg-[var(--danger)]\",\n info: \"bg-[var(--info)]\",\n income: \"bg-[var(--money-income)]\",\n expense: \"bg-[var(--money-expense)]\",\n};\n\nconst TRACK_HEIGHT: Record<ProgressBarSize, string> = { sm: \"h-1\", slim: \"h-1.5\", md: \"h-2\", lg: \"h-3\" };\n\n// A segment with no tone of its own takes the next categorical chart colour, so a\n// stacked bar of unnamed parts is still a set of tellable parts rather than one fill.\nconst SERIES_FILL = [\n \"bg-[var(--chart-1)]\",\n \"bg-[var(--chart-2)]\",\n \"bg-[var(--chart-3)]\",\n \"bg-[var(--chart-4)]\",\n \"bg-[var(--chart-5)]\",\n \"bg-[var(--chart-6)]\",\n \"bg-[var(--chart-7)]\",\n \"bg-[var(--chart-8)]\",\n \"bg-[var(--chart-9)]\",\n];\n\n/** Each segment's fill classes: its own classes, else its tone, else (with no `color`)\n * the next chart colour. \"Next\" counts the UNCOLOURED segments only — a segment with\n * its own `className`/`tone`/`color` does not use up `--chart-1`, so an \"Other\" in grey\n * ahead of two plain parts leaves them `--chart-1` and `--chart-2`. */\nfunction segmentFills(segments: ProgressBarSegment[]): Array<string | undefined> {\n let next = 0;\n return segments.map((seg) => {\n if (seg.className) return seg.className;\n if (seg.tone) return FILL[seg.tone];\n return seg.color ? undefined : SERIES_FILL[next++ % SERIES_FILL.length];\n });\n}\n\n/** One part of a stacked bar. See {@link ProgressBarProps.segments}. */\nexport interface ProgressBarSegment {\n /** Its size, on the bar's own `min`–`max` scale. */\n value: number;\n /** A kit tone. Left out (and no `color`/`className`), the next `--chart-N` colour. */\n tone?: ProgressBarTone;\n /** Any CSS colour (`var(--token)`), for a palette the tones do not cover. Wins over `tone`. */\n color?: string;\n /** Classes for the segment's fill — keksdose's `STEP_CLASS` ramp. Wins over `tone`. */\n className?: string;\n /** The part's name: said in the bar's `aria-valuetext` and shown in the legend. */\n label?: ReactNode;\n /** A stable React key, when `label` is not a string. */\n key?: string;\n /**\n * List it in the `legend` but leave it out of the bar — its value, its share of the\n * `aria-valuetext`, and the sum. keksdose's cut card (reports/cut-card:132) names\n * EVERY bucket under its bar, the empty ones included, while the bar draws only the\n * ones with a share: pass the empty buckets as `legendOnly` entries.\n */\n legendOnly?: boolean;\n}\n\nexport interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n /** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way\n * and nobody knows how much is left (an import that reports no count). */\n value?: number;\n min?: number;\n /**\n * The top of the scale (default 100). `null` is a quota with NO ceiling — kastlan's\n * billing usage on an unlimited plan (usage-progress-bar:25): nothing to fill, so no\n * bar is drawn; the label row shows the value and \"Unlimited\" (`progressBar.unlimited`)\n * as plain text. `formatValue` is then called with `max` = `Infinity`; without it,\n * the value is a plain number in the kit's locale.\n */\n max?: number | null;\n /**\n * `progress` (default): a task moving toward completion — `role=\"progressbar\"`.\n * `meter`: a share of a known whole that is not going anywhere (budget used, disk\n * space, one category's slice of spending) — `role=\"meter\"`. keksdose's share bars\n * were progress bars to a screen reader, which reads \"busy\" into a figure that is\n * simply a figure. A meter is never indeterminate; a missing value reads as `min`.\n */\n variant?: \"progress\" | \"meter\";\n /** Visible label above the bar, and its accessible name. Without it, pass\n * `aria-label` (an indeterminate bar with neither is named `common.loading`). */\n label?: ReactNode;\n /** Show the value text at the end of the label row. */\n showValue?: boolean;\n /**\n * The value as words — both the visible `showValue` text and `aria-valuetext`.\n * Default: the fraction as a percentage in the kit's locale (\"42 %\" in French).\n * Pass one for anything that is not a percentage: `(v, max) => \\`${v} of ${max} files\\``.\n */\n formatValue?: (value: number, max: number, min: number) => string;\n tone?: ProgressBarTone;\n size?: ProgressBarSize;\n locale?: string;\n /**\n * Draw a METER OF PARTS: one bar, several fills side by side in order, a 2px gap\n * between them — keksdose's discretionary-spending bar (cut-card:133), a whole\n * split into the steps of a scale. Forces `variant=\"meter\"`: parts of a whole are\n * never \"busy\". `aria-valuenow` is the sum; `aria-valuetext` names every part\n * (\"Rent: 40%, Food: 25%\"), each part's value through `formatValue` and joined in the\n * kit's locale — the words keksdose's hand-drawn bar had to hide from a reader\n * altogether, leaving the legend as the only way in. The sum is clamped to `max`,\n * and parts past it are cut off at the track's end.\n */\n segments?: ProgressBarSegment[];\n /** With `segments`: a list under the bar naming each part with its swatch and value\n * — the table view of the bar, in text tokens rather than the segment colour. */\n legend?: boolean;\n /**\n * What a legend row shows at its end, in place of the part's formatted value — which\n * it is handed, with the segment and its index. keksdose's cut card shows the share\n * AND a private outflow amount per bucket (cut-card:150), which one figure could not:\n * `legendValue={(seg, share) => <>{share} <Amount … /></>}`.\n */\n legendValue?: (segment: ProgressBarSegment, formatted: string, index: number) => ReactNode;\n /** A muted line under the bar — the plan's reset date, \"3 of 5 seats in use\". It\n * describes the bar (`aria-describedby`). */\n hint?: ReactNode;\n /**\n * When `value` is past `max`, say by how much, in a danger-coloured line under the bar\n * (kastlan usage-progress-bar:46): `true` prints `progressBar.overLimit` with the\n * excess as a plain number; a function renders the line itself from the excess (a\n * currency, \"2 seats\"). The bar stays full at `max` either way — colour it with\n * `tone=\"danger\"` if it should read as over. Ignored for `segments` and `max={null}`.\n */\n overage?: boolean | ((over: number) => ReactNode);\n labels?: Partial<ProgressBarLabels>;\n}\n\n/**\n * A horizontal bar that fills toward `max`.\n *\n * keksdose (import progress, budget share bars) and kastlan (upload and job\n * progress) each drew their own div-in-a-div. None had a role, so a reader heard\n * nothing at all; the share bars that did have one claimed to be progress.\n *\n * `aria-valuetext` is always set when there is a value: a bare `aria-valuenow=\"37\"`\n * is read as \"37\" by some readers and \"37 percent\" by others, and neither is right\n * when `max` is 212 files.\n *\n * The indeterminate sweep stops under `prefers-reduced-motion`. It becomes a still,\n * half-strength bar across the whole track — still visibly \"something is happening\",\n * with nothing moving. The sweep is a Tailwind arbitrary `animate-[…]` rather than\n * tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an\n * unlayered rule beats every layered utility, `motion-reduce:animate-none` included.\n */\nexport function ProgressBar({\n value,\n min = 0,\n max: maxProp = 100,\n variant = \"progress\",\n label,\n showValue = false,\n formatValue,\n tone = \"brand\",\n size = \"md\",\n locale,\n segments,\n legend = false,\n legendValue,\n hint,\n overage,\n labels,\n className,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: ProgressBarProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const text = useKitLabels(\"progressBar\", DEFAULT_PROGRESS_BAR_LABELS, labels);\n const kitLocale = useKitLocale(locale);\n const labelId = useId();\n const hintId = useId();\n const hintNode =\n hint != null ? (\n <p id={hintId} data-part=\"hint\" className=\"mt-1 text-xs text-[var(--text-muted)]\">\n {hint}\n </p>\n ) : null;\n\n if (maxProp === null) {\n // No ceiling: no bar, no role — a meter with no maximum is not one. The value and\n // \"Unlimited\" are the content, so they are plain visible text.\n const shown =\n value === undefined || !Number.isFinite(value)\n ? undefined\n : formatValue\n ? formatValue(value, Infinity, min)\n : new Intl.NumberFormat(kitLocale).format(value);\n return (\n <div {...rest} className={cn(\"min-w-0\", className)} data-state=\"unlimited\">\n <div className=\"flex items-baseline gap-2 text-sm\">\n {label != null && (\n <span id={labelId} className=\"min-w-0 text-[var(--text-secondary)]\">\n {label}\n </span>\n )}\n <span data-part=\"unlimited\" className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {shown !== undefined && `${shown} · `}\n {text.unlimited}\n </span>\n </div>\n {hintNode}\n </div>\n );\n }\n const max = maxProp;\n\n const stacked = segments !== undefined;\n const meter = stacked || variant === \"meter\";\n const span = max - min;\n const partValue = (v: number) => (Number.isFinite(v) ? Math.max(0, v - min) : 0);\n // Colours are dealt over EVERY segment, legend-only ones included, so an entry keeps\n // its swatch whether or not it is in the bar this time.\n const fills = stacked ? segmentFills(segments) : [];\n const barParts = stacked ? segments.filter((seg) => !seg.legendOnly) : [];\n const partTotal = stacked ? min + barParts.reduce((sum, seg) => sum + partValue(seg.value), 0) : undefined;\n const format = (v: number) =>\n formatValue\n ? formatValue(v, max, min)\n : new Intl.NumberFormat(kitLocale, { style: \"percent\", maximumFractionDigits: 0 }).format(\n span <= 0 ? 0 : (Math.min(max, Math.max(min, v)) - min) / span,\n );\n const partTexts = stacked\n ? barParts.map((seg) => {\n const text = format(min + partValue(seg.value));\n return typeof seg.label === \"string\" || typeof seg.label === \"number\"\n ? common.fieldValue(String(seg.label), text)\n : text;\n })\n : [];\n const current =\n partTotal ?? (value === undefined || !Number.isFinite(value) ? (meter ? min : undefined) : value);\n const indeterminate = current === undefined;\n const clamped = indeterminate ? undefined : Math.min(max, Math.max(min, current));\n const fraction = clamped === undefined || span <= 0 ? 0 : (clamped - min) / span;\n\n const valueText = clamped === undefined ? undefined : format(clamped);\n const ariaValueText =\n stacked && partTexts.length > 0\n ? new Intl.ListFormat(kitLocale, { style: \"short\", type: \"unit\" }).format(partTexts)\n : valueText;\n\n const over =\n overage && !stacked && value !== undefined && Number.isFinite(value) && value > max ? value - max : 0;\n const overageNode =\n over > 0 ? (\n <p id={`${hintId}-over`} data-part=\"overage\" className=\"mt-1 text-xs text-[var(--danger)]\">\n {typeof overage === \"function\"\n ? overage(over)\n : text.overLimit(new Intl.NumberFormat(kitLocale).format(over))}\n </p>\n ) : null;\n const describedBy =\n [ariaDescribedBy, hintNode && hintId, overageNode && `${hintId}-over`].filter(Boolean).join(\" \") || undefined;\n\n const labelledBy = ariaLabelledBy ?? (label != null ? labelId : undefined);\n const name = labelledBy ? undefined : (ariaLabel ?? (indeterminate ? common.loading : undefined));\n\n return (\n <div className={cn(\"min-w-0\", className)} data-state={indeterminate ? \"indeterminate\" : \"determinate\"}>\n {(label != null || (showValue && valueText !== undefined)) && (\n <div className=\"mb-1 flex items-baseline gap-2 text-sm\">\n {label != null && (\n <span id={labelId} className=\"min-w-0 text-[var(--text-secondary)]\">\n {label}\n </span>\n )}\n {showValue && valueText !== undefined && (\n // Hidden: the same words are the bar's aria-valuetext, and reading them\n // twice in a row is noise.\n <span aria-hidden className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {valueText}\n </span>\n )}\n </div>\n )}\n <div\n {...rest}\n role={meter ? \"meter\" : \"progressbar\"}\n aria-label={name}\n aria-labelledby={labelledBy}\n aria-describedby={describedBy}\n aria-valuemin={indeterminate ? undefined : min}\n aria-valuemax={indeterminate ? undefined : max}\n aria-valuenow={clamped}\n aria-valuetext={ariaValueText}\n aria-busy={!meter && indeterminate ? true : undefined}\n // `relative` + `overflow-hidden`: the indeterminate segment is positioned in\n // here and must not paint past the rounded ends.\n className={cn(\"relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]\", TRACK_HEIGHT[size])}\n >\n {stacked ? (\n // The parts in a flex row with a 2px gap, so two neighbouring steps of one\n // hue are parted by the track rather than by their difference in lightness.\n <div className=\"flex h-full w-full gap-0.5\">\n {segments.map((seg, i) =>\n seg.legendOnly ? null : (\n <div\n key={seg.key ?? i}\n data-part=\"segment\"\n className={cn(\n \"h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none\",\n fills[i],\n )}\n style={{\n width: `${span <= 0 ? 0 : (partValue(seg.value) / span) * 100}%`,\n backgroundColor: seg.className ? undefined : seg.color,\n }}\n />\n ),\n )}\n </div>\n ) : indeterminate ? (\n <div\n data-part=\"fill\"\n className={cn(\n // `left-0`, deliberately physical: the keyframes (tokens.css\n // `indeterminate-sweep`) translate along the physical x axis, and under\n // RTL the same keyframes are run in REVERSE — from the right edge back\n // to the left. A logical `start-0` would park the segment at the right\n // in RTL and the reversed sweep would then start off-screen.\n \"absolute inset-y-0 left-0 w-1/3 rounded-full\",\n \"animate-[indeterminate-sweep_1.4s_ease-in-out_infinite] rtl:[animation-direction:reverse]\",\n \"motion-reduce:w-full motion-reduce:animate-none motion-reduce:opacity-50\",\n FILL[tone],\n )}\n />\n ) : (\n <div\n data-part=\"fill\"\n // Block flow starts at the inline start, so a plain width fills from the\n // right under `dir=\"rtl\"` with nothing extra.\n className={cn(\"h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none\", FILL[tone])}\n style={{ width: `${fraction * 100}%` }}\n />\n )}\n </div>\n {stacked && legend && segments.length > 0 && (\n <ul className=\"mt-2 space-y-1 text-sm\" data-part=\"legend\">\n {segments.map((seg, i) => (\n <li key={seg.key ?? i} className=\"flex items-center gap-2\">\n <span\n aria-hidden\n className={cn(\n \"size-2.5 shrink-0 rounded-sm\",\n fills[i],\n )}\n style={{ backgroundColor: seg.className ? undefined : seg.color }}\n />\n {seg.label != null && (\n <span className=\"min-w-0 flex-1 truncate text-[var(--text-secondary)]\">{seg.label}</span>\n )}\n <span className=\"ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]\">\n {legendValue\n ? legendValue(seg, format(min + partValue(seg.value)), i)\n : format(min + partValue(seg.value))}\n </span>\n </li>\n ))}\n </ul>\n )}\n {overageNode}\n {hintNode}\n </div>\n );\n}\n"],"mappings":"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","names":["text"]}
|
|
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":"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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 { w as ChartTooltipPlacement } from '../kit-labels-DE--LmCk.js';
|
|
9
9
|
import 'recharts';
|
|
10
10
|
import './data-table-labels.js';
|
|
11
11
|
import './mini-calendar.js';
|
|
@@ -37,7 +37,7 @@ import './dialog-frame.js';
|
|
|
37
37
|
import './modal.js';
|
|
38
38
|
import '../hooks/use-search-param-state.js';
|
|
39
39
|
import './measured-grid.js';
|
|
40
|
-
import '../feedback-attachment-
|
|
40
|
+
import '../feedback-attachment-WFiXJ8We.js';
|
|
41
41
|
import '../feedback/feedback-thread.js';
|
|
42
42
|
import '../feedback/feedback-inbox.js';
|
|
43
43
|
import './account-settings-labels.js';
|
|
@@ -45,12 +45,12 @@ import './confirm-dialog.js';
|
|
|
45
45
|
import './floating-panel.js';
|
|
46
46
|
import './bulk-action-bar.js';
|
|
47
47
|
import './list.js';
|
|
48
|
+
import './signed-amount.js';
|
|
48
49
|
import './breadcrumbs.js';
|
|
49
50
|
import './toast.js';
|
|
50
51
|
import './description-list.js';
|
|
51
52
|
import './line-items.js';
|
|
52
53
|
import './progress-bar.js';
|
|
53
|
-
import './signed-amount.js';
|
|
54
54
|
import './error-boundary.js';
|
|
55
55
|
import './authed-image.js';
|
|
56
56
|
import '../hooks/use-authed-src.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { aN as SelectProps } from '../kit-labels-DE--LmCk.js';
|
|
4
4
|
import { ThemePreference } from '../theme/theme-store.js';
|
|
5
5
|
import './data-table-labels.js';
|
|
6
6
|
import './mini-calendar.js';
|
|
@@ -33,7 +33,7 @@ import './dialog-frame.js';
|
|
|
33
33
|
import './modal.js';
|
|
34
34
|
import '../hooks/use-search-param-state.js';
|
|
35
35
|
import './measured-grid.js';
|
|
36
|
-
import '../feedback-attachment-
|
|
36
|
+
import '../feedback-attachment-WFiXJ8We.js';
|
|
37
37
|
import '../feedback/feedback-thread.js';
|
|
38
38
|
import '../feedback/feedback-inbox.js';
|
|
39
39
|
import './account-settings-labels.js';
|
|
@@ -41,12 +41,12 @@ import './confirm-dialog.js';
|
|
|
41
41
|
import './floating-panel.js';
|
|
42
42
|
import './bulk-action-bar.js';
|
|
43
43
|
import './list.js';
|
|
44
|
+
import './signed-amount.js';
|
|
44
45
|
import './breadcrumbs.js';
|
|
45
46
|
import './toast.js';
|
|
46
47
|
import './description-list.js';
|
|
47
48
|
import './line-items.js';
|
|
48
49
|
import './progress-bar.js';
|
|
49
|
-
import './signed-amount.js';
|
|
50
50
|
import './error-boundary.js';
|
|
51
51
|
import './authed-image.js';
|
|
52
52
|
import '../hooks/use-authed-src.js';
|
|
@@ -39,6 +39,16 @@ type GoodDirection = "up" | "down";
|
|
|
39
39
|
* news `success`, bad news `danger`, zero muted. Setting `goodDirection` implies it.
|
|
40
40
|
*/
|
|
41
41
|
type SignedAmountTone = "signed" | "income" | "expense" | "neutral" | "verdict";
|
|
42
|
+
/**
|
|
43
|
+
* The colours a VERDICT is painted in (`goodDirection` set, or `tone="verdict"`):
|
|
44
|
+
* - `status` (default) — `success` / `danger`: good news, bad news.
|
|
45
|
+
* - `money` — `income` / `expense` (the `.text-money-pos` / `.text-money-neg` pair):
|
|
46
|
+
* the app's ONE money palette (keksdose dev#434). A grocery price going UP is money
|
|
47
|
+
* going out, so with `goodDirection="down"` it wears the expense colour every other
|
|
48
|
+
* outflow in the app wears — not a red that says "error". keksdose coloured its
|
|
49
|
+
* three price-change sites by hand for want of this.
|
|
50
|
+
*/
|
|
51
|
+
type VerdictPalette = "status" | "money";
|
|
42
52
|
/** What a signed figure says to a screen reader, whose readers disagree about a
|
|
43
53
|
* leading "+" (most skip it) and a "−" (some read "dash", some nothing). */
|
|
44
54
|
interface SignedAmountLabels {
|
|
@@ -69,8 +79,16 @@ interface SignedAmountProps extends SignedFormatProps, Omit<ComponentPropsWithou
|
|
|
69
79
|
value: number;
|
|
70
80
|
/** See {@link SignedAmountTone}. Default `signed`, or `verdict` with `goodDirection`. */
|
|
71
81
|
tone?: SignedAmountTone;
|
|
72
|
-
/** Which way is good news — colours by verdict (`success` / `danger
|
|
82
|
+
/** Which way is good news — colours by verdict (`success` / `danger`, or the money
|
|
83
|
+
* pair with `palette="money"`). */
|
|
73
84
|
goodDirection?: GoodDirection;
|
|
85
|
+
/** See {@link VerdictPalette}. Default `status`. Only a verdict reads it. */
|
|
86
|
+
palette?: VerdictPalette;
|
|
87
|
+
/** A magnitude up to which the figure counts as NO change: no sign, no arrow but the
|
|
88
|
+
* flat dash, muted, spoken without "plus"/"minus" (inclusive; default 0). The digits
|
|
89
|
+
* still print `|value|` — use a band below the display precision, where they round
|
|
90
|
+
* to zero anyway (`flatWithin={0.005}` at two digits). */
|
|
91
|
+
flatWithin?: number;
|
|
74
92
|
/** Print "+" before a positive figure (default `true`). A negative always shows its
|
|
75
93
|
* minus; zero shows neither. */
|
|
76
94
|
showPlus?: boolean;
|
|
@@ -92,13 +110,19 @@ interface SignedAmountProps extends SignedFormatProps, Omit<ComponentPropsWithou
|
|
|
92
110
|
* words instead ("plus CHF 50.00" / "minus CHF 50.00", `signedAmount.*`): a "+" is
|
|
93
111
|
* skipped by most screen readers, and `Intl`'s "−" is read as "dash" or not at all.
|
|
94
112
|
*/
|
|
95
|
-
declare function SignedAmount({ value, currency, unit, compact, digits, format, locale, tone, goodDirection, showPlus, arrow, sensitive, labels, className, ...rest }: SignedAmountProps): react.JSX.Element;
|
|
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;
|
|
96
114
|
interface DeltaProps extends SignedFormatProps, Omit<ComponentPropsWithoutRef<"span">, "children"> {
|
|
97
115
|
/** The change. Above zero is "up". */
|
|
98
116
|
value: number;
|
|
99
117
|
/** Which way is good news. Unset, the change is stated and not judged (muted); set,
|
|
100
118
|
* it is coloured `success` / `danger` and its sentence says "(better)" / "(worse)". */
|
|
101
119
|
goodDirection?: GoodDirection;
|
|
120
|
+
/** See {@link VerdictPalette}: `money` paints good news `income` and bad news
|
|
121
|
+
* `expense` instead of `success` / `danger`. Default `status`. */
|
|
122
|
+
palette?: VerdictPalette;
|
|
123
|
+
/** See {@link SignedAmountProps.flatWithin}: up to this magnitude the change is
|
|
124
|
+
* "unchanged" — flat dash, muted, no verdict. Default 0. */
|
|
125
|
+
flatWithin?: number;
|
|
102
126
|
/** What it is measured against, after it: "vs last month". */
|
|
103
127
|
label?: ReactNode;
|
|
104
128
|
/** Hide the arrow (shown by default, as on a StatTile). At zero the arrow is a flat
|
|
@@ -117,6 +141,6 @@ interface DeltaProps extends SignedFormatProps, Omit<ComponentPropsWithoutRef<"s
|
|
|
117
141
|
* (lease-columns:219, a "+1.25%" chip), the breakdown rows of the rent calculator
|
|
118
142
|
* (breakdown-row:5, a trend icon coloured by hand).
|
|
119
143
|
*/
|
|
120
|
-
declare function Delta({ value, currency, unit, compact, digits, format, locale, goodDirection, label, arrow, sensitive, labels, className, ...rest }: DeltaProps): react.JSX.Element | null;
|
|
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;
|
|
121
145
|
|
|
122
|
-
export { DEFAULT_SIGNED_AMOUNT_LABELS, Delta, type DeltaProps, type GoodDirection, SignedAmount, type SignedAmountLabels, type SignedAmountProps, type SignedAmountTone, type TextTone, Tone, type ToneProps, toneTextClass };
|
|
146
|
+
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 };
|
|
@@ -36,13 +36,18 @@ function useMagnitudeFormatter({ currency, unit, compact, digits, format, locale
|
|
|
36
36
|
return formatNumber(magnitude, { locale: kitLocale, compact, digits });
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
function directionOf(value) {
|
|
40
|
-
|
|
39
|
+
function directionOf(value, flatWithin = 0) {
|
|
40
|
+
if (Math.abs(value) <= Math.max(0, flatWithin)) return "flat";
|
|
41
|
+
return value > 0 ? "up" : "down";
|
|
41
42
|
}
|
|
42
|
-
function
|
|
43
|
+
function verdictClass(good, palette) {
|
|
44
|
+
if (palette === "money") return good ? TEXT_TONE.income : TEXT_TONE.expense;
|
|
45
|
+
return good ? TEXT_TONE.success : TEXT_TONE.danger;
|
|
46
|
+
}
|
|
47
|
+
function signedToneClass(tone, dir, goodDirection, palette) {
|
|
43
48
|
if (tone === "verdict" || goodDirection && tone === "signed") {
|
|
44
49
|
if (dir === "flat" || !goodDirection) return TEXT_TONE.muted;
|
|
45
|
-
return dir === goodDirection
|
|
50
|
+
return verdictClass(dir === goodDirection, palette);
|
|
46
51
|
}
|
|
47
52
|
if (tone === "signed") return dir === "flat" ? TEXT_TONE.muted : dir === "up" ? TEXT_TONE.income : TEXT_TONE.expense;
|
|
48
53
|
if (tone === "neutral") return "";
|
|
@@ -59,6 +64,8 @@ function SignedAmount({
|
|
|
59
64
|
locale,
|
|
60
65
|
tone = "signed",
|
|
61
66
|
goodDirection,
|
|
67
|
+
palette = "status",
|
|
68
|
+
flatWithin,
|
|
62
69
|
showPlus = true,
|
|
63
70
|
arrow = false,
|
|
64
71
|
sensitive = false,
|
|
@@ -69,7 +76,7 @@ function SignedAmount({
|
|
|
69
76
|
const text = useKitLabels("signedAmount", DEFAULT_SIGNED_AMOUNT_LABELS, labels);
|
|
70
77
|
const fmt = useMagnitudeFormatter({ currency, unit, compact, digits, format, locale });
|
|
71
78
|
const finite = Number.isFinite(value);
|
|
72
|
-
const dir = finite ? directionOf(value) : "flat";
|
|
79
|
+
const dir = finite ? directionOf(value, flatWithin) : "flat";
|
|
73
80
|
const amount = finite ? fmt(Math.abs(value)) : "\u2014";
|
|
74
81
|
const sign = dir === "up" ? showPlus ? "+" : "" : dir === "down" ? "\u2212" : "";
|
|
75
82
|
const spoken = dir === "up" ? text.positive(amount) : dir === "down" ? text.negative(amount) : amount;
|
|
@@ -82,7 +89,7 @@ function SignedAmount({
|
|
|
82
89
|
"data-direction": dir,
|
|
83
90
|
className: cn(
|
|
84
91
|
"relative inline-flex items-center gap-0.5 whitespace-nowrap tabular-nums",
|
|
85
|
-
signedToneClass(tone, dir, goodDirection),
|
|
92
|
+
signedToneClass(tone, dir, goodDirection, palette),
|
|
86
93
|
className
|
|
87
94
|
),
|
|
88
95
|
children: [
|
|
@@ -105,6 +112,8 @@ function Delta({
|
|
|
105
112
|
format,
|
|
106
113
|
locale,
|
|
107
114
|
goodDirection,
|
|
115
|
+
palette = "status",
|
|
116
|
+
flatWithin,
|
|
108
117
|
label,
|
|
109
118
|
arrow = true,
|
|
110
119
|
sensitive = false,
|
|
@@ -116,14 +125,14 @@ function Delta({
|
|
|
116
125
|
const fmt = useMagnitudeFormatter({ currency, unit, compact, digits, format, locale });
|
|
117
126
|
if (!Number.isFinite(value)) return null;
|
|
118
127
|
const amount = fmt(Math.abs(value));
|
|
119
|
-
const dir = directionOf(value);
|
|
128
|
+
const dir = directionOf(value, flatWithin);
|
|
120
129
|
const Icon = ARROW[dir];
|
|
121
130
|
let spoken = dir === "up" ? text.increase(amount) : dir === "down" ? text.decrease(amount) : text.unchanged;
|
|
122
131
|
let tone = TEXT_TONE.muted;
|
|
123
132
|
if (goodDirection && dir !== "flat") {
|
|
124
133
|
const good = dir === goodDirection;
|
|
125
134
|
spoken = good ? text.better(spoken) : text.worse(spoken);
|
|
126
|
-
tone = good
|
|
135
|
+
tone = verdictClass(good, palette);
|
|
127
136
|
}
|
|
128
137
|
return /* @__PURE__ */ jsxs(
|
|
129
138
|
"span",
|
|
@@ -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/** 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\nfunction directionOf(value: number): \"up\" | \"down\" | \"flat\" {\n return value > 0 ? \"up\" : value < 0 ? \"down\" : \"flat\";\n}\n\nfunction signedToneClass(\n tone: SignedAmountTone,\n dir: \"up\" | \"down\" | \"flat\",\n goodDirection: GoodDirection | undefined,\n): string {\n if (tone === \"verdict\" || (goodDirection && tone === \"signed\")) {\n if (dir === \"flat\" || !goodDirection) return TEXT_TONE.muted;\n return dir === goodDirection ? TEXT_TONE.success : TEXT_TONE.danger;\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`). */\n goodDirection?: GoodDirection;\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 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) : \"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),\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 /** 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 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);\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 = good ? TEXT_TONE.success : TEXT_TONE.danger;\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":";AA4DS,cAoJH,YApJG;AA3DT,SAAS,WAAW,SAAS,aAAa;AAC1C,SAAS,UAAU;AACnB,SAAS,aAAa,cAAc,qBAAqB;AACzD,SAAS,cAAc,oBAAoB;AAC3C,SAAS,gCAAgC;AA2BzC,MAAM,YAAsC;AAAA,EAC1C,SAAS;AAAA,EACT,OAAO;AAAA,EACP,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AACR;AAOO,SAAS,cAAc,MAAwB;AACpD,SAAO,UAAU,IAAI;AACvB;AAOO,SAAS,KAAK,EAAE,MAAM,WAAW,GAAG,KAAK,GAAc;AAC5D,SAAO,oBAAC,UAAM,GAAG,MAAM,WAAW,GAAG,UAAU,IAAI,GAAG,SAAS,GAAG;AACpE;AA4BO,MAAM,+BAAmD;AAAA,EAC9D,UAAU,CAAC,WAAW,QAAQ,MAAM;AAAA,EACpC,UAAU,CAAC,WAAW,SAAS,MAAM;AACvC;AAiBA,SAAS,sBAAsB,EAAE,UAAU,MAAM,SAAS,QAAQ,QAAQ,OAAO,GAAsB;AACrG,QAAM,YAAY,aAAa,MAAM;AACrC,SAAO,CAAC,cAA8B;AACpC,QAAI,OAAQ,QAAO,OAAO,SAAS;AACnC,QAAI,SAAS,UAAW,QAAO,cAAc,WAAW,EAAE,QAAQ,WAAW,SAAS,OAAO,CAAC;AAC9F,QAAI,SAAU,QAAO,YAAY,WAAW,UAAU,EAAE,QAAQ,WAAW,SAAS,OAAO,CAAC;AAC5F,WAAO,aAAa,WAAW,EAAE,QAAQ,WAAW,SAAS,OAAO,CAAC;AAAA,EACvE;AACF;AAEA,SAAS,YAAY,OAAuC;AAC1D,SAAO,QAAQ,IAAI,OAAO,QAAQ,IAAI,SAAS;AACjD;AAEA,SAAS,gBACP,MACA,KACA,eACQ;AACR,MAAI,SAAS,aAAc,iBAAiB,SAAS,UAAW;AAC9D,QAAI,QAAQ,UAAU,CAAC,cAAe,QAAO,UAAU;AACvD,WAAO,QAAQ,gBAAgB,UAAU,UAAU,UAAU;AAAA,EAC/D;AACA,MAAI,SAAS,SAAU,QAAO,QAAQ,SAAS,UAAU,QAAQ,QAAQ,OAAO,UAAU,SAAS,UAAU;AAC7G,MAAI,SAAS,UAAW,QAAO;AAC/B,SAAO,UAAU,IAAI;AACvB;AAEA,MAAM,QAAQ,EAAE,IAAI,SAAS,MAAM,WAAW,MAAM,MAAM;AAgCnD,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,OAAO,aAAa,gBAAgB,8BAA8B,MAAM;AAC9E,QAAM,MAAM,sBAAsB,EAAE,UAAU,MAAM,SAAS,QAAQ,QAAQ,OAAO,CAAC;AACrF,QAAM,SAAS,OAAO,SAAS,KAAK;AACpC,QAAM,MAAM,SAAS,YAAY,KAAK,IAAI;AAC1C,QAAM,SAAS,SAAS,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI;AAG/C,QAAM,OAAO,QAAQ,OAAQ,WAAW,MAAM,KAAM,QAAQ,SAAS,WAAM;AAC3E,QAAM,SAAS,QAAQ,OAAO,KAAK,SAAS,MAAM,IAAI,QAAQ,SAAS,KAAK,SAAS,MAAM,IAAI;AAC/F,QAAM,OAAO,MAAM,GAAG;AACtB,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,gBAAc,YAAY,KAAK;AAAA,MAC/B,kBAAgB;AAAA,MAChB,WAAW;AAAA,QACT;AAAA,QACA,gBAAgB,MAAM,KAAK,aAAa;AAAA,QACxC;AAAA,MACF;AAAA,MAEC;AAAA,iBAAS,oBAAC,QAAK,eAAW,MAAC,WAAU,uBAAsB;AAAA,QAC5D,qBAAC,UAAK,eAAW,MACd;AAAA;AAAA,UACA;AAAA,WACH;AAAA,QACA,oBAAC,UAAK,WAAU,WAAW,kBAAO;AAAA;AAAA;AAAA,EACpC;AAEJ;AA2BO,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAe;AACb,QAAM,OAAO,aAAa,YAAY,0BAA0B,MAAM;AACtE,QAAM,MAAM,sBAAsB,EAAE,UAAU,MAAM,SAAS,QAAQ,QAAQ,OAAO,CAAC;AACrF,MAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,QAAM,SAAS,IAAI,KAAK,IAAI,KAAK,CAAC;AAClC,QAAM,MAAM,YAAY,KAAK;AAC7B,QAAM,OAAO,MAAM,GAAG;AACtB,MAAI,SAAS,QAAQ,OAAO,KAAK,SAAS,MAAM,IAAI,QAAQ,SAAS,KAAK,SAAS,MAAM,IAAI,KAAK;AAClG,MAAI,OAAO,UAAU;AACrB,MAAI,iBAAiB,QAAQ,QAAQ;AACnC,UAAM,OAAO,QAAQ;AACrB,aAAS,OAAO,KAAK,OAAO,MAAM,IAAI,KAAK,MAAM,MAAM;AACvD,WAAO,OAAO,UAAU,UAAU,UAAU;AAAA,EAC9C;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,kBAAgB;AAAA,MAChB,WAAW,GAAG,sEAAsE,SAAS;AAAA,MAE7F;AAAA,6BAAC,UAAK,gBAAc,YAAY,KAAK,QAAW,WAAW,GAAG,gDAAgD,IAAI,GAC/G;AAAA,mBAAS,oBAAC,QAAK,WAAU,uBAAsB,eAAW,MAAC;AAAA,UAU5D,oBAAC,UAAK,eAAW,MAAE,kBAAO;AAAA,UAC1B,oBAAC,UAAK,WAAU,WAAW,kBAAO;AAAA,WACpC;AAAA,QACC,SAAS,QAAQ,oBAAC,UAAK,WAAU,4BAA4B,iBAAM;AAAA;AAAA;AAAA,EACtE;AAEJ;","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 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":[]}
|
|
@@ -163,6 +163,16 @@ interface StatTileProps extends Omit<ComponentPropsWithoutRef<"div">, "children"
|
|
|
163
163
|
loading?: boolean;
|
|
164
164
|
/** `"sm"` for dense grids (a metrics wall); `"md"` the default KPI. */
|
|
165
165
|
size?: "sm" | "md";
|
|
166
|
+
/**
|
|
167
|
+
* The tile's frame. `"card"` (default) is the bordered, shadowed card on the page.
|
|
168
|
+
* `"inset"` is a tile INSIDE a card: {@link Card}'s `inset` — the raised
|
|
169
|
+
* `--bg-surface-2`, a small radius, no border or shadow, since the card round it
|
|
170
|
+
* already has them. `"plain"` drops the frame altogether — no fill and no padding —
|
|
171
|
+
* for figures set straight into a card's content (a summary row under a card title),
|
|
172
|
+
* where any box would be a box in a box. keksdose wrote both by hand over the card
|
|
173
|
+
* tile (`border-0 shadow-none bg-transparent p-0`) for its tiles inside a card.
|
|
174
|
+
*/
|
|
175
|
+
variant?: "card" | "inset" | "plain";
|
|
166
176
|
/** Makes the tile a link. See {@link renderLink} for a router. */
|
|
167
177
|
href?: string;
|
|
168
178
|
/** Renders the link for `href` — pass your router's `<Link>` here, since a plain
|
|
@@ -188,11 +198,30 @@ interface StatTileProps extends Omit<ComponentPropsWithoutRef<"div">, "children"
|
|
|
188
198
|
* the hint sits above the overlay and stays its own tab stop. The link's name is the
|
|
189
199
|
* label, and it is described by the value and the delta.
|
|
190
200
|
*/
|
|
191
|
-
declare function StatTile({ label, value, currency, compact, numberFormat, format, locale, unit, valueLabel, tone, delta, goodDirection, hint, truncateLabel, name, description, projection, subValues, trend, sparkline, icon, footer, sensitive, loading, size, href, renderLink, onClick, labels, className, ...rest }: StatTileProps): react.JSX.Element;
|
|
201
|
+
declare function StatTile({ label, value, currency, compact, numberFormat, format, locale, unit, valueLabel, tone, delta, goodDirection, hint, truncateLabel, name, description, projection, subValues, trend, sparkline, icon, footer, sensitive, loading, size, variant, href, renderLink, onClick, labels, className, ...rest }: StatTileProps): react.JSX.Element;
|
|
192
202
|
interface StatTileGridProps extends ComponentPropsWithoutRef<"div"> {
|
|
193
203
|
/** The narrowest a tile may get before the grid drops a column. The default fits
|
|
194
204
|
* two tiles on a 320px phone. */
|
|
195
205
|
minTileWidth?: string;
|
|
206
|
+
/**
|
|
207
|
+
* Let a row that is not full stretch its tiles across the whole width. Default
|
|
208
|
+
* `true` (CSS `auto-fit`): one or two tiles fill the row, as they always have.
|
|
209
|
+
* `false` (`auto-fill`): the empty columns are kept, so every tile is the width of
|
|
210
|
+
* a column whatever the count — a lone "Balance" tile in a card stays tile-sized
|
|
211
|
+
* instead of becoming a banner the width of the card, and the tiles of a short row
|
|
212
|
+
* line up with the columns of a full one above it.
|
|
213
|
+
*/
|
|
214
|
+
stretch?: boolean;
|
|
215
|
+
/**
|
|
216
|
+
* Every tile in the grid is loading: each shows its skeleton (as with its own
|
|
217
|
+
* `loading`), and the grid says "Loading…" ONCE, in a polite status region, instead
|
|
218
|
+
* of each tile carrying its own sr-only "Loading…" — a wall of eight tiles was read
|
|
219
|
+
* as eight "Loading…"s in a row. The grid is `aria-busy` meanwhile. A tile's own
|
|
220
|
+
* `loading` inside a grid that is not loading still speaks for itself.
|
|
221
|
+
*/
|
|
222
|
+
loading?: boolean;
|
|
223
|
+
/** The grid's one sentence — `loading` from {@link StatTileLabels}. */
|
|
224
|
+
labels?: Partial<Pick<StatTileLabels, "loading">>;
|
|
196
225
|
}
|
|
197
226
|
/**
|
|
198
227
|
* The row of tiles every dashboard in the three apps lays out by hand, each with its
|
|
@@ -201,6 +230,6 @@ interface StatTileGridProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
201
230
|
* the band where the sidebar was open. This fits as many columns as `minTileWidth`
|
|
202
231
|
* allows in the space the grid actually has, and needs no breakpoint at all.
|
|
203
232
|
*/
|
|
204
|
-
declare function StatTileGrid({ minTileWidth, className, style, ...rest }: StatTileGridProps): react.JSX.Element;
|
|
233
|
+
declare function StatTileGrid({ minTileWidth, stretch, loading, labels, className, style, children, ...rest }: StatTileGridProps): react.JSX.Element;
|
|
205
234
|
|
|
206
235
|
export { DEFAULT_STAT_TILE_LABELS, StatTile, type StatTileDelta, StatTileGrid, type StatTileGridProps, type StatTileLabels, type StatTileLinkProps, type StatTileProps, type StatTileSubValue, type StatTileTone };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { isValidElement, useId, useLayoutEffect, useState } from "react";
|
|
3
|
+
import { createContext, isValidElement, useContext, useId, useLayoutEffect, useState } from "react";
|
|
4
4
|
import { ArrowDown, ArrowUp, Minus } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
6
|
import { useKitLabels, useKitLink, useKitLocale } from "../i18n/kit-labels.js";
|
|
@@ -75,6 +75,7 @@ function StatTile({
|
|
|
75
75
|
sensitive = false,
|
|
76
76
|
loading = false,
|
|
77
77
|
size = "md",
|
|
78
|
+
variant = "card",
|
|
78
79
|
href,
|
|
79
80
|
renderLink,
|
|
80
81
|
onClick,
|
|
@@ -83,6 +84,8 @@ function StatTile({
|
|
|
83
84
|
...rest
|
|
84
85
|
}) {
|
|
85
86
|
const text = useKitLabels("statTile", DEFAULT_STAT_TILE_LABELS, labels);
|
|
87
|
+
const gridLoading = useContext(StatTileGridLoadingContext);
|
|
88
|
+
loading = loading || gridLoading;
|
|
86
89
|
const kitLink = useKitLink();
|
|
87
90
|
const [labelEl, setLabelEl] = useState(null);
|
|
88
91
|
const labelCut = useIsTruncated(truncateLabel ? labelEl : null);
|
|
@@ -151,7 +154,7 @@ function StatTile({
|
|
|
151
154
|
);
|
|
152
155
|
const headline = loading ? /* @__PURE__ */ jsxs("span", { id: valueId, className: "relative block py-0.5", children: [
|
|
153
156
|
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn(SKELETON_CLASS, "block h-6 w-24 max-w-full") }),
|
|
154
|
-
/* @__PURE__ */ jsx("span", { className: "sr-only", children: text.loading })
|
|
157
|
+
!gridLoading && /* @__PURE__ */ jsx("span", { className: "sr-only", children: text.loading })
|
|
155
158
|
] }) : hasValue ? /* @__PURE__ */ jsxs("span", { id: valueId, "data-private": priv, className: cn(valueClass, valueLabel != null && "ms-auto"), children: [
|
|
156
159
|
show(value),
|
|
157
160
|
unit != null && /* @__PURE__ */ jsx("span", { className: "ms-1 text-[0.7em] font-medium text-[var(--text-muted)]", children: unit })
|
|
@@ -176,13 +179,18 @@ function StatTile({
|
|
|
176
179
|
Card,
|
|
177
180
|
{
|
|
178
181
|
...rest,
|
|
182
|
+
variant: variant === "card" ? "default" : "inset",
|
|
183
|
+
"data-variant": variant === "card" ? void 0 : variant,
|
|
179
184
|
"aria-busy": loading || void 0,
|
|
180
185
|
className: cn(
|
|
181
186
|
// @container: the headline sizes read the tile's width. relative: the stretched
|
|
182
187
|
// link's overlay. overflow-hidden: at pathological widths a figure clips inside
|
|
183
188
|
// the border instead of painting over the neighbouring tile.
|
|
184
189
|
"@container relative flex min-w-0 flex-col overflow-hidden",
|
|
185
|
-
|
|
190
|
+
// `plain` is the inset card with its fill and padding taken away — the figures
|
|
191
|
+
// align with the content edge of the card they sit in. (Not `p-0` over the
|
|
192
|
+
// padding: the padding's `sm:p-4` would still win from `sm` up.)
|
|
193
|
+
variant === "plain" ? "rounded-none bg-transparent p-0" : PADDING[size],
|
|
186
194
|
interactive && "transition-colors hover:border-[var(--border-strong)] hover:bg-[var(--bg-hover)]",
|
|
187
195
|
className
|
|
188
196
|
),
|
|
@@ -259,11 +267,34 @@ function useIsTruncated(el) {
|
|
|
259
267
|
}, [el]);
|
|
260
268
|
return cut;
|
|
261
269
|
}
|
|
262
|
-
|
|
270
|
+
const StatTileGridLoadingContext = createContext(false);
|
|
271
|
+
function StatTileGrid({
|
|
272
|
+
minTileWidth = "9rem",
|
|
273
|
+
stretch = true,
|
|
274
|
+
loading = false,
|
|
275
|
+
labels,
|
|
276
|
+
className,
|
|
277
|
+
style,
|
|
278
|
+
children,
|
|
279
|
+
...rest
|
|
280
|
+
}) {
|
|
281
|
+
const text = useKitLabels("statTile", DEFAULT_STAT_TILE_LABELS, labels);
|
|
263
282
|
const columns = {
|
|
264
|
-
gridTemplateColumns: `repeat(auto-fit, minmax(min(100%, ${minTileWidth}), 1fr))`
|
|
283
|
+
gridTemplateColumns: `repeat(${stretch ? "auto-fit" : "auto-fill"}, minmax(min(100%, ${minTileWidth}), 1fr))`
|
|
265
284
|
};
|
|
266
|
-
return /* @__PURE__ */
|
|
285
|
+
return /* @__PURE__ */ jsxs(
|
|
286
|
+
"div",
|
|
287
|
+
{
|
|
288
|
+
...rest,
|
|
289
|
+
"aria-busy": loading || rest["aria-busy"] || void 0,
|
|
290
|
+
className: cn("relative grid gap-3", className),
|
|
291
|
+
style: { ...columns, ...style },
|
|
292
|
+
children: [
|
|
293
|
+
/* @__PURE__ */ jsx("span", { role: "status", className: "sr-only", children: loading ? text.loading : "" }),
|
|
294
|
+
/* @__PURE__ */ jsx(StatTileGridLoadingContext.Provider, { value: loading, children })
|
|
295
|
+
]
|
|
296
|
+
}
|
|
297
|
+
);
|
|
267
298
|
}
|
|
268
299
|
export {
|
|
269
300
|
DEFAULT_STAT_TILE_LABELS,
|