@zentauri-ui/zentauri-components 1.8.0 → 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 (253) hide show
  1. package/README.md +25 -10
  2. package/cli/registry.json +12 -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-7TGUGTTQ.mjs +147 -0
  62. package/dist/chunk-7TGUGTTQ.mjs.map +1 -0
  63. package/dist/chunk-CQMV7BB6.js +50 -0
  64. package/dist/chunk-CQMV7BB6.js.map +1 -0
  65. package/dist/chunk-DN7TYUJ6.js +119 -0
  66. package/dist/chunk-DN7TYUJ6.js.map +1 -0
  67. package/dist/chunk-F3V4POW3.mjs +8 -0
  68. package/dist/chunk-F3V4POW3.mjs.map +1 -0
  69. package/dist/{chunk-G2WARVAM.mjs → chunk-HZIRD3SR.mjs} +35 -15
  70. package/dist/chunk-HZIRD3SR.mjs.map +1 -0
  71. package/dist/{chunk-G66SXATZ.js → chunk-IL4LH2XX.js} +50 -4
  72. package/dist/chunk-IL4LH2XX.js.map +1 -0
  73. package/dist/chunk-LREMK2XR.js +97 -0
  74. package/dist/chunk-LREMK2XR.js.map +1 -0
  75. package/dist/chunk-O2KM3ETC.mjs +95 -0
  76. package/dist/chunk-O2KM3ETC.mjs.map +1 -0
  77. package/dist/chunk-ODBG4Y6R.mjs +48 -0
  78. package/dist/chunk-ODBG4Y6R.mjs.map +1 -0
  79. package/dist/{chunk-ZIFMIS7D.mjs → chunk-OL3BJSRC.mjs} +51 -5
  80. package/dist/chunk-OL3BJSRC.mjs.map +1 -0
  81. package/dist/{chunk-QNUDODDX.js → chunk-PWPMKXEG.js} +36 -14
  82. package/dist/chunk-PWPMKXEG.js.map +1 -0
  83. package/dist/chunk-RKX5MERK.js +150 -0
  84. package/dist/chunk-RKX5MERK.js.map +1 -0
  85. package/dist/chunk-VYI3GS2C.mjs +115 -0
  86. package/dist/chunk-VYI3GS2C.mjs.map +1 -0
  87. package/dist/chunk-XRM7GOIE.js +10 -0
  88. package/dist/chunk-XRM7GOIE.js.map +1 -0
  89. package/dist/design-system/copy-button.d.ts +43 -0
  90. package/dist/design-system/copy-button.d.ts.map +1 -0
  91. package/dist/design-system/index.d.ts +2 -0
  92. package/dist/design-system/index.d.ts.map +1 -1
  93. package/dist/design-system/kbd.d.ts +44 -0
  94. package/dist/design-system/kbd.d.ts.map +1 -0
  95. package/dist/hooks/index.d.ts +2 -0
  96. package/dist/hooks/index.d.ts.map +1 -1
  97. package/dist/hooks/useClipboard.js +6 -44
  98. package/dist/hooks/useClipboard.js.map +1 -1
  99. package/dist/hooks/useClipboard.mjs +1 -46
  100. package/dist/hooks/useClipboard.mjs.map +1 -1
  101. package/dist/hooks/useIsomorphicLayoutEffect.js +6 -4
  102. package/dist/hooks/useIsomorphicLayoutEffect.js.map +1 -1
  103. package/dist/hooks/useIsomorphicLayoutEffect.mjs +1 -6
  104. package/dist/hooks/useIsomorphicLayoutEffect.mjs.map +1 -1
  105. package/dist/hooks/useTableFilter/index.d.ts +3 -0
  106. package/dist/hooks/useTableFilter/index.d.ts.map +1 -0
  107. package/dist/hooks/useTableFilter/types.d.ts +20 -0
  108. package/dist/hooks/useTableFilter/types.d.ts.map +1 -0
  109. package/dist/hooks/useTableFilter/useTableFilter.d.ts +3 -0
  110. package/dist/hooks/useTableFilter/useTableFilter.d.ts.map +1 -0
  111. package/dist/hooks/useTableFilter.js +124 -0
  112. package/dist/hooks/useTableFilter.js.map +1 -0
  113. package/dist/hooks/useTableFilter.mjs +122 -0
  114. package/dist/hooks/useTableFilter.mjs.map +1 -0
  115. package/dist/hooks/useTableSort/index.d.ts +3 -0
  116. package/dist/hooks/useTableSort/index.d.ts.map +1 -0
  117. package/dist/hooks/useTableSort/types.d.ts +15 -0
  118. package/dist/hooks/useTableSort/types.d.ts.map +1 -0
  119. package/dist/hooks/useTableSort/useTableSort.d.ts +3 -0
  120. package/dist/hooks/useTableSort/useTableSort.d.ts.map +1 -0
  121. package/dist/hooks/useTableSort.js +99 -0
  122. package/dist/hooks/useTableSort.js.map +1 -0
  123. package/dist/hooks/useTableSort.mjs +97 -0
  124. package/dist/hooks/useTableSort.mjs.map +1 -0
  125. package/dist/ui/copy-button/animated/animations.d.ts +3 -0
  126. package/dist/ui/copy-button/animated/animations.d.ts.map +1 -0
  127. package/dist/ui/copy-button/animated/copy-button-animated.d.ts +6 -0
  128. package/dist/ui/copy-button/animated/copy-button-animated.d.ts.map +1 -0
  129. package/dist/ui/copy-button/animated/index.d.ts +4 -0
  130. package/dist/ui/copy-button/animated/index.d.ts.map +1 -0
  131. package/dist/ui/copy-button/animated/types.d.ts +26 -0
  132. package/dist/ui/copy-button/animated/types.d.ts.map +1 -0
  133. package/dist/ui/copy-button/animated.js +59 -0
  134. package/dist/ui/copy-button/animated.js.map +1 -0
  135. package/dist/ui/copy-button/animated.mjs +56 -0
  136. package/dist/ui/copy-button/animated.mjs.map +1 -0
  137. package/dist/ui/copy-button/copy-button-base.d.ts +6 -0
  138. package/dist/ui/copy-button/copy-button-base.d.ts.map +1 -0
  139. package/dist/ui/copy-button/copy-button.d.ts +6 -0
  140. package/dist/ui/copy-button/copy-button.d.ts.map +1 -0
  141. package/dist/ui/copy-button/index.d.ts +4 -0
  142. package/dist/ui/copy-button/index.d.ts.map +1 -0
  143. package/dist/ui/copy-button/types.d.ts +32 -0
  144. package/dist/ui/copy-button/types.d.ts.map +1 -0
  145. package/dist/ui/copy-button/variants.d.ts +6 -0
  146. package/dist/ui/copy-button/variants.d.ts.map +1 -0
  147. package/dist/ui/copy-button.js +20 -0
  148. package/dist/ui/copy-button.js.map +1 -0
  149. package/dist/ui/copy-button.mjs +15 -0
  150. package/dist/ui/copy-button.mjs.map +1 -0
  151. package/dist/ui/kbd/animated/animations.d.ts +3 -0
  152. package/dist/ui/kbd/animated/animations.d.ts.map +1 -0
  153. package/dist/ui/kbd/animated/index.d.ts +4 -0
  154. package/dist/ui/kbd/animated/index.d.ts.map +1 -0
  155. package/dist/ui/kbd/animated/kbd-animated.d.ts +6 -0
  156. package/dist/ui/kbd/animated/kbd-animated.d.ts.map +1 -0
  157. package/dist/ui/kbd/animated/types.d.ts +10 -0
  158. package/dist/ui/kbd/animated/types.d.ts.map +1 -0
  159. package/dist/ui/kbd/animated.js +42 -0
  160. package/dist/ui/kbd/animated.js.map +1 -0
  161. package/dist/ui/kbd/animated.mjs +39 -0
  162. package/dist/ui/kbd/animated.mjs.map +1 -0
  163. package/dist/ui/kbd/index.d.ts +4 -0
  164. package/dist/ui/kbd/index.d.ts.map +1 -0
  165. package/dist/ui/kbd/kbd-base.d.ts +6 -0
  166. package/dist/ui/kbd/kbd-base.d.ts.map +1 -0
  167. package/dist/ui/kbd/kbd.d.ts +6 -0
  168. package/dist/ui/kbd/kbd.d.ts.map +1 -0
  169. package/dist/ui/kbd/types.d.ts +17 -0
  170. package/dist/ui/kbd/types.d.ts.map +1 -0
  171. package/dist/ui/kbd/variants.d.ts +8 -0
  172. package/dist/ui/kbd/variants.d.ts.map +1 -0
  173. package/dist/ui/kbd.js +23 -0
  174. package/dist/ui/kbd.js.map +1 -0
  175. package/dist/ui/kbd.mjs +14 -0
  176. package/dist/ui/kbd.mjs.map +1 -0
  177. package/dist/ui/marquee/marquee.d.ts.map +1 -1
  178. package/dist/ui/marquee.js +82 -21
  179. package/dist/ui/marquee.js.map +1 -1
  180. package/dist/ui/marquee.mjs +83 -22
  181. package/dist/ui/marquee.mjs.map +1 -1
  182. package/dist/ui/table/animated.js +8 -8
  183. package/dist/ui/table/animated.mjs +2 -2
  184. package/dist/ui/table/index.d.ts +1 -1
  185. package/dist/ui/table/index.d.ts.map +1 -1
  186. package/dist/ui/table/table-base.d.ts +2 -2
  187. package/dist/ui/table/table-base.d.ts.map +1 -1
  188. package/dist/ui/table/types.d.ts +9 -1
  189. package/dist/ui/table/types.d.ts.map +1 -1
  190. package/dist/ui/table.js +14 -14
  191. package/dist/ui/table.mjs +1 -1
  192. package/package.json +1 -1
  193. package/src/charts/charts.test.tsx +80 -0
  194. package/src/charts/funnel/Funnel.tsx +105 -0
  195. package/src/charts/funnel/index.ts +14 -0
  196. package/src/charts/pie/Pie.tsx +28 -1
  197. package/src/charts/radar/Radar.tsx +84 -0
  198. package/src/charts/radar/index.ts +16 -0
  199. package/src/charts/scatter/Scatter.tsx +104 -0
  200. package/src/charts/scatter/index.ts +16 -0
  201. package/src/charts/shared/chart-frame.tsx +4 -2
  202. package/src/charts/shared/types.ts +42 -2
  203. package/src/charts/stacked-bar/StackedBar.tsx +12 -0
  204. package/src/charts/stacked-bar/index.ts +16 -0
  205. package/src/design-system/copy-button.ts +81 -0
  206. package/src/design-system/index.ts +2 -0
  207. package/src/design-system/kbd.ts +83 -0
  208. package/src/hooks/index.ts +12 -0
  209. package/src/hooks/useTableFilter/index.ts +7 -0
  210. package/src/hooks/useTableFilter/types.ts +28 -0
  211. package/src/hooks/useTableFilter/useTableFilter.test.ts +141 -0
  212. package/src/hooks/useTableFilter/useTableFilter.ts +153 -0
  213. package/src/hooks/useTableSort/index.ts +5 -0
  214. package/src/hooks/useTableSort/types.ts +23 -0
  215. package/src/hooks/useTableSort/useTableSort.test.ts +150 -0
  216. package/src/hooks/useTableSort/useTableSort.ts +121 -0
  217. package/src/ui/copy-button/animated/animations.ts +22 -0
  218. package/src/ui/copy-button/animated/copy-button-animated.tsx +39 -0
  219. package/src/ui/copy-button/animated/index.ts +10 -0
  220. package/src/ui/copy-button/animated/types.ts +21 -0
  221. package/src/ui/copy-button/copy-button-base.tsx +88 -0
  222. package/src/ui/copy-button/copy-button.test.tsx +82 -0
  223. package/src/ui/copy-button/copy-button.tsx +9 -0
  224. package/src/ui/copy-button/index.ts +10 -0
  225. package/src/ui/copy-button/types.ts +37 -0
  226. package/src/ui/copy-button/variants.ts +29 -0
  227. package/src/ui/divider/divider.test.tsx +55 -0
  228. package/src/ui/empty-state/empty-state.test.tsx +88 -0
  229. package/src/ui/kbd/animated/animations.ts +15 -0
  230. package/src/ui/kbd/animated/index.ts +9 -0
  231. package/src/ui/kbd/animated/kbd-animated.tsx +26 -0
  232. package/src/ui/kbd/animated/types.ts +16 -0
  233. package/src/ui/kbd/index.ts +5 -0
  234. package/src/ui/kbd/kbd-base.tsx +50 -0
  235. package/src/ui/kbd/kbd.test.tsx +48 -0
  236. package/src/ui/kbd/kbd.tsx +9 -0
  237. package/src/ui/kbd/types.ts +21 -0
  238. package/src/ui/kbd/variants.ts +31 -0
  239. package/src/ui/marquee/marquee.test.tsx +45 -4
  240. package/src/ui/marquee/marquee.tsx +100 -18
  241. package/src/ui/skeleton/skeleton.test.tsx +85 -0
  242. package/src/ui/table/index.ts +3 -0
  243. package/src/ui/table/table-base.tsx +69 -4
  244. package/src/ui/table/table.test.tsx +207 -0
  245. package/src/ui/table/types.ts +13 -1
  246. package/dist/chunk-G2WARVAM.mjs.map +0 -1
  247. package/dist/chunk-G66SXATZ.js.map +0 -1
  248. package/dist/chunk-OULU7OC4.mjs +0 -21
  249. package/dist/chunk-OULU7OC4.mjs.map +0 -1
  250. package/dist/chunk-QNUDODDX.js.map +0 -1
  251. package/dist/chunk-Z6S36PDD.js +0 -24
  252. package/dist/chunk-Z6S36PDD.js.map +0 -1
  253. package/dist/chunk-ZIFMIS7D.mjs.map +0 -1
@@ -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
+ });
@@ -0,0 +1,153 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo, useState } from "react";
4
+
5
+ import type {
6
+ TableFilterState,
7
+ UseTableFilterParams,
8
+ UseTableFilterResult,
9
+ } from "./types";
10
+
11
+ function normalizeFilters<TKey extends string>(
12
+ filters: TableFilterState<TKey> | null | undefined,
13
+ ): TableFilterState<TKey> {
14
+ if (!filters || typeof filters !== "object") {
15
+ return {};
16
+ }
17
+
18
+ return Object.fromEntries(
19
+ Object.entries(filters).filter(
20
+ (entry): entry is [TKey, string] =>
21
+ typeof entry[1] === "string" && entry[1].trim().length > 0,
22
+ ),
23
+ ) as TableFilterState<TKey>;
24
+ }
25
+
26
+ function defaultColumnValue<TData, TKey extends string>(
27
+ row: TData,
28
+ filterKey: TKey,
29
+ ): unknown {
30
+ if (row && typeof row === "object" && filterKey in row) {
31
+ return (row as Record<TKey, unknown>)[filterKey];
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ export function useTableFilter<TData, TKey extends string = string>({
37
+ data,
38
+ filters,
39
+ defaultFilters = {},
40
+ onFiltersChange,
41
+ getColumnValue = defaultColumnValue,
42
+ filterPredicate,
43
+ }: UseTableFilterParams<TData, TKey>): UseTableFilterResult<TData, TKey> {
44
+ const [internalFilters, setInternalFilters] = useState<
45
+ TableFilterState<TKey>
46
+ >(() => normalizeFilters(defaultFilters));
47
+
48
+ const isControlled = filters !== undefined;
49
+ const currentFilters = useMemo(
50
+ () => normalizeFilters(isControlled ? filters : internalFilters),
51
+ [filters, internalFilters, isControlled],
52
+ );
53
+
54
+ const setFilters = useCallback(
55
+ (nextFilters: TableFilterState<TKey>) => {
56
+ const normalized = normalizeFilters(nextFilters);
57
+ if (!isControlled) {
58
+ setInternalFilters(normalized);
59
+ }
60
+ onFiltersChange?.(normalized);
61
+ },
62
+ [isControlled, onFiltersChange],
63
+ );
64
+
65
+ const updateFilters = useCallback(
66
+ (
67
+ updater: (
68
+ previousFilters: TableFilterState<TKey>,
69
+ ) => TableFilterState<TKey>,
70
+ ) => {
71
+ if (isControlled) {
72
+ const normalized = normalizeFilters(updater(currentFilters));
73
+ onFiltersChange?.(normalized);
74
+ return;
75
+ }
76
+
77
+ setInternalFilters((previousFilters) => {
78
+ const normalized = normalizeFilters(updater(previousFilters));
79
+ onFiltersChange?.(normalized);
80
+ return normalized;
81
+ });
82
+ },
83
+ [currentFilters, isControlled, onFiltersChange],
84
+ );
85
+
86
+ const setFilter = useCallback(
87
+ (filterKey: TKey, value: string) => {
88
+ updateFilters((previousFilters) => ({
89
+ ...previousFilters,
90
+ [filterKey]: value,
91
+ }));
92
+ },
93
+ [updateFilters],
94
+ );
95
+
96
+ const clearFilter = useCallback(
97
+ (filterKey: TKey) => {
98
+ updateFilters((previousFilters) => {
99
+ const nextFilters = { ...previousFilters };
100
+ delete nextFilters[filterKey];
101
+ return nextFilters;
102
+ });
103
+ },
104
+ [updateFilters],
105
+ );
106
+
107
+ const clearFilters = useCallback(() => {
108
+ setFilters({});
109
+ }, [setFilters]);
110
+
111
+ const activeFilters = useMemo(
112
+ () =>
113
+ (Object.entries(currentFilters) as [TKey, string][]).map(
114
+ ([filterKey, filterValue]) => ({
115
+ filterKey,
116
+ filterValue,
117
+ lowerFilterValue: filterValue.toLowerCase(),
118
+ }),
119
+ ),
120
+ [currentFilters],
121
+ );
122
+
123
+ const filteredData = useMemo(() => {
124
+ if (activeFilters.length === 0) {
125
+ return [...data];
126
+ }
127
+
128
+ return data.filter((row) =>
129
+ activeFilters.every(({ filterKey, filterValue, lowerFilterValue }) => {
130
+ if (filterPredicate) {
131
+ return filterPredicate(row, filterValue, filterKey);
132
+ }
133
+
134
+ const columnValue = getColumnValue(row, filterKey);
135
+ if (columnValue == null) {
136
+ return false;
137
+ }
138
+
139
+ return String(columnValue).toLowerCase().includes(lowerFilterValue);
140
+ }),
141
+ );
142
+ }, [activeFilters, data, filterPredicate, getColumnValue]);
143
+
144
+ return {
145
+ filters: currentFilters,
146
+ filteredData,
147
+ hasActiveFilters: activeFilters.length > 0,
148
+ setFilter,
149
+ setFilters,
150
+ clearFilter,
151
+ clearFilters,
152
+ };
153
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ type UseTableSortParams,
3
+ type UseTableSortResult,
4
+ } from "./types";
5
+ export { useTableSort } from "./useTableSort";
@@ -0,0 +1,23 @@
1
+ import type {
2
+ TableHeadCellProps,
3
+ TableSortDirection,
4
+ TableSortState,
5
+ } from "../../ui/table/types";
6
+
7
+ export type UseTableSortParams<TKey extends string = string> = {
8
+ sortKey?: TKey;
9
+ defaultSortKey?: TKey;
10
+ sortDirection?: TableSortDirection;
11
+ defaultSortDirection?: TableSortDirection;
12
+ onSortChange?: (nextSort: TableSortState<TKey>) => void;
13
+ };
14
+
15
+ export type UseTableSortResult<TKey extends string = string> =
16
+ TableSortState<TKey> & {
17
+ setSort: (nextSort: TableSortState<TKey>) => void;
18
+ clearSort: () => void;
19
+ toggleSort: (sortKey: TKey) => void;
20
+ getSortProps: (
21
+ sortKey: TKey,
22
+ ) => Pick<TableHeadCellProps, "sortKey" | "sortDirection" | "onSortChange">;
23
+ };
@@ -0,0 +1,150 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import type { TableSortDirection } from "../../ui/table/types";
5
+ import { useTableSort } from "./useTableSort";
6
+
7
+ describe("useTableSort", () => {
8
+ it("should default to no active sort", () => {
9
+ const { result } = renderHook(() => useTableSort());
10
+ expect(result.current.sortKey).toBeUndefined();
11
+ expect(result.current.sortDirection).toBe("none");
12
+ });
13
+
14
+ it("should seed state from default sort params", () => {
15
+ const { result } = renderHook(() =>
16
+ useTableSort({
17
+ defaultSortKey: "name",
18
+ defaultSortDirection: "ascending",
19
+ }),
20
+ );
21
+ expect(result.current.sortKey).toBe("name");
22
+ expect(result.current.sortDirection).toBe("ascending");
23
+ });
24
+
25
+ it("should cycle a column through ascending, descending, and none", () => {
26
+ const { result } = renderHook(() => useTableSort<"name">());
27
+
28
+ act(() => {
29
+ result.current.toggleSort("name");
30
+ });
31
+ expect(result.current).toMatchObject({
32
+ sortKey: "name",
33
+ sortDirection: "ascending",
34
+ });
35
+
36
+ act(() => {
37
+ result.current.toggleSort("name");
38
+ });
39
+ expect(result.current).toMatchObject({
40
+ sortKey: "name",
41
+ sortDirection: "descending",
42
+ });
43
+
44
+ act(() => {
45
+ result.current.toggleSort("name");
46
+ });
47
+ expect(result.current).toMatchObject({
48
+ sortKey: undefined,
49
+ sortDirection: "none",
50
+ });
51
+ });
52
+
53
+ it("should support controlled sort state", () => {
54
+ const handleSortChange = vi.fn();
55
+ const { result, rerender } = renderHook(
56
+ ({
57
+ sortKey,
58
+ sortDirection,
59
+ }: {
60
+ sortKey?: string;
61
+ sortDirection?: TableSortDirection;
62
+ }) =>
63
+ useTableSort({
64
+ sortKey,
65
+ sortDirection,
66
+ onSortChange: handleSortChange,
67
+ }),
68
+ {
69
+ initialProps: {
70
+ sortKey: "createdAt" as string,
71
+ sortDirection: "ascending" as TableSortDirection,
72
+ },
73
+ },
74
+ );
75
+
76
+ act(() => {
77
+ result.current.toggleSort("createdAt");
78
+ });
79
+ expect(handleSortChange).toHaveBeenCalledWith({
80
+ sortKey: "createdAt",
81
+ sortDirection: "descending",
82
+ });
83
+ expect(result.current.sortDirection).toBe("ascending");
84
+
85
+ rerender({ sortKey: "createdAt", sortDirection: "descending" });
86
+ expect(result.current.sortDirection).toBe("descending");
87
+ });
88
+
89
+ it("should allow only sortKey to be controlled", () => {
90
+ const handleSortChange = vi.fn();
91
+ const { result } = renderHook(() =>
92
+ useTableSort({
93
+ sortKey: "name",
94
+ onSortChange: handleSortChange,
95
+ }),
96
+ );
97
+
98
+ act(() => {
99
+ result.current.toggleSort("name");
100
+ });
101
+
102
+ expect(handleSortChange).toHaveBeenCalledWith({
103
+ sortKey: "name",
104
+ sortDirection: "ascending",
105
+ });
106
+ expect(result.current.sortKey).toBe("name");
107
+ expect(result.current.sortDirection).toBe("ascending");
108
+ });
109
+
110
+ it("should allow only sortDirection to be controlled", () => {
111
+ const handleSortChange = vi.fn();
112
+ const { result } = renderHook(() =>
113
+ useTableSort<"name">({
114
+ sortDirection: "ascending",
115
+ onSortChange: handleSortChange,
116
+ }),
117
+ );
118
+
119
+ act(() => {
120
+ result.current.toggleSort("name");
121
+ });
122
+
123
+ expect(handleSortChange).toHaveBeenCalledWith({
124
+ sortKey: "name",
125
+ sortDirection: "ascending",
126
+ });
127
+ expect(result.current.sortKey).toBe("name");
128
+ expect(result.current.sortDirection).toBe("ascending");
129
+ });
130
+
131
+ it("should return TableHead-compatible sort props", () => {
132
+ const { result } = renderHook(() =>
133
+ useTableSort({
134
+ defaultSortKey: "status",
135
+ defaultSortDirection: "descending",
136
+ }),
137
+ );
138
+ const props = result.current.getSortProps("status");
139
+ expect(props).toMatchObject({
140
+ sortKey: "status",
141
+ sortDirection: "descending",
142
+ });
143
+
144
+ act(() => {
145
+ props.onSortChange?.({ sortKey: "status", sortDirection: "none" });
146
+ });
147
+ expect(result.current.sortKey).toBeUndefined();
148
+ expect(result.current.sortDirection).toBe("none");
149
+ });
150
+ });
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo, useState } from "react";
4
+
5
+ import type { TableSortDirection, TableSortState } from "../../ui/table/types";
6
+ import type { UseTableSortParams, UseTableSortResult } from "./types";
7
+
8
+ function nextSortDirection(
9
+ currentDirection: TableSortDirection,
10
+ ): TableSortDirection {
11
+ if (currentDirection === "ascending") {
12
+ return "descending";
13
+ }
14
+ if (currentDirection === "descending") {
15
+ return "none";
16
+ }
17
+ return "ascending";
18
+ }
19
+
20
+ function normalizeSortState<TKey extends string>(
21
+ nextSort: TableSortState<TKey>,
22
+ ): TableSortState<TKey> {
23
+ if (!nextSort.sortKey || nextSort.sortDirection === "none") {
24
+ return { sortKey: undefined, sortDirection: "none" };
25
+ }
26
+ return nextSort;
27
+ }
28
+
29
+ export function useTableSort<TKey extends string = string>({
30
+ sortKey,
31
+ defaultSortKey,
32
+ sortDirection,
33
+ defaultSortDirection = "none",
34
+ onSortChange,
35
+ }: UseTableSortParams<TKey> = {}): UseTableSortResult<TKey> {
36
+ const [internalSort, setInternalSort] = useState<TableSortState<TKey>>(() =>
37
+ normalizeSortState({
38
+ sortKey: defaultSortKey,
39
+ sortDirection: defaultSortDirection,
40
+ }),
41
+ );
42
+
43
+ const isSortKeyControlled = sortKey !== undefined;
44
+ const isSortDirectionControlled = sortDirection !== undefined;
45
+ const currentSort = normalizeSortState({
46
+ sortKey: isSortKeyControlled ? sortKey : internalSort.sortKey,
47
+ sortDirection: isSortDirectionControlled
48
+ ? sortDirection
49
+ : internalSort.sortDirection,
50
+ });
51
+
52
+ const setSort = useCallback(
53
+ (nextSort: TableSortState<TKey>) => {
54
+ const normalized = normalizeSortState(nextSort);
55
+ if (!isSortKeyControlled || !isSortDirectionControlled) {
56
+ setInternalSort((previousSort) => ({
57
+ sortKey: isSortKeyControlled
58
+ ? previousSort.sortKey
59
+ : normalized.sortKey,
60
+ sortDirection: isSortDirectionControlled
61
+ ? previousSort.sortDirection
62
+ : normalized.sortDirection,
63
+ }));
64
+ }
65
+ onSortChange?.(normalized);
66
+ },
67
+ [isSortDirectionControlled, isSortKeyControlled, onSortChange],
68
+ );
69
+
70
+ const clearSort = useCallback(() => {
71
+ setSort({ sortKey: undefined, sortDirection: "none" });
72
+ }, [setSort]);
73
+
74
+ const toggleSort = useCallback(
75
+ (nextSortKey: TKey) => {
76
+ const direction =
77
+ currentSort.sortKey === nextSortKey
78
+ ? nextSortDirection(currentSort.sortDirection)
79
+ : "ascending";
80
+
81
+ setSort({
82
+ sortKey: nextSortKey,
83
+ sortDirection: direction,
84
+ });
85
+ },
86
+ [currentSort.sortDirection, currentSort.sortKey, setSort],
87
+ );
88
+
89
+ const getSortProps = useCallback(
90
+ (nextSortKey: TKey) => ({
91
+ sortKey: nextSortKey,
92
+ sortDirection:
93
+ currentSort.sortKey === nextSortKey
94
+ ? currentSort.sortDirection
95
+ : "none",
96
+ onSortChange: (nextSort: TableSortState) => {
97
+ setSort(nextSort as TableSortState<TKey>);
98
+ },
99
+ }),
100
+ [currentSort.sortDirection, currentSort.sortKey, setSort],
101
+ );
102
+
103
+ return useMemo(
104
+ () => ({
105
+ sortKey: currentSort.sortKey,
106
+ sortDirection: currentSort.sortDirection,
107
+ setSort,
108
+ clearSort,
109
+ toggleSort,
110
+ getSortProps,
111
+ }),
112
+ [
113
+ clearSort,
114
+ currentSort.sortDirection,
115
+ currentSort.sortKey,
116
+ getSortProps,
117
+ setSort,
118
+ toggleSort,
119
+ ],
120
+ );
121
+ }
@@ -0,0 +1,22 @@
1
+ import type { CopyButtonAnimationPresets } from "./types";
2
+
3
+ export const copyButtonAnimationPresets: CopyButtonAnimationPresets = {
4
+ swap: {
5
+ initial: { opacity: 0, scale: 0.6, rotate: -45 },
6
+ animate: { opacity: 1, scale: 1, rotate: 0 },
7
+ exit: { opacity: 0, scale: 0.6, rotate: 45 },
8
+ transition: { type: "spring", stiffness: 520, damping: 24 },
9
+ },
10
+ pop: {
11
+ initial: { opacity: 0, scale: 0.4, rotate: 0 },
12
+ animate: { opacity: 1, scale: 1, rotate: 0 },
13
+ exit: { opacity: 0, scale: 0.4, rotate: 0 },
14
+ transition: { type: "spring", stiffness: 600, damping: 20 },
15
+ },
16
+ fade: {
17
+ initial: { opacity: 0, scale: 1, rotate: 0 },
18
+ animate: { opacity: 1, scale: 1, rotate: 0 },
19
+ exit: { opacity: 0, scale: 1, rotate: 0 },
20
+ transition: { duration: 0.16 },
21
+ },
22
+ };
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { AnimatePresence, motion } from "framer-motion";
4
+
5
+ import { CopyButtonBase } from "../copy-button-base";
6
+ import type { CopyButtonIconRenderer } from "../types";
7
+
8
+ import { copyButtonAnimationPresets } from "./animations";
9
+ import type { CopyButtonAnimatedProps } from "./types";
10
+
11
+ export function CopyButtonAnimated({
12
+ animation = "swap",
13
+ ...props
14
+ }: CopyButtonAnimatedProps) {
15
+ const preset = copyButtonAnimationPresets[animation];
16
+
17
+ const renderIcon: CopyButtonIconRenderer = ({
18
+ copied,
19
+ copyIcon,
20
+ copiedIcon,
21
+ }) => (
22
+ <AnimatePresence initial={false} mode="wait">
23
+ <motion.span
24
+ key={copied ? "copied" : "idle"}
25
+ className="inline-flex items-center justify-center"
26
+ initial={preset.initial}
27
+ animate={preset.animate}
28
+ exit={preset.exit}
29
+ transition={preset.transition}
30
+ >
31
+ {copied ? copiedIcon : copyIcon}
32
+ </motion.span>
33
+ </AnimatePresence>
34
+ );
35
+
36
+ return <CopyButtonBase {...props} renderIcon={renderIcon} />;
37
+ }
38
+
39
+ CopyButtonAnimated.displayName = "CopyButtonAnimated";
@@ -0,0 +1,10 @@
1
+ "use client";
2
+
3
+ export { CopyButtonAnimated } from "./copy-button-animated";
4
+ export { copyButtonAnimationPresets } from "./animations";
5
+ export type {
6
+ CopyButtonAnimatedProps,
7
+ CopyButtonAnimation,
8
+ CopyButtonAnimationPreset,
9
+ CopyButtonAnimationPresets,
10
+ } from "./types";
@@ -0,0 +1,21 @@
1
+ import type { Transition } from "framer-motion";
2
+
3
+ import type { CopyButtonProps } from "../types";
4
+
5
+ export type CopyButtonAnimation = "swap" | "pop" | "fade";
6
+
7
+ export type CopyButtonAnimatedProps = CopyButtonProps & {
8
+ animation?: CopyButtonAnimation;
9
+ };
10
+
11
+ export type CopyButtonAnimationPreset = {
12
+ initial: { opacity: number; scale: number; rotate: number };
13
+ animate: { opacity: number; scale: number; rotate: number };
14
+ exit: { opacity: number; scale: number; rotate: number };
15
+ transition: Transition;
16
+ };
17
+
18
+ export type CopyButtonAnimationPresets = Record<
19
+ CopyButtonAnimation,
20
+ CopyButtonAnimationPreset
21
+ >;