@eifi1/ui-kit 0.10.0 → 0.11.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 +20 -16
- package/dist/components/alert-banner.d.ts +26 -1
- package/dist/components/alert-banner.js +10 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -0
- package/dist/components/bulk-action-bar.d.ts +51 -5
- package/dist/components/bulk-action-bar.js +88 -41
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +58 -3
- package/dist/components/button-group.js +56 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +2 -0
- package/dist/components/calendar-heatmap.d.ts +124 -0
- package/dist/components/calendar-heatmap.js +295 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- package/dist/components/choice-card.d.ts +34 -2
- package/dist/components/choice-card.js +49 -0
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/copy-button.d.ts +1 -0
- package/dist/components/data-table.js +2 -0
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/field.d.ts +43 -0
- package/dist/components/field.js +52 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +1 -0
- package/dist/components/file-dropzone.d.ts +1 -0
- package/dist/components/floating-panel.d.ts +147 -4
- package/dist/components/floating-panel.js +188 -28
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/list.d.ts +14 -1
- package/dist/components/list.js +7 -1
- package/dist/components/list.js.map +1 -1
- package/dist/components/menu-item.d.ts +11 -1
- package/dist/components/menu-item.js +6 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +70 -5
- package/dist/components/mini-calendar.js +166 -62
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/nav-pills.d.ts +66 -0
- package/dist/components/nav-pills.js +65 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +2 -0
- package/dist/components/number-input.d.ts +2 -0
- package/dist/components/numpad-sheet.d.ts +2 -0
- package/dist/components/progress-bar.d.ts +31 -2
- package/dist/components/progress-bar.js +73 -7
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +4 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +1 -0
- package/dist/components/status-dot.d.ts +6 -2
- package/dist/components/status-dot.js +6 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/table.d.ts +40 -8
- package/dist/components/table.js +41 -7
- package/dist/components/table.js.map +1 -1
- package/dist/components/text.d.ts +8 -1
- package/dist/components/text.js +1 -0
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +1 -0
- package/dist/components/toggle-group.d.ts +15 -0
- package/dist/components/toggle-group.js +23 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +11 -3
- package/dist/components/tooltip.js +2 -0
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +55 -4
- package/dist/components/ui.js +84 -7
- package/dist/components/ui.js.map +1 -1
- package/dist/components/user-avatar.d.ts +25 -3
- package/dist/components/user-avatar.js +32 -3
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -0
- package/dist/i18n/defaults.d.ts +2 -0
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +3 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +2 -0
- package/dist/i18n/locales/de-CH.d.ts +2 -0
- package/dist/i18n/locales/de-informal.d.ts +2 -0
- package/dist/i18n/locales/de.d.ts +2 -0
- package/dist/i18n/locales/de.js +9 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +2 -0
- package/dist/i18n/locales/es.js +9 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +2 -0
- package/dist/i18n/locales/fr.js +9 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +2 -0
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +2 -0
- package/dist/i18n/locales/it.js +9 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +2 -0
- package/dist/i18n/locales/zh.js +9 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +14 -10
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clipping.d.ts +21 -5
- package/dist/lib/clipping.js +3 -0
- package/dist/lib/clipping.js.map +1 -1
- package/dist/lib/dates.d.ts +17 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/rhf/form.d.ts +1 -0
- package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
- package/dist/rhf/use-rhf-wizard-step.js +38 -0
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
- package/dist/rhf.d.ts +2 -0
- package/dist/rhf.js +1 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +73 -3
- package/dist/shell/topbar-action-menu.js +100 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +2 -1
- package/dist/wizard/stepper-nav.d.ts +1 -0
- package/dist/wizard/wizard-context.d.ts +7 -1
- package/dist/wizard/wizard-context.js +4 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/alert-banner.tsx +36 -7
- package/src/components/bulk-action-bar.tsx +163 -57
- package/src/components/button-group.tsx +124 -4
- package/src/components/calendar-heatmap.tsx +504 -0
- package/src/components/choice-card.tsx +97 -1
- package/src/components/data-table.tsx +7 -0
- package/src/components/field.tsx +137 -0
- package/src/components/floating-panel.tsx +366 -15
- package/src/components/list.tsx +28 -2
- package/src/components/menu-item.tsx +22 -1
- package/src/components/mini-calendar.tsx +245 -54
- package/src/components/nav-pills.tsx +145 -0
- package/src/components/progress-bar.tsx +129 -11
- package/src/components/series-chart.tsx +21 -0
- package/src/components/status-dot.tsx +10 -2
- package/src/components/table.tsx +85 -11
- package/src/components/text.tsx +9 -1
- package/src/components/toggle-group.tsx +48 -3
- package/src/components/tooltip.tsx +17 -3
- package/src/components/ui.tsx +149 -11
- package/src/components/user-avatar.tsx +57 -3
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/kit-labels.tsx +2 -0
- package/src/i18n/locales/de.ts +9 -0
- package/src/i18n/locales/es.ts +9 -0
- package/src/i18n/locales/fr.ts +9 -0
- package/src/i18n/locales/hu.ts +9 -0
- package/src/i18n/locales/it.ts +9 -0
- package/src/i18n/locales/zh.ts +9 -0
- package/src/index.ts +5 -0
- package/src/lib/clipping.ts +22 -4
- package/src/lib/dates.ts +37 -0
- package/src/rhf/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +2 -0
- package/src/shell/topbar-action-menu.tsx +239 -49
- package/src/wizard/wizard-context.tsx +9 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
const PILL = "inline-flex items-center gap-1.5 whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-strong)] [&_svg]:size-4 [&_svg]:shrink-0";
|
|
5
|
+
const PILL_SM = "gap-1 px-2 py-1 text-xs [&_svg]:size-3.5";
|
|
6
|
+
const PILL_CURRENT = "bg-[var(--bg-inverse)] text-[var(--text-inverse)]";
|
|
7
|
+
const PILL_IDLE = "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]";
|
|
8
|
+
const PILL_DISABLED = "cursor-not-allowed opacity-50 hover:bg-transparent hover:text-[var(--text-secondary)]";
|
|
9
|
+
function NavPills({
|
|
10
|
+
items,
|
|
11
|
+
current,
|
|
12
|
+
onSelect,
|
|
13
|
+
renderLink,
|
|
14
|
+
currentType,
|
|
15
|
+
size = "md",
|
|
16
|
+
landmark = true,
|
|
17
|
+
itemClassName,
|
|
18
|
+
className,
|
|
19
|
+
...rest
|
|
20
|
+
}) {
|
|
21
|
+
const Root = landmark ? "nav" : "div";
|
|
22
|
+
return /* @__PURE__ */ jsx(Root, { ...rest, role: landmark ? void 0 : "group", className: cn("min-w-0", className), children: /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-wrap gap-1 p-0", children: items.map((item) => {
|
|
23
|
+
const isCurrent = item.value === current;
|
|
24
|
+
const Icon = item.icon;
|
|
25
|
+
const pillClass = cn(
|
|
26
|
+
PILL,
|
|
27
|
+
size === "sm" && PILL_SM,
|
|
28
|
+
isCurrent ? PILL_CURRENT : PILL_IDLE,
|
|
29
|
+
item.disabled && item.href === void 0 && PILL_DISABLED,
|
|
30
|
+
itemClassName
|
|
31
|
+
);
|
|
32
|
+
const children = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
33
|
+
Icon && /* @__PURE__ */ jsx(Icon, { "aria-hidden": true }),
|
|
34
|
+
item.label
|
|
35
|
+
] });
|
|
36
|
+
let pill;
|
|
37
|
+
if (item.href !== void 0) {
|
|
38
|
+
const props = {
|
|
39
|
+
href: item.href,
|
|
40
|
+
className: pillClass,
|
|
41
|
+
children,
|
|
42
|
+
"aria-current": isCurrent ? currentType ?? "page" : void 0,
|
|
43
|
+
onClick: () => onSelect?.(item.value)
|
|
44
|
+
};
|
|
45
|
+
pill = renderLink ? renderLink(props) : /* @__PURE__ */ jsx("a", { href: props.href, className: props.className, "aria-current": props["aria-current"], onClick: props.onClick, children: props.children });
|
|
46
|
+
} else {
|
|
47
|
+
pill = /* @__PURE__ */ jsx(
|
|
48
|
+
"button",
|
|
49
|
+
{
|
|
50
|
+
type: "button",
|
|
51
|
+
disabled: item.disabled,
|
|
52
|
+
"aria-current": isCurrent ? currentType ?? "true" : void 0,
|
|
53
|
+
onClick: () => onSelect?.(item.value),
|
|
54
|
+
className: pillClass,
|
|
55
|
+
children
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return /* @__PURE__ */ jsx("li", { className: "flex", children: pill }, item.value);
|
|
60
|
+
}) }) });
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
NavPills
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=nav-pills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/nav-pills.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ComponentType, MouseEvent, ReactElement, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\n\nexport interface NavPillItem<T extends string> {\n value: T;\n label: ReactNode;\n /** Where the pill leads. With one it is a link (see `renderLink`); without, a button\n * that calls `onSelect`. */\n href?: string;\n /** A Lucide icon (or any component taking a `className`), before the label. */\n icon?: ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>;\n /** Buttons only — a link cannot be disabled. */\n disabled?: boolean;\n}\n\n/** What {@link NavPillsProps.renderLink} is handed. Spread it onto your router's link\n * — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface NavPillLinkProps {\n href: string;\n className: string;\n children: ReactNode;\n \"aria-current\": NavPillsCurrent | undefined;\n onClick: (e: MouseEvent<HTMLAnchorElement>) => void;\n}\n\n/** The `aria-current` token the current pill carries. */\nexport type NavPillsCurrent = \"page\" | \"step\" | \"location\" | \"true\";\n\nexport interface NavPillsProps<T extends string> extends Omit<ComponentPropsWithoutRef<\"nav\">, \"children\" | \"onSelect\"> {\n items: NavPillItem<T>[];\n /** The pill that is current. `null` or a value not in `items`: none is. */\n current: T | null;\n /** Called with the pill's value on a click — of a button, and of a link too (before\n * the router follows it), for a caller that keeps the choice in state as well. */\n onSelect?: (value: T) => void;\n /** Your router's link. Default `<a>`. The API `Breadcrumbs`, `Chip` and `ListItem` share. */\n renderLink?: (props: NavPillLinkProps) => ReactElement;\n /** What `aria-current` says on the current pill. Default `page` where the items\n * are links and `true` where they are buttons (keksdose's swipe-surface picker\n * switches a panel on the same page, which is not a page). */\n currentType?: NavPillsCurrent;\n /** `sm`: 12px pills — the compact size of a {@link ToggleGroup}. Default `md`. */\n size?: \"sm\" | \"md\";\n /** Render a `<nav>` landmark (default) or a plain `<div role=\"group\">` for a picker\n * that is not the page's navigation. Name either with `aria-label`. */\n landmark?: boolean;\n /** Classes for every pill. */\n itemClassName?: string;\n}\n\n// The pill is a ToggleGroup segment's (`rounded`, `px-3 py-1.5`, the inverse fill for\n// the current one), so a strip of these beside a segmented control reads as the same\n// family. Unlike the segments, a pill never truncates: the strip WRAPS instead.\nconst PILL =\n \"inline-flex items-center gap-1.5 whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-strong)] [&_svg]:size-4 [&_svg]:shrink-0\";\nconst PILL_SM = \"gap-1 px-2 py-1 text-xs [&_svg]:size-3.5\";\nconst PILL_CURRENT = \"bg-[var(--bg-inverse)] text-[var(--text-inverse)]\";\nconst PILL_IDLE = \"text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]\";\nconst PILL_DISABLED = \"cursor-not-allowed opacity-50 hover:bg-transparent hover:text-[var(--text-secondary)]\";\n\n/**\n * A set of links or buttons that WRAPS onto as many rows as it needs, the current one\n * filled and marked `aria-current`.\n *\n * keksdose's swipe-settings card (swipe-settings-card:152) hand-rolled this: four\n * surface names that do not fit a 406px phone, and a sideways scroll would hide the\n * ones that do not fit behind a gesture nothing announces (live #332: *\"Have the tabs\n * in more rows, rather than scrolling sideways\"*). It is not {@link Tabs} with `wrap`\n * because its items are not tabs: a `role=\"tab\"` promises a tabpanel, a roving tab\n * stop and arrow keys, and a set of links to pages (or of buttons that switch the rest\n * of a card) promises none of that. So every pill is its own tab stop, the set is a\n * list (a screen reader hears \"list, 4 items\"), and \"current\" is `aria-current` — the\n * attribute a link to where you already are carries.\n */\nexport function NavPills<T extends string>({\n items,\n current,\n onSelect,\n renderLink,\n currentType,\n size = \"md\",\n landmark = true,\n itemClassName,\n className,\n ...rest\n}: NavPillsProps<T>) {\n const Root = landmark ? \"nav\" : \"div\";\n return (\n <Root {...rest} role={landmark ? undefined : \"group\"} className={cn(\"min-w-0\", className)}>\n <ul className=\"m-0 flex list-none flex-wrap gap-1 p-0\">\n {items.map((item) => {\n const isCurrent = item.value === current;\n const Icon = item.icon;\n const pillClass = cn(\n PILL,\n size === \"sm\" && PILL_SM,\n isCurrent ? PILL_CURRENT : PILL_IDLE,\n item.disabled && item.href === undefined && PILL_DISABLED,\n itemClassName,\n );\n const children = (\n <>\n {Icon && <Icon aria-hidden />}\n {item.label}\n </>\n );\n let pill: ReactNode;\n if (item.href !== undefined) {\n const props: NavPillLinkProps = {\n href: item.href,\n className: pillClass,\n children,\n \"aria-current\": isCurrent ? (currentType ?? \"page\") : undefined,\n onClick: () => onSelect?.(item.value),\n };\n pill = renderLink ? (\n renderLink(props)\n ) : (\n <a href={props.href} className={props.className} aria-current={props[\"aria-current\"]} onClick={props.onClick}>\n {props.children}\n </a>\n );\n } else {\n pill = (\n <button\n type=\"button\"\n disabled={item.disabled}\n aria-current={isCurrent ? (currentType ?? \"true\") : undefined}\n onClick={() => onSelect?.(item.value)}\n className={pillClass}\n >\n {children}\n </button>\n );\n }\n return (\n <li key={item.value} className=\"flex\">\n {pill}\n </li>\n );\n })}\n </ul>\n </Root>\n );\n}\n"],"mappings":";AAqGY,mBACW,KADX;AApGZ,SAAS,UAAU;AAoDnB,MAAM,OACJ;AACF,MAAM,UAAU;AAChB,MAAM,eAAe;AACrB,MAAM,YAAY;AAClB,MAAM,gBAAgB;AAgBf,SAAS,SAA2B;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,OAAO,WAAW,QAAQ;AAChC,SACE,oBAAC,QAAM,GAAG,MAAM,MAAM,WAAW,SAAY,SAAS,WAAW,GAAG,WAAW,SAAS,GACtF,8BAAC,QAAG,WAAU,0CACX,gBAAM,IAAI,CAAC,SAAS;AACnB,UAAM,YAAY,KAAK,UAAU;AACjC,UAAM,OAAO,KAAK;AAClB,UAAM,YAAY;AAAA,MAChB;AAAA,MACA,SAAS,QAAQ;AAAA,MACjB,YAAY,eAAe;AAAA,MAC3B,KAAK,YAAY,KAAK,SAAS,UAAa;AAAA,MAC5C;AAAA,IACF;AACA,UAAM,WACJ,iCACG;AAAA,cAAQ,oBAAC,QAAK,eAAW,MAAC;AAAA,MAC1B,KAAK;AAAA,OACR;AAEF,QAAI;AACJ,QAAI,KAAK,SAAS,QAAW;AAC3B,YAAM,QAA0B;AAAA,QAC9B,MAAM,KAAK;AAAA,QACX,WAAW;AAAA,QACX;AAAA,QACA,gBAAgB,YAAa,eAAe,SAAU;AAAA,QACtD,SAAS,MAAM,WAAW,KAAK,KAAK;AAAA,MACtC;AACA,aAAO,aACL,WAAW,KAAK,IAEhB,oBAAC,OAAE,MAAM,MAAM,MAAM,WAAW,MAAM,WAAW,gBAAc,MAAM,cAAc,GAAG,SAAS,MAAM,SAClG,gBAAM,UACT;AAAA,IAEJ,OAAO;AACL,aACE;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,UAAU,KAAK;AAAA,UACf,gBAAc,YAAa,eAAe,SAAU;AAAA,UACpD,SAAS,MAAM,WAAW,KAAK,KAAK;AAAA,UACpC,WAAW;AAAA,UAEV;AAAA;AAAA,MACH;AAAA,IAEJ;AACA,WACE,oBAAC,QAAoB,WAAU,QAC5B,kBADM,KAAK,KAEd;AAAA,EAEJ,CAAC,GACH,GACF;AAEJ;","names":[]}
|
|
@@ -5,6 +5,7 @@ import './calculator.js';
|
|
|
5
5
|
import '../i18n/kit-labels.js';
|
|
6
6
|
import './data-table-labels.js';
|
|
7
7
|
import './mini-calendar.js';
|
|
8
|
+
import './calendar-heatmap.js';
|
|
8
9
|
import './popover.js';
|
|
9
10
|
import './chip.js';
|
|
10
11
|
import 'lucide-react';
|
|
@@ -12,6 +13,7 @@ import './field-sync.js';
|
|
|
12
13
|
import './ui.js';
|
|
13
14
|
import './tooltip.js';
|
|
14
15
|
import '../hooks/use-anchored-rect.js';
|
|
16
|
+
import '../lib/clipping.js';
|
|
15
17
|
import '../wizard/types.js';
|
|
16
18
|
import '../tour/tour.js';
|
|
17
19
|
import '../search/command-palette.js';
|
|
@@ -5,6 +5,7 @@ import { NumberPadSheetLabels } from './numpad-sheet.js';
|
|
|
5
5
|
import '../i18n/kit-labels.js';
|
|
6
6
|
import './data-table-labels.js';
|
|
7
7
|
import './mini-calendar.js';
|
|
8
|
+
import './calendar-heatmap.js';
|
|
8
9
|
import './popover.js';
|
|
9
10
|
import './chip.js';
|
|
10
11
|
import 'lucide-react';
|
|
@@ -12,6 +13,7 @@ import './field-sync.js';
|
|
|
12
13
|
import './ui.js';
|
|
13
14
|
import './tooltip.js';
|
|
14
15
|
import '../hooks/use-anchored-rect.js';
|
|
16
|
+
import '../lib/clipping.js';
|
|
15
17
|
import '../wizard/types.js';
|
|
16
18
|
import '../tour/tour.js';
|
|
17
19
|
import '../search/command-palette.js';
|
|
@@ -3,6 +3,7 @@ import { ReactNode } from 'react';
|
|
|
3
3
|
import { CalculatorLabels } from '../i18n/kit-labels.js';
|
|
4
4
|
import './data-table-labels.js';
|
|
5
5
|
import './mini-calendar.js';
|
|
6
|
+
import './calendar-heatmap.js';
|
|
6
7
|
import './popover.js';
|
|
7
8
|
import './chip.js';
|
|
8
9
|
import 'lucide-react';
|
|
@@ -10,6 +11,7 @@ import './field-sync.js';
|
|
|
10
11
|
import './ui.js';
|
|
11
12
|
import './tooltip.js';
|
|
12
13
|
import '../hooks/use-anchored-rect.js';
|
|
14
|
+
import '../lib/clipping.js';
|
|
13
15
|
import '../wizard/types.js';
|
|
14
16
|
import '../tour/tour.js';
|
|
15
17
|
import '../search/command-palette.js';
|
|
@@ -17,6 +17,21 @@ type ProgressBarTone = "brand" | "neutral" | "success" | "warning" | "danger" |
|
|
|
17
17
|
* line of text where 8px outweighs the text.
|
|
18
18
|
*/
|
|
19
19
|
type ProgressBarSize = "sm" | "slim" | "md" | "lg";
|
|
20
|
+
/** One part of a stacked bar. See {@link ProgressBarProps.segments}. */
|
|
21
|
+
interface ProgressBarSegment {
|
|
22
|
+
/** Its size, on the bar's own `min`–`max` scale. */
|
|
23
|
+
value: number;
|
|
24
|
+
/** A kit tone. Left out (and no `color`/`className`), the next `--chart-N` colour. */
|
|
25
|
+
tone?: ProgressBarTone;
|
|
26
|
+
/** Any CSS colour (`var(--token)`), for a palette the tones do not cover. Wins over `tone`. */
|
|
27
|
+
color?: string;
|
|
28
|
+
/** Classes for the segment's fill — keksdose's `STEP_CLASS` ramp. Wins over `tone`. */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** The part's name: said in the bar's `aria-valuetext` and shown in the legend. */
|
|
31
|
+
label?: ReactNode;
|
|
32
|
+
/** A stable React key, when `label` is not a string. */
|
|
33
|
+
key?: string;
|
|
34
|
+
}
|
|
20
35
|
interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
|
|
21
36
|
/** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way
|
|
22
37
|
* and nobody knows how much is left (an import that reports no count). */
|
|
@@ -45,6 +60,20 @@ interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
|
|
|
45
60
|
tone?: ProgressBarTone;
|
|
46
61
|
size?: ProgressBarSize;
|
|
47
62
|
locale?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Draw a METER OF PARTS: one bar, several fills side by side in order, a 2px gap
|
|
65
|
+
* between them — keksdose's discretionary-spending bar (cut-card:133), a whole
|
|
66
|
+
* split into the steps of a scale. Forces `variant="meter"`: parts of a whole are
|
|
67
|
+
* never "busy". `aria-valuenow` is the sum; `aria-valuetext` names every part
|
|
68
|
+
* ("Rent: 40%, Food: 25%"), each part's value through `formatValue` and joined in the
|
|
69
|
+
* kit's locale — the words keksdose's hand-drawn bar had to hide from a reader
|
|
70
|
+
* altogether, leaving the legend as the only way in. The sum is clamped to `max`,
|
|
71
|
+
* and parts past it are cut off at the track's end.
|
|
72
|
+
*/
|
|
73
|
+
segments?: ProgressBarSegment[];
|
|
74
|
+
/** With `segments`: a list under the bar naming each part with its swatch and value
|
|
75
|
+
* — the table view of the bar, in text tokens rather than the segment colour. */
|
|
76
|
+
legend?: boolean;
|
|
48
77
|
}
|
|
49
78
|
/**
|
|
50
79
|
* A horizontal bar that fills toward `max`.
|
|
@@ -63,6 +92,6 @@ interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
|
|
|
63
92
|
* tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an
|
|
64
93
|
* unlayered rule beats every layered utility, `motion-reduce:animate-none` included.
|
|
65
94
|
*/
|
|
66
|
-
declare function ProgressBar({ value, min, max, variant, label, showValue, formatValue, tone, size, locale, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...rest }: ProgressBarProps): react.JSX.Element;
|
|
95
|
+
declare function ProgressBar({ value, min, max, variant, label, showValue, formatValue, tone, size, locale, segments, legend, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...rest }: ProgressBarProps): react.JSX.Element;
|
|
67
96
|
|
|
68
|
-
export { ProgressBar, type ProgressBarProps, type ProgressBarSize, type ProgressBarTone };
|
|
97
|
+
export { ProgressBar, type ProgressBarProps, type ProgressBarSegment, type ProgressBarSize, type ProgressBarTone };
|
|
@@ -14,6 +14,25 @@ const FILL = {
|
|
|
14
14
|
expense: "bg-[var(--money-expense)]"
|
|
15
15
|
};
|
|
16
16
|
const TRACK_HEIGHT = { sm: "h-1", slim: "h-1.5", md: "h-2", lg: "h-3" };
|
|
17
|
+
const SERIES_FILL = [
|
|
18
|
+
"bg-[var(--chart-1)]",
|
|
19
|
+
"bg-[var(--chart-2)]",
|
|
20
|
+
"bg-[var(--chart-3)]",
|
|
21
|
+
"bg-[var(--chart-4)]",
|
|
22
|
+
"bg-[var(--chart-5)]",
|
|
23
|
+
"bg-[var(--chart-6)]",
|
|
24
|
+
"bg-[var(--chart-7)]",
|
|
25
|
+
"bg-[var(--chart-8)]",
|
|
26
|
+
"bg-[var(--chart-9)]"
|
|
27
|
+
];
|
|
28
|
+
function segmentFills(segments) {
|
|
29
|
+
let next = 0;
|
|
30
|
+
return segments.map((seg) => {
|
|
31
|
+
if (seg.className) return seg.className;
|
|
32
|
+
if (seg.tone) return FILL[seg.tone];
|
|
33
|
+
return seg.color ? void 0 : SERIES_FILL[next++ % SERIES_FILL.length];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
17
36
|
function ProgressBar({
|
|
18
37
|
value,
|
|
19
38
|
min = 0,
|
|
@@ -25,6 +44,8 @@ function ProgressBar({
|
|
|
25
44
|
tone = "brand",
|
|
26
45
|
size = "md",
|
|
27
46
|
locale,
|
|
47
|
+
segments,
|
|
48
|
+
legend = false,
|
|
28
49
|
className,
|
|
29
50
|
"aria-label": ariaLabel,
|
|
30
51
|
"aria-labelledby": ariaLabelledBy,
|
|
@@ -33,13 +54,25 @@ function ProgressBar({
|
|
|
33
54
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
34
55
|
const kitLocale = useKitLocale(locale);
|
|
35
56
|
const labelId = useId();
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const indeterminate = current === void 0;
|
|
57
|
+
const stacked = segments !== void 0;
|
|
58
|
+
const meter = stacked || variant === "meter";
|
|
39
59
|
const span = max - min;
|
|
60
|
+
const partValue = (v) => Number.isFinite(v) ? Math.max(0, v - min) : 0;
|
|
61
|
+
const fills = stacked ? segmentFills(segments) : [];
|
|
62
|
+
const partTotal = stacked ? min + segments.reduce((sum, seg) => sum + partValue(seg.value), 0) : void 0;
|
|
63
|
+
const format = (v) => formatValue ? formatValue(v, max, min) : new Intl.NumberFormat(kitLocale, { style: "percent", maximumFractionDigits: 0 }).format(
|
|
64
|
+
span <= 0 ? 0 : (Math.min(max, Math.max(min, v)) - min) / span
|
|
65
|
+
);
|
|
66
|
+
const partTexts = stacked ? segments.map((seg) => {
|
|
67
|
+
const text = format(min + partValue(seg.value));
|
|
68
|
+
return typeof seg.label === "string" || typeof seg.label === "number" ? common.fieldValue(String(seg.label), text) : text;
|
|
69
|
+
}) : [];
|
|
70
|
+
const current = partTotal ?? (value === void 0 || !Number.isFinite(value) ? meter ? min : void 0 : value);
|
|
71
|
+
const indeterminate = current === void 0;
|
|
40
72
|
const clamped = indeterminate ? void 0 : Math.min(max, Math.max(min, current));
|
|
41
73
|
const fraction = clamped === void 0 || span <= 0 ? 0 : (clamped - min) / span;
|
|
42
|
-
const valueText = clamped === void 0 ? void 0 :
|
|
74
|
+
const valueText = clamped === void 0 ? void 0 : format(clamped);
|
|
75
|
+
const ariaValueText = stacked && partTexts.length > 0 ? new Intl.ListFormat(kitLocale, { style: "short", type: "unit" }).format(partTexts) : valueText;
|
|
43
76
|
const labelledBy = ariaLabelledBy ?? (label != null ? labelId : void 0);
|
|
44
77
|
const name = labelledBy ? void 0 : ariaLabel ?? (indeterminate ? common.loading : void 0);
|
|
45
78
|
return /* @__PURE__ */ jsxs("div", { className: cn("min-w-0", className), "data-state": indeterminate ? "indeterminate" : "determinate", children: [
|
|
@@ -59,10 +92,28 @@ function ProgressBar({
|
|
|
59
92
|
"aria-valuemin": indeterminate ? void 0 : min,
|
|
60
93
|
"aria-valuemax": indeterminate ? void 0 : max,
|
|
61
94
|
"aria-valuenow": clamped,
|
|
62
|
-
"aria-valuetext":
|
|
95
|
+
"aria-valuetext": ariaValueText,
|
|
63
96
|
"aria-busy": !meter && indeterminate ? true : void 0,
|
|
64
97
|
className: cn("relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]", TRACK_HEIGHT[size]),
|
|
65
|
-
children:
|
|
98
|
+
children: stacked ? (
|
|
99
|
+
// The parts in a flex row with a 2px gap, so two neighbouring steps of one
|
|
100
|
+
// hue are parted by the track rather than by their difference in lightness.
|
|
101
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-full w-full gap-0.5", children: segments.map((seg, i) => /* @__PURE__ */ jsx(
|
|
102
|
+
"div",
|
|
103
|
+
{
|
|
104
|
+
"data-part": "segment",
|
|
105
|
+
className: cn(
|
|
106
|
+
"h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none",
|
|
107
|
+
fills[i]
|
|
108
|
+
),
|
|
109
|
+
style: {
|
|
110
|
+
width: `${span <= 0 ? 0 : partValue(seg.value) / span * 100}%`,
|
|
111
|
+
backgroundColor: seg.className ? void 0 : seg.color
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
seg.key ?? i
|
|
115
|
+
)) })
|
|
116
|
+
) : indeterminate ? /* @__PURE__ */ jsx(
|
|
66
117
|
"div",
|
|
67
118
|
{
|
|
68
119
|
"data-part": "fill",
|
|
@@ -87,7 +138,22 @@ function ProgressBar({
|
|
|
87
138
|
}
|
|
88
139
|
)
|
|
89
140
|
}
|
|
90
|
-
)
|
|
141
|
+
),
|
|
142
|
+
stacked && legend && segments.length > 0 && /* @__PURE__ */ jsx("ul", { className: "mt-2 space-y-1 text-sm", "data-part": "legend", children: segments.map((seg, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2", children: [
|
|
143
|
+
/* @__PURE__ */ jsx(
|
|
144
|
+
"span",
|
|
145
|
+
{
|
|
146
|
+
"aria-hidden": true,
|
|
147
|
+
className: cn(
|
|
148
|
+
"size-2.5 shrink-0 rounded-sm",
|
|
149
|
+
fills[i]
|
|
150
|
+
),
|
|
151
|
+
style: { backgroundColor: seg.className ? void 0 : seg.color }
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
seg.label != null && /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[var(--text-secondary)]", children: seg.label }),
|
|
155
|
+
/* @__PURE__ */ jsx("span", { className: "ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]", children: format(min + partValue(seg.value)) })
|
|
156
|
+
] }, seg.key ?? i)) })
|
|
91
157
|
] });
|
|
92
158
|
}
|
|
93
159
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/progress-bar.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\n/**\n * `income` / `expense` are the money pair (`--money-income` / `--money-expense`), the\n * names `Chip`, `StatTile` and `Sparkline` already use for them: a share bar of what\n * came in or went out is money, and painting it `success` / `warning` said \"good\" and\n * \"careful\" about a figure that is neither. keksdose's report meters (food-group-card,\n * drilldown-modal) sit beside amounts that are already in the money colours.\n */\nexport type ProgressBarTone =\n | \"brand\"\n | \"neutral\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\"\n | \"income\"\n | \"expense\";\n/**\n * Track heights: `sm` 4px, `slim` 6px, `md` 8px, `lg` 12px. `slim` is a word and not a\n * letter because the letters are taken in order — an `xs` at 6px would be THICKER than\n * `sm` — and renaming `sm` would move every bar already on it. 6px is the Slider's\n * track and keksdose's hand-drawn meters (`h-1.5`, landing-visuals), for a bar under a\n * line of text where 8px outweighs the text.\n */\nexport type ProgressBarSize = \"sm\" | \"slim\" | \"md\" | \"lg\";\n\nconst FILL: Record<ProgressBarTone, string> = {\n brand: \"bg-[var(--brand)]\",\n neutral: \"bg-[var(--text-muted)]\",\n success: \"bg-[var(--success)]\",\n warning: \"bg-[var(--warning)]\",\n danger: \"bg-[var(--danger)]\",\n info: \"bg-[var(--info)]\",\n income: \"bg-[var(--money-income)]\",\n expense: \"bg-[var(--money-expense)]\",\n};\n\nconst TRACK_HEIGHT: Record<ProgressBarSize, string> = { sm: \"h-1\", slim: \"h-1.5\", md: \"h-2\", lg: \"h-3\" };\n\nexport interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n /** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way\n * and nobody knows how much is left (an import that reports no count). */\n value?: number;\n min?: number;\n max?: number;\n /**\n * `progress` (default): a task moving toward completion — `role=\"progressbar\"`.\n * `meter`: a share of a known whole that is not going anywhere (budget used, disk\n * space, one category's slice of spending) — `role=\"meter\"`. keksdose's share bars\n * were progress bars to a screen reader, which reads \"busy\" into a figure that is\n * simply a figure. A meter is never indeterminate; a missing value reads as `min`.\n */\n variant?: \"progress\" | \"meter\";\n /** Visible label above the bar, and its accessible name. Without it, pass\n * `aria-label` (an indeterminate bar with neither is named `common.loading`). */\n label?: ReactNode;\n /** Show the value text at the end of the label row. */\n showValue?: boolean;\n /**\n * The value as words — both the visible `showValue` text and `aria-valuetext`.\n * Default: the fraction as a percentage in the kit's locale (\"42 %\" in French).\n * Pass one for anything that is not a percentage: `(v, max) => \\`${v} of ${max} files\\``.\n */\n formatValue?: (value: number, max: number, min: number) => string;\n tone?: ProgressBarTone;\n size?: ProgressBarSize;\n locale?: string;\n}\n\n/**\n * A horizontal bar that fills toward `max`.\n *\n * keksdose (import progress, budget share bars) and kastlan (upload and job\n * progress) each drew their own div-in-a-div. None had a role, so a reader heard\n * nothing at all; the share bars that did have one claimed to be progress.\n *\n * `aria-valuetext` is always set when there is a value: a bare `aria-valuenow=\"37\"`\n * is read as \"37\" by some readers and \"37 percent\" by others, and neither is right\n * when `max` is 212 files.\n *\n * The indeterminate sweep stops under `prefers-reduced-motion`. It becomes a still,\n * half-strength bar across the whole track — still visibly \"something is happening\",\n * with nothing moving. The sweep is a Tailwind arbitrary `animate-[…]` rather than\n * tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an\n * unlayered rule beats every layered utility, `motion-reduce:animate-none` included.\n */\nexport function ProgressBar({\n value,\n min = 0,\n max = 100,\n variant = \"progress\",\n label,\n showValue = false,\n formatValue,\n tone = \"brand\",\n size = \"md\",\n locale,\n className,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n ...rest\n}: ProgressBarProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const kitLocale = useKitLocale(locale);\n const labelId = useId();\n\n const meter = variant === \"meter\";\n const current = value === undefined || !Number.isFinite(value) ? (meter ? min : undefined) : value;\n const indeterminate = current === undefined;\n const span = max - min;\n const clamped = indeterminate ? undefined : Math.min(max, Math.max(min, current));\n const fraction = clamped === undefined || span <= 0 ? 0 : (clamped - min) / span;\n\n const valueText =\n clamped === undefined\n ? undefined\n : formatValue\n ? formatValue(clamped, max, min)\n : new Intl.NumberFormat(kitLocale, { style: \"percent\", maximumFractionDigits: 0 }).format(fraction);\n\n const labelledBy = ariaLabelledBy ?? (label != null ? labelId : undefined);\n const name = labelledBy ? undefined : (ariaLabel ?? (indeterminate ? common.loading : undefined));\n\n return (\n <div className={cn(\"min-w-0\", className)} data-state={indeterminate ? \"indeterminate\" : \"determinate\"}>\n {(label != null || (showValue && valueText !== undefined)) && (\n <div className=\"mb-1 flex items-baseline gap-2 text-sm\">\n {label != null && (\n <span id={labelId} className=\"min-w-0 text-[var(--text-secondary)]\">\n {label}\n </span>\n )}\n {showValue && valueText !== undefined && (\n // Hidden: the same words are the bar's aria-valuetext, and reading them\n // twice in a row is noise.\n <span aria-hidden className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {valueText}\n </span>\n )}\n </div>\n )}\n <div\n {...rest}\n role={meter ? \"meter\" : \"progressbar\"}\n aria-label={name}\n aria-labelledby={labelledBy}\n aria-valuemin={indeterminate ? undefined : min}\n aria-valuemax={indeterminate ? undefined : max}\n aria-valuenow={clamped}\n aria-valuetext={valueText}\n aria-busy={!meter && indeterminate ? true : undefined}\n // `relative` + `overflow-hidden`: the indeterminate segment is positioned in\n // here and must not paint past the rounded ends.\n className={cn(\"relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]\", TRACK_HEIGHT[size])}\n >\n {indeterminate ? (\n <div\n data-part=\"fill\"\n className={cn(\n // `left-0`, deliberately physical: the keyframes (tokens.css\n // `indeterminate-sweep`) translate along the physical x axis, and under\n // RTL the same keyframes are run in REVERSE — from the right edge back\n // to the left. A logical `start-0` would park the segment at the right\n // in RTL and the reversed sweep would then start off-screen.\n \"absolute inset-y-0 left-0 w-1/3 rounded-full\",\n \"animate-[indeterminate-sweep_1.4s_ease-in-out_infinite] rtl:[animation-direction:reverse]\",\n \"motion-reduce:w-full motion-reduce:animate-none motion-reduce:opacity-50\",\n FILL[tone],\n )}\n />\n ) : (\n <div\n data-part=\"fill\"\n // Block flow starts at the inline start, so a plain width fills from the\n // right under `dir=\"rtl\"` with nothing extra.\n className={cn(\"h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none\", FILL[tone])}\n style={{ width: `${fraction * 100}%` }}\n />\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AAkIQ,SAEI,KAFJ;AAlIR,SAAS,aAAa;AAEtB,SAAS,UAAU;AACnB,SAAS,uBAAuB,cAAc,oBAAoB;AA2BlE,MAAM,OAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AACX;AAEA,MAAM,eAAgD,EAAE,IAAI,OAAO,MAAM,SAAS,IAAI,OAAO,IAAI,MAAM;AAiDhG,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AAAA,EACN,UAAU;AAAA,EACV;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,mBAAmB;AAAA,EACnB,GAAG;AACL,GAAqB;AACnB,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,YAAY,aAAa,MAAM;AACrC,QAAM,UAAU,MAAM;AAEtB,QAAM,QAAQ,YAAY;AAC1B,QAAM,UAAU,UAAU,UAAa,CAAC,OAAO,SAAS,KAAK,IAAK,QAAQ,MAAM,SAAa;AAC7F,QAAM,gBAAgB,YAAY;AAClC,QAAM,OAAO,MAAM;AACnB,QAAM,UAAU,gBAAgB,SAAY,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,OAAO,CAAC;AAChF,QAAM,WAAW,YAAY,UAAa,QAAQ,IAAI,KAAK,UAAU,OAAO;AAE5E,QAAM,YACJ,YAAY,SACR,SACA,cACE,YAAY,SAAS,KAAK,GAAG,IAC7B,IAAI,KAAK,aAAa,WAAW,EAAE,OAAO,WAAW,uBAAuB,EAAE,CAAC,EAAE,OAAO,QAAQ;AAExG,QAAM,aAAa,mBAAmB,SAAS,OAAO,UAAU;AAChE,QAAM,OAAO,aAAa,SAAa,cAAc,gBAAgB,OAAO,UAAU;AAEtF,SACE,qBAAC,SAAI,WAAW,GAAG,WAAW,SAAS,GAAG,cAAY,gBAAgB,kBAAkB,eACpF;AAAA,cAAS,QAAS,aAAa,cAAc,WAC7C,qBAAC,SAAI,WAAU,0CACZ;AAAA,eAAS,QACR,oBAAC,UAAK,IAAI,SAAS,WAAU,wCAC1B,iBACH;AAAA,MAED,aAAa,cAAc;AAAA;AAAA,MAG1B,oBAAC,UAAK,eAAW,MAAC,WAAU,0DACzB,qBACH;AAAA,OAEJ;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ,MAAM,QAAQ,UAAU;AAAA,QACxB,cAAY;AAAA,QACZ,mBAAiB;AAAA,QACjB,iBAAe,gBAAgB,SAAY;AAAA,QAC3C,iBAAe,gBAAgB,SAAY;AAAA,QAC3C,iBAAe;AAAA,QACf,kBAAgB;AAAA,QAChB,aAAW,CAAC,SAAS,gBAAgB,OAAO;AAAA,QAG5C,WAAW,GAAG,sEAAsE,aAAa,IAAI,CAAC;AAAA,QAErG,0BACC;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YACV,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAMT;AAAA,cACA;AAAA,cACA;AAAA,cACA,KAAK,IAAI;AAAA,YACX;AAAA;AAAA,QACF,IAEA;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YAGV,WAAW,GAAG,qFAAqF,KAAK,IAAI,CAAC;AAAA,YAC7G,OAAO,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI;AAAA;AAAA,QACvC;AAAA;AAAA,IAEJ;AAAA,KACF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/progress-bar.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\n/**\n * `income` / `expense` are the money pair (`--money-income` / `--money-expense`), the\n * names `Chip`, `StatTile` and `Sparkline` already use for them: a share bar of what\n * came in or went out is money, and painting it `success` / `warning` said \"good\" and\n * \"careful\" about a figure that is neither. keksdose's report meters (food-group-card,\n * drilldown-modal) sit beside amounts that are already in the money colours.\n */\nexport type ProgressBarTone =\n | \"brand\"\n | \"neutral\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\"\n | \"income\"\n | \"expense\";\n/**\n * Track heights: `sm` 4px, `slim` 6px, `md` 8px, `lg` 12px. `slim` is a word and not a\n * letter because the letters are taken in order — an `xs` at 6px would be THICKER than\n * `sm` — and renaming `sm` would move every bar already on it. 6px is the Slider's\n * track and keksdose's hand-drawn meters (`h-1.5`, landing-visuals), for a bar under a\n * line of text where 8px outweighs the text.\n */\nexport type ProgressBarSize = \"sm\" | \"slim\" | \"md\" | \"lg\";\n\nconst FILL: Record<ProgressBarTone, string> = {\n brand: \"bg-[var(--brand)]\",\n neutral: \"bg-[var(--text-muted)]\",\n success: \"bg-[var(--success)]\",\n warning: \"bg-[var(--warning)]\",\n danger: \"bg-[var(--danger)]\",\n info: \"bg-[var(--info)]\",\n income: \"bg-[var(--money-income)]\",\n expense: \"bg-[var(--money-expense)]\",\n};\n\nconst TRACK_HEIGHT: Record<ProgressBarSize, string> = { sm: \"h-1\", slim: \"h-1.5\", md: \"h-2\", lg: \"h-3\" };\n\n// A segment with no tone of its own takes the next categorical chart colour, so a\n// stacked bar of unnamed parts is still a set of tellable parts rather than one fill.\nconst SERIES_FILL = [\n \"bg-[var(--chart-1)]\",\n \"bg-[var(--chart-2)]\",\n \"bg-[var(--chart-3)]\",\n \"bg-[var(--chart-4)]\",\n \"bg-[var(--chart-5)]\",\n \"bg-[var(--chart-6)]\",\n \"bg-[var(--chart-7)]\",\n \"bg-[var(--chart-8)]\",\n \"bg-[var(--chart-9)]\",\n];\n\n/** Each segment's fill classes: its own classes, else its tone, else (with no `color`)\n * the next chart colour. \"Next\" counts the UNCOLOURED segments only — a segment with\n * its own `className`/`tone`/`color` does not use up `--chart-1`, so an \"Other\" in grey\n * ahead of two plain parts leaves them `--chart-1` and `--chart-2`. */\nfunction segmentFills(segments: ProgressBarSegment[]): Array<string | undefined> {\n let next = 0;\n return segments.map((seg) => {\n if (seg.className) return seg.className;\n if (seg.tone) return FILL[seg.tone];\n return seg.color ? undefined : SERIES_FILL[next++ % SERIES_FILL.length];\n });\n}\n\n/** One part of a stacked bar. See {@link ProgressBarProps.segments}. */\nexport interface ProgressBarSegment {\n /** Its size, on the bar's own `min`–`max` scale. */\n value: number;\n /** A kit tone. Left out (and no `color`/`className`), the next `--chart-N` colour. */\n tone?: ProgressBarTone;\n /** Any CSS colour (`var(--token)`), for a palette the tones do not cover. Wins over `tone`. */\n color?: string;\n /** Classes for the segment's fill — keksdose's `STEP_CLASS` ramp. Wins over `tone`. */\n className?: string;\n /** The part's name: said in the bar's `aria-valuetext` and shown in the legend. */\n label?: ReactNode;\n /** A stable React key, when `label` is not a string. */\n key?: string;\n}\n\nexport interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n /** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way\n * and nobody knows how much is left (an import that reports no count). */\n value?: number;\n min?: number;\n max?: number;\n /**\n * `progress` (default): a task moving toward completion — `role=\"progressbar\"`.\n * `meter`: a share of a known whole that is not going anywhere (budget used, disk\n * space, one category's slice of spending) — `role=\"meter\"`. keksdose's share bars\n * were progress bars to a screen reader, which reads \"busy\" into a figure that is\n * simply a figure. A meter is never indeterminate; a missing value reads as `min`.\n */\n variant?: \"progress\" | \"meter\";\n /** Visible label above the bar, and its accessible name. Without it, pass\n * `aria-label` (an indeterminate bar with neither is named `common.loading`). */\n label?: ReactNode;\n /** Show the value text at the end of the label row. */\n showValue?: boolean;\n /**\n * The value as words — both the visible `showValue` text and `aria-valuetext`.\n * Default: the fraction as a percentage in the kit's locale (\"42 %\" in French).\n * Pass one for anything that is not a percentage: `(v, max) => \\`${v} of ${max} files\\``.\n */\n formatValue?: (value: number, max: number, min: number) => string;\n tone?: ProgressBarTone;\n size?: ProgressBarSize;\n locale?: string;\n /**\n * Draw a METER OF PARTS: one bar, several fills side by side in order, a 2px gap\n * between them — keksdose's discretionary-spending bar (cut-card:133), a whole\n * split into the steps of a scale. Forces `variant=\"meter\"`: parts of a whole are\n * never \"busy\". `aria-valuenow` is the sum; `aria-valuetext` names every part\n * (\"Rent: 40%, Food: 25%\"), each part's value through `formatValue` and joined in the\n * kit's locale — the words keksdose's hand-drawn bar had to hide from a reader\n * altogether, leaving the legend as the only way in. The sum is clamped to `max`,\n * and parts past it are cut off at the track's end.\n */\n segments?: ProgressBarSegment[];\n /** With `segments`: a list under the bar naming each part with its swatch and value\n * — the table view of the bar, in text tokens rather than the segment colour. */\n legend?: boolean;\n}\n\n/**\n * A horizontal bar that fills toward `max`.\n *\n * keksdose (import progress, budget share bars) and kastlan (upload and job\n * progress) each drew their own div-in-a-div. None had a role, so a reader heard\n * nothing at all; the share bars that did have one claimed to be progress.\n *\n * `aria-valuetext` is always set when there is a value: a bare `aria-valuenow=\"37\"`\n * is read as \"37\" by some readers and \"37 percent\" by others, and neither is right\n * when `max` is 212 files.\n *\n * The indeterminate sweep stops under `prefers-reduced-motion`. It becomes a still,\n * half-strength bar across the whole track — still visibly \"something is happening\",\n * with nothing moving. The sweep is a Tailwind arbitrary `animate-[…]` rather than\n * tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an\n * unlayered rule beats every layered utility, `motion-reduce:animate-none` included.\n */\nexport function ProgressBar({\n value,\n min = 0,\n max = 100,\n variant = \"progress\",\n label,\n showValue = false,\n formatValue,\n tone = \"brand\",\n size = \"md\",\n locale,\n segments,\n legend = false,\n className,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n ...rest\n}: ProgressBarProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const kitLocale = useKitLocale(locale);\n const labelId = useId();\n\n const stacked = segments !== undefined;\n const meter = stacked || variant === \"meter\";\n const span = max - min;\n const partValue = (v: number) => (Number.isFinite(v) ? Math.max(0, v - min) : 0);\n const fills = stacked ? segmentFills(segments) : [];\n const partTotal = stacked ? min + segments.reduce((sum, seg) => sum + partValue(seg.value), 0) : undefined;\n const format = (v: number) =>\n formatValue\n ? formatValue(v, max, min)\n : new Intl.NumberFormat(kitLocale, { style: \"percent\", maximumFractionDigits: 0 }).format(\n span <= 0 ? 0 : (Math.min(max, Math.max(min, v)) - min) / span,\n );\n const partTexts = stacked\n ? segments.map((seg) => {\n const text = format(min + partValue(seg.value));\n return typeof seg.label === \"string\" || typeof seg.label === \"number\"\n ? common.fieldValue(String(seg.label), text)\n : text;\n })\n : [];\n const current =\n partTotal ?? (value === undefined || !Number.isFinite(value) ? (meter ? min : undefined) : value);\n const indeterminate = current === undefined;\n const clamped = indeterminate ? undefined : Math.min(max, Math.max(min, current));\n const fraction = clamped === undefined || span <= 0 ? 0 : (clamped - min) / span;\n\n const valueText = clamped === undefined ? undefined : format(clamped);\n const ariaValueText =\n stacked && partTexts.length > 0\n ? new Intl.ListFormat(kitLocale, { style: \"short\", type: \"unit\" }).format(partTexts)\n : valueText;\n\n const labelledBy = ariaLabelledBy ?? (label != null ? labelId : undefined);\n const name = labelledBy ? undefined : (ariaLabel ?? (indeterminate ? common.loading : undefined));\n\n return (\n <div className={cn(\"min-w-0\", className)} data-state={indeterminate ? \"indeterminate\" : \"determinate\"}>\n {(label != null || (showValue && valueText !== undefined)) && (\n <div className=\"mb-1 flex items-baseline gap-2 text-sm\">\n {label != null && (\n <span id={labelId} className=\"min-w-0 text-[var(--text-secondary)]\">\n {label}\n </span>\n )}\n {showValue && valueText !== undefined && (\n // Hidden: the same words are the bar's aria-valuetext, and reading them\n // twice in a row is noise.\n <span aria-hidden className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {valueText}\n </span>\n )}\n </div>\n )}\n <div\n {...rest}\n role={meter ? \"meter\" : \"progressbar\"}\n aria-label={name}\n aria-labelledby={labelledBy}\n aria-valuemin={indeterminate ? undefined : min}\n aria-valuemax={indeterminate ? undefined : max}\n aria-valuenow={clamped}\n aria-valuetext={ariaValueText}\n aria-busy={!meter && indeterminate ? true : undefined}\n // `relative` + `overflow-hidden`: the indeterminate segment is positioned in\n // here and must not paint past the rounded ends.\n className={cn(\"relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]\", TRACK_HEIGHT[size])}\n >\n {stacked ? (\n // The parts in a flex row with a 2px gap, so two neighbouring steps of one\n // hue are parted by the track rather than by their difference in lightness.\n <div className=\"flex h-full w-full gap-0.5\">\n {segments.map((seg, i) => (\n <div\n key={seg.key ?? i}\n data-part=\"segment\"\n className={cn(\n \"h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none\",\n fills[i],\n )}\n style={{\n width: `${span <= 0 ? 0 : (partValue(seg.value) / span) * 100}%`,\n backgroundColor: seg.className ? undefined : seg.color,\n }}\n />\n ))}\n </div>\n ) : indeterminate ? (\n <div\n data-part=\"fill\"\n className={cn(\n // `left-0`, deliberately physical: the keyframes (tokens.css\n // `indeterminate-sweep`) translate along the physical x axis, and under\n // RTL the same keyframes are run in REVERSE — from the right edge back\n // to the left. A logical `start-0` would park the segment at the right\n // in RTL and the reversed sweep would then start off-screen.\n \"absolute inset-y-0 left-0 w-1/3 rounded-full\",\n \"animate-[indeterminate-sweep_1.4s_ease-in-out_infinite] rtl:[animation-direction:reverse]\",\n \"motion-reduce:w-full motion-reduce:animate-none motion-reduce:opacity-50\",\n FILL[tone],\n )}\n />\n ) : (\n <div\n data-part=\"fill\"\n // Block flow starts at the inline start, so a plain width fills from the\n // right under `dir=\"rtl\"` with nothing extra.\n className={cn(\"h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none\", FILL[tone])}\n style={{ width: `${fraction * 100}%` }}\n />\n )}\n </div>\n {stacked && legend && segments.length > 0 && (\n <ul className=\"mt-2 space-y-1 text-sm\" data-part=\"legend\">\n {segments.map((seg, i) => (\n <li key={seg.key ?? i} className=\"flex items-center gap-2\">\n <span\n aria-hidden\n className={cn(\n \"size-2.5 shrink-0 rounded-sm\",\n fills[i],\n )}\n style={{ backgroundColor: seg.className ? undefined : seg.color }}\n />\n {seg.label != null && (\n <span className=\"min-w-0 flex-1 truncate text-[var(--text-secondary)]\">{seg.label}</span>\n )}\n <span className=\"ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]\">\n {format(min + partValue(seg.value))}\n </span>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -75,6 +75,18 @@ interface SeriesChartSeries {
|
|
|
75
75
|
activeDot?: boolean | {
|
|
76
76
|
r?: number;
|
|
77
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* The shortest a bar of this series is drawn, in px — recharts' `minPointSize`. A
|
|
80
|
+
* zero or a value too small to see keeps a visible stub, so an empty slot still reads
|
|
81
|
+
* as a slot with a value rather than a gap: keksdose's weekday pattern, where a
|
|
82
|
+
* Sunday with no spending drew a 2% stub by hand. Only the DRAWING changes: the axis
|
|
83
|
+
* is fitted to the values, and the tooltip, the keyboard stops and every accessible
|
|
84
|
+
* name still say 0. A hole (no value in the row) stays a hole. Overrides the chart's
|
|
85
|
+
* {@link SeriesChartProps.minBarLength}; `0` turns it off for this series. Meant for
|
|
86
|
+
* unstacked bars — in a `stack` the stub is drawn over the layer above it. Lines and
|
|
87
|
+
* areas ignore it.
|
|
88
|
+
*/
|
|
89
|
+
minBarLength?: number;
|
|
78
90
|
}
|
|
79
91
|
/** What a series draws. See {@link SeriesChartSeries.type}. */
|
|
80
92
|
type SeriesChartType = "line" | "bar" | "area";
|
|
@@ -422,6 +434,10 @@ interface SeriesChartProps {
|
|
|
422
434
|
locale?: string;
|
|
423
435
|
/** Onto the chart's root (`ChartContainer`) — e.g. an app's own grid ink. */
|
|
424
436
|
className?: string;
|
|
437
|
+
/** The shortest bar drawn, in px, for every bar series that does not set its own
|
|
438
|
+
* {@link SeriesChartSeries.minBarLength}. Drawing only: domain, tooltip and names
|
|
439
|
+
* keep the real value. Default: none — a zero draws nothing. */
|
|
440
|
+
minBarLength?: number;
|
|
425
441
|
}
|
|
426
442
|
/** How much of an axis band the rotated title takes, in px. Reserved rather than
|
|
427
443
|
* shared with the ticks: a title merely offset inside the band prints on top of a
|
|
@@ -288,7 +288,8 @@ function SeriesPlot({
|
|
|
288
288
|
zoom,
|
|
289
289
|
labels: labelsProp,
|
|
290
290
|
locale: localeProp,
|
|
291
|
-
className
|
|
291
|
+
className,
|
|
292
|
+
minBarLength
|
|
292
293
|
}) {
|
|
293
294
|
const labels = useKitLabels("seriesChart", DEFAULT_SERIES_CHART_LABELS, labelsProp);
|
|
294
295
|
const locale = useKitLocale(localeProp);
|
|
@@ -576,6 +577,7 @@ function SeriesPlot({
|
|
|
576
577
|
const curve = entry.step ? "stepAfter" : entry.curve ?? "monotone";
|
|
577
578
|
const width = entry.strokeWidth ?? (entry.step ? STEP_WIDTH : LINE_WIDTH);
|
|
578
579
|
if (type === "bar") {
|
|
580
|
+
const stub = entry.minBarLength ?? minBarLength;
|
|
579
581
|
return /* @__PURE__ */ jsx(
|
|
580
582
|
Bar,
|
|
581
583
|
{
|
|
@@ -585,6 +587,7 @@ function SeriesPlot({
|
|
|
585
587
|
fill: color,
|
|
586
588
|
fillOpacity: entry.fillOpacity,
|
|
587
589
|
radius: entry.stack === void 0 ? BAR_RADIUS : 0,
|
|
590
|
+
...stub ? { minPointSize: (_value, index) => finite(plotted[index]?.[entry.key]) === void 0 ? 0 : stub } : {},
|
|
588
591
|
...focusedStop?.key === entry.key ? { shape: focusedBarShape(focusedStop.index) } : {},
|
|
589
592
|
onClick: onPointClick ? (_bar, index, event) => {
|
|
590
593
|
event.stopPropagation();
|