@zuilib/charts 0.3.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 (41) hide show
  1. package/README.md +140 -0
  2. package/dist/authoring.d.ts +47 -0
  3. package/dist/authoring.js +11 -0
  4. package/dist/chart-defaults.d.ts +60 -0
  5. package/dist/chart-defaults.js +24 -0
  6. package/dist/chart-frame-B9fxUxGG.d.ts +90 -0
  7. package/dist/chart-frame.d.ts +4 -0
  8. package/dist/chart-frame.js +14 -0
  9. package/dist/chart-legend.d.ts +25 -0
  10. package/dist/chart-legend.js +13 -0
  11. package/dist/chart-spec-CH0tAkLG.d.ts +226 -0
  12. package/dist/chart-tooltip.d.ts +44 -0
  13. package/dist/chart-tooltip.js +11 -0
  14. package/dist/chart.d.ts +78 -0
  15. package/dist/chart.js +24 -0
  16. package/dist/chunk-3VGLTPGL.js +19 -0
  17. package/dist/chunk-6QTIAMA3.js +169 -0
  18. package/dist/chunk-7WABBHMT.js +88 -0
  19. package/dist/chunk-7WJ26NHR.js +48 -0
  20. package/dist/chunk-BFTWWI46.js +148 -0
  21. package/dist/chunk-D7K23UFP.js +43 -0
  22. package/dist/chunk-G4FXJJ66.js +169 -0
  23. package/dist/chunk-HNJKYOPE.js +45 -0
  24. package/dist/chunk-IPOBQIYU.js +13 -0
  25. package/dist/chunk-MJDIQVJX.js +148 -0
  26. package/dist/chunk-PV27RZSV.js +61 -0
  27. package/dist/chunk-SHQZRND7.js +83 -0
  28. package/dist/chunk-UN3XPBZ5.js +17 -0
  29. package/dist/chunk-WVWGHGZE.js +90 -0
  30. package/dist/chunk-YMPZODMC.js +1084 -0
  31. package/dist/cross-filter.d.ts +62 -0
  32. package/dist/cross-filter.js +13 -0
  33. package/dist/format.d.ts +17 -0
  34. package/dist/format.js +7 -0
  35. package/dist/index.d.ts +13 -0
  36. package/dist/index.js +93 -0
  37. package/dist/interop.d.ts +46 -0
  38. package/dist/interop.js +12 -0
  39. package/dist/registry.d.ts +111 -0
  40. package/dist/registry.js +25 -0
  41. package/package.json +98 -0
@@ -0,0 +1,169 @@
1
+ import {
2
+ createFormatters
3
+ } from "./chunk-7WABBHMT.js";
4
+ import {
5
+ defaultChartLabels
6
+ } from "./chunk-UN3XPBZ5.js";
7
+ import {
8
+ ChartContext
9
+ } from "./chunk-IPOBQIYU.js";
10
+ import {
11
+ CHART_COLOR_COUNT,
12
+ seriesColorProperty
13
+ } from "./chunk-7WJ26NHR.js";
14
+
15
+ // src/chart-frame.tsx
16
+ import {
17
+ Children,
18
+ cloneElement,
19
+ forwardRef,
20
+ isValidElement,
21
+ useCallback,
22
+ useId,
23
+ useMemo,
24
+ useState
25
+ } from "react";
26
+ import { ResponsiveContainer } from "recharts";
27
+ import Alert from "@zuilib/components/alert";
28
+ import { cn } from "@zuilib/components/lib/cn";
29
+
30
+ // src/legend-toggle.ts
31
+ function legendToggle(allFields, hidden, field, options) {
32
+ if (options?.additive) {
33
+ return hidden.includes(field) ? hidden.filter((f) => f !== field) : [...hidden, field];
34
+ }
35
+ const visible = allFields.filter((f) => !hidden.includes(f));
36
+ const isolated = visible.length === 1 && visible[0] === field;
37
+ return isolated ? [] : allFields.filter((f) => f !== field);
38
+ }
39
+
40
+ // src/chart-frame.tsx
41
+ import { jsx, jsxs } from "react/jsx-runtime";
42
+ var ChartFrame = forwardRef(
43
+ ({
44
+ config,
45
+ children,
46
+ width,
47
+ height,
48
+ aspect = 16 / 9,
49
+ loading = false,
50
+ error,
51
+ emptyState,
52
+ empty = false,
53
+ ariaLabel,
54
+ labels: labelOverrides,
55
+ formats: customFormats,
56
+ locale,
57
+ hiddenSeries,
58
+ defaultHiddenSeries,
59
+ onHiddenSeriesChange,
60
+ categoryField,
61
+ categories,
62
+ className,
63
+ style,
64
+ ...props
65
+ }, ref) => {
66
+ const chartId = useId().replace(/:/g, "");
67
+ const labels = useMemo(() => ({ ...defaultChartLabels, ...labelOverrides }), [labelOverrides]);
68
+ const formats = useMemo(() => createFormatters(locale, customFormats), [locale, customFormats]);
69
+ const [innerHidden, setInnerHidden] = useState(defaultHiddenSeries ?? []);
70
+ const hidden = hiddenSeries ?? innerHidden;
71
+ const allFields = useMemo(() => Object.keys(config), [config]);
72
+ const onLegendClick = useCallback(
73
+ (field, options) => {
74
+ const next = legendToggle(allFields, hiddenSeries ?? innerHidden, field, options);
75
+ if (hiddenSeries === void 0) setInnerHidden(next);
76
+ onHiddenSeriesChange?.(next);
77
+ },
78
+ [allFields, hiddenSeries, innerHidden, onHiddenSeriesChange]
79
+ );
80
+ const context = useMemo(
81
+ () => ({ config, formats, labels, hiddenSeries: hidden, onLegendClick, categoryField, categories }),
82
+ [config, formats, labels, hidden, onLegendClick, categoryField, categories]
83
+ );
84
+ const colorStyle = useMemo(() => {
85
+ const vars = {};
86
+ Object.entries(config).forEach(([field, series], index) => {
87
+ const slot = Math.min(index + 1, CHART_COLOR_COUNT);
88
+ vars[seriesColorProperty(field)] = series.color ?? (series.theme ? series.theme.light : `var(--chart-${slot})`);
89
+ });
90
+ return vars;
91
+ }, [config]);
92
+ const themed = useMemo(
93
+ () => Object.entries(config).filter((entry) => !!entry[1].theme && !entry[1].color),
94
+ [config]
95
+ );
96
+ const fixedSize = typeof width === "number" && typeof height === "number";
97
+ let body;
98
+ if (loading) {
99
+ body = /* @__PURE__ */ jsx(
100
+ "div",
101
+ {
102
+ "data-slot": "chart-skeleton",
103
+ role: "status",
104
+ "aria-label": labels.loading,
105
+ className: "bg-muted h-full min-h-32 w-full animate-pulse rounded-md motion-reduce:animate-none"
106
+ }
107
+ );
108
+ } else if (error !== void 0 && error !== null && error !== false) {
109
+ body = /* @__PURE__ */ jsxs(Alert, { tone: "danger", "data-slot": "chart-error", children: [
110
+ /* @__PURE__ */ jsx(Alert.Title, { children: labels.chartError }),
111
+ typeof error === "boolean" ? null : /* @__PURE__ */ jsx(Alert.Description, { children: error })
112
+ ] });
113
+ } else if (empty) {
114
+ body = /* @__PURE__ */ jsx(
115
+ "div",
116
+ {
117
+ "data-slot": "chart-empty",
118
+ className: "text-muted-foreground flex h-full min-h-32 w-full items-center justify-center text-sm",
119
+ children: emptyState ?? labels.noData
120
+ }
121
+ );
122
+ } else {
123
+ const chart = Children.only(children);
124
+ if (fixedSize && isValidElement(chart)) {
125
+ body = cloneElement(chart, { width, height });
126
+ } else {
127
+ body = /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", ...height !== void 0 ? { height } : { aspect }, children: chart });
128
+ }
129
+ }
130
+ return /* @__PURE__ */ jsxs(
131
+ "div",
132
+ {
133
+ ref,
134
+ "data-slot": "chart",
135
+ "data-chart": chartId,
136
+ role: "figure",
137
+ "aria-label": ariaLabel,
138
+ className: cn(
139
+ "text-foreground text-xs [&_.recharts-surface]:outline-none [&_.recharts-sector]:outline-none",
140
+ fixedSize ? "" : "w-full",
141
+ className
142
+ ),
143
+ style: { ...colorStyle, ...style },
144
+ ...props,
145
+ children: [
146
+ themed.length > 0 && /* @__PURE__ */ jsx(
147
+ "style",
148
+ {
149
+ dangerouslySetInnerHTML: {
150
+ __html: [
151
+ `[data-chart="${chartId}"]{${themed.map(([field, s]) => `${seriesColorProperty(field)}:${s.theme.light};`).join("")}}`,
152
+ `.dark [data-chart="${chartId}"]{${themed.map(([field, s]) => `${seriesColorProperty(field)}:${s.theme.dark};`).join("")}}`
153
+ ].join("\n")
154
+ }
155
+ }
156
+ ),
157
+ /* @__PURE__ */ jsx(ChartContext.Provider, { value: context, children: body })
158
+ ]
159
+ }
160
+ );
161
+ }
162
+ );
163
+ ChartFrame.displayName = "ChartFrame";
164
+ var chart_frame_default = ChartFrame;
165
+
166
+ export {
167
+ ChartFrame,
168
+ chart_frame_default
169
+ };
@@ -0,0 +1,45 @@
1
+ import {
2
+ DIMMED_OPACITY
3
+ } from "./chunk-7WJ26NHR.js";
4
+
5
+ // src/charts/mark-interaction.ts
6
+ function prefersReducedMotion() {
7
+ try {
8
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+ function reportMarkClick(interaction, value, seriesField, row, additive) {
14
+ interaction.onMarkClick?.({ field: interaction.field, value, seriesField, row, additive });
15
+ const { crossFilter, field } = interaction;
16
+ if (!crossFilter || interaction.interaction === "none") return;
17
+ if (interaction.interaction === "highlight") {
18
+ const current = crossFilter.state.highlight;
19
+ const active = current?.field === field && current.values.includes(value);
20
+ crossFilter.setHighlight(active ? null : { field, values: [value] });
21
+ } else {
22
+ crossFilter.toggleValue(field, value, { additive });
23
+ }
24
+ }
25
+ function activeCategoryValues(interaction) {
26
+ const highlight = interaction.crossFilter?.state.highlight;
27
+ if (highlight && highlight.field === interaction.field) return highlight.values;
28
+ return interaction.crossFilter?.valuesFor(interaction.field) ?? null;
29
+ }
30
+ function markDimOpacity(active, value) {
31
+ if (active === null) return void 0;
32
+ return typeof value !== "undefined" && active.includes(value) ? void 0 : DIMMED_OPACITY;
33
+ }
34
+ function categoryOf(row, field) {
35
+ const value = row?.[field];
36
+ return typeof value === "string" || typeof value === "number" ? value : null;
37
+ }
38
+
39
+ export {
40
+ prefersReducedMotion,
41
+ reportMarkClick,
42
+ activeCategoryValues,
43
+ markDimOpacity,
44
+ categoryOf
45
+ };
@@ -0,0 +1,13 @@
1
+ // src/chart-context.ts
2
+ import { createContext, useContext } from "react";
3
+ var ChartContext = createContext(null);
4
+ function useChartContext() {
5
+ const context = useContext(ChartContext);
6
+ if (!context) throw new Error("Chart components must be used inside a <ChartFrame>");
7
+ return context;
8
+ }
9
+
10
+ export {
11
+ ChartContext,
12
+ useChartContext
13
+ };
@@ -0,0 +1,148 @@
1
+ import {
2
+ defaultChartRegistry
3
+ } from "./chunk-YMPZODMC.js";
4
+ import {
5
+ prefersReducedMotion
6
+ } from "./chunk-HNJKYOPE.js";
7
+ import {
8
+ chart_frame_default
9
+ } from "./chunk-6QTIAMA3.js";
10
+ import {
11
+ createFormatters
12
+ } from "./chunk-7WABBHMT.js";
13
+ import {
14
+ warnOnce
15
+ } from "./chunk-3VGLTPGL.js";
16
+ import {
17
+ useOptionalCrossFilter
18
+ } from "./chunk-WVWGHGZE.js";
19
+ import {
20
+ chartColorVar
21
+ } from "./chunk-7WJ26NHR.js";
22
+
23
+ // src/chart.tsx
24
+ import { useMemo, useState } from "react";
25
+ import { jsx } from "react/jsx-runtime";
26
+ function configFromSpec(spec, registry = defaultChartRegistry) {
27
+ const fromDef = registry.types.get(spec.type)?.configFrom?.(spec);
28
+ if (fromDef) return fromDef;
29
+ if ("series" in spec && Array.isArray(spec.series)) {
30
+ return Object.fromEntries(
31
+ spec.series.map((series) => [
32
+ series.field,
33
+ {
34
+ label: series.label ?? series.field,
35
+ color: series.paletteSlot ? chartColorVar(series.paletteSlot) : void 0,
36
+ format: series.format
37
+ }
38
+ ])
39
+ );
40
+ }
41
+ return {};
42
+ }
43
+ function crossFilterFieldOf(spec, registry = defaultChartRegistry) {
44
+ const field = registry.types.get(spec.type)?.crossFilterField?.(spec);
45
+ if (field !== void 0) return field;
46
+ const xKey = spec.xKey;
47
+ return typeof xKey === "string" ? xKey : "";
48
+ }
49
+ function categoriesOf(spec, rows, registry) {
50
+ const field = categoryFieldOf(spec, registry);
51
+ if (!field) return void 0;
52
+ const values = [];
53
+ for (const row of rows) {
54
+ const value = row[field];
55
+ if ((typeof value === "string" || typeof value === "number") && !values.includes(value)) values.push(value);
56
+ }
57
+ return values.map((value, index) => ({ value, color: chartColorVar(index + 1) }));
58
+ }
59
+ function categoryFieldOf(spec, registry) {
60
+ return registry.types.get(spec.type)?.categoryField?.(spec);
61
+ }
62
+ function renderSpec(props, registry) {
63
+ const def = registry.types.get(props.spec.type);
64
+ if (!def) {
65
+ warnOnce(`type-${props.spec.type}`, `@zuilib/charts: unknown chart type "${props.spec.type}"`);
66
+ return null;
67
+ }
68
+ return /* @__PURE__ */ jsx(def.render, { ...props });
69
+ }
70
+ function Chart({
71
+ spec,
72
+ rows,
73
+ registry,
74
+ formats,
75
+ locale,
76
+ className,
77
+ width,
78
+ height,
79
+ aspect,
80
+ loading,
81
+ error,
82
+ emptyState,
83
+ ariaLabel,
84
+ hiddenSeries,
85
+ defaultHiddenSeries,
86
+ onHiddenSeriesChange,
87
+ onMarkClick,
88
+ labels,
89
+ animate
90
+ }) {
91
+ const chartRegistry = registry ?? defaultChartRegistry;
92
+ const crossFilter = useOptionalCrossFilter();
93
+ const formatters = useMemo(() => createFormatters(locale, formats), [locale, formats]);
94
+ const config = useMemo(() => configFromSpec(spec, chartRegistry), [spec, chartRegistry]);
95
+ const [innerHidden, setInnerHidden] = useState(defaultHiddenSeries ?? []);
96
+ const hidden = hiddenSeries ?? innerHidden;
97
+ const onHiddenChange = (next) => {
98
+ if (hiddenSeries === void 0) setInnerHidden(next);
99
+ onHiddenSeriesChange?.(next);
100
+ };
101
+ const markInteraction = {
102
+ field: crossFilterFieldOf(spec, chartRegistry),
103
+ crossFilter,
104
+ interaction: spec.interaction ?? "filter",
105
+ onMarkClick
106
+ };
107
+ return /* @__PURE__ */ jsx(
108
+ chart_frame_default,
109
+ {
110
+ config,
111
+ className,
112
+ width,
113
+ height,
114
+ aspect,
115
+ loading,
116
+ error,
117
+ emptyState,
118
+ empty: rows.length === 0,
119
+ ariaLabel: ariaLabel ?? spec.title,
120
+ labels,
121
+ formats,
122
+ locale,
123
+ hiddenSeries: hidden,
124
+ onHiddenSeriesChange: onHiddenChange,
125
+ categoryField: categoryFieldOf(spec, chartRegistry),
126
+ categories: useMemo(() => categoriesOf(spec, rows, chartRegistry), [spec, rows, chartRegistry]),
127
+ children: renderSpec(
128
+ {
129
+ spec,
130
+ rows,
131
+ formats: formatters,
132
+ hiddenSeries: hidden,
133
+ markInteraction,
134
+ animate: animate ?? !prefersReducedMotion()
135
+ },
136
+ chartRegistry
137
+ )
138
+ }
139
+ );
140
+ }
141
+ var chart_default = Chart;
142
+
143
+ export {
144
+ configFromSpec,
145
+ crossFilterFieldOf,
146
+ Chart,
147
+ chart_default
148
+ };
@@ -0,0 +1,61 @@
1
+ import {
2
+ useChartContext
3
+ } from "./chunk-IPOBQIYU.js";
4
+ import {
5
+ seriesColorVar
6
+ } from "./chunk-7WJ26NHR.js";
7
+
8
+ // src/chart-tooltip.tsx
9
+ import { cn } from "@zuilib/components/lib/cn";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ function ChartTooltipPanel({
12
+ active,
13
+ payload,
14
+ label,
15
+ labelFormat,
16
+ valueFormat = "number",
17
+ showLabel = true,
18
+ className
19
+ }) {
20
+ const { config, formats } = useChartContext();
21
+ if (!active || !payload?.length) return null;
22
+ const labelText = formats.resolve(labelFormat, "none")(label);
23
+ return /* @__PURE__ */ jsxs(
24
+ "div",
25
+ {
26
+ "data-slot": "chart-tooltip",
27
+ className: cn(
28
+ "bg-popover text-popover-foreground border-border min-w-32 rounded-md border px-2.5 py-1.5 shadow-md",
29
+ className
30
+ ),
31
+ children: [
32
+ showLabel && labelText !== "" && /* @__PURE__ */ jsx("div", { "data-slot": "chart-tooltip-label", className: "mb-1 font-medium", children: labelText }),
33
+ /* @__PURE__ */ jsx("ul", { className: "grid gap-1", children: payload.map((entry, index) => {
34
+ const field = entry.dataKey != null ? String(entry.dataKey) : String(entry.name ?? index);
35
+ const series = config[field];
36
+ const name = series?.label ?? String(entry.name ?? field);
37
+ const format = formats.resolve(series?.format, valueFormat);
38
+ return /* @__PURE__ */ jsxs("li", { "data-slot": "chart-tooltip-row", className: "flex items-center gap-1.5", children: [
39
+ /* @__PURE__ */ jsx(
40
+ "span",
41
+ {
42
+ "data-slot": "chart-tooltip-chip",
43
+ "aria-hidden": true,
44
+ className: "size-2.5 shrink-0 rounded-xs",
45
+ style: { backgroundColor: series ? seriesColorVar(field) : entry.color }
46
+ }
47
+ ),
48
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: name }),
49
+ /* @__PURE__ */ jsx("span", { className: "text-foreground ms-auto font-mono font-medium tabular-nums", children: format(entry.value) })
50
+ ] }, `${field}-${index}`);
51
+ }) })
52
+ ]
53
+ }
54
+ );
55
+ }
56
+ var chart_tooltip_default = ChartTooltipPanel;
57
+
58
+ export {
59
+ ChartTooltipPanel,
60
+ chart_tooltip_default
61
+ };
@@ -0,0 +1,83 @@
1
+ import {
2
+ useOptionalCrossFilter
3
+ } from "./chunk-WVWGHGZE.js";
4
+ import {
5
+ fillLabel
6
+ } from "./chunk-UN3XPBZ5.js";
7
+ import {
8
+ useChartContext
9
+ } from "./chunk-IPOBQIYU.js";
10
+ import {
11
+ seriesColorVar
12
+ } from "./chunk-7WJ26NHR.js";
13
+
14
+ // src/chart-legend.tsx
15
+ import { cn } from "@zuilib/components/lib/cn";
16
+ import { focusRingClasses } from "@zuilib/components/lib/focus";
17
+ import { jsx, jsxs } from "react/jsx-runtime";
18
+ function ChartLegendList({ payload, className }) {
19
+ const { config, labels, hiddenSeries, onLegendClick, categoryField, categories } = useChartContext();
20
+ const crossFilter = useOptionalCrossFilter();
21
+ const categoryEntries = categories?.map((c) => ({ key: String(c.value), name: String(c.value), color: c.color, series: false })) ?? (payload ?? []).map((entry) => {
22
+ const value = String(entry.value ?? "");
23
+ return { key: value, name: value, color: entry.color, series: false };
24
+ });
25
+ const entries = categoryField ? categoryEntries : Object.keys(config).map((field) => ({
26
+ key: field,
27
+ name: config[field].label,
28
+ color: seriesColorVar(field),
29
+ series: true
30
+ }));
31
+ if (!entries.length) return null;
32
+ const active = categoryField && crossFilter ? crossFilter.valuesFor(categoryField) : null;
33
+ return /* @__PURE__ */ jsx(
34
+ "ul",
35
+ {
36
+ "data-slot": "chart-legend",
37
+ "aria-label": labels.legend,
38
+ className: cn("flex flex-wrap items-center justify-center gap-x-3 gap-y-1 pt-2", className),
39
+ children: entries.map((entry) => {
40
+ const off = entry.series ? hiddenSeries.includes(entry.key) : active !== null && !active.includes(entry.key);
41
+ return /* @__PURE__ */ jsx("li", { "data-slot": "chart-legend-item", className: "flex", children: /* @__PURE__ */ jsxs(
42
+ "button",
43
+ {
44
+ type: "button",
45
+ "aria-pressed": !off,
46
+ "aria-label": fillLabel(labels.toggleSeries, { series: entry.name }),
47
+ "data-slot": "chart-legend-button",
48
+ className: cn(
49
+ "text-muted-foreground flex cursor-pointer items-center gap-1.5 rounded-sm px-1 py-0.5 text-xs transition-opacity",
50
+ focusRingClasses,
51
+ off && "opacity-45"
52
+ ),
53
+ onClick: (event) => {
54
+ if (entry.series) {
55
+ onLegendClick(entry.key, { additive: event.shiftKey });
56
+ } else if (categoryField) {
57
+ crossFilter?.toggleValue(categoryField, entry.key, { additive: event.shiftKey });
58
+ }
59
+ },
60
+ children: [
61
+ /* @__PURE__ */ jsx(
62
+ "span",
63
+ {
64
+ "data-slot": "chart-legend-chip",
65
+ "aria-hidden": true,
66
+ className: "size-2.5 shrink-0 rounded-xs",
67
+ style: { backgroundColor: entry.color }
68
+ }
69
+ ),
70
+ entry.name
71
+ ]
72
+ }
73
+ ) }, entry.key);
74
+ })
75
+ }
76
+ );
77
+ }
78
+ var chart_legend_default = ChartLegendList;
79
+
80
+ export {
81
+ ChartLegendList,
82
+ chart_legend_default
83
+ };
@@ -0,0 +1,17 @@
1
+ // src/labels.ts
2
+ var defaultChartLabels = {
3
+ noData: "No data",
4
+ loading: "Loading chart\u2026",
5
+ chartError: "Chart failed to load",
6
+ toggleSeries: "Toggle {series}",
7
+ legend: "Chart legend",
8
+ filterBy: "Filter by {value}"
9
+ };
10
+ function fillLabel(template, values) {
11
+ return template.replace(/\{(\w+)\}/g, (match, name) => name in values ? String(values[name]) : match);
12
+ }
13
+
14
+ export {
15
+ defaultChartLabels,
16
+ fillLabel
17
+ };
@@ -0,0 +1,90 @@
1
+ // src/cross-filter.tsx
2
+ import {
3
+ createContext,
4
+ useCallback,
5
+ useContext,
6
+ useMemo,
7
+ useRef,
8
+ useState
9
+ } from "react";
10
+
11
+ // src/cross-filter-state.ts
12
+ function toggleValueIn(state, field, value, options) {
13
+ const entry = state.filters.find((f) => f.field === field);
14
+ const others = state.filters.filter((f) => f.field !== field);
15
+ const has = entry?.values.includes(value) ?? false;
16
+ let values;
17
+ if (options?.additive) {
18
+ values = has ? (entry?.values ?? []).filter((v) => v !== value) : [...entry?.values ?? [], value];
19
+ } else {
20
+ values = has && entry?.values.length === 1 ? [] : [value];
21
+ }
22
+ return {
23
+ ...state,
24
+ filters: values.length ? [...others, { field, values }] : others
25
+ };
26
+ }
27
+
28
+ // src/cross-filter.tsx
29
+ import { jsx } from "react/jsx-runtime";
30
+ var defaultCrossFilterState = { filters: [], highlight: null };
31
+ var CrossFilterContext = createContext(null);
32
+ function CrossFilterProvider({ state, defaultState, onStateChange, children }) {
33
+ const [inner, setInner] = useState(defaultState ?? defaultCrossFilterState);
34
+ const controlled = state !== void 0;
35
+ const current = controlled ? state : inner;
36
+ const currentRef = useRef(current);
37
+ currentRef.current = current;
38
+ const emitRef = useRef(onStateChange);
39
+ emitRef.current = onStateChange;
40
+ const emit = useCallback(
41
+ (next) => {
42
+ currentRef.current = next;
43
+ if (!controlled) setInner(next);
44
+ emitRef.current?.(next);
45
+ },
46
+ [controlled]
47
+ );
48
+ const toggleValue = useCallback(
49
+ (field, value, options) => emit(toggleValueIn(currentRef.current, field, value, options)),
50
+ [emit]
51
+ );
52
+ const clearField = useCallback(
53
+ (field) => emit({ ...currentRef.current, filters: currentRef.current.filters.filter((f) => f.field !== field) }),
54
+ [emit]
55
+ );
56
+ const clearAll = useCallback(() => emit(defaultCrossFilterState), [emit]);
57
+ const setHighlight = useCallback(
58
+ (highlight) => emit({ ...currentRef.current, highlight }),
59
+ [emit]
60
+ );
61
+ const result = useMemo(
62
+ () => ({
63
+ state: current,
64
+ filtered: current.filters.some((f) => f.values.length > 0),
65
+ toggleValue,
66
+ clearField,
67
+ clearAll,
68
+ setHighlight,
69
+ valuesFor: (field) => current.filters.find((f) => f.field === field)?.values ?? null
70
+ }),
71
+ [current, toggleValue, clearField, clearAll, setHighlight]
72
+ );
73
+ return /* @__PURE__ */ jsx(CrossFilterContext.Provider, { value: result, children });
74
+ }
75
+ function useCrossFilter() {
76
+ const context = useContext(CrossFilterContext);
77
+ if (!context) throw new Error("useCrossFilter must be used inside a <CrossFilterProvider>");
78
+ return context;
79
+ }
80
+ function useOptionalCrossFilter() {
81
+ return useContext(CrossFilterContext);
82
+ }
83
+ var cross_filter_default = CrossFilterProvider;
84
+
85
+ export {
86
+ defaultCrossFilterState,
87
+ useCrossFilter,
88
+ useOptionalCrossFilter,
89
+ cross_filter_default
90
+ };