@seekalfred/primitives 0.7.3 → 0.8.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 +315 -4
- package/dist/alert/alert.d.ts +2 -2
- package/dist/alert/alert.js +1 -1
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert-dialog/alert-dialog.d.ts +14 -0
- package/dist/alert-dialog/alert-dialog.js +52 -27
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/announcement-bar/announcement-bar.d.ts +15 -4
- package/dist/announcement-bar/announcement-bar.js +31 -6
- package/dist/announcement-bar/announcement-bar.js.map +1 -1
- package/dist/avatar/avatar.d.ts +7 -0
- package/dist/avatar/avatar.js +33 -12
- package/dist/avatar/avatar.js.map +1 -1
- package/dist/badge/badge.d.ts +16 -1
- package/dist/badge/badge.js +12 -5
- package/dist/badge/badge.js.map +1 -1
- package/dist/bar-chart/bar-chart-series.d.ts +75 -0
- package/dist/bar-chart/bar-chart-series.js +401 -0
- package/dist/bar-chart/bar-chart-series.js.map +1 -0
- package/dist/bar-chart/bar-chart.d.ts +25 -1
- package/dist/bar-chart/bar-chart.js +26 -1
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/index.d.ts +2 -1
- package/dist/button/button.d.ts +24 -2
- package/dist/button/button.js +62 -13
- package/dist/button/button.js.map +1 -1
- package/dist/card/card.js +1 -1
- package/dist/card/card.js.map +1 -1
- package/dist/checkbox/checkbox.d.ts +25 -1
- package/dist/checkbox/checkbox.js +103 -28
- package/dist/checkbox/checkbox.js.map +1 -1
- package/dist/checkbox/index.d.ts +2 -2
- package/dist/checkbox/index.js +2 -2
- package/dist/chip-group/chip-group.d.ts +11 -0
- package/dist/chip-group/chip-group.js +50 -34
- package/dist/chip-group/chip-group.js.map +1 -1
- package/dist/chip-group/index.d.ts +2 -2
- package/dist/code-block/code-block.d.ts +45 -0
- package/dist/code-block/code-block.js +74 -0
- package/dist/code-block/code-block.js.map +1 -0
- package/dist/code-block/index.d.ts +2 -0
- package/dist/code-block/index.js +2 -0
- package/dist/command-palette/command-palette.d.ts +128 -0
- package/dist/command-palette/command-palette.js +371 -0
- package/dist/command-palette/command-palette.js.map +1 -0
- package/dist/command-palette/index.d.ts +2 -0
- package/dist/command-palette/index.js +2 -0
- package/dist/copy-button/copy-button.d.ts +19 -3
- package/dist/copy-button/copy-button.js +7 -4
- package/dist/copy-button/copy-button.js.map +1 -1
- package/dist/dialog/dialog.js +1 -1
- package/dist/dialog/dialog.js.map +1 -1
- package/dist/empty-state/empty-state.d.ts +17 -0
- package/dist/empty-state/empty-state.js +9 -7
- package/dist/empty-state/empty-state.js.map +1 -1
- package/dist/file-drop-zone/file-drop-zone.d.ts +29 -3
- package/dist/file-drop-zone/file-drop-zone.js +26 -8
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
- package/dist/gauge/gauge.d.ts +18 -1
- package/dist/gauge/gauge.js +35 -21
- package/dist/gauge/gauge.js.map +1 -1
- package/dist/gauge/index.d.ts +2 -2
- package/dist/index.d.ts +14 -9
- package/dist/index.js +14 -10
- package/dist/info-tooltip/index.d.ts +2 -0
- package/dist/info-tooltip/index.js +2 -0
- package/dist/info-tooltip/info-tooltip.d.ts +35 -0
- package/dist/info-tooltip/info-tooltip.js +49 -0
- package/dist/info-tooltip/info-tooltip.js.map +1 -0
- package/dist/input/input.d.ts +7 -0
- package/dist/input/input.js +4 -2
- package/dist/input/input.js.map +1 -1
- package/dist/lib/field-shell.js +2 -6
- package/dist/lib/field-shell.js.map +1 -1
- package/dist/lib/required-marker.js +20 -0
- package/dist/lib/required-marker.js.map +1 -0
- package/dist/lib/selection-control.js +32 -1
- package/dist/lib/selection-control.js.map +1 -1
- package/dist/line-chart/index.d.ts +2 -2
- package/dist/line-chart/line-chart.d.ts +106 -3
- package/dist/line-chart/line-chart.js +185 -46
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/mention-input/mention-input.js +1 -1
- package/dist/money-input/index.d.ts +2 -2
- package/dist/money-input/money-input.d.ts +14 -0
- package/dist/money-input/money-input.js +17 -7
- package/dist/money-input/money-input.js.map +1 -1
- package/dist/morph-select/morph-select.d.ts +8 -1
- package/dist/morph-select/morph-select.js +8 -5
- package/dist/morph-select/morph-select.js.map +1 -1
- package/dist/multi-select/multi-select.d.ts +31 -0
- package/dist/multi-select/multi-select.js +123 -6
- package/dist/multi-select/multi-select.js.map +1 -1
- package/dist/number-field/number-field.js +1 -1
- package/dist/password-field/password-field.js +1 -1
- package/dist/password-strength/password-strength.js +1 -1
- package/dist/phone-input/phone-input.js +1 -1
- package/dist/popover/popover.js +1 -1
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/index.js +2 -2
- package/dist/progress/progress.d.ts +53 -2
- package/dist/progress/progress.js +146 -36
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/radio-group.d.ts +12 -3
- package/dist/radio-group/radio-group.js +62 -52
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/rich-text-editor/rich-text-editor.js +1 -1
- package/dist/scroll-area/scroll-area.d.ts +1 -1
- package/dist/scrub-input/scrub-input.d.ts +11 -1
- package/dist/scrub-input/scrub-input.js +22 -10
- package/dist/scrub-input/scrub-input.js.map +1 -1
- package/dist/search-field/search-field.js +1 -1
- package/dist/select/select.d.ts +10 -1
- package/dist/select/select.js +4 -3
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +1 -1
- package/dist/shortcut-recorder/shortcut-recorder.js +1 -1
- package/dist/sparkline/sparkline.d.ts +27 -2
- package/dist/sparkline/sparkline.js +115 -18
- package/dist/sparkline/sparkline.js.map +1 -1
- package/dist/stat-card/index.d.ts +2 -2
- package/dist/stat-card/index.js +2 -2
- package/dist/stat-card/stat-card.d.ts +32 -1
- package/dist/stat-card/stat-card.js +52 -9
- package/dist/stat-card/stat-card.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +211 -3
- package/dist/table/table.js +473 -93
- package/dist/table/table.js.map +1 -1
- package/dist/tabs/tabs.d.ts +4 -2
- package/dist/tabs/tabs.js +7 -5
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/tag-input/tag-input.js +1 -1
- package/dist/text-shimmer/text-shimmer.d.ts +18 -2
- package/dist/text-shimmer/text-shimmer.js +5 -2
- package/dist/text-shimmer/text-shimmer.js.map +1 -1
- package/dist/textarea/textarea.d.ts +7 -0
- package/dist/textarea/textarea.js +4 -2
- package/dist/textarea/textarea.js.map +1 -1
- package/dist/toast/toast.d.ts +2 -2
- package/dist/toggle-button/index.d.ts +2 -0
- package/dist/toggle-button/index.js +2 -0
- package/dist/toggle-button/toggle-button.d.ts +93 -0
- package/dist/toggle-button/toggle-button.js +199 -0
- package/dist/toggle-button/toggle-button.js.map +1 -0
- package/package.json +19 -3
package/dist/badge/badge.js
CHANGED
|
@@ -73,6 +73,10 @@ const badgeVariants = cva([
|
|
|
73
73
|
shape: {
|
|
74
74
|
rounded: "rounded-md",
|
|
75
75
|
pill: "rounded-full"
|
|
76
|
+
},
|
|
77
|
+
wrap: {
|
|
78
|
+
true: "min-w-0 max-w-full shrink whitespace-normal break-words text-start",
|
|
79
|
+
false: ""
|
|
76
80
|
}
|
|
77
81
|
},
|
|
78
82
|
compoundVariants: [
|
|
@@ -171,7 +175,8 @@ const badgeVariants = cva([
|
|
|
171
175
|
tone: "neutral",
|
|
172
176
|
appearance: "soft",
|
|
173
177
|
size: "md",
|
|
174
|
-
shape: "rounded"
|
|
178
|
+
shape: "rounded",
|
|
179
|
+
wrap: false
|
|
175
180
|
}
|
|
176
181
|
});
|
|
177
182
|
/** The name an icon swaps under: a different component swaps; the same one re-rendering does not. */
|
|
@@ -180,27 +185,29 @@ function iconKey(icon) {
|
|
|
180
185
|
const type = icon.type;
|
|
181
186
|
return typeof type === "string" ? type : type.displayName ?? type.name ?? "icon";
|
|
182
187
|
}
|
|
183
|
-
const Badge = forwardRef(function Badge({ className, tone, appearance, size, shape, icon, children, ...props }, ref) {
|
|
188
|
+
const Badge = forwardRef(function Badge({ className, tone, appearance, size, shape, wrap = false, icon, children, ...props }, ref) {
|
|
184
189
|
const content = useRef(null);
|
|
185
190
|
const text = typeof children === "string" || typeof children === "number" ? String(children) : null;
|
|
186
191
|
const glyph = icon === void 0 || icon === null ? "" : iconKey(icon);
|
|
187
|
-
useMorphWidth(content, `${text ?? ""}|${glyph}
|
|
192
|
+
useMorphWidth(content, `${text ?? ""}|${glyph}`, !wrap);
|
|
188
193
|
return /* @__PURE__ */ jsx("span", {
|
|
189
194
|
ref,
|
|
190
195
|
...props,
|
|
191
196
|
"data-slot": "badge",
|
|
192
197
|
"data-tone": tone ?? "neutral",
|
|
198
|
+
"data-wrap": wrap || void 0,
|
|
193
199
|
className: cn(badgeVariants({
|
|
194
200
|
tone,
|
|
195
201
|
appearance,
|
|
196
202
|
size,
|
|
197
|
-
shape
|
|
203
|
+
shape,
|
|
204
|
+
wrap
|
|
198
205
|
}), className),
|
|
199
206
|
children: /* @__PURE__ */ jsx("span", {
|
|
200
207
|
className: "button-slot",
|
|
201
208
|
children: /* @__PURE__ */ jsxs("span", {
|
|
202
209
|
ref: content,
|
|
203
|
-
className: "inline-flex flex-none items-center gap-1",
|
|
210
|
+
className: wrap ? "inline-flex min-w-0 items-center gap-1" : "inline-flex flex-none items-center gap-1",
|
|
204
211
|
children: [glyph ? /* @__PURE__ */ jsx(SwapText, {
|
|
205
212
|
swapKey: glyph,
|
|
206
213
|
motion: "glyph",
|
package/dist/badge/badge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"badge.js","names":[],"sources":["../../src/badge/badge.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n forwardRef,\n isValidElement,\n useRef,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useMorphWidth } from \"../lib/motion.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Badge -- the status pill, in every shape the product asks for.\n *\n * Presentational, so there is no React Aria base. RAC's `Tag` looks similar\n * and is not equivalent: it is focusable, selectable and removable, and it is\n * illegal outside a `TagGroup` collection. A status pill is none of those.\n *\n * THIS ABSORBED THE PRODUCT'S `Pill`. The app shipped two components for one\n * job -- `badge.tsx` with a `variant` axis and `pill.tsx` with a `tone` axis --\n * across 22 and 22 consumer files. They disagreed about everything: shape\n * (`rounded-md` against `rounded-full`), size (one fixed, one `sm`/`md`/`lg`)\n * and naming, so `pill` accepted `blue` AND `info`, `green` AND `success`, for\n * the same colour. Converging them is why `variant` is gone.\n *\n * The axes are the two questions a call site actually answers -- what does\n * this MEAN (`tone`) and how loud should it be (`appearance`) -- rather than\n * one axis whose values are the cross product. Six tones by three appearances\n * is eighteen `variant` values otherwise, and shipping eighteen is how a\n * design system ends up with `softPrimary` next to `primaryOutline`.\n *\n * Every string below is a STATIC literal, including the compound ones.\n * Tailwind scans source text, so a class assembled at runtime is invisible to\n * it and would never be emitted.\n *\n * Three things in the legacy are deliberately not ported:\n *\n * - `asChild` and its `[a&]:hover:` rules. Zero call sites; the hover styles\n * existed only to colour the anchor asChild produced.\n * - `focus-visible:ring-*`. A span is not focusable, so the ring could never\n * appear. A consumer who adds `tabIndex` owns the indicator, and a ring is\n * the wrong one anyway -- box-shadows vanish under forced-colors.\n * - `aria-invalid:*`. A badge is never invalid; that was boilerplate copied\n * across every shadcn control.\n */\nexport const badgeVariants = cva(\n [\n \"inline-flex w-fit shrink-0 items-center justify-center gap-1\",\n \"border-(length:--border-width) border-transparent\",\n // NO leading-* here. It lives on each `size` instead, AFTER that size's\n // text-* class: tailwind-merge treats a later text-* as owning\n // line-height, so a leading-* in this base string is silently stripped the\n // moment a size variant adds one. The NoUaLeak story caught exactly that.\n \"font-sans font-medium\",\n \"whitespace-nowrap\",\n // box-border and font-sans are not boilerplate. The reset in\n // @seekalfred/styles is OPT-IN, so a consumer may load none: an unstyled\n // span inherits the host's serif, and content-box would add the padding\n // to any width a consumer sets.\n \"box-border\",\n \"[&_svg]:pointer-events-none [&_svg]:size-3\",\n // Colour, border and fill ease over 240ms as the tone changes.\n \"motion-safe:transition-colors motion-safe:duration-(--duration-standard) motion-safe:ease-settle\",\n ],\n {\n variants: {\n /** What the badge MEANS. Colour follows from it; it is never picked directly. */\n tone: {\n neutral: \"\",\n primary: \"\",\n success: \"\",\n warning: \"\",\n danger: \"\",\n info: \"\",\n },\n /** How loud it is. */\n appearance: {\n solid: \"\",\n soft: \"\",\n outline: \"bg-transparent\",\n },\n size: {\n sm: \"px-2 py-0.5 text-xs leading-(--leading-snug)\",\n md: \"px-2.5 py-0.5 text-xs leading-(--leading-snug)\",\n lg: \"px-3 py-1 text-sm leading-(--leading-snug)\",\n },\n /*\n * The product's two components disagreed on this and nothing else:\n * `badge.tsx` was rounded-md, `pill.tsx` was rounded-full. Both survive\n * as one axis rather than one winning, because a count-bearing badge and\n * a verdict pill really do read differently.\n */\n shape: { rounded: \"rounded-md\", pill: \"rounded-full\" },\n },\n compoundVariants: [\n // SOLID -- the fill carries the meaning, so the text must clear 4.5:1 on it.\n /*\n * muted-foreground, NOT `secondary`. --color-secondary resolves to\n * periwinkle.400, so a \"neutral\" badge rendered lavender -- caught in a\n * browser, not by any assertion, because the story compared it against\n * the same wrong token it was painted with. A tone called neutral has to\n * be neutral; the periwinkle fill the product uses for its `secondary`\n * badge is the brand's SECOND colour, not an absence of one.\n */\n { tone: \"neutral\", appearance: \"solid\", class: \"bg-muted-foreground text-background\" },\n { tone: \"primary\", appearance: \"solid\", class: \"bg-primary text-primary-foreground\" },\n { tone: \"success\", appearance: \"solid\", class: \"bg-success-strong text-success-strong-foreground\" },\n { tone: \"warning\", appearance: \"solid\", class: \"bg-warning-strong text-warning-strong-foreground\" },\n { tone: \"danger\", appearance: \"solid\", class: \"bg-destructive text-destructive-foreground\" },\n { tone: \"info\", appearance: \"solid\", class: \"bg-info-strong text-info-strong-foreground\" },\n\n /*\n * SOFT -- a tint, and the default, because this is what a status pill is.\n * The ink is one step darker than the obvious pairing for success,\n * warning and info: the product measured 4.13, 3.48 and 3.34 on its own\n * tints and had to darken all three. Our `-foreground` tokens already\n * encode that correction, which is why they are used rather than\n * `-strong`.\n */\n { tone: \"neutral\", appearance: \"soft\", class: \"bg-muted text-muted-foreground\" },\n // No primary-subtle-foreground token: plain `foreground` on this tint\n // measures 18.70 light / 9.78 dark, already graded for the Table's\n // highlighted row.\n { tone: \"primary\", appearance: \"soft\", class: \"bg-primary-subtle text-foreground\" },\n { tone: \"success\", appearance: \"soft\", class: \"bg-success-subtle text-success-foreground\" },\n { tone: \"warning\", appearance: \"soft\", class: \"bg-warning-subtle text-warning-foreground\" },\n {\n tone: \"danger\",\n appearance: \"soft\",\n class: \"bg-destructive-subtle text-destructive-subtle-foreground\",\n },\n { tone: \"info\", appearance: \"soft\", class: \"bg-info-subtle text-info-foreground\" },\n\n /*\n * OUTLINE -- no fill, so the border is the only thing separating the\n * badge from the page and is held to the 3:1 non-text floor of WCAG\n * 1.4.11. Text AND border are `-strong`, not `-foreground`: with no tint\n * behind it the label sits on `background`, and `-strong on background`\n * is the pair already graded at 4.5. `-foreground` is graded only\n * against its own `-subtle`, so using it here would have been a ratio\n * nobody had measured.\n */\n { tone: \"neutral\", appearance: \"outline\", class: \"border-border text-foreground\" },\n { tone: \"primary\", appearance: \"outline\", class: \"border-primary-strong text-foreground\" },\n {\n tone: \"success\",\n appearance: \"outline\",\n class: \"border-success-strong text-success-strong\",\n },\n {\n tone: \"warning\",\n appearance: \"outline\",\n class: \"border-warning-strong text-warning-strong\",\n },\n {\n tone: \"danger\",\n appearance: \"outline\",\n class: \"border-destructive-strong text-destructive-strong\",\n },\n { tone: \"info\", appearance: \"outline\", class: \"border-info-strong text-info-strong\" },\n ],\n defaultVariants: { tone: \"neutral\", appearance: \"soft\", size: \"md\", shape: \"rounded\" },\n },\n);\n\nexport interface BadgeProps\n extends ComponentPropsWithoutRef<\"span\">, VariantProps<typeof badgeVariants> {\n /**\n * A leading icon. A different icon component swaps in place (the new one grows in\n * from small and blurred); the same one re-rendering stays still. Mark it\n * `aria-hidden` when the label already says what it means.\n */\n icon?: ReactNode;\n}\n\n/** The name an icon swaps under: a different component swaps; the same one re-rendering does not. */\nfunction iconKey(icon: ReactNode) {\n if (!isValidElement(icon)) return \"icon\";\n const type = icon.type as string | { displayName?: string; name?: string };\n return typeof type === \"string\" ? type : (type.displayName ?? type.name ?? \"icon\");\n}\n\nexport const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge(\n { className, tone, appearance, size, shape, icon, children, ...props },\n ref,\n) {\n const content = useRef<HTMLSpanElement>(null);\n const text = typeof children === \"string\" || typeof children === \"number\" ? String(children) : null;\n const glyph = icon === undefined || icon === null ? \"\" : iconKey(icon);\n // The pill follows its content: a new label or icon swaps in place while the width springs to the new size.\n useMorphWidth(content, `${text ?? \"\"}|${glyph}`);\n return (\n <span\n ref={ref}\n {...props}\n data-slot=\"badge\"\n data-tone={tone ?? \"neutral\"}\n className={cn(badgeVariants({ tone, appearance, size, shape }), className)}\n >\n <span className=\"button-slot\">\n <span ref={content} className=\"inline-flex flex-none items-center gap-1\">\n {glyph ? (\n <SwapText swapKey={glyph} motion=\"glyph\" className=\"inline-flex items-center\">\n {icon}\n </SwapText>\n ) : null}\n {text === null ? (\n children\n ) : (\n <SwapText swapKey={text} className=\"inline-block\">\n {text}\n </SwapText>\n )}\n </span>\n </span>\n </span>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAa,gBAAgB,IAC3B;CACE;CACA;CAKA;CACA;CAKA;CACA;CAEA;AACF,GACA;CACE,UAAU;;EAER,MAAM;GACJ,SAAS;GACT,SAAS;GACT,SAAS;GACT,SAAS;GACT,QAAQ;GACR,MAAM;EACR;;EAEA,YAAY;GACV,OAAO;GACP,MAAM;GACN,SAAS;EACX;EACA,MAAM;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EAOA,OAAO;GAAE,SAAS;GAAc,MAAM;EAAe;CACvD;CACA,kBAAkB;EAUhB;GAAE,MAAM;GAAW,YAAY;GAAS,OAAO;EAAsC;EACrF;GAAE,MAAM;GAAW,YAAY;GAAS,OAAO;EAAqC;EACpF;GAAE,MAAM;GAAW,YAAY;GAAS,OAAO;EAAmD;EAClG;GAAE,MAAM;GAAW,YAAY;GAAS,OAAO;EAAmD;EAClG;GAAE,MAAM;GAAU,YAAY;GAAS,OAAO;EAA6C;EAC3F;GAAE,MAAM;GAAQ,YAAY;GAAS,OAAO;EAA6C;EAUzF;GAAE,MAAM;GAAW,YAAY;GAAQ,OAAO;EAAiC;EAI/E;GAAE,MAAM;GAAW,YAAY;GAAQ,OAAO;EAAoC;EAClF;GAAE,MAAM;GAAW,YAAY;GAAQ,OAAO;EAA4C;EAC1F;GAAE,MAAM;GAAW,YAAY;GAAQ,OAAO;EAA4C;EAC1F;GACE,MAAM;GACN,YAAY;GACZ,OAAO;EACT;EACA;GAAE,MAAM;GAAQ,YAAY;GAAQ,OAAO;EAAsC;EAWjF;GAAE,MAAM;GAAW,YAAY;GAAW,OAAO;EAAgC;EACjF;GAAE,MAAM;GAAW,YAAY;GAAW,OAAO;EAAwC;EACzF;GACE,MAAM;GACN,YAAY;GACZ,OAAO;EACT;EACA;GACE,MAAM;GACN,YAAY;GACZ,OAAO;EACT;EACA;GACE,MAAM;GACN,YAAY;GACZ,OAAO;EACT;EACA;GAAE,MAAM;GAAQ,YAAY;GAAW,OAAO;EAAsC;CACtF;CACA,iBAAiB;EAAE,MAAM;EAAW,YAAY;EAAQ,MAAM;EAAM,OAAO;CAAU;AACvF,CACF;;AAaA,SAAS,QAAQ,MAAiB;CAChC,IAAI,CAAC,eAAe,IAAI,GAAG,OAAO;CAClC,MAAM,OAAO,KAAK;CAClB,OAAO,OAAO,SAAS,WAAW,OAAQ,KAAK,eAAe,KAAK,QAAQ;AAC7E;AAEA,MAAa,QAAQ,WAAwC,SAAS,MACpE,EAAE,WAAW,MAAM,YAAY,MAAM,OAAO,MAAM,UAAU,GAAG,SAC/D,KACA;CACA,MAAM,UAAU,OAAwB,IAAI;CAC5C,MAAM,OAAO,OAAO,aAAa,YAAY,OAAO,aAAa,WAAW,OAAO,QAAQ,IAAI;CAC/F,MAAM,QAAQ,SAAS,KAAA,KAAa,SAAS,OAAO,KAAK,QAAQ,IAAI;CAErE,cAAc,SAAS,GAAG,QAAQ,GAAG,GAAG,OAAO;CAC/C,OACE,oBAAC,QAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,aAAW,QAAQ;EACnB,WAAW,GAAG,cAAc;GAAE;GAAM;GAAY;GAAM;EAAM,CAAC,GAAG,SAAS;EAEzE,UAAA,oBAAC,QAAD;GAAM,WAAU;GACd,UAAA,qBAAC,QAAD;IAAM,KAAK;IAAS,WAAU;IAA9B,UAAA,CACG,QACC,oBAAC,UAAD;KAAU,SAAS;KAAO,QAAO;KAAQ,WAAU;KAChD,UAAA;IACO,CAAA,IACR,MACH,SAAS,OACR,WAEA,oBAAC,UAAD;KAAU,SAAS;KAAM,WAAU;KAChC,UAAA;IACO,CAAA,CAER;;EACF,CAAA;CACF,CAAA;AAEV,CAAC"}
|
|
1
|
+
{"version":3,"file":"badge.js","names":[],"sources":["../../src/badge/badge.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n forwardRef,\n isValidElement,\n useRef,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useMorphWidth } from \"../lib/motion.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Badge -- the status pill, in every shape the product asks for.\n *\n * Presentational, so there is no React Aria base. RAC's `Tag` looks similar\n * and is not equivalent: it is focusable, selectable and removable, and it is\n * illegal outside a `TagGroup` collection. A status pill is none of those.\n *\n * THIS ABSORBED THE PRODUCT'S `Pill`. The app shipped two components for one\n * job -- `badge.tsx` with a `variant` axis and `pill.tsx` with a `tone` axis --\n * across 22 and 22 consumer files. They disagreed about everything: shape\n * (`rounded-md` against `rounded-full`), size (one fixed, one `sm`/`md`/`lg`)\n * and naming, so `pill` accepted `blue` AND `info`, `green` AND `success`, for\n * the same colour. Converging them is why `variant` is gone.\n *\n * The axes are the two questions a call site actually answers -- what does\n * this MEAN (`tone`) and how loud should it be (`appearance`) -- rather than\n * one axis whose values are the cross product. Six tones by three appearances\n * is eighteen `variant` values otherwise, and shipping eighteen is how a\n * design system ends up with `softPrimary` next to `primaryOutline`.\n *\n * Every string below is a STATIC literal, including the compound ones.\n * Tailwind scans source text, so a class assembled at runtime is invisible to\n * it and would never be emitted.\n *\n * Three things in the legacy are deliberately not ported:\n *\n * - `asChild` and its `[a&]:hover:` rules. Zero call sites; the hover styles\n * existed only to colour the anchor asChild produced.\n * - `focus-visible:ring-*`. A span is not focusable, so the ring could never\n * appear. A consumer who adds `tabIndex` owns the indicator, and a ring is\n * the wrong one anyway -- box-shadows vanish under forced-colors.\n * - `aria-invalid:*`. A badge is never invalid; that was boilerplate copied\n * across every shadcn control.\n */\nexport const badgeVariants = cva(\n [\n \"inline-flex w-fit shrink-0 items-center justify-center gap-1\",\n \"border-(length:--border-width) border-transparent\",\n // NO leading-* here. It lives on each `size` instead, AFTER that size's\n // text-* class: tailwind-merge treats a later text-* as owning\n // line-height, so a leading-* in this base string is silently stripped the\n // moment a size variant adds one. The NoUaLeak story caught exactly that.\n \"font-sans font-medium\",\n \"whitespace-nowrap\",\n // box-border and font-sans are not boilerplate. The reset in\n // @seekalfred/styles is OPT-IN, so a consumer may load none: an unstyled\n // span inherits the host's serif, and content-box would add the padding\n // to any width a consumer sets.\n \"box-border\",\n \"[&_svg]:pointer-events-none [&_svg]:size-3\",\n // Colour, border and fill ease over 240ms as the tone changes.\n \"motion-safe:transition-colors motion-safe:duration-(--duration-standard) motion-safe:ease-settle\",\n ],\n {\n variants: {\n /** What the badge MEANS. Colour follows from it; it is never picked directly. */\n tone: {\n neutral: \"\",\n primary: \"\",\n success: \"\",\n warning: \"\",\n danger: \"\",\n info: \"\",\n },\n /** How loud it is. */\n appearance: {\n solid: \"\",\n soft: \"\",\n outline: \"bg-transparent\",\n },\n size: {\n sm: \"px-2 py-0.5 text-xs leading-(--leading-snug)\",\n md: \"px-2.5 py-0.5 text-xs leading-(--leading-snug)\",\n lg: \"px-3 py-1 text-sm leading-(--leading-snug)\",\n },\n /*\n * The product's two components disagreed on this and nothing else:\n * `badge.tsx` was rounded-md, `pill.tsx` was rounded-full. Both survive\n * as one axis rather than one winning, because a count-bearing badge and\n * a verdict pill really do read differently.\n */\n shape: { rounded: \"rounded-md\", pill: \"rounded-full\" },\n /*\n * Opt-in wrapping, for a tag in a table cell or a narrow column. Off, the\n * badge is one line that never shrinks. On, it may shrink to its container\n * and break the label across lines (long unbroken words too), the text\n * starts at the leading edge, and the box grows in height. The base\n * `whitespace-nowrap`, `w-fit` and `shrink-0` lose to these by merge order\n * (cn runs tailwind-merge over our own classes).\n */\n wrap: {\n true: \"min-w-0 max-w-full shrink whitespace-normal break-words text-start\",\n false: \"\",\n },\n },\n compoundVariants: [\n // SOLID -- the fill carries the meaning, so the text must clear 4.5:1 on it.\n /*\n * muted-foreground, NOT `secondary`. --color-secondary resolves to\n * periwinkle.400, so a \"neutral\" badge rendered lavender -- caught in a\n * browser, not by any assertion, because the story compared it against\n * the same wrong token it was painted with. A tone called neutral has to\n * be neutral; the periwinkle fill the product uses for its `secondary`\n * badge is the brand's SECOND colour, not an absence of one.\n */\n { tone: \"neutral\", appearance: \"solid\", class: \"bg-muted-foreground text-background\" },\n { tone: \"primary\", appearance: \"solid\", class: \"bg-primary text-primary-foreground\" },\n { tone: \"success\", appearance: \"solid\", class: \"bg-success-strong text-success-strong-foreground\" },\n { tone: \"warning\", appearance: \"solid\", class: \"bg-warning-strong text-warning-strong-foreground\" },\n { tone: \"danger\", appearance: \"solid\", class: \"bg-destructive text-destructive-foreground\" },\n { tone: \"info\", appearance: \"solid\", class: \"bg-info-strong text-info-strong-foreground\" },\n\n /*\n * SOFT -- a tint, and the default, because this is what a status pill is.\n * The ink is one step darker than the obvious pairing for success,\n * warning and info: the product measured 4.13, 3.48 and 3.34 on its own\n * tints and had to darken all three. Our `-foreground` tokens already\n * encode that correction, which is why they are used rather than\n * `-strong`.\n */\n { tone: \"neutral\", appearance: \"soft\", class: \"bg-muted text-muted-foreground\" },\n // No primary-subtle-foreground token: plain `foreground` on this tint\n // measures 18.70 light / 9.78 dark, already graded for the Table's\n // highlighted row.\n { tone: \"primary\", appearance: \"soft\", class: \"bg-primary-subtle text-foreground\" },\n { tone: \"success\", appearance: \"soft\", class: \"bg-success-subtle text-success-foreground\" },\n { tone: \"warning\", appearance: \"soft\", class: \"bg-warning-subtle text-warning-foreground\" },\n {\n tone: \"danger\",\n appearance: \"soft\",\n class: \"bg-destructive-subtle text-destructive-subtle-foreground\",\n },\n { tone: \"info\", appearance: \"soft\", class: \"bg-info-subtle text-info-foreground\" },\n\n /*\n * OUTLINE -- no fill, so the border is the only thing separating the\n * badge from the page and is held to the 3:1 non-text floor of WCAG\n * 1.4.11. Text AND border are `-strong`, not `-foreground`: with no tint\n * behind it the label sits on `background`, and `-strong on background`\n * is the pair already graded at 4.5. `-foreground` is graded only\n * against its own `-subtle`, so using it here would have been a ratio\n * nobody had measured.\n */\n { tone: \"neutral\", appearance: \"outline\", class: \"border-border text-foreground\" },\n { tone: \"primary\", appearance: \"outline\", class: \"border-primary-strong text-foreground\" },\n {\n tone: \"success\",\n appearance: \"outline\",\n class: \"border-success-strong text-success-strong\",\n },\n {\n tone: \"warning\",\n appearance: \"outline\",\n class: \"border-warning-strong text-warning-strong\",\n },\n {\n tone: \"danger\",\n appearance: \"outline\",\n class: \"border-destructive-strong text-destructive-strong\",\n },\n { tone: \"info\", appearance: \"outline\", class: \"border-info-strong text-info-strong\" },\n ],\n defaultVariants: { tone: \"neutral\", appearance: \"soft\", size: \"md\", shape: \"rounded\", wrap: false },\n },\n);\n\nexport interface BadgeProps\n extends ComponentPropsWithoutRef<\"span\">, VariantProps<typeof badgeVariants> {\n /**\n * A leading icon. A different icon component swaps in place (the new one grows in\n * from small and blurred); the same one re-rendering stays still. Mark it\n * `aria-hidden` when the label already says what it means.\n */\n icon?: ReactNode;\n /**\n * Let the label wrap onto more lines instead of staying on one. For a tag in\n * a table cell or a narrow column: the badge shrinks to its container, the\n * label breaks (even a long word with no spaces), the text starts at the\n * leading edge and the height grows with the lines. Off by default, and then\n * a badge is one line exactly as before. With a long label prefer\n * `shape=\"rounded\"`: a multi-line `pill` has very round ends.\n *\n * The width does not spring to a new label while this is on (that morph\n * measures the unwrapped width).\n *\n * @default false\n */\n wrap?: boolean;\n}\n\n/** The name an icon swaps under: a different component swaps; the same one re-rendering does not. */\nfunction iconKey(icon: ReactNode) {\n if (!isValidElement(icon)) return \"icon\";\n const type = icon.type as string | { displayName?: string; name?: string };\n return typeof type === \"string\" ? type : (type.displayName ?? type.name ?? \"icon\");\n}\n\nexport const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge(\n { className, tone, appearance, size, shape, wrap = false, icon, children, ...props },\n ref,\n) {\n const content = useRef<HTMLSpanElement>(null);\n const text = typeof children === \"string\" || typeof children === \"number\" ? String(children) : null;\n const glyph = icon === undefined || icon === null ? \"\" : iconKey(icon);\n // The pill follows its content: a new label or icon swaps in place while the width springs to the new size.\n useMorphWidth(content, `${text ?? \"\"}|${glyph}`, !wrap);\n return (\n <span\n ref={ref}\n {...props}\n data-slot=\"badge\"\n data-tone={tone ?? \"neutral\"}\n data-wrap={wrap || undefined}\n className={cn(badgeVariants({ tone, appearance, size, shape, wrap }), className)}\n >\n <span className=\"button-slot\">\n <span\n ref={content}\n className={wrap ? \"inline-flex min-w-0 items-center gap-1\" : \"inline-flex flex-none items-center gap-1\"}\n >\n {glyph ? (\n <SwapText swapKey={glyph} motion=\"glyph\" className=\"inline-flex items-center\">\n {icon}\n </SwapText>\n ) : null}\n {text === null ? (\n children\n ) : (\n <SwapText swapKey={text} className=\"inline-block\">\n {text}\n </SwapText>\n )}\n </span>\n </span>\n </span>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAa,gBAAgB,IAC3B;CACE;CACA;CAKA;CACA;CAKA;CACA;CAEA;AACF,GACA;CACE,UAAU;;EAER,MAAM;GACJ,SAAS;GACT,SAAS;GACT,SAAS;GACT,SAAS;GACT,QAAQ;GACR,MAAM;EACR;;EAEA,YAAY;GACV,OAAO;GACP,MAAM;GACN,SAAS;EACX;EACA,MAAM;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EAOA,OAAO;GAAE,SAAS;GAAc,MAAM;EAAe;EASrD,MAAM;GACJ,MAAM;GACN,OAAO;EACT;CACF;CACA,kBAAkB;EAUhB;GAAE,MAAM;GAAW,YAAY;GAAS,OAAO;EAAsC;EACrF;GAAE,MAAM;GAAW,YAAY;GAAS,OAAO;EAAqC;EACpF;GAAE,MAAM;GAAW,YAAY;GAAS,OAAO;EAAmD;EAClG;GAAE,MAAM;GAAW,YAAY;GAAS,OAAO;EAAmD;EAClG;GAAE,MAAM;GAAU,YAAY;GAAS,OAAO;EAA6C;EAC3F;GAAE,MAAM;GAAQ,YAAY;GAAS,OAAO;EAA6C;EAUzF;GAAE,MAAM;GAAW,YAAY;GAAQ,OAAO;EAAiC;EAI/E;GAAE,MAAM;GAAW,YAAY;GAAQ,OAAO;EAAoC;EAClF;GAAE,MAAM;GAAW,YAAY;GAAQ,OAAO;EAA4C;EAC1F;GAAE,MAAM;GAAW,YAAY;GAAQ,OAAO;EAA4C;EAC1F;GACE,MAAM;GACN,YAAY;GACZ,OAAO;EACT;EACA;GAAE,MAAM;GAAQ,YAAY;GAAQ,OAAO;EAAsC;EAWjF;GAAE,MAAM;GAAW,YAAY;GAAW,OAAO;EAAgC;EACjF;GAAE,MAAM;GAAW,YAAY;GAAW,OAAO;EAAwC;EACzF;GACE,MAAM;GACN,YAAY;GACZ,OAAO;EACT;EACA;GACE,MAAM;GACN,YAAY;GACZ,OAAO;EACT;EACA;GACE,MAAM;GACN,YAAY;GACZ,OAAO;EACT;EACA;GAAE,MAAM;GAAQ,YAAY;GAAW,OAAO;EAAsC;CACtF;CACA,iBAAiB;EAAE,MAAM;EAAW,YAAY;EAAQ,MAAM;EAAM,OAAO;EAAW,MAAM;CAAM;AACpG,CACF;;AA2BA,SAAS,QAAQ,MAAiB;CAChC,IAAI,CAAC,eAAe,IAAI,GAAG,OAAO;CAClC,MAAM,OAAO,KAAK;CAClB,OAAO,OAAO,SAAS,WAAW,OAAQ,KAAK,eAAe,KAAK,QAAQ;AAC7E;AAEA,MAAa,QAAQ,WAAwC,SAAS,MACpE,EAAE,WAAW,MAAM,YAAY,MAAM,OAAO,OAAO,OAAO,MAAM,UAAU,GAAG,SAC7E,KACA;CACA,MAAM,UAAU,OAAwB,IAAI;CAC5C,MAAM,OAAO,OAAO,aAAa,YAAY,OAAO,aAAa,WAAW,OAAO,QAAQ,IAAI;CAC/F,MAAM,QAAQ,SAAS,KAAA,KAAa,SAAS,OAAO,KAAK,QAAQ,IAAI;CAErE,cAAc,SAAS,GAAG,QAAQ,GAAG,GAAG,SAAS,CAAC,IAAI;CACtD,OACE,oBAAC,QAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,aAAW,QAAQ;EACnB,aAAW,QAAQ,KAAA;EACnB,WAAW,GAAG,cAAc;GAAE;GAAM;GAAY;GAAM;GAAO;EAAK,CAAC,GAAG,SAAS;EAE/E,UAAA,oBAAC,QAAD;GAAM,WAAU;GACd,UAAA,qBAAC,QAAD;IACE,KAAK;IACL,WAAW,OAAO,2CAA2C;IAF/D,UAAA,CAIG,QACC,oBAAC,UAAD;KAAU,SAAS;KAAO,QAAO;KAAQ,WAAU;KAChD,UAAA;IACO,CAAA,IACR,MACH,SAAS,OACR,WAEA,oBAAC,UAAD;KAAU,SAAS;KAAM,WAAU;KAChC,UAAA;IACO,CAAA,CAER;;EACF,CAAA;CACF,CAAA;AAEV,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/bar-chart/bar-chart-series.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The series mode of BarChart: several measures per category, side by side
|
|
4
|
+
* (`layout="grouped"`) or stacked into one bar (`layout="stacked"`), on a
|
|
5
|
+
* categorical axis in data order, upright (`orientation="vertical"`) or as
|
|
6
|
+
* ranked rows (`orientation="horizontal"`). One series is a plain categorical
|
|
7
|
+
* or horizontal bar chart.
|
|
8
|
+
*
|
|
9
|
+
* Bars grow from the baseline the first time the chart is seen, a beat apart
|
|
10
|
+
* (no motion under reduced motion). Hover, drag or the arrow keys step through
|
|
11
|
+
* the categories: the active category keeps its colour while the rest step
|
|
12
|
+
* back, and the legend reads that category's values. The legend toggles a
|
|
13
|
+
* series off and on. A visually hidden table carries every value, and the plot
|
|
14
|
+
* is a slider over the categories.
|
|
15
|
+
*/
|
|
16
|
+
export interface BarChartSeries {
|
|
17
|
+
/** Stable identity, the key of this series' number in each datum's `values`. */
|
|
18
|
+
key: string;
|
|
19
|
+
/** Name in the legend and the data table. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** Any CSS colour. Defaults to chart-1 to chart-4 in series order, then neutral steps. */
|
|
22
|
+
color?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface BarChartSeriesDatum {
|
|
25
|
+
/** Stable identity of the category. */
|
|
26
|
+
key: string;
|
|
27
|
+
/** Full label for the readout and screen readers, such as "Brand search". */
|
|
28
|
+
label: string;
|
|
29
|
+
/** Short label on the category axis. Defaults to `label`. */
|
|
30
|
+
axisLabel?: string;
|
|
31
|
+
/**
|
|
32
|
+
* One value per series key. A missing value (`undefined`, `null` or not a
|
|
33
|
+
* finite number) draws no bar and reads as `missingLabel`. Negative values
|
|
34
|
+
* are drawn as zero: bars rise from a zero baseline.
|
|
35
|
+
*/
|
|
36
|
+
values: Record<string, number | null | undefined>;
|
|
37
|
+
}
|
|
38
|
+
export interface BarChartSeriesProps {
|
|
39
|
+
data: BarChartSeriesDatum[];
|
|
40
|
+
/** The measures. Passing `series` is what turns on this mode. */
|
|
41
|
+
series: BarChartSeries[];
|
|
42
|
+
/** What is measured, such as "Spend by campaign". Names the chart for assistive technology. */
|
|
43
|
+
label: string;
|
|
44
|
+
/** The range on show, such as "Sep 15–21, 2026". Read with the name; not printed. */
|
|
45
|
+
period?: string;
|
|
46
|
+
/** Series side by side in each category, or stacked into one bar. @default "grouped" */
|
|
47
|
+
layout?: "grouped" | "stacked";
|
|
48
|
+
/** Upright columns, or rows that grow from the inline start (ranked lists). @default "vertical" */
|
|
49
|
+
orientation?: "vertical" | "horizontal";
|
|
50
|
+
/** Unit after each value in the readout and the table, such as "min". */
|
|
51
|
+
unit?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Plot height in pixels. Upright: @default 176. Horizontal: 32px per
|
|
54
|
+
* category by default.
|
|
55
|
+
*/
|
|
56
|
+
height?: number;
|
|
57
|
+
/** Formats values in the legend readout and the data table. */
|
|
58
|
+
formatValue?: (value: number, series: BarChartSeries) => string;
|
|
59
|
+
/** Formats the value axis. Defaults to a compact number, such as 1.2K. */
|
|
60
|
+
formatTick?: (value: number) => string;
|
|
61
|
+
/** Header of the first column in the data table read by screen readers. @default "Category" */
|
|
62
|
+
categoryLabel?: string;
|
|
63
|
+
/** Read in place of a missing value. @default "No data" */
|
|
64
|
+
missingLabel?: string;
|
|
65
|
+
/** Series toggles above the plot. Shown by default when there is more than one series. */
|
|
66
|
+
legend?: boolean;
|
|
67
|
+
/** Controlled hidden series keys. */
|
|
68
|
+
hiddenSeries?: string[];
|
|
69
|
+
defaultHiddenSeries?: string[];
|
|
70
|
+
onHiddenSeriesChange?: (hidden: string[]) => void;
|
|
71
|
+
/** Extra classes for the root. Merged last. */
|
|
72
|
+
className?: string;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//# sourceMappingURL=bar-chart-series.d.ts.map
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { mergeRefs } from "../lib/merge-refs.js";
|
|
4
|
+
import { prefersReducedMotion, useIsomorphicLayoutEffect } from "../lib/motion.js";
|
|
5
|
+
import { forwardRef, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/bar-chart/bar-chart-series.tsx
|
|
8
|
+
/** Room past the top gridline, the share of a slot a category fills, the widest bar, the gap inside a group, the data-end radius, a row's default height. */
|
|
9
|
+
const TOP = 12;
|
|
10
|
+
const FILL = .62;
|
|
11
|
+
const MAX_BAR = 28;
|
|
12
|
+
const INNER_GAP = 2;
|
|
13
|
+
const RADIUS = 4;
|
|
14
|
+
const ROW = 32;
|
|
15
|
+
/** Stagger between categories on the first reveal, and its cap. */
|
|
16
|
+
const STAGGER_MS = 35;
|
|
17
|
+
const STAGGER_CAP_MS = 360;
|
|
18
|
+
const NEUTRAL = [
|
|
19
|
+
56,
|
|
20
|
+
42,
|
|
21
|
+
32
|
|
22
|
+
];
|
|
23
|
+
const GROUPED = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
|
|
24
|
+
const COMPACT = new Intl.NumberFormat("en-US", {
|
|
25
|
+
notation: "compact",
|
|
26
|
+
maximumFractionDigits: 1
|
|
27
|
+
});
|
|
28
|
+
const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
29
|
+
const colorOf = (item, index) => item.color ?? (index < 4 ? `var(--color-chart-${index + 1})` : `color-mix(in oklab, var(--color-foreground) ${NEUTRAL[(index - 4) % NEUTRAL.length]}%, var(--color-surface))`);
|
|
30
|
+
/** The number to draw, or null when there is none. */
|
|
31
|
+
const numberOf = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
32
|
+
/** Clean gridlines: the smallest step of 1, 2, 3 or 5 that covers the data in four rows or fewer. */
|
|
33
|
+
function scaleFor(max) {
|
|
34
|
+
const safe = Math.max(max, 1);
|
|
35
|
+
const magnitude = 10 ** Math.floor(Math.log10(safe / 4));
|
|
36
|
+
for (const factor of [
|
|
37
|
+
1,
|
|
38
|
+
2,
|
|
39
|
+
3,
|
|
40
|
+
5,
|
|
41
|
+
10
|
|
42
|
+
]) {
|
|
43
|
+
const step = factor * magnitude;
|
|
44
|
+
const rows = Math.ceil(safe / step);
|
|
45
|
+
if (rows <= 4) return {
|
|
46
|
+
top: rows * step,
|
|
47
|
+
ticks: Array.from({ length: rows + 1 }, (_, row) => row * step)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
top: safe,
|
|
52
|
+
ticks: [0, safe]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One bar as a path in plot pixels: `start` along the category axis, `size`
|
|
57
|
+
* across it, from `from` to `to` along the value axis. Only the data end is
|
|
58
|
+
* rounded, and only when `round`.
|
|
59
|
+
*/
|
|
60
|
+
function barPath(vertical, start, size, from, to, round, base) {
|
|
61
|
+
const f = (value) => value.toFixed(2);
|
|
62
|
+
const length = to - from;
|
|
63
|
+
if (size <= 0 || length <= .01) return "";
|
|
64
|
+
const r = round ? Math.min(RADIUS, size / 2, length) : 0;
|
|
65
|
+
if (vertical) {
|
|
66
|
+
const bottom = base - from;
|
|
67
|
+
const top = base - to;
|
|
68
|
+
const x = start;
|
|
69
|
+
return `M${f(x)} ${f(bottom)}V${f(top + r)}A${f(r)} ${f(r)} 0 0 1 ${f(x + r)} ${f(top)}H${f(x + size - r)}A${f(r)} ${f(r)} 0 0 1 ${f(x + size)} ${f(top + r)}V${f(bottom)}Z`;
|
|
70
|
+
}
|
|
71
|
+
const y = start;
|
|
72
|
+
return `M${f(from)} ${f(y)}H${f(to - r)}A${f(r)} ${f(r)} 0 0 1 ${f(to)} ${f(y + r)}V${f(y + size - r)}A${f(r)} ${f(r)} 0 0 1 ${f(to - r)} ${f(y + size)}H${f(from)}Z`;
|
|
73
|
+
}
|
|
74
|
+
const SeriesBarChart = forwardRef(function SeriesBarChart({ data, series, label, period, layout = "grouped", orientation = "vertical", unit = "", height: heightProp, formatValue = (value) => GROUPED.format(value), formatTick = (value) => COMPACT.format(value), categoryLabel = "Category", missingLabel = "No data", legend, hiddenSeries, defaultHiddenSeries = [], onHiddenSeriesChange, className }, ref) {
|
|
75
|
+
const figure = useRef(null);
|
|
76
|
+
const refs = useMemo(() => mergeRefs(ref, figure), [ref]);
|
|
77
|
+
const plot = useRef(null);
|
|
78
|
+
const titleId = useId();
|
|
79
|
+
const vertical = orientation === "vertical";
|
|
80
|
+
const stacked = layout === "stacked";
|
|
81
|
+
const height = heightProp ?? (vertical ? 176 : Math.max(1, data.length) * ROW);
|
|
82
|
+
const suffix = unit ? ` ${unit}` : "";
|
|
83
|
+
const count = Math.max(data.length, 1);
|
|
84
|
+
const last = data.length - 1;
|
|
85
|
+
const [ownHidden, setOwnHidden] = useState(defaultHiddenSeries);
|
|
86
|
+
const hidden = hiddenSeries ?? ownHidden;
|
|
87
|
+
const toggle = (key) => {
|
|
88
|
+
const next = hidden.includes(key) ? hidden.filter((item) => item !== key) : [...hidden, key];
|
|
89
|
+
if (hiddenSeries === void 0) setOwnHidden(next);
|
|
90
|
+
onHiddenSeriesChange?.(next);
|
|
91
|
+
};
|
|
92
|
+
const visible = series.filter((item) => !hidden.includes(item.key));
|
|
93
|
+
const showLegend = legend ?? series.length > 1;
|
|
94
|
+
const peak = data.reduce((max, item) => {
|
|
95
|
+
const values = visible.map((line) => Math.max(0, numberOf(item.values[line.key]) ?? 0));
|
|
96
|
+
return Math.max(max, stacked ? values.reduce((sum, value) => sum + value, 0) : Math.max(0, ...values));
|
|
97
|
+
}, 0);
|
|
98
|
+
const { top, ticks } = useMemo(() => scaleFor(peak), [peak]);
|
|
99
|
+
const [active, setActive] = useState(null);
|
|
100
|
+
const index = active === null ? null : Math.min(active, last);
|
|
101
|
+
const scrubbing = index !== null && last >= 0;
|
|
102
|
+
const [width, setWidth] = useState(0);
|
|
103
|
+
useIsomorphicLayoutEffect(() => {
|
|
104
|
+
const node = plot.current;
|
|
105
|
+
if (!node) return;
|
|
106
|
+
const read = () => setWidth(node.clientWidth);
|
|
107
|
+
read();
|
|
108
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
109
|
+
const observer = new ResizeObserver(read);
|
|
110
|
+
observer.observe(node);
|
|
111
|
+
return () => observer.disconnect();
|
|
112
|
+
}, []);
|
|
113
|
+
const [shown, setShown] = useState(false);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const node = figure.current;
|
|
116
|
+
if (!node || prefersReducedMotion() || typeof IntersectionObserver === "undefined") {
|
|
117
|
+
setShown(true);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
121
|
+
if (!entry?.isIntersecting) return;
|
|
122
|
+
observer.disconnect();
|
|
123
|
+
setShown(true);
|
|
124
|
+
}, { threshold: .35 });
|
|
125
|
+
observer.observe(node);
|
|
126
|
+
return () => observer.disconnect();
|
|
127
|
+
}, []);
|
|
128
|
+
const along = vertical ? width : height;
|
|
129
|
+
const length = Math.max(0, vertical ? height - TOP : width - TOP);
|
|
130
|
+
const slot = along / count;
|
|
131
|
+
const groupSize = Math.min(slot * FILL, (stacked ? 1 : Math.max(1, visible.length)) * MAX_BAR);
|
|
132
|
+
const barSize = stacked ? groupSize : Math.max(0, (groupSize - INNER_GAP * (visible.length - 1)) / Math.max(1, visible.length));
|
|
133
|
+
const toPx = (value) => value / (top || 1) * length;
|
|
134
|
+
const step = Math.min(STAGGER_MS, STAGGER_CAP_MS / count);
|
|
135
|
+
const bars = data.flatMap((item, at) => {
|
|
136
|
+
const groupStart = at * slot + (slot - groupSize) / 2;
|
|
137
|
+
let stack = 0;
|
|
138
|
+
const values = series.map((line) => ({
|
|
139
|
+
line,
|
|
140
|
+
value: numberOf(item.values[line.key])
|
|
141
|
+
}));
|
|
142
|
+
const outermost = [...values].reverse().find(({ line, value }) => !hidden.includes(line.key) && (value ?? 0) > 0)?.line.key;
|
|
143
|
+
return values.flatMap(({ line, value }, seriesAt) => {
|
|
144
|
+
if (hidden.includes(line.key) || value === null || value <= 0 || !width) return [];
|
|
145
|
+
const place = visible.indexOf(line);
|
|
146
|
+
const start = stacked ? groupStart : groupStart + place * (barSize + INNER_GAP);
|
|
147
|
+
const from = stacked ? toPx(stack) : 0;
|
|
148
|
+
stack += value;
|
|
149
|
+
const to = stacked ? toPx(stack) : toPx(value);
|
|
150
|
+
const d = barPath(vertical, start, barSize, from, to, !stacked || line.key === outermost, height);
|
|
151
|
+
return d ? [{
|
|
152
|
+
key: `${item.key}:${line.key}`,
|
|
153
|
+
category: item.key,
|
|
154
|
+
series: line.key,
|
|
155
|
+
d,
|
|
156
|
+
color: colorOf(line, seriesAt),
|
|
157
|
+
at
|
|
158
|
+
}] : [];
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
const pointAt = (clientX, clientY) => {
|
|
162
|
+
const rect = plot.current?.getBoundingClientRect();
|
|
163
|
+
if (!rect) return 0;
|
|
164
|
+
const offset = vertical ? clientX - rect.left : clientY - rect.top;
|
|
165
|
+
const size = vertical ? rect.width : rect.height;
|
|
166
|
+
if (!size) return 0;
|
|
167
|
+
const rtl = vertical && plot.current && getComputedStyle(plot.current).direction === "rtl";
|
|
168
|
+
const at = Math.floor((rtl ? size - offset : offset) / size * count);
|
|
169
|
+
return clamp(at, 0, last);
|
|
170
|
+
};
|
|
171
|
+
const onKeyDown = (event) => {
|
|
172
|
+
if (event.key === "Escape" && scrubbing) {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
setActive(null);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const from = index ?? -1;
|
|
178
|
+
const page = Math.min(5, count);
|
|
179
|
+
const next = {
|
|
180
|
+
ArrowLeft: from - 1,
|
|
181
|
+
ArrowUp: from - 1,
|
|
182
|
+
ArrowRight: index === null ? 0 : from + 1,
|
|
183
|
+
ArrowDown: index === null ? 0 : from + 1,
|
|
184
|
+
PageUp: from - page,
|
|
185
|
+
PageDown: from + page,
|
|
186
|
+
Home: 0,
|
|
187
|
+
End: last
|
|
188
|
+
}[event.key];
|
|
189
|
+
if (next === void 0 || last < 0) return;
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
setActive(clamp(next, 0, last));
|
|
192
|
+
};
|
|
193
|
+
const read = (item, line) => {
|
|
194
|
+
const value = numberOf(item.values[line.key]);
|
|
195
|
+
return value === null ? missingLabel : `${formatValue(value, line)}${suffix}`;
|
|
196
|
+
};
|
|
197
|
+
const totalOf = (item) => visible.reduce((sum, line) => sum + (numberOf(item.values[line.key]) ?? 0), 0);
|
|
198
|
+
const reading = scrubbing ? data[index] : null;
|
|
199
|
+
const valueText = reading ? `${reading.label}: ${visible.map((line) => `${line.label} ${read(reading, line)}`).join(", ")}` : data.length ? `${data.length} ${categoryLabel.toLowerCase()}` : "No data";
|
|
200
|
+
const name = period ? `${label}, ${period}` : label;
|
|
201
|
+
const [tickHost, tickStyle] = vertical ? ["relative min-w-0 [grid-area:1/2]", (value) => ({ translate: `0 ${height - toPx(value)}px` })] : ["relative min-w-0 [grid-area:2/2]", (value) => ({ insetInlineStart: `${toPx(value)}px` })];
|
|
202
|
+
return /* @__PURE__ */ jsxs("figure", {
|
|
203
|
+
ref: refs,
|
|
204
|
+
"data-slot": "bar-chart",
|
|
205
|
+
"data-layout": layout,
|
|
206
|
+
"data-orientation": orientation,
|
|
207
|
+
"aria-labelledby": titleId,
|
|
208
|
+
className: cn("m-0 grid min-w-0 content-start gap-4 box-border font-sans text-foreground", className),
|
|
209
|
+
children: [
|
|
210
|
+
/* @__PURE__ */ jsx("figcaption", {
|
|
211
|
+
id: titleId,
|
|
212
|
+
className: "sr-only",
|
|
213
|
+
children: name
|
|
214
|
+
}),
|
|
215
|
+
showLegend ? /* @__PURE__ */ jsx("div", {
|
|
216
|
+
role: "group",
|
|
217
|
+
"aria-label": `${label} series`,
|
|
218
|
+
className: "-mx-2 flex min-w-0 flex-wrap gap-x-1 gap-y-0.5",
|
|
219
|
+
children: series.map((line, at) => /* @__PURE__ */ jsxs("button", {
|
|
220
|
+
type: "button",
|
|
221
|
+
"data-slot": "bar-chart-toggle",
|
|
222
|
+
"aria-pressed": !hidden.includes(line.key),
|
|
223
|
+
onClick: () => toggle(line.key),
|
|
224
|
+
style: { "--series": colorOf(line, at) },
|
|
225
|
+
className: "chart-legend-toggle",
|
|
226
|
+
children: [
|
|
227
|
+
/* @__PURE__ */ jsx("span", {
|
|
228
|
+
"aria-hidden": "true",
|
|
229
|
+
className: "bar-chart-swatch"
|
|
230
|
+
}),
|
|
231
|
+
/* @__PURE__ */ jsx("span", {
|
|
232
|
+
className: "chart-toggle-label",
|
|
233
|
+
children: line.label
|
|
234
|
+
}),
|
|
235
|
+
reading && !hidden.includes(line.key) ? /* @__PURE__ */ jsx("span", {
|
|
236
|
+
"data-slot": "bar-chart-toggle-value",
|
|
237
|
+
"aria-hidden": "true",
|
|
238
|
+
className: "tabular-nums text-foreground",
|
|
239
|
+
children: read(reading, line)
|
|
240
|
+
}) : null
|
|
241
|
+
]
|
|
242
|
+
}, line.key))
|
|
243
|
+
}) : null,
|
|
244
|
+
/* @__PURE__ */ jsxs("div", {
|
|
245
|
+
"data-slot": "bar-chart-chart",
|
|
246
|
+
"data-scrubbing": scrubbing || void 0,
|
|
247
|
+
"data-shown": shown || void 0,
|
|
248
|
+
className: vertical ? "bar-chart-frame" : "bar-chart-frame-horizontal",
|
|
249
|
+
children: [
|
|
250
|
+
/* @__PURE__ */ jsx("div", {
|
|
251
|
+
"aria-hidden": "true",
|
|
252
|
+
"data-slot": "bar-chart-categories",
|
|
253
|
+
className: cn("relative min-w-0", vertical ? "overflow-hidden [grid-area:2/1]" : "[grid-area:1/1]"),
|
|
254
|
+
children: data.map((item, at) => /* @__PURE__ */ jsx("span", {
|
|
255
|
+
"data-category": item.key,
|
|
256
|
+
"data-active": scrubbing && index === at || void 0,
|
|
257
|
+
className: "bar-chart-category",
|
|
258
|
+
style: vertical ? {
|
|
259
|
+
insetInlineStart: `${at * slot}px`,
|
|
260
|
+
width: `${slot}px`
|
|
261
|
+
} : { height: `${slot}px` },
|
|
262
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
263
|
+
className: "truncate",
|
|
264
|
+
children: item.axisLabel ?? item.label
|
|
265
|
+
})
|
|
266
|
+
}, item.key))
|
|
267
|
+
}),
|
|
268
|
+
/* @__PURE__ */ jsxs("div", {
|
|
269
|
+
ref: plot,
|
|
270
|
+
className: cn("relative min-w-0", vertical ? "[grid-area:1/1]" : "[grid-area:1/2]"),
|
|
271
|
+
style: { height },
|
|
272
|
+
children: [/* @__PURE__ */ jsxs("svg", {
|
|
273
|
+
width: "100%",
|
|
274
|
+
height,
|
|
275
|
+
"aria-hidden": "true",
|
|
276
|
+
focusable: "false",
|
|
277
|
+
className: "bar-chart-plot block overflow-hidden",
|
|
278
|
+
children: [
|
|
279
|
+
ticks.slice(1).map((value) => vertical ? /* @__PURE__ */ jsx("line", {
|
|
280
|
+
className: "line-chart-grid",
|
|
281
|
+
x1: 0,
|
|
282
|
+
x2: "100%",
|
|
283
|
+
y1: Math.round(height - toPx(value)) + .5,
|
|
284
|
+
y2: Math.round(height - toPx(value)) + .5
|
|
285
|
+
}, value) : /* @__PURE__ */ jsx("line", {
|
|
286
|
+
className: "line-chart-grid",
|
|
287
|
+
y1: 0,
|
|
288
|
+
y2: "100%",
|
|
289
|
+
x1: Math.round(toPx(value)) + .5,
|
|
290
|
+
x2: Math.round(toPx(value)) + .5
|
|
291
|
+
}, value)),
|
|
292
|
+
bars.map((item) => /* @__PURE__ */ jsx("path", {
|
|
293
|
+
"data-slot": "bar-chart-bar",
|
|
294
|
+
"data-category": item.category,
|
|
295
|
+
"data-series": item.series,
|
|
296
|
+
"data-active": scrubbing && index === item.at || void 0,
|
|
297
|
+
d: item.d,
|
|
298
|
+
className: "bar-chart-series-bar",
|
|
299
|
+
style: {
|
|
300
|
+
"--series": item.color,
|
|
301
|
+
transformOrigin: vertical ? `0 ${height}px` : "0 0",
|
|
302
|
+
transitionDelay: shown ? `0s, ${item.at * step}ms` : void 0
|
|
303
|
+
}
|
|
304
|
+
}, item.key)),
|
|
305
|
+
vertical ? /* @__PURE__ */ jsx("line", {
|
|
306
|
+
className: "line-chart-baseline",
|
|
307
|
+
x1: 0,
|
|
308
|
+
x2: "100%",
|
|
309
|
+
y1: height - .5,
|
|
310
|
+
y2: height - .5
|
|
311
|
+
}) : /* @__PURE__ */ jsx("line", {
|
|
312
|
+
className: "line-chart-baseline",
|
|
313
|
+
y1: 0,
|
|
314
|
+
y2: "100%",
|
|
315
|
+
x1: .5,
|
|
316
|
+
x2: .5
|
|
317
|
+
})
|
|
318
|
+
]
|
|
319
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
320
|
+
"data-slot": "bar-chart-scrubber",
|
|
321
|
+
role: "slider",
|
|
322
|
+
tabIndex: data.length ? 0 : -1,
|
|
323
|
+
"aria-label": `${label}, explore by ${categoryLabel.toLowerCase()}`,
|
|
324
|
+
"aria-orientation": vertical ? "horizontal" : "vertical",
|
|
325
|
+
"aria-valuemin": 1,
|
|
326
|
+
"aria-valuemax": Math.max(1, data.length),
|
|
327
|
+
"aria-valuenow": (index ?? 0) + 1,
|
|
328
|
+
"aria-valuetext": valueText,
|
|
329
|
+
onPointerDown: (event) => {
|
|
330
|
+
if (event.pointerType !== "mouse") event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
331
|
+
setActive(pointAt(event.clientX, event.clientY));
|
|
332
|
+
},
|
|
333
|
+
onPointerMove: (event) => {
|
|
334
|
+
if (event.pointerType === "mouse" || event.buttons) setActive(pointAt(event.clientX, event.clientY));
|
|
335
|
+
},
|
|
336
|
+
onPointerUp: (event) => {
|
|
337
|
+
if (event.pointerType !== "mouse") setActive(null);
|
|
338
|
+
},
|
|
339
|
+
onPointerCancel: () => setActive(null),
|
|
340
|
+
onPointerLeave: (event) => {
|
|
341
|
+
if (event.pointerType === "mouse") setActive(null);
|
|
342
|
+
},
|
|
343
|
+
onKeyDown,
|
|
344
|
+
onBlur: () => setActive(null),
|
|
345
|
+
onFocus: (event) => {
|
|
346
|
+
if (event.currentTarget.matches(":focus-visible") && last >= 0) setActive((current) => current ?? 0);
|
|
347
|
+
},
|
|
348
|
+
className: "bar-chart-scrubber"
|
|
349
|
+
})]
|
|
350
|
+
}),
|
|
351
|
+
/* @__PURE__ */ jsx("div", {
|
|
352
|
+
"aria-hidden": "true",
|
|
353
|
+
"data-slot": "bar-chart-ticks",
|
|
354
|
+
className: tickHost,
|
|
355
|
+
children: ticks.map((value) => /* @__PURE__ */ jsx("span", {
|
|
356
|
+
"data-tick": value,
|
|
357
|
+
className: vertical ? "line-chart-tick" : "bar-chart-tick-horizontal",
|
|
358
|
+
style: tickStyle(value),
|
|
359
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
360
|
+
className: cn("block text-xs leading-none tabular-nums text-muted-foreground", vertical ? "-translate-y-1/2" : ""),
|
|
361
|
+
children: formatTick(value)
|
|
362
|
+
})
|
|
363
|
+
}, value))
|
|
364
|
+
})
|
|
365
|
+
]
|
|
366
|
+
}),
|
|
367
|
+
/* @__PURE__ */ jsx("div", {
|
|
368
|
+
className: "sr-only",
|
|
369
|
+
children: /* @__PURE__ */ jsxs("table", { children: [
|
|
370
|
+
/* @__PURE__ */ jsx("caption", { children: name }),
|
|
371
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
372
|
+
/* @__PURE__ */ jsx("th", {
|
|
373
|
+
scope: "col",
|
|
374
|
+
children: categoryLabel
|
|
375
|
+
}),
|
|
376
|
+
visible.map((line) => /* @__PURE__ */ jsx("th", {
|
|
377
|
+
scope: "col",
|
|
378
|
+
children: line.label
|
|
379
|
+
}, line.key)),
|
|
380
|
+
stacked && visible.length > 1 ? /* @__PURE__ */ jsx("th", {
|
|
381
|
+
scope: "col",
|
|
382
|
+
children: "Total"
|
|
383
|
+
}) : null
|
|
384
|
+
] }) }),
|
|
385
|
+
/* @__PURE__ */ jsx("tbody", { children: data.map((item) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
386
|
+
/* @__PURE__ */ jsx("th", {
|
|
387
|
+
scope: "row",
|
|
388
|
+
children: item.label
|
|
389
|
+
}),
|
|
390
|
+
visible.map((line) => /* @__PURE__ */ jsx("td", { children: read(item, line) }, line.key)),
|
|
391
|
+
stacked && visible.length > 1 ? /* @__PURE__ */ jsxs("td", { children: [formatValue(totalOf(item), visible[0]), suffix] }) : null
|
|
392
|
+
] }, item.key)) })
|
|
393
|
+
] })
|
|
394
|
+
})
|
|
395
|
+
]
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
//#endregion
|
|
399
|
+
export { SeriesBarChart };
|
|
400
|
+
|
|
401
|
+
//# sourceMappingURL=bar-chart-series.js.map
|