@robr0/design-system 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +4 -4
  2. package/charts.d.ts +1 -0
  3. package/charts.js +2 -0
  4. package/components/AppLayout/AppLayout.d.ts +7 -1
  5. package/components/AppLayout/AppLayout.js +2 -1
  6. package/components/AppSidebar/AppSidebar.css +104 -40
  7. package/components/AppSidebar/AppSidebar.d.ts +14 -1
  8. package/components/AppSidebar/AppSidebar.js +26 -17
  9. package/components/AvatarGroup/AvatarGroup.css +61 -0
  10. package/components/AvatarGroup/AvatarGroup.d.ts +32 -0
  11. package/components/AvatarGroup/AvatarGroup.js +31 -0
  12. package/components/Badge/Badge.css +4 -5
  13. package/components/Chart/AreaChart.d.ts +5 -1
  14. package/components/Chart/AreaChart.js +5 -4
  15. package/components/Chart/BarChart.d.ts +3 -1
  16. package/components/Chart/BarChart.js +3 -3
  17. package/components/Chart/Chart.css +45 -0
  18. package/components/Chart/ComboChart.d.ts +40 -0
  19. package/components/Chart/ComboChart.js +142 -0
  20. package/components/Chart/LineChart.d.ts +3 -1
  21. package/components/Chart/LineChart.js +3 -3
  22. package/components/Chart/PieChart.d.ts +3 -1
  23. package/components/Chart/PieChart.js +3 -3
  24. package/components/Chart/RadarChart.d.ts +3 -1
  25. package/components/Chart/RadarChart.js +3 -3
  26. package/components/Chart/RadialChart.d.ts +7 -1
  27. package/components/Chart/RadialChart.js +51 -43
  28. package/components/Chart/ScatterChart.d.ts +3 -1
  29. package/components/Chart/ScatterChart.js +3 -3
  30. package/components/Chart/StackedBarChart.d.ts +3 -1
  31. package/components/Chart/StackedBarChart.js +3 -3
  32. package/components/Chart/Treemap.d.ts +3 -1
  33. package/components/Chart/Treemap.js +3 -3
  34. package/components/Chart/palette.js +1 -3
  35. package/components/FilterBar/FilterBar.css +202 -0
  36. package/components/FilterBar/FilterBar.d.ts +55 -0
  37. package/components/FilterBar/FilterBar.js +249 -0
  38. package/components/FunnelChart/FunnelChart.css +41 -0
  39. package/components/FunnelChart/FunnelChart.d.ts +40 -0
  40. package/components/FunnelChart/FunnelChart.js +48 -0
  41. package/components/Gauge/Gauge.css +93 -0
  42. package/components/Gauge/Gauge.d.ts +56 -0
  43. package/components/Gauge/Gauge.js +95 -0
  44. package/components/LegendTile/LegendTile.css +60 -0
  45. package/components/LegendTile/LegendTile.d.ts +30 -0
  46. package/components/LegendTile/LegendTile.js +21 -0
  47. package/components/Panel/Panel.css +26 -0
  48. package/components/Panel/Panel.d.ts +22 -0
  49. package/components/Panel/Panel.js +19 -0
  50. package/components/SplitPane/SplitPane.css +106 -0
  51. package/components/SplitPane/SplitPane.d.ts +36 -0
  52. package/components/SplitPane/SplitPane.js +130 -0
  53. package/components/Stat/Stat.css +35 -2
  54. package/components/Stat/Stat.d.ts +3 -1
  55. package/components/Stat/Stat.js +7 -1
  56. package/components/StreamingText/StreamingText.css +40 -0
  57. package/components/StreamingText/StreamingText.d.ts +44 -0
  58. package/components/StreamingText/StreamingText.js +61 -0
  59. package/components/registry.json +73 -0
  60. package/components/registry.json.d.ts +73 -0
  61. package/components/registry.json.js +1 -1
  62. package/index.d.ts +8 -0
  63. package/index.js +16 -0
  64. package/package.json +1 -1
  65. package/tokens/motion.d.ts +7 -3
  66. package/tokens/motion.js +3 -1
  67. package/tokens/registry.json +4 -1
  68. package/tokens/registry.json.d.ts +4 -1
  69. package/tokens/registry.json.js +1 -1
  70. package/tokens/tokens-dark.css +3 -0
  71. package/tokens/tokens-light.css +11 -0
  72. package/tokens/tokens-primitives.css +2 -0
  73. package/tokens/tokens-typography.css +12 -5
@@ -4,8 +4,7 @@ import { ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, Ba
4
4
  import { getChartSeriesColors } from "./palette.js";
5
5
  import "./Chart.css";
6
6
  function getCSSVar(name, fallback) {
7
- if (typeof window === "undefined") return fallback;
8
- return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
7
+ return `var(${name}, ${fallback})`;
9
8
  }
10
9
  function StackedTooltip({ active, payload, label }) {
11
10
  if (!active || !payload?.length) return null;
@@ -27,10 +26,11 @@ const StackedBarChart = ({
27
26
  summaryItems,
28
27
  height = 350,
29
28
  yAxisFormatter,
29
+ bare = false,
30
30
  className = ""
31
31
  }) => {
32
32
  const baseClass = "ds-chart";
33
- const classes = [baseClass, className].filter(Boolean).join(" ");
33
+ const classes = [baseClass, bare ? `${baseClass}--bare` : "", className].filter(Boolean).join(" ");
34
34
  const textSecondary = getCSSVar("--color-text-secondary", "#A2A2A2");
35
35
  const gridColor = getCSSVar("--color-divider", "#232323");
36
36
  const cursorColor = getCSSVar("--color-bg-container-secondary", "#303030");
@@ -23,6 +23,8 @@ export interface TreemapProps {
23
23
  summaryItems?: ChartSummaryItem[];
24
24
  /** Chart area height in pixels */
25
25
  height?: number;
26
+ /** Strip the card chrome (border, padding, fill) when the chart sits inside another panel that supplies the surface */
27
+ bare?: boolean;
26
28
  /** Additional CSS classes on the wrapper */
27
29
  className?: string;
28
30
  }
@@ -30,4 +32,4 @@ export interface TreemapProps {
30
32
  * Treemap chart built on Recharts with design-system tokens.
31
33
  * Displays hierarchical data as nested coloured rectangles sized by value.
32
34
  */
33
- export declare const Treemap: ({ data, dataKey, title, subtitle, summaryItems, height, className, }: TreemapProps) => import("react").JSX.Element;
35
+ export declare const Treemap: ({ data, dataKey, title, subtitle, summaryItems, height, bare, className, }: TreemapProps) => import("react").JSX.Element;
@@ -4,8 +4,7 @@ import { ResponsiveContainer, Treemap as Treemap$1, Tooltip } from "recharts";
4
4
  import { getChartSeriesColors } from "./palette.js";
5
5
  import "./Chart.css";
6
6
  function getCSSVar(name, fallback) {
7
- if (typeof window === "undefined") return fallback;
8
- return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
7
+ return `var(${name}, ${fallback})`;
9
8
  }
10
9
  function TreemapContent(props) {
11
10
  const { x, y, width, height, name, index, color } = props;
@@ -58,10 +57,11 @@ const Treemap = ({
58
57
  subtitle,
59
58
  summaryItems,
60
59
  height = 350,
60
+ bare = false,
61
61
  className = ""
62
62
  }) => {
63
63
  const baseClass = "ds-chart";
64
- const classes = [baseClass, className].filter(Boolean).join(" ");
64
+ const classes = [baseClass, bare ? `${baseClass}--bare` : "", className].filter(Boolean).join(" ");
65
65
  const renderTooltip = useCallback(
66
66
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
67
67
  (props) => /* @__PURE__ */ jsx(TreemapTooltip, { ...props }),
@@ -8,10 +8,8 @@ const SERIES_FALLBACKS = [
8
8
  "#1E47B0"
9
9
  ];
10
10
  function getChartSeriesColors() {
11
- if (typeof window === "undefined") return SERIES_FALLBACKS;
12
- const style = getComputedStyle(document.documentElement);
13
11
  return SERIES_FALLBACKS.map(
14
- (fallback, i) => style.getPropertyValue(`--color-chart-series-${i + 1}`).trim() || fallback
12
+ (fallback, i) => `var(--color-chart-series-${i + 1}, ${fallback})`
15
13
  );
16
14
  }
17
15
  export {
@@ -0,0 +1,202 @@
1
+ /* ============================================
2
+ FILTER BAR COMPONENT
3
+ A row of filter chips, each opening a
4
+ popover listbox. The chips share Chip's pill
5
+ language; active state uses the teal fill
6
+ convention from Chip's selected state.
7
+ ============================================ */
8
+
9
+ /* Base */
10
+
11
+ .ds-filter-bar {
12
+ display: flex;
13
+ align-items: center;
14
+ flex-wrap: wrap;
15
+ gap: var(--gap-sm);
16
+ }
17
+
18
+ .ds-filter-bar__filter {
19
+ position: relative;
20
+ display: inline-flex;
21
+ }
22
+
23
+ /* Chip shell — the pill holding the trigger and the clear button */
24
+
25
+ .ds-filter-bar__chip {
26
+ display: inline-flex;
27
+ align-items: center;
28
+ gap: var(--gap-sm);
29
+ padding: var(--padding-xs) var(--padding-sm-md);
30
+ border: var(--border-xs) solid var(--color-bg-container-border);
31
+ border-radius: var(--radius-full);
32
+ background-color: var(--color-bg-page-primary);
33
+ font-family: var(--font-paragraph-sm-em-family);
34
+ font-size: var(--font-paragraph-sm-em-size);
35
+ font-weight: var(--font-paragraph-sm-em-weight);
36
+ line-height: var(--font-paragraph-sm-em-line-height);
37
+ letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
38
+ color: var(--color-text-secondary);
39
+ white-space: nowrap;
40
+ transition:
41
+ background-color var(--motion-duration-base) var(--motion-ease-standard),
42
+ border-color var(--motion-duration-base) var(--motion-ease-standard),
43
+ color var(--motion-duration-base) var(--motion-ease-standard);
44
+ }
45
+
46
+ .ds-filter-bar--compact .ds-filter-bar__chip {
47
+ gap: var(--gap-xs);
48
+ padding: var(--padding-xxxs) var(--padding-sm);
49
+ }
50
+
51
+ .ds-filter-bar__chip:hover:not(.ds-filter-bar__chip--active) {
52
+ background-color: var(--color-action-passive-bg-hover);
53
+ }
54
+
55
+ .ds-filter-bar__chip--open:not(.ds-filter-bar__chip--active) {
56
+ background-color: var(--color-action-passive-bg-active);
57
+ }
58
+
59
+ /* Active — teal fill, matching Chip's selected convention */
60
+
61
+ .ds-filter-bar__chip--active {
62
+ background-color: var(--color-action-primary-bg);
63
+ border-color: var(--color-action-primary-bg);
64
+ color: var(--color-action-primary-text);
65
+ }
66
+
67
+ .ds-filter-bar__chip--active:hover {
68
+ background-color: var(--color-action-primary-bg-hover);
69
+ }
70
+
71
+ /* Trigger + clear share the shell's typography and colour */
72
+
73
+ .ds-filter-bar__trigger,
74
+ .ds-filter-bar__chip-clear {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ gap: inherit;
78
+ margin: 0;
79
+ padding: 0;
80
+ border: none;
81
+ background: none;
82
+ font: inherit;
83
+ letter-spacing: inherit;
84
+ color: inherit;
85
+ cursor: pointer;
86
+ }
87
+
88
+ .ds-filter-bar__trigger:focus-visible,
89
+ .ds-filter-bar__chip-clear:focus-visible,
90
+ .ds-filter-bar__clear-all:focus-visible {
91
+ outline: var(--border-md) solid var(--color-action-primary-bg);
92
+ outline-offset: var(--gap-xxs);
93
+ border-radius: var(--radius-full);
94
+ }
95
+
96
+ .ds-filter-bar__icon {
97
+ flex-shrink: 0;
98
+ --icon-size: var(--icon-size-sm);
99
+ line-height: 1;
100
+ }
101
+
102
+ .ds-filter-bar__caret {
103
+ flex-shrink: 0;
104
+ --icon-size: var(--icon-size-sm);
105
+ line-height: 1;
106
+ transition: transform var(--motion-duration-base) var(--motion-ease-standard);
107
+ }
108
+
109
+ .ds-filter-bar__chip--open .ds-filter-bar__caret {
110
+ transform: rotate(180deg);
111
+ }
112
+
113
+ .ds-filter-bar__chip-clear {
114
+ flex-shrink: 0;
115
+ border-radius: var(--radius-full);
116
+ }
117
+
118
+ .ds-filter-bar__chip-clear .material-symbols-rounded {
119
+ --icon-size: var(--icon-size-sm);
120
+ line-height: 1;
121
+ }
122
+
123
+ /* Popover listbox */
124
+
125
+ .ds-filter-bar__panel {
126
+ position: absolute;
127
+ top: calc(100% + var(--gap-xxs));
128
+ left: 0;
129
+ z-index: 10;
130
+ min-width: 100%;
131
+ max-height: 280px;
132
+ overflow-y: auto;
133
+ margin: 0;
134
+ padding: var(--padding-xxs);
135
+ list-style: none;
136
+ border: var(--border-xs) solid var(--color-bg-container-border);
137
+ border-radius: var(--radius-md);
138
+ background-color: var(--color-bg-container-primary);
139
+ box-shadow: var(--shadow-floating);
140
+ }
141
+
142
+ .ds-filter-bar__option {
143
+ display: flex;
144
+ align-items: center;
145
+ gap: var(--gap-xs);
146
+ padding: var(--padding-xs) var(--padding-sm);
147
+ border-radius: var(--radius-sm);
148
+ font-family: var(--font-paragraph-sm-family);
149
+ font-size: var(--font-paragraph-sm-size);
150
+ font-weight: var(--font-paragraph-sm-weight);
151
+ line-height: var(--font-paragraph-sm-line-height);
152
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
153
+ color: var(--color-text-primary);
154
+ white-space: nowrap;
155
+ cursor: pointer;
156
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-standard);
157
+ }
158
+
159
+ .ds-filter-bar__option:hover,
160
+ .ds-filter-bar__option--focused {
161
+ background-color: var(--color-action-passive-bg-hover);
162
+ }
163
+
164
+ .ds-filter-bar__option--selected {
165
+ color: var(--color-text-primary);
166
+ }
167
+
168
+ .ds-filter-bar__option-check {
169
+ flex-shrink: 0;
170
+ width: var(--icon-size-sm);
171
+ --icon-size: var(--icon-size-sm);
172
+ line-height: 1;
173
+ color: var(--color-action-primary-bg);
174
+ }
175
+
176
+ /* Clear all */
177
+
178
+ .ds-filter-bar__clear-all {
179
+ margin: 0;
180
+ padding: var(--padding-xs) var(--padding-sm);
181
+ border: none;
182
+ background: none;
183
+ border-radius: var(--radius-full);
184
+ font-family: var(--font-paragraph-sm-em-family);
185
+ font-size: var(--font-paragraph-sm-em-size);
186
+ font-weight: var(--font-paragraph-sm-em-weight);
187
+ line-height: var(--font-paragraph-sm-em-line-height);
188
+ letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
189
+ color: var(--color-text-secondary);
190
+ cursor: pointer;
191
+ transition: color var(--motion-duration-base) var(--motion-ease-standard);
192
+ }
193
+
194
+ .ds-filter-bar__clear-all:hover {
195
+ color: var(--color-text-primary);
196
+ }
197
+
198
+ /* Icons take the colour of the text around them (see Chip.css for why
199
+ this is explicit rather than inherited). */
200
+ .ds-filter-bar .material-symbols-rounded {
201
+ color: inherit;
202
+ }
@@ -0,0 +1,55 @@
1
+ import { default as React } from 'react';
2
+ /** One choice inside a filter's popover. */
3
+ export interface FilterBarOption {
4
+ /** Stored value, reported through `onValuesChange`. */
5
+ value: string;
6
+ /** Display label. */
7
+ label: string;
8
+ }
9
+ /** One filter: a chip on the bar with a popover of options behind it. */
10
+ export interface FilterBarFilter {
11
+ /** Stable key for this filter — the key under which its values are reported. */
12
+ id: string;
13
+ /** Chip label, e.g. "Status". */
14
+ label: string;
15
+ /** Material Symbol icon name shown before the label. */
16
+ icon?: string;
17
+ /** The choices this filter offers. */
18
+ options: FilterBarOption[];
19
+ /** Whether several options can be active at once. Single-select closes on choice. */
20
+ multiple?: boolean;
21
+ }
22
+ /** Props owned by FilterBar itself — everything else falls through to the root div. */
23
+ type FilterBarOwnProps = {
24
+ /** The filters on the bar, in display order. */
25
+ filters: FilterBarFilter[];
26
+ /** Active options per filter id (controlled). Pair with `onValuesChange`. */
27
+ values?: Record<string, string[]>;
28
+ /** Active options per filter id (uncontrolled initial state). */
29
+ defaultValues?: Record<string, string[]>;
30
+ /**
31
+ * Fires with the full active-filter map on every change. Filters with
32
+ * nothing active are absent from the map, so an empty object means
33
+ * unfiltered.
34
+ */
35
+ onValuesChange?: (values: Record<string, string[]>) => void;
36
+ /** Label for the button that clears every filter at once. */
37
+ clearLabel?: string;
38
+ /** Component size */
39
+ size?: 'default' | 'compact';
40
+ /** Additional CSS classes */
41
+ className?: string;
42
+ };
43
+ export interface FilterBarProps extends FilterBarOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof FilterBarOwnProps | 'children'> {
44
+ }
45
+ /**
46
+ * FilterBar — a row of filter chips for narrowing a collection: each chip
47
+ * opens a popover of options, active filters show their choice and grow a
48
+ * clear button, and a clear-all appears once anything is active. State is one
49
+ * map of filter id to active values, controlled or uncontrolled, so wiring it
50
+ * to a DataTable is a single callback. Popovers are real listboxes: arrow
51
+ * keys move, Enter and Space toggle, Escape closes, focus never leaves the
52
+ * chip.
53
+ */
54
+ export declare const FilterBar: React.ForwardRefExoticComponent<FilterBarProps & React.RefAttributes<HTMLDivElement>>;
55
+ export {};
@@ -0,0 +1,249 @@
1
+ "use client";
2
+ import { jsxs, jsx } from "react/jsx-runtime";
3
+ import React, { useId, useState, useRef, useCallback, useEffect } from "react";
4
+ import "./FilterBar.css";
5
+ import "../../fonts/material-symbols.css";
6
+ const FilterBar = React.forwardRef(
7
+ ({
8
+ filters,
9
+ values,
10
+ defaultValues,
11
+ onValuesChange,
12
+ clearLabel = "Clear all",
13
+ size = "default",
14
+ className = "",
15
+ ...rest
16
+ }, ref) => {
17
+ const baseClass = "ds-filter-bar";
18
+ const generatedId = useId();
19
+ const [uncontrolledValues, setUncontrolledValues] = useState(
20
+ defaultValues ?? {}
21
+ );
22
+ const [openId, setOpenId] = useState(null);
23
+ const [focusedIndex, setFocusedIndex] = useState(-1);
24
+ const filterRefs = useRef(/* @__PURE__ */ new Map());
25
+ const activeValues = values ?? uncontrolledValues;
26
+ const applyValues = (next) => {
27
+ if (values === void 0) setUncontrolledValues(next);
28
+ onValuesChange?.(next);
29
+ };
30
+ const closePopover = useCallback(() => {
31
+ setOpenId(null);
32
+ setFocusedIndex(-1);
33
+ }, []);
34
+ useEffect(() => {
35
+ if (!openId) return;
36
+ const handler = (e) => {
37
+ const node = filterRefs.current.get(openId);
38
+ if (node && !node.contains(e.target)) closePopover();
39
+ };
40
+ document.addEventListener("mousedown", handler);
41
+ return () => document.removeEventListener("mousedown", handler);
42
+ }, [openId, closePopover]);
43
+ const toggleOption = (filter, optionValue) => {
44
+ const current = activeValues[filter.id] ?? [];
45
+ let nextForFilter;
46
+ if (filter.multiple === false) {
47
+ nextForFilter = current[0] === optionValue ? [] : [optionValue];
48
+ } else {
49
+ nextForFilter = current.includes(optionValue) ? current.filter((v) => v !== optionValue) : [...current, optionValue];
50
+ }
51
+ const next = { ...activeValues };
52
+ if (nextForFilter.length > 0) next[filter.id] = nextForFilter;
53
+ else delete next[filter.id];
54
+ applyValues(next);
55
+ if (filter.multiple === false) closePopover();
56
+ };
57
+ const clearFilter = (filterId) => {
58
+ const next = { ...activeValues };
59
+ delete next[filterId];
60
+ applyValues(next);
61
+ if (openId === filterId) closePopover();
62
+ };
63
+ const handleTriggerClick = (filter) => {
64
+ setFocusedIndex(-1);
65
+ setOpenId((current) => current === filter.id ? null : filter.id);
66
+ };
67
+ const handleTriggerKeyDown = (e, filter) => {
68
+ const isOpen = openId === filter.id;
69
+ switch (e.key) {
70
+ case "Enter":
71
+ case " ":
72
+ e.preventDefault();
73
+ if (!isOpen) {
74
+ setOpenId(filter.id);
75
+ setFocusedIndex(0);
76
+ } else if (focusedIndex >= 0) {
77
+ toggleOption(filter, filter.options[focusedIndex].value);
78
+ } else {
79
+ closePopover();
80
+ }
81
+ break;
82
+ case "ArrowDown":
83
+ e.preventDefault();
84
+ if (!isOpen) {
85
+ setOpenId(filter.id);
86
+ setFocusedIndex(0);
87
+ } else {
88
+ setFocusedIndex((prev) => Math.min(prev + 1, filter.options.length - 1));
89
+ }
90
+ break;
91
+ case "ArrowUp":
92
+ e.preventDefault();
93
+ if (isOpen) setFocusedIndex((prev) => Math.max(prev - 1, 0));
94
+ break;
95
+ case "Home":
96
+ if (isOpen) {
97
+ e.preventDefault();
98
+ setFocusedIndex(0);
99
+ }
100
+ break;
101
+ case "End":
102
+ if (isOpen) {
103
+ e.preventDefault();
104
+ setFocusedIndex(filter.options.length - 1);
105
+ }
106
+ break;
107
+ case "Escape":
108
+ case "Tab":
109
+ closePopover();
110
+ break;
111
+ }
112
+ };
113
+ const summarize = (filter) => {
114
+ const active = activeValues[filter.id] ?? [];
115
+ if (active.length === 0) return filter.label;
116
+ if (active.length === 1) {
117
+ const option = filter.options.find((o) => o.value === active[0]);
118
+ return `${filter.label}: ${option?.label ?? active[0]}`;
119
+ }
120
+ return `${filter.label}: ${active.length}`;
121
+ };
122
+ const anyActive = filters.some((f) => (activeValues[f.id] ?? []).length > 0);
123
+ const classes = [baseClass, size === "compact" && `${baseClass}--compact`, className].filter(Boolean).join(" ");
124
+ return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
125
+ filters.map((filter) => {
126
+ const isOpen = openId === filter.id;
127
+ const active = activeValues[filter.id] ?? [];
128
+ const isActive = active.length > 0;
129
+ const listboxId = `${generatedId}-${filter.id}-listbox`;
130
+ const activeDescendant = isOpen && focusedIndex >= 0 ? `${generatedId}-${filter.id}-option-${focusedIndex}` : void 0;
131
+ const chipClasses = [
132
+ `${baseClass}__chip`,
133
+ isActive && `${baseClass}__chip--active`,
134
+ isOpen && `${baseClass}__chip--open`
135
+ ].filter(Boolean).join(" ");
136
+ return /* @__PURE__ */ jsxs(
137
+ "div",
138
+ {
139
+ className: `${baseClass}__filter`,
140
+ ref: (node) => {
141
+ if (node) filterRefs.current.set(filter.id, node);
142
+ else filterRefs.current.delete(filter.id);
143
+ },
144
+ children: [
145
+ /* @__PURE__ */ jsxs("span", { className: chipClasses, children: [
146
+ /* @__PURE__ */ jsxs(
147
+ "button",
148
+ {
149
+ type: "button",
150
+ className: `${baseClass}__trigger`,
151
+ "aria-haspopup": "listbox",
152
+ "aria-expanded": isOpen,
153
+ "aria-controls": isOpen ? listboxId : void 0,
154
+ "aria-activedescendant": activeDescendant,
155
+ onClick: () => handleTriggerClick(filter),
156
+ onKeyDown: (e) => handleTriggerKeyDown(e, filter),
157
+ children: [
158
+ filter.icon && /* @__PURE__ */ jsx(
159
+ "span",
160
+ {
161
+ className: `${baseClass}__icon material-symbols-rounded`,
162
+ "aria-hidden": "true",
163
+ children: filter.icon
164
+ }
165
+ ),
166
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__summary`, children: summarize(filter) }),
167
+ /* @__PURE__ */ jsx(
168
+ "span",
169
+ {
170
+ className: `${baseClass}__caret material-symbols-rounded`,
171
+ "aria-hidden": "true",
172
+ children: "expand_more"
173
+ }
174
+ )
175
+ ]
176
+ }
177
+ ),
178
+ isActive && /* @__PURE__ */ jsx(
179
+ "button",
180
+ {
181
+ type: "button",
182
+ className: `${baseClass}__chip-clear`,
183
+ "aria-label": `Clear ${filter.label}`,
184
+ onClick: () => clearFilter(filter.id),
185
+ children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", "aria-hidden": "true", children: "close" })
186
+ }
187
+ )
188
+ ] }),
189
+ isOpen && /* @__PURE__ */ jsx(
190
+ "ul",
191
+ {
192
+ id: listboxId,
193
+ className: `${baseClass}__panel`,
194
+ role: "listbox",
195
+ "aria-label": filter.label,
196
+ "aria-multiselectable": filter.multiple !== false || void 0,
197
+ children: filter.options.map((option, index) => {
198
+ const selected = active.includes(option.value);
199
+ const optionClasses = [
200
+ `${baseClass}__option`,
201
+ selected && `${baseClass}__option--selected`,
202
+ index === focusedIndex && `${baseClass}__option--focused`
203
+ ].filter(Boolean).join(" ");
204
+ return /* @__PURE__ */ jsxs(
205
+ "li",
206
+ {
207
+ id: `${generatedId}-${filter.id}-option-${index}`,
208
+ className: optionClasses,
209
+ role: "option",
210
+ "aria-selected": selected,
211
+ onClick: () => toggleOption(filter, option.value),
212
+ children: [
213
+ /* @__PURE__ */ jsx(
214
+ "span",
215
+ {
216
+ className: `${baseClass}__option-check material-symbols-rounded`,
217
+ "aria-hidden": "true",
218
+ children: selected ? "check" : ""
219
+ }
220
+ ),
221
+ option.label
222
+ ]
223
+ },
224
+ option.value
225
+ );
226
+ })
227
+ }
228
+ )
229
+ ]
230
+ },
231
+ filter.id
232
+ );
233
+ }),
234
+ anyActive && /* @__PURE__ */ jsx(
235
+ "button",
236
+ {
237
+ type: "button",
238
+ className: `${baseClass}__clear-all`,
239
+ onClick: () => applyValues({}),
240
+ children: clearLabel
241
+ }
242
+ )
243
+ ] });
244
+ }
245
+ );
246
+ FilterBar.displayName = "FilterBar";
247
+ export {
248
+ FilterBar
249
+ };
@@ -0,0 +1,41 @@
1
+ /* ============================================
2
+ FUNNEL CHART COMPONENT
3
+ Side-by-side vertical bars stepping down with
4
+ each stage's share of the first. Geometry and
5
+ colour arrive per stage as inline custom
6
+ properties (--funnel-stage-size, a clamped
7
+ percentage number; --funnel-stage-color, a
8
+ chart-series token), so the rules here stay
9
+ token-only.
10
+ ============================================ */
11
+
12
+ /* Base */
13
+
14
+ .ds-funnel-chart {
15
+ display: flex;
16
+ align-items: center;
17
+ gap: var(--gap-xxs);
18
+ }
19
+
20
+ /* Parts */
21
+
22
+ .ds-funnel-chart__stage {
23
+ flex: 1;
24
+ min-width: 0;
25
+ height: calc(var(--funnel-stage-size) * 1%);
26
+ border-radius: var(--radius-sm);
27
+ background-color: var(--funnel-stage-color);
28
+ display: grid;
29
+ place-items: center;
30
+ }
31
+
32
+ .ds-funnel-chart__pct {
33
+ background-color: var(--color-bg-container-primary);
34
+ color: var(--color-text-primary);
35
+ border-radius: var(--radius-full);
36
+ padding: var(--padding-xxxs) var(--padding-xs);
37
+ font-family: var(--font-caption-family);
38
+ font-size: var(--font-caption-size);
39
+ font-weight: var(--font-paragraph-sm-em-weight);
40
+ line-height: var(--font-caption-line-height);
41
+ }
@@ -0,0 +1,40 @@
1
+ import { default as React } from 'react';
2
+ /** One stage of the funnel, ordered widest first. */
3
+ export interface FunnelStage {
4
+ /** Stage name, read into the chart's accessible label. */
5
+ label: string;
6
+ /** Raw count for the stage — drives the bar's height as a share of the first stage. */
7
+ value: number;
8
+ /**
9
+ * Preformatted value ("96.4K") for the accessible label and any consumer
10
+ * legend. Falls back to the raw `value`, locale-formatted.
11
+ */
12
+ displayValue?: string;
13
+ }
14
+ /** Props owned by FunnelChart itself — everything else falls through to the root div. */
15
+ type FunnelChartOwnProps = {
16
+ /** Ordered stages, first stage widest. Each later stage's height is its share of the first. */
17
+ data: FunnelStage[];
18
+ /** Chart height in pixels. */
19
+ height?: number;
20
+ /**
21
+ * Floor percentage for a stage's height, so steep drop-offs stay readable.
22
+ * Shares are clamped to the range from this floor up to 100.
23
+ */
24
+ minStageShare?: number;
25
+ /** Additional CSS classes */
26
+ className?: string;
27
+ };
28
+ export interface FunnelChartProps extends FunnelChartOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof FunnelChartOwnProps | 'children'> {
29
+ }
30
+ /**
31
+ * FunnelChart — ordered stages as side-by-side vertical bars whose heights
32
+ * step down with each stage's share of the first, each carrying a centred
33
+ * percentage pill. Pure JSX and CSS computed from props: no recharts, no
34
+ * hooks, no browser APIs, so it deliberately omits 'use client' and renders
35
+ * from a Server Component. Degenerate data stays safe: an empty array
36
+ * renders an empty root, and a zero or negative first value draws every
37
+ * stage at the floor height.
38
+ */
39
+ export declare const FunnelChart: React.ForwardRefExoticComponent<FunnelChartProps & React.RefAttributes<HTMLDivElement>>;
40
+ export {};