@zentauri-ui/zentauri-components 1.8.1 → 1.8.2

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 (156) hide show
  1. package/README.md +21 -10
  2. package/cli/registry.json +10 -0
  3. package/dist/charts/area.js +9 -10
  4. package/dist/charts/area.js.map +1 -1
  5. package/dist/charts/area.mjs +2 -3
  6. package/dist/charts/area.mjs.map +1 -1
  7. package/dist/charts/bar.js +10 -95
  8. package/dist/charts/bar.js.map +1 -1
  9. package/dist/charts/bar.mjs +2 -95
  10. package/dist/charts/bar.mjs.map +1 -1
  11. package/dist/charts/bubble.js +8 -9
  12. package/dist/charts/bubble.js.map +1 -1
  13. package/dist/charts/bubble.mjs +2 -3
  14. package/dist/charts/bubble.mjs.map +1 -1
  15. package/dist/charts/funnel/Funnel.d.ts +6 -0
  16. package/dist/charts/funnel/Funnel.d.ts.map +1 -0
  17. package/dist/charts/funnel/index.d.ts +4 -0
  18. package/dist/charts/funnel/index.d.ts.map +1 -0
  19. package/dist/charts/funnel.js +102 -0
  20. package/dist/charts/funnel.js.map +1 -0
  21. package/dist/charts/funnel.mjs +89 -0
  22. package/dist/charts/funnel.mjs.map +1 -0
  23. package/dist/charts/line.js +8 -9
  24. package/dist/charts/line.js.map +1 -1
  25. package/dist/charts/line.mjs +2 -3
  26. package/dist/charts/line.mjs.map +1 -1
  27. package/dist/charts/pie/Pie.d.ts +1 -1
  28. package/dist/charts/pie/Pie.d.ts.map +1 -1
  29. package/dist/charts/pie.js +19 -6
  30. package/dist/charts/pie.js.map +1 -1
  31. package/dist/charts/pie.mjs +17 -4
  32. package/dist/charts/pie.mjs.map +1 -1
  33. package/dist/charts/radar/Radar.d.ts +6 -0
  34. package/dist/charts/radar/Radar.d.ts.map +1 -0
  35. package/dist/charts/radar/index.d.ts +4 -0
  36. package/dist/charts/radar/index.d.ts.map +1 -0
  37. package/dist/charts/radar.js +94 -0
  38. package/dist/charts/radar.js.map +1 -0
  39. package/dist/charts/radar.mjs +81 -0
  40. package/dist/charts/radar.mjs.map +1 -0
  41. package/dist/charts/scatter/Scatter.d.ts +6 -0
  42. package/dist/charts/scatter/Scatter.d.ts.map +1 -0
  43. package/dist/charts/scatter/index.d.ts +4 -0
  44. package/dist/charts/scatter/index.d.ts.map +1 -0
  45. package/dist/charts/scatter.js +116 -0
  46. package/dist/charts/scatter.js.map +1 -0
  47. package/dist/charts/scatter.mjs +103 -0
  48. package/dist/charts/scatter.mjs.map +1 -0
  49. package/dist/charts/shared/chart-frame.d.ts +2 -1
  50. package/dist/charts/shared/chart-frame.d.ts.map +1 -1
  51. package/dist/charts/shared/types.d.ts +22 -2
  52. package/dist/charts/shared/types.d.ts.map +1 -1
  53. package/dist/charts/stacked-bar/StackedBar.d.ts +6 -0
  54. package/dist/charts/stacked-bar/StackedBar.d.ts.map +1 -0
  55. package/dist/charts/stacked-bar/index.d.ts +4 -0
  56. package/dist/charts/stacked-bar/index.d.ts.map +1 -0
  57. package/dist/charts/stacked-bar.js +29 -0
  58. package/dist/charts/stacked-bar.js.map +1 -0
  59. package/dist/charts/stacked-bar.mjs +15 -0
  60. package/dist/charts/stacked-bar.mjs.map +1 -0
  61. package/dist/chunk-F3V4POW3.mjs +8 -0
  62. package/dist/chunk-F3V4POW3.mjs.map +1 -0
  63. package/dist/{chunk-G2WARVAM.mjs → chunk-HZIRD3SR.mjs} +35 -15
  64. package/dist/chunk-HZIRD3SR.mjs.map +1 -0
  65. package/dist/{chunk-G66SXATZ.js → chunk-IL4LH2XX.js} +50 -4
  66. package/dist/chunk-IL4LH2XX.js.map +1 -0
  67. package/dist/chunk-LREMK2XR.js +97 -0
  68. package/dist/chunk-LREMK2XR.js.map +1 -0
  69. package/dist/chunk-O2KM3ETC.mjs +95 -0
  70. package/dist/chunk-O2KM3ETC.mjs.map +1 -0
  71. package/dist/{chunk-ZIFMIS7D.mjs → chunk-OL3BJSRC.mjs} +51 -5
  72. package/dist/chunk-OL3BJSRC.mjs.map +1 -0
  73. package/dist/{chunk-QNUDODDX.js → chunk-PWPMKXEG.js} +36 -14
  74. package/dist/chunk-PWPMKXEG.js.map +1 -0
  75. package/dist/chunk-XRM7GOIE.js +10 -0
  76. package/dist/chunk-XRM7GOIE.js.map +1 -0
  77. package/dist/hooks/index.d.ts +2 -0
  78. package/dist/hooks/index.d.ts.map +1 -1
  79. package/dist/hooks/useIsomorphicLayoutEffect.js +6 -4
  80. package/dist/hooks/useIsomorphicLayoutEffect.js.map +1 -1
  81. package/dist/hooks/useIsomorphicLayoutEffect.mjs +1 -6
  82. package/dist/hooks/useIsomorphicLayoutEffect.mjs.map +1 -1
  83. package/dist/hooks/useTableFilter/index.d.ts +3 -0
  84. package/dist/hooks/useTableFilter/index.d.ts.map +1 -0
  85. package/dist/hooks/useTableFilter/types.d.ts +20 -0
  86. package/dist/hooks/useTableFilter/types.d.ts.map +1 -0
  87. package/dist/hooks/useTableFilter/useTableFilter.d.ts +3 -0
  88. package/dist/hooks/useTableFilter/useTableFilter.d.ts.map +1 -0
  89. package/dist/hooks/useTableFilter.js +124 -0
  90. package/dist/hooks/useTableFilter.js.map +1 -0
  91. package/dist/hooks/useTableFilter.mjs +122 -0
  92. package/dist/hooks/useTableFilter.mjs.map +1 -0
  93. package/dist/hooks/useTableSort/index.d.ts +3 -0
  94. package/dist/hooks/useTableSort/index.d.ts.map +1 -0
  95. package/dist/hooks/useTableSort/types.d.ts +15 -0
  96. package/dist/hooks/useTableSort/types.d.ts.map +1 -0
  97. package/dist/hooks/useTableSort/useTableSort.d.ts +3 -0
  98. package/dist/hooks/useTableSort/useTableSort.d.ts.map +1 -0
  99. package/dist/hooks/useTableSort.js +99 -0
  100. package/dist/hooks/useTableSort.js.map +1 -0
  101. package/dist/hooks/useTableSort.mjs +97 -0
  102. package/dist/hooks/useTableSort.mjs.map +1 -0
  103. package/dist/ui/marquee/marquee.d.ts.map +1 -1
  104. package/dist/ui/marquee.js +82 -21
  105. package/dist/ui/marquee.js.map +1 -1
  106. package/dist/ui/marquee.mjs +83 -22
  107. package/dist/ui/marquee.mjs.map +1 -1
  108. package/dist/ui/table/animated.js +8 -8
  109. package/dist/ui/table/animated.mjs +2 -2
  110. package/dist/ui/table/index.d.ts +1 -1
  111. package/dist/ui/table/index.d.ts.map +1 -1
  112. package/dist/ui/table/table-base.d.ts +2 -2
  113. package/dist/ui/table/table-base.d.ts.map +1 -1
  114. package/dist/ui/table/types.d.ts +9 -1
  115. package/dist/ui/table/types.d.ts.map +1 -1
  116. package/dist/ui/table.js +14 -14
  117. package/dist/ui/table.mjs +1 -1
  118. package/package.json +1 -1
  119. package/src/charts/charts.test.tsx +80 -0
  120. package/src/charts/funnel/Funnel.tsx +105 -0
  121. package/src/charts/funnel/index.ts +14 -0
  122. package/src/charts/pie/Pie.tsx +28 -1
  123. package/src/charts/radar/Radar.tsx +84 -0
  124. package/src/charts/radar/index.ts +16 -0
  125. package/src/charts/scatter/Scatter.tsx +104 -0
  126. package/src/charts/scatter/index.ts +16 -0
  127. package/src/charts/shared/chart-frame.tsx +4 -2
  128. package/src/charts/shared/types.ts +42 -2
  129. package/src/charts/stacked-bar/StackedBar.tsx +12 -0
  130. package/src/charts/stacked-bar/index.ts +16 -0
  131. package/src/hooks/index.ts +12 -0
  132. package/src/hooks/useTableFilter/index.ts +7 -0
  133. package/src/hooks/useTableFilter/types.ts +28 -0
  134. package/src/hooks/useTableFilter/useTableFilter.test.ts +141 -0
  135. package/src/hooks/useTableFilter/useTableFilter.ts +153 -0
  136. package/src/hooks/useTableSort/index.ts +5 -0
  137. package/src/hooks/useTableSort/types.ts +23 -0
  138. package/src/hooks/useTableSort/useTableSort.test.ts +150 -0
  139. package/src/hooks/useTableSort/useTableSort.ts +121 -0
  140. package/src/ui/divider/divider.test.tsx +55 -0
  141. package/src/ui/empty-state/empty-state.test.tsx +88 -0
  142. package/src/ui/marquee/marquee.test.tsx +45 -4
  143. package/src/ui/marquee/marquee.tsx +100 -18
  144. package/src/ui/skeleton/skeleton.test.tsx +85 -0
  145. package/src/ui/table/index.ts +3 -0
  146. package/src/ui/table/table-base.tsx +69 -4
  147. package/src/ui/table/table.test.tsx +207 -0
  148. package/src/ui/table/types.ts +13 -1
  149. package/dist/chunk-G2WARVAM.mjs.map +0 -1
  150. package/dist/chunk-G66SXATZ.js.map +0 -1
  151. package/dist/chunk-OULU7OC4.mjs +0 -21
  152. package/dist/chunk-OULU7OC4.mjs.map +0 -1
  153. package/dist/chunk-QNUDODDX.js.map +0 -1
  154. package/dist/chunk-Z6S36PDD.js +0 -24
  155. package/dist/chunk-Z6S36PDD.js.map +0 -1
  156. package/dist/chunk-ZIFMIS7D.mjs.map +0 -1
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import {
4
+ Legend,
5
+ PolarAngleAxis,
6
+ PolarGrid,
7
+ PolarRadiusAxis,
8
+ Radar,
9
+ RadarChart as RechartsRadarChart,
10
+ Tooltip,
11
+ } from "recharts";
12
+
13
+ import { ChartFrame } from "../shared/chart-frame";
14
+ import { getSeriesFill, resolveColor } from "../shared/colors";
15
+ import type { RadarChartProps } from "../shared/types";
16
+
17
+ export function RadarChart<
18
+ TDatum extends Record<string, number | string | null | undefined>,
19
+ >({
20
+ appearance,
21
+ className,
22
+ containerStyle,
23
+ data,
24
+ density,
25
+ emptyState = null,
26
+ height = 320,
27
+ series,
28
+ showGrid = true,
29
+ showLegend = false,
30
+ showTooltip = true,
31
+ tooltipColor = "#0f172a",
32
+ style,
33
+ xKey,
34
+ ...props
35
+ }: RadarChartProps<TDatum>) {
36
+ const hasData = data.length > 0 && series.length > 0;
37
+
38
+ return (
39
+ <ChartFrame
40
+ appearance={appearance}
41
+ className={className}
42
+ containerStyle={containerStyle}
43
+ density={density}
44
+ emptyState={emptyState}
45
+ hasData={hasData}
46
+ height={height}
47
+ style={style}
48
+ {...props}
49
+ >
50
+ <RechartsRadarChart data={data}>
51
+ {showGrid ? <PolarGrid stroke="currentColor" opacity={0.18} /> : null}
52
+ <PolarAngleAxis
53
+ dataKey={String(xKey)}
54
+ tick={{ fill: "currentColor", fontSize: 12 }}
55
+ />
56
+ <PolarRadiusAxis tick={{ fill: "currentColor", fontSize: 11 }} />
57
+ {showTooltip ? (
58
+ <Tooltip
59
+ contentStyle={{ color: tooltipColor }}
60
+ labelStyle={{ color: tooltipColor }}
61
+ itemStyle={{ color: tooltipColor }}
62
+ />
63
+ ) : null}
64
+ {showLegend ? <Legend /> : null}
65
+ {series.map((item, index) => {
66
+ const color = resolveColor(item.color, index);
67
+ const fill = getSeriesFill(item, index, 0.24);
68
+ return (
69
+ <Radar
70
+ key={item.dataKey}
71
+ dataKey={item.dataKey}
72
+ name={item.name}
73
+ stroke={item.stroke ?? color.stroke}
74
+ fill={fill ?? color.fill}
75
+ fillOpacity={0.72}
76
+ />
77
+ );
78
+ })}
79
+ </RechartsRadarChart>
80
+ </ChartFrame>
81
+ );
82
+ }
83
+
84
+ RadarChart.displayName = "RadarChart";
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ export { RadarChart } from "./Radar";
4
+ export type {
5
+ ChartColor,
6
+ ChartDatum,
7
+ ChartMargin,
8
+ ChartSeries,
9
+ ChartSharedStatic,
10
+ RadarChartProps,
11
+ } from "../shared/types";
12
+ export {
13
+ chartColorValues,
14
+ chartPalette,
15
+ chartVariants,
16
+ } from "../shared/variants";
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import {
4
+ Scatter,
5
+ ScatterChart as RechartsScatterChart,
6
+ XAxis,
7
+ YAxis,
8
+ } from "recharts";
9
+
10
+ import {
11
+ ChartDecorators,
12
+ ChartFrame,
13
+ defaultChartMargin,
14
+ } from "../shared/chart-frame";
15
+ import { resolveColor } from "../shared/colors";
16
+ import type { ScatterChartProps } from "../shared/types";
17
+
18
+ export function ScatterChart<
19
+ TDatum extends Record<string, number | string | null | undefined>,
20
+ >({
21
+ appearance,
22
+ className,
23
+ containerStyle,
24
+ data,
25
+ density,
26
+ emptyState = null,
27
+ height = 320,
28
+ margin = defaultChartMargin,
29
+ series,
30
+ showGrid = true,
31
+ showLegend = false,
32
+ showTooltip = true,
33
+ tooltipColor = "#0f172a",
34
+ style,
35
+ syncId,
36
+ xKey,
37
+ ...props
38
+ }: ScatterChartProps<TDatum>) {
39
+ const hasData = data.length > 0 && series.length > 0;
40
+ const xAxisKey = String(xKey);
41
+ const isNumericX = typeof data[0]?.[xAxisKey] === "number";
42
+
43
+ return (
44
+ <ChartFrame
45
+ appearance={appearance}
46
+ className={className}
47
+ containerStyle={containerStyle}
48
+ density={density}
49
+ emptyState={emptyState}
50
+ hasData={hasData}
51
+ height={height}
52
+ style={style}
53
+ {...props}
54
+ >
55
+ <RechartsScatterChart data={data} margin={margin} syncId={syncId}>
56
+ <ChartDecorators
57
+ axis={
58
+ <>
59
+ <XAxis
60
+ dataKey={xAxisKey}
61
+ type={isNumericX ? "number" : "category"}
62
+ minTickGap={24}
63
+ tickLine={false}
64
+ tickMargin={10}
65
+ axisLine={false}
66
+ fontSize={12}
67
+ />
68
+ <YAxis
69
+ dataKey="__chartY"
70
+ type="number"
71
+ width={40}
72
+ tickLine={false}
73
+ tickMargin={8}
74
+ axisLine={false}
75
+ fontSize={12}
76
+ />
77
+ </>
78
+ }
79
+ showGrid={showGrid}
80
+ showLegend={showLegend}
81
+ showTooltip={showTooltip}
82
+ tooltipColor={tooltipColor}
83
+ />
84
+ {series.map((item, index) => {
85
+ const color = resolveColor(item.color, index);
86
+ return (
87
+ <Scatter
88
+ key={item.dataKey}
89
+ data={data.map((entry) => ({
90
+ ...entry,
91
+ __chartY: entry[item.dataKey],
92
+ }))}
93
+ name={item.name}
94
+ fill={item.fill ?? color.stroke}
95
+ line={item.stroke ? { stroke: item.stroke } : false}
96
+ />
97
+ );
98
+ })}
99
+ </RechartsScatterChart>
100
+ </ChartFrame>
101
+ );
102
+ }
103
+
104
+ ScatterChart.displayName = "ScatterChart";
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ export { ScatterChart } from "./Scatter";
4
+ export type {
5
+ ChartColor,
6
+ ChartDatum,
7
+ ChartMargin,
8
+ ChartSeries,
9
+ ChartSharedStatic,
10
+ ScatterChartProps,
11
+ } from "../shared/types";
12
+ export {
13
+ chartColorValues,
14
+ chartPalette,
15
+ chartVariants,
16
+ } from "../shared/variants";
@@ -39,6 +39,7 @@ type ChartFrameProps = HTMLAttributes<HTMLDivElement> & {
39
39
  emptyState?: ReactNode;
40
40
  hasData: boolean;
41
41
  height: number;
42
+ overlay?: ReactNode;
42
43
  style?: CSSProperties;
43
44
  children: ReactNode;
44
45
  };
@@ -54,6 +55,7 @@ export function ChartFrame({
54
55
  emptyState = null,
55
56
  hasData,
56
57
  height,
58
+ overlay,
57
59
  style,
58
60
  ...props
59
61
  }: ChartFrameProps) {
@@ -63,8 +65,7 @@ export function ChartFrame({
63
65
  "--chart-height": `${height}px`,
64
66
  ...style,
65
67
  } as CSSProperties;
66
- const canRenderChart =
67
- (size?.width ?? 0) > 0 && (size?.height ?? 0) > 0;
68
+ const canRenderChart = (size?.width ?? 0) > 0 && (size?.height ?? 0) > 0;
68
69
 
69
70
  if (!hasData) {
70
71
  return (
@@ -99,6 +100,7 @@ export function ChartFrame({
99
100
  </ResponsiveContainer>
100
101
  ) : null}
101
102
  </div>
103
+ {overlay}
102
104
  </div>
103
105
  );
104
106
  }
@@ -2,9 +2,18 @@ import type { VariantProps } from "class-variance-authority";
2
2
  import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
3
3
 
4
4
  import type { chartPalette, chartVariants } from "./variants";
5
- import { PieProps } from "recharts";
5
+ import type { PieProps } from "recharts";
6
6
 
7
- export type ChartType = "area" | "bar" | "bubble" | "line" | "pie";
7
+ export type ChartType =
8
+ | "area"
9
+ | "bar"
10
+ | "bubble"
11
+ | "funnel"
12
+ | "line"
13
+ | "pie"
14
+ | "radar"
15
+ | "scatter"
16
+ | "stacked-bar";
8
17
 
9
18
  export type ChartColor = keyof typeof chartPalette;
10
19
 
@@ -61,6 +70,17 @@ export type LineChartProps<TDatum extends ChartDatum = ChartDatum> =
61
70
  export type BubbleChartProps<TDatum extends ChartDatum = ChartDatum> =
62
71
  BaseChartProps<TDatum>;
63
72
 
73
+ export type RadarChartProps<TDatum extends ChartDatum = ChartDatum> =
74
+ BaseChartProps<TDatum>;
75
+
76
+ export type ScatterChartProps<TDatum extends ChartDatum = ChartDatum> =
77
+ BaseChartProps<TDatum>;
78
+
79
+ export type StackedBarChartProps<TDatum extends ChartDatum = ChartDatum> = Omit<
80
+ BaseChartProps<TDatum>,
81
+ "stacked"
82
+ >;
83
+
64
84
  export type PieChartProps<TDatum extends ChartDatum = ChartDatum> =
65
85
  ChartSharedStatic &
66
86
  Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
@@ -75,6 +95,8 @@ export type PieChartProps<TDatum extends ChartDatum = ChartDatum> =
75
95
  containerStyle?: CSSProperties;
76
96
  paddingAngle?: PieProps["paddingAngle"];
77
97
  cornerRadius?: PieProps["cornerRadius"];
98
+ center?: ReactNode;
99
+ colorKey?: keyof TDatum & string;
78
100
  label?: boolean;
79
101
  labelLine?: boolean;
80
102
  labelColor?: string;
@@ -84,3 +106,21 @@ export type PieChartProps<TDatum extends ChartDatum = ChartDatum> =
84
106
  outerRadius?: PieProps["outerRadius"];
85
107
  shape?: PieProps["shape"];
86
108
  };
109
+
110
+ export type FunnelChartProps<TDatum extends ChartDatum = ChartDatum> =
111
+ ChartSharedStatic &
112
+ Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
113
+ data: TDatum[];
114
+ dataKey: keyof TDatum & string;
115
+ nameKey: keyof TDatum & string;
116
+ height?: number;
117
+ showLegend?: boolean;
118
+ showTooltip?: boolean;
119
+ tooltipColor?: string;
120
+ emptyState?: ReactNode;
121
+ containerStyle?: CSSProperties;
122
+ colorKey?: keyof TDatum & string;
123
+ label?: boolean;
124
+ stroke?: string;
125
+ fill?: string;
126
+ };
@@ -0,0 +1,12 @@
1
+ "use client";
2
+
3
+ import { BarChart } from "../bar/Bar";
4
+ import type { StackedBarChartProps } from "../shared/types";
5
+
6
+ export function StackedBarChart<
7
+ TDatum extends Record<string, number | string | null | undefined>,
8
+ >(props: StackedBarChartProps<TDatum>) {
9
+ return <BarChart {...props} stacked />;
10
+ }
11
+
12
+ StackedBarChart.displayName = "StackedBarChart";
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ export { StackedBarChart } from "./StackedBar";
4
+ export type {
5
+ ChartColor,
6
+ ChartDatum,
7
+ ChartMargin,
8
+ ChartSeries,
9
+ ChartSharedStatic,
10
+ StackedBarChartProps,
11
+ } from "../shared/types";
12
+ export {
13
+ chartColorValues,
14
+ chartPalette,
15
+ chartVariants,
16
+ } from "../shared/variants";
@@ -49,6 +49,18 @@ export {
49
49
  useSessionStorage,
50
50
  type UseSessionStorageResult,
51
51
  } from "./useSessionStorage";
52
+ export {
53
+ useTableFilter,
54
+ type TableFilterPredicate,
55
+ type TableFilterState,
56
+ type UseTableFilterParams,
57
+ type UseTableFilterResult,
58
+ } from "./useTableFilter";
59
+ export {
60
+ useTableSort,
61
+ type UseTableSortParams,
62
+ type UseTableSortResult,
63
+ } from "./useTableSort";
52
64
  export { useThrottledCallback } from "./useThrottledCallback";
53
65
  export { useToggle } from "./useToggle";
54
66
  export { useWindowSize, type WindowSize } from "./useWindowSize";
@@ -0,0 +1,7 @@
1
+ export {
2
+ type TableFilterPredicate,
3
+ type TableFilterState,
4
+ type UseTableFilterParams,
5
+ type UseTableFilterResult,
6
+ } from "./types";
7
+ export { useTableFilter } from "./useTableFilter";
@@ -0,0 +1,28 @@
1
+ export type TableFilterState<TKey extends string = string> = Partial<
2
+ Record<TKey, string>
3
+ >;
4
+
5
+ export type TableFilterPredicate<TData, TKey extends string = string> = (
6
+ row: TData,
7
+ filterValue: string,
8
+ filterKey: TKey,
9
+ ) => boolean;
10
+
11
+ export type UseTableFilterParams<TData, TKey extends string = string> = {
12
+ data: readonly TData[];
13
+ filters?: TableFilterState<TKey>;
14
+ defaultFilters?: TableFilterState<TKey>;
15
+ onFiltersChange?: (filters: TableFilterState<TKey>) => void;
16
+ getColumnValue?: (row: TData, filterKey: TKey) => unknown;
17
+ filterPredicate?: TableFilterPredicate<TData, TKey>;
18
+ };
19
+
20
+ export type UseTableFilterResult<TData, TKey extends string = string> = {
21
+ filters: TableFilterState<TKey>;
22
+ filteredData: TData[];
23
+ hasActiveFilters: boolean;
24
+ setFilter: (filterKey: TKey, value: string) => void;
25
+ setFilters: (filters: TableFilterState<TKey>) => void;
26
+ clearFilter: (filterKey: TKey) => void;
27
+ clearFilters: () => void;
28
+ };
@@ -0,0 +1,141 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { useTableFilter } from "./useTableFilter";
5
+
6
+ const rows = [
7
+ { name: "Atlas", status: "active", seats: 12 },
8
+ { name: "Beacon", status: "paused", seats: 4 },
9
+ { name: "Comet", status: "active", seats: 8 },
10
+ ] as const;
11
+
12
+ describe("useTableFilter", () => {
13
+ it("should return all rows when no filters are active", () => {
14
+ const { result } = renderHook(() => useTableFilter({ data: rows }));
15
+ expect(result.current.filteredData).toEqual(rows);
16
+ expect(result.current.hasActiveFilters).toBe(false);
17
+ });
18
+
19
+ it("should filter rows by a string column value", () => {
20
+ const { result } = renderHook(() =>
21
+ useTableFilter({
22
+ data: rows,
23
+ defaultFilters: { status: "active" },
24
+ }),
25
+ );
26
+ expect(result.current.filteredData.map((row) => row.name)).toEqual([
27
+ "Atlas",
28
+ "Comet",
29
+ ]);
30
+ expect(result.current.hasActiveFilters).toBe(true);
31
+ });
32
+
33
+ it("should combine multiple column filters", () => {
34
+ const { result } = renderHook(() =>
35
+ useTableFilter({
36
+ data: rows,
37
+ defaultFilters: { status: "active", name: "com" },
38
+ }),
39
+ );
40
+ expect(result.current.filteredData).toEqual([rows[2]]);
41
+ });
42
+
43
+ it("should update and clear filters in uncontrolled mode", () => {
44
+ const { result } = renderHook(() => useTableFilter({ data: rows }));
45
+
46
+ act(() => {
47
+ result.current.setFilter("name", "bea");
48
+ });
49
+ expect(result.current.filteredData).toEqual([rows[1]]);
50
+
51
+ act(() => {
52
+ result.current.clearFilter("name");
53
+ });
54
+ expect(result.current.filteredData).toEqual(rows);
55
+
56
+ act(() => {
57
+ result.current.setFilters({ status: "paused" });
58
+ });
59
+ expect(result.current.filteredData).toEqual([rows[1]]);
60
+
61
+ act(() => {
62
+ result.current.clearFilters();
63
+ });
64
+ expect(result.current.filteredData).toEqual(rows);
65
+ });
66
+
67
+ it("should preserve batched uncontrolled filter updates", () => {
68
+ const { result } = renderHook(() => useTableFilter({ data: rows }));
69
+
70
+ act(() => {
71
+ result.current.setFilter("status", "active");
72
+ result.current.setFilter("name", "atlas");
73
+ });
74
+
75
+ expect(result.current.filters).toEqual({
76
+ status: "active",
77
+ name: "atlas",
78
+ });
79
+ expect(result.current.filteredData).toEqual([rows[0]]);
80
+ });
81
+
82
+ it("should support controlled filters", () => {
83
+ const handleFiltersChange = vi.fn();
84
+ const { result, rerender } = renderHook(
85
+ ({ filters }: { filters: Record<string, string> }) =>
86
+ useTableFilter({
87
+ data: rows,
88
+ filters,
89
+ onFiltersChange: handleFiltersChange,
90
+ }),
91
+ { initialProps: { filters: { status: "active" } } },
92
+ );
93
+
94
+ expect(result.current.filteredData.length).toBe(2);
95
+ act(() => {
96
+ result.current.setFilter("name", "atlas");
97
+ });
98
+ expect(handleFiltersChange).toHaveBeenCalledWith({
99
+ status: "active",
100
+ name: "atlas",
101
+ });
102
+ expect(result.current.filters).toEqual({ status: "active" });
103
+
104
+ rerender({ filters: { status: "active", name: "atlas" } });
105
+ expect(result.current.filteredData).toEqual([rows[0]]);
106
+ });
107
+
108
+ it("should support custom value accessors and predicates", () => {
109
+ const { result } = renderHook(() =>
110
+ useTableFilter({
111
+ data: rows,
112
+ defaultFilters: { seats: "10" },
113
+ getColumnValue: (row, key) => row[key],
114
+ filterPredicate: (row, value, key) => Number(row[key]) >= Number(value),
115
+ }),
116
+ );
117
+ expect(result.current.filteredData).toEqual([rows[0]]);
118
+ });
119
+
120
+ it("should remove blank filter values", () => {
121
+ const { result } = renderHook(() =>
122
+ useTableFilter({
123
+ data: rows,
124
+ defaultFilters: { name: " " },
125
+ }),
126
+ );
127
+ expect(result.current.filters).toEqual({});
128
+ expect(result.current.hasActiveFilters).toBe(false);
129
+ });
130
+
131
+ it("should guard against null filters at runtime", () => {
132
+ const { result } = renderHook(() =>
133
+ useTableFilter({
134
+ data: rows,
135
+ filters: null as unknown as Record<string, string>,
136
+ }),
137
+ );
138
+ expect(result.current.filters).toEqual({});
139
+ expect(result.current.filteredData).toEqual(rows);
140
+ });
141
+ });