exsorted-react 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ink01101011
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # exsorted-react
2
+
3
+ TypeScript-first React sorting hook powered by [exsorted](https://www.npmjs.com/package/exsorted).
4
+
5
+ - React support: 18 and 19
6
+ - Public API: useSortedList, singleKeyAccessors, and related public types
7
+ - Exsorted resolution: uses peer version when available, otherwise falls back to bundled dependency
8
+
9
+ ## API Surface
10
+
11
+ ```ts
12
+ import { singleKeyAccessors, useSortedList } from "exsorted-react";
13
+ ```
14
+
15
+ ## singleKeyAccessors
16
+
17
+ Minimal helper for one-field sorting while keeping accessors required.
18
+
19
+ ```ts
20
+ const sorted = useSortedList(products, {
21
+ accessors: singleKeyAccessors("price", (item: Product) => item.price),
22
+ initialKey: "price",
23
+ });
24
+ ```
25
+
26
+ ## useSortedList
27
+
28
+ ```ts
29
+ const result = useSortedList(items, options);
30
+ ```
31
+
32
+ ### Next.js SSR / RSC
33
+
34
+ `useSortedList` is a client hook. In Next.js App Router, call it only in Client Components.
35
+
36
+ ```tsx
37
+ "use client";
38
+
39
+ import { useSortedList } from "exsorted-react";
40
+ ```
41
+
42
+ `singleKeyAccessors` is a pure helper and can be called in server code, but it does not make
43
+ `useSortedList` callable in Server Components.
44
+
45
+ If external read-only state provides a key that is not present in `accessors`, the hook safely falls
46
+ back to the first accessor key to avoid error-triggered re-render loops.
47
+
48
+ ### Modes
49
+
50
+ - Standalone mode: pass accessors with optional initialKey and initialDirection
51
+ - Controlled mode: pass sort controller object
52
+ - Read-only state mode: pass state object
53
+
54
+ ### Options
55
+
56
+ - accessors (required): map of key to value accessor
57
+ - comparator (optional): comparator used for sorting
58
+ - sorter (optional): custom compare-based exsorted sorter, default is mergeSort
59
+ - initialKey and initialDirection (standalone mode only)
60
+ - sort (controlled mode only)
61
+ - state (read-only state mode only)
62
+
63
+ ### Sorter Compatibility
64
+
65
+ sorter must match compare-based signature:
66
+
67
+ ```ts
68
+ (arr, compareFn?) => arr;
69
+ ```
70
+
71
+ Not supported because function interfaces differ:
72
+
73
+ - exsorted/non-compare: countingSort, radixSort, bucketSort, pigeonholeSort
74
+ - exsorted/standard: introSort
75
+
76
+ ### Return Value
77
+
78
+ - items: visible items (sorted or original depending on mode/actions)
79
+ - previousItems: previous visible snapshot
80
+ - originalItems: input source reference
81
+ - isSorting: transition pending state
82
+ - isSorted: true when sorted view is active and not pending
83
+ - sort, sortKey, direction: effective sort state
84
+ - setSortKey, setDirection, toggleDirection, setSort, reset: sort controls
85
+ - restoreOriginal, restoreSorted: view-mode controls
86
+
87
+ ## Examples
88
+
89
+ ### Standalone Mode
90
+
91
+ ```ts
92
+ import { useSortedList } from "exsorted-react";
93
+ import { quickSort } from "exsorted";
94
+
95
+ type Product = {
96
+ id: string;
97
+ name: string;
98
+ price: number;
99
+ rating: number;
100
+ };
101
+
102
+ const sorted = useSortedList(products, {
103
+ accessors: {
104
+ name: (item: Product) => item.name,
105
+ price: (item: Product) => item.price,
106
+ rating: (item: Product) => item.rating,
107
+ },
108
+ sorter: quickSort,
109
+ initialKey: "price",
110
+ });
111
+
112
+ sorted.setSortKey("name");
113
+ sorted.toggleDirection();
114
+ ```
115
+
116
+ ### Single-key Minimal Mode
117
+
118
+ ```ts
119
+ import { singleKeyAccessors, useSortedList } from "exsorted-react";
120
+
121
+ const sorted = useSortedList(products, {
122
+ accessors: singleKeyAccessors("price", (item: Product) => item.price),
123
+ initialKey: "price",
124
+ });
125
+ ```
126
+
127
+ ### Controlled Mode
128
+
129
+ ```ts
130
+ import { useState } from "react";
131
+ import { useSortedList } from "exsorted-react";
132
+
133
+ const [sort, setSort] = useState({ key: "name" as const, direction: "asc" as const });
134
+
135
+ const sorted = useSortedList(products, {
136
+ accessors: {
137
+ name: (item: Product) => item.name,
138
+ price: (item: Product) => item.price,
139
+ },
140
+ sort: {
141
+ sort,
142
+ setSort,
143
+ reset: () => setSort({ key: "name", direction: "asc" }),
144
+ },
145
+ });
146
+ ```
147
+
148
+ ### Read-only State Mode
149
+
150
+ ```ts
151
+ const sorted = useSortedList(products, {
152
+ accessors: {
153
+ name: (item: Product) => item.name,
154
+ price: (item: Product) => item.price,
155
+ },
156
+ state: { key: "price", direction: "desc" },
157
+ });
158
+ ```
159
+
160
+ ### Custom Comparator
161
+
162
+ ```ts
163
+ const sorted = useSortedList(products, {
164
+ accessors: {
165
+ name: (item: Product) => item.name,
166
+ price: (item: Product) => item.price,
167
+ },
168
+ initialKey: "name",
169
+ comparator: (a, b) => a.name.localeCompare(b.name, "en", { sensitivity: "base" }),
170
+ });
171
+ ```
172
+
173
+ Tree-shaking note: import and pass only the sorter algorithm you need.
package/dist/index.cjs ADDED
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ singleKeyAccessors: () => singleKeyAccessors,
24
+ useSortedList: () => useSortedList
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/hooks/useSortedList.ts
29
+ var import_react2 = require("react");
30
+
31
+ // src/hooks/useSortState.ts
32
+ var import_react = require("react");
33
+ var defaultGetNextDirection = (current) => current === "asc" ? "desc" : "asc";
34
+ var resolveInitialKey = (options) => {
35
+ if ("keys" in options) {
36
+ if (options.initialKey !== void 0) {
37
+ return options.initialKey;
38
+ }
39
+ const firstKey = options.keys[0];
40
+ if (firstKey === void 0) {
41
+ throw new Error(
42
+ "useSortState expected at least one key in options.keys when initialKey is omitted."
43
+ );
44
+ }
45
+ return firstKey;
46
+ }
47
+ return options.initialKey;
48
+ };
49
+ function useSortState(options) {
50
+ const initialDirection = options.initialDirection ?? "asc";
51
+ const initialKey = (0, import_react.useMemo)(() => resolveInitialKey(options), [options]);
52
+ const initialSort = (0, import_react.useMemo)(
53
+ () => ({ key: initialKey, direction: initialDirection }),
54
+ [initialDirection, initialKey]
55
+ );
56
+ const [sort, setSort] = (0, import_react.useState)(initialSort);
57
+ const setSortKey = (0, import_react.useCallback)((nextKey) => {
58
+ setSort((previous) => ({ ...previous, key: nextKey }));
59
+ }, []);
60
+ const setDirection = (0, import_react.useCallback)((nextDirection) => {
61
+ setSort((previous) => ({ ...previous, direction: nextDirection }));
62
+ }, []);
63
+ const toggleDirection = (0, import_react.useCallback)(() => {
64
+ const getNextDirection = options.getNextDirection ?? defaultGetNextDirection;
65
+ setSort((previous) => ({
66
+ ...previous,
67
+ direction: getNextDirection(previous.direction)
68
+ }));
69
+ }, [options]);
70
+ const reset = (0, import_react.useCallback)(() => {
71
+ setSort(initialSort);
72
+ }, [initialSort]);
73
+ return {
74
+ sort,
75
+ sortKey: sort.key,
76
+ direction: sort.direction,
77
+ setSortKey,
78
+ setDirection,
79
+ toggleDirection,
80
+ setSort,
81
+ reset
82
+ };
83
+ }
84
+
85
+ // src/utils/useSortedList.utils.ts
86
+ var import_merge = require("exsorted/merge");
87
+ var collator = new Intl.Collator(void 0, {
88
+ numeric: true,
89
+ sensitivity: "base"
90
+ });
91
+ var comparePrimitiveValues = (left, right) => {
92
+ if (left === right) {
93
+ return 0;
94
+ }
95
+ if (left == null) {
96
+ return 1;
97
+ }
98
+ if (right == null) {
99
+ return -1;
100
+ }
101
+ if (left instanceof Date && right instanceof Date) {
102
+ return left.getTime() - right.getTime();
103
+ }
104
+ if (typeof left === "boolean" && typeof right === "boolean") {
105
+ return Number(left) - Number(right);
106
+ }
107
+ if (typeof left === "number" && typeof right === "number") {
108
+ return left - right;
109
+ }
110
+ return collator.compare(String(left), String(right));
111
+ };
112
+ var withDirection = (comparison, direction) => direction === "asc" ? comparison : -comparison;
113
+ var singleKeyAccessors = (key, accessor) => ({ [key]: accessor });
114
+ var toggleSortDirection = (direction) => direction === "asc" ? "desc" : "asc";
115
+ var buildComparator = (accessor, state, customComparator) => {
116
+ const compareBase = customComparator ?? ((left, right) => comparePrimitiveValues(accessor(left), accessor(right)));
117
+ return (left, right) => withDirection(compareBase(left, right), state.direction);
118
+ };
119
+ var sortList = (items, state, accessor, comparator, sorter) => {
120
+ if (!accessor) {
121
+ return [...items];
122
+ }
123
+ const selectedSorter = sorter ?? import_merge.mergeSort;
124
+ const directionalComparator = buildComparator(accessor, state, comparator);
125
+ return selectedSorter([...items], directionalComparator);
126
+ };
127
+ var resolveDefaultKey = (accessors) => {
128
+ const firstKey = Object.keys(accessors)[0];
129
+ if (firstKey === void 0) {
130
+ throw new Error("useSortedList expected at least one accessor key.");
131
+ }
132
+ return firstKey;
133
+ };
134
+ var resolveStandaloneInitialDirection = (options) => {
135
+ if ("initialDirection" in options && options.initialDirection !== void 0) {
136
+ return options.initialDirection;
137
+ }
138
+ return "asc";
139
+ };
140
+ var resolveStandaloneInitialKey = (options, fallbackKey) => {
141
+ if ("initialKey" in options && options.initialKey !== void 0) {
142
+ return options.initialKey;
143
+ }
144
+ return fallbackKey;
145
+ };
146
+ var resolveAccessorWithFallback = (accessors, key, fallbackKey) => {
147
+ const directAccessor = accessors[key];
148
+ if (directAccessor !== void 0) {
149
+ return { accessor: directAccessor, resolvedKey: key };
150
+ }
151
+ return { accessor: accessors[fallbackKey], resolvedKey: fallbackKey };
152
+ };
153
+ var resolveExternalController = (options) => "sort" in options ? options.sort : void 0;
154
+ var resolveExternalState = (options) => "state" in options ? options.state : void 0;
155
+ var areArraysShallowEqual = (left, right) => {
156
+ if (left.length !== right.length) {
157
+ return false;
158
+ }
159
+ for (let index = 0; index < left.length; index += 1) {
160
+ if (!Object.is(left[index], right[index])) {
161
+ return false;
162
+ }
163
+ }
164
+ return true;
165
+ };
166
+ var resolveVisibleItems = (params) => {
167
+ const { isOriginalMode, sourceItems, isPending, previousItems, sortedItems } = params;
168
+ if (isOriginalMode) {
169
+ return [...sourceItems];
170
+ }
171
+ if (isPending) {
172
+ return previousItems;
173
+ }
174
+ return sortedItems;
175
+ };
176
+
177
+ // src/hooks/useSortedList.ts
178
+ function useSortedList(items, options) {
179
+ const { accessors, comparator, sorter } = options;
180
+ const [isPending, startTransition] = (0, import_react2.useTransition)();
181
+ const defaultKey = (0, import_react2.useMemo)(() => resolveDefaultKey(accessors), [accessors]);
182
+ const externalSortController = resolveExternalController(options);
183
+ const externalState = resolveExternalState(options);
184
+ const standaloneInitialDirection = resolveStandaloneInitialDirection(options);
185
+ const standaloneInitialKey = resolveStandaloneInitialKey(options, defaultKey);
186
+ const localSortController = useSortState({
187
+ initialKey: standaloneInitialKey,
188
+ initialDirection: standaloneInitialDirection
189
+ });
190
+ const activeSortController = externalSortController ?? (externalState === void 0 ? localSortController : void 0);
191
+ const activeSort = externalState ?? activeSortController?.sort ?? localSortController.sort;
192
+ const setSort = (0, import_react2.useCallback)(
193
+ (next) => {
194
+ activeSortController?.setSort(next);
195
+ },
196
+ [activeSortController]
197
+ );
198
+ const setSortKey = (0, import_react2.useCallback)(
199
+ (nextKey) => {
200
+ setSort((previous) => ({ ...previous, key: nextKey }));
201
+ },
202
+ [setSort]
203
+ );
204
+ const setDirection = (0, import_react2.useCallback)(
205
+ (nextDirection) => {
206
+ setSort((previous) => ({ ...previous, direction: nextDirection }));
207
+ },
208
+ [setSort]
209
+ );
210
+ const toggleDirection = (0, import_react2.useCallback)(() => {
211
+ setSort((previous) => ({
212
+ ...previous,
213
+ direction: toggleSortDirection(previous.direction)
214
+ }));
215
+ }, [setSort]);
216
+ const reset = (0, import_react2.useCallback)(() => {
217
+ activeSortController?.reset();
218
+ }, [activeSortController]);
219
+ const deferredItems = (0, import_react2.useDeferredValue)(items);
220
+ const deferredState = (0, import_react2.useDeferredValue)(activeSort);
221
+ const effectiveItems = deferredItems;
222
+ const effectiveState = deferredState;
223
+ const effectiveComparator = comparator;
224
+ const { accessor, resolvedKey } = resolveAccessorWithFallback(
225
+ accessors,
226
+ effectiveState.key,
227
+ defaultKey
228
+ );
229
+ const effectiveSort = resolvedKey === effectiveState.key ? effectiveState : { ...effectiveState, key: resolvedKey };
230
+ const nextSorted = (0, import_react2.useMemo)(
231
+ () => sortList(effectiveItems, effectiveSort, accessor, effectiveComparator, sorter),
232
+ [accessor, effectiveComparator, effectiveItems, effectiveSort, sorter]
233
+ );
234
+ const [sorted, setSorted] = (0, import_react2.useState)(nextSorted);
235
+ const [isOriginalMode, setIsOriginalMode] = (0, import_react2.useState)(false);
236
+ const previousItemsRef = (0, import_react2.useRef)(nextSorted);
237
+ const restoreOriginal = (0, import_react2.useCallback)(() => {
238
+ setIsOriginalMode(true);
239
+ }, []);
240
+ const restoreSorted = (0, import_react2.useCallback)(() => {
241
+ setIsOriginalMode(false);
242
+ }, []);
243
+ (0, import_react2.useEffect)(() => {
244
+ if (areArraysShallowEqual(sorted, nextSorted)) {
245
+ return;
246
+ }
247
+ startTransition(() => {
248
+ setSorted((current) => {
249
+ if (areArraysShallowEqual(current, nextSorted)) {
250
+ return current;
251
+ }
252
+ previousItemsRef.current = current;
253
+ return nextSorted;
254
+ });
255
+ });
256
+ }, [nextSorted, sorted, startTransition]);
257
+ const visibleItems = resolveVisibleItems({
258
+ isOriginalMode,
259
+ sourceItems: items,
260
+ isPending,
261
+ previousItems: previousItemsRef.current,
262
+ sortedItems: sorted
263
+ });
264
+ return {
265
+ items: visibleItems,
266
+ previousItems: previousItemsRef.current,
267
+ originalItems: items,
268
+ isSorting: isPending,
269
+ isSorted: !isPending && !isOriginalMode,
270
+ sort: activeSort,
271
+ sortKey: activeSort.key,
272
+ direction: activeSort.direction,
273
+ setSortKey,
274
+ setDirection,
275
+ toggleDirection,
276
+ setSort,
277
+ reset,
278
+ restoreOriginal,
279
+ restoreSorted
280
+ };
281
+ }
282
+ // Annotate the CommonJS export names for ESM import in node:
283
+ 0 && (module.exports = {
284
+ singleKeyAccessors,
285
+ useSortedList
286
+ });
@@ -0,0 +1,222 @@
1
+ import { CompareFn } from 'exsorted/types';
2
+
3
+ /**
4
+ * Direction used when applying a sort operation.
5
+ */
6
+ type SortDirection = "asc" | "desc";
7
+ /**
8
+ * Primitive values supported by the default comparator.
9
+ */
10
+ type SortPrimitive$1 = string | number | boolean | Date | null | undefined;
11
+ /**
12
+ * Current sort state containing the active key and direction.
13
+ *
14
+ * @template TKey key union for sortable fields
15
+ */
16
+ interface SortState<TKey extends PropertyKey = string> {
17
+ key: TKey;
18
+ direction: SortDirection;
19
+ }
20
+ /**
21
+ * Map of sort keys to accessor functions.
22
+ *
23
+ * Each accessor extracts a comparable primitive from an item.
24
+ *
25
+ * @template TItem item type in the source list
26
+ * @template TKey union of valid sort keys
27
+ */
28
+ type SortAccessors<TItem, TKey extends PropertyKey> = Record<TKey, (item: TItem) => SortPrimitive$1>;
29
+ /**
30
+ * External controller contract for controlled sorting mode.
31
+ *
32
+ * @template TKey key union for sortable fields
33
+ */
34
+ interface SortController<TKey extends PropertyKey> {
35
+ sort: SortState<TKey>;
36
+ setSort: (next: SortState<TKey> | ((previous: SortState<TKey>) => SortState<TKey>)) => void;
37
+ reset: () => void;
38
+ }
39
+ /**
40
+ * Exsorted-compatible sorting function signature.
41
+ *
42
+ * Accepts only strict comparator-based sorters.
43
+ *
44
+ * Excluded intentionally:
45
+ * - non-compare family from `exsorted/non-compare`:
46
+ * `countingSort`, `radixSort`, `bucketSort`, `pigeonholeSort`
47
+ * - `introSort` from `exsorted/standard` (overload includes threshold variants)
48
+ *
49
+ * The trailing `...extra: never[]` prevents passing sorters with additional
50
+ * non-compatible parameters.
51
+ */
52
+ type ExsortedSorter<TItem> = (arr: TItem[], compareFn?: CompareFn<TItem>, ...extra: never[]) => TItem[];
53
+ /**
54
+ * Configuration object for useSortedList.
55
+ *
56
+ * Supports three modes:
57
+ * - standalone mode: no external state/controller, optional initialKey/initialDirection
58
+ * - controller mode: provide `sort` to fully control state and actions
59
+ * - state mode: provide `state` for read-only external state observation
60
+ *
61
+ * @template TItem item type in the source list
62
+ * @template TKey union of valid sort keys
63
+ */
64
+ type UseSortedListOptions<TItem, TKey extends PropertyKey> = {
65
+ /**
66
+ * Accessors used to derive sortable values from each item.
67
+ */
68
+ accessors: SortAccessors<TItem, TKey>;
69
+ /**
70
+ * Custom comparator used for all sort keys.
71
+ */
72
+ comparator?: CompareFn<TItem>;
73
+ /**
74
+ * Custom Exsorted sorter (defaults to mergeSort in the hook).
75
+ */
76
+ sorter?: ExsortedSorter<TItem>;
77
+ } & ({
78
+ /**
79
+ * Controlled mode: external controller with state + mutators.
80
+ */
81
+ sort: SortController<TKey>;
82
+ state?: never;
83
+ initialKey?: never;
84
+ initialDirection?: never;
85
+ } | {
86
+ /**
87
+ * Read-only external state mode.
88
+ */
89
+ state: SortState<TKey>;
90
+ sort?: never;
91
+ initialKey?: never;
92
+ initialDirection?: never;
93
+ } | {
94
+ sort?: never;
95
+ state?: never;
96
+ /**
97
+ * Standalone mode initial key.
98
+ *
99
+ * If omitted, the first accessor key is used.
100
+ */
101
+ initialKey?: TKey;
102
+ /**
103
+ * Standalone mode initial direction.
104
+ *
105
+ * Defaults to `asc`.
106
+ */
107
+ initialDirection?: SortDirection;
108
+ });
109
+ /**
110
+ * Return shape for useSortedList.
111
+ *
112
+ * @template TItem item type in the source list
113
+ * @template TKey union of valid sort keys
114
+ */
115
+ interface UseSortedListResult<TItem, TKey extends PropertyKey = string> {
116
+ /**
117
+ * Current visible list, which may be sorted or original depending on mode/actions.
118
+ */
119
+ items: TItem[];
120
+ /**
121
+ * Last visible list snapshot prior to the most recent sorted update.
122
+ */
123
+ previousItems: TItem[];
124
+ /**
125
+ * Original source list reference from hook input.
126
+ */
127
+ originalItems: readonly TItem[];
128
+ /**
129
+ * True while a deferred transition is applying a new sorted result.
130
+ */
131
+ isSorting: boolean;
132
+ /**
133
+ * True when sorted view is active and no transition is pending.
134
+ */
135
+ isSorted: boolean;
136
+ /**
137
+ * Current effective sort state.
138
+ */
139
+ sort: SortState<TKey>;
140
+ /**
141
+ * Shortcut to `sort.key`.
142
+ */
143
+ sortKey: TKey;
144
+ /**
145
+ * Shortcut to `sort.direction`.
146
+ */
147
+ direction: SortDirection;
148
+ /**
149
+ * Updates only the active sort key.
150
+ */
151
+ setSortKey: (nextKey: TKey) => void;
152
+ /**
153
+ * Updates only the active sort direction.
154
+ */
155
+ setDirection: (nextDirection: SortDirection) => void;
156
+ /**
157
+ * Toggles direction between `asc` and `desc`.
158
+ */
159
+ toggleDirection: () => void;
160
+ /**
161
+ * Replaces sort state directly or via an updater callback.
162
+ */
163
+ setSort: (next: SortState<TKey> | ((previous: SortState<TKey>) => SortState<TKey>)) => void;
164
+ /**
165
+ * Resets sorting state to the initial standalone/controller state.
166
+ */
167
+ reset: () => void;
168
+ /**
169
+ * Shows original input order in `items`.
170
+ */
171
+ restoreOriginal: () => void;
172
+ /**
173
+ * Switches back to sorted view in `items`.
174
+ */
175
+ restoreSorted: () => void;
176
+ }
177
+
178
+ /**
179
+ * Internal primitive union used by accessor and comparator helpers.
180
+ */
181
+ type SortPrimitive = string | number | boolean | Date | null | undefined;
182
+ /**
183
+ * Internal accessor map used to infer key unions for hook APIs.
184
+ */
185
+ type SortAccessorRecord<TItem> = Record<string, (item: TItem) => SortPrimitive>;
186
+ /**
187
+ * Extracts string keys from accessor maps for stable public API typing.
188
+ */
189
+ type SortKey<TAccessors extends Record<string, (...args: any[]) => unknown>> = Extract<keyof TAccessors, string>;
190
+
191
+ /**
192
+ * Sorts a readonly list using Exsorted with stable, non-mutating behavior.
193
+ *
194
+ * Supports 3 modes:
195
+ * - standalone mode: pass initialKey/initialDirection (or neither) and the hook owns sort state
196
+ * - controller mode: pass sort controller from outside for fully controlled state/actions
197
+ * - state mode: pass state only for read-only external state (actions become no-ops)
198
+ *
199
+ * @template TItem item type in the source list
200
+ * @template TAccessors accessor map used for sort keys and inferred key union
201
+ * @param items source list (never mutated)
202
+ * @param options sorting config, accessors, and state mode controls
203
+ * @returns visible items, previous/original snapshots, status flags, and sort actions
204
+ * @example
205
+ * const sorted = useSortedList(products, {
206
+ * accessors: {
207
+ * name: (item) => item.name,
208
+ * price: (item) => item.price,
209
+ * },
210
+ * initialKey: "price",
211
+ * });
212
+ */
213
+ declare function useSortedList<TItem, const TAccessors extends SortAccessorRecord<TItem>>(items: readonly TItem[], options: UseSortedListOptions<TItem, SortKey<TAccessors>> & {
214
+ accessors: TAccessors;
215
+ }): UseSortedListResult<TItem, SortKey<TAccessors>>;
216
+
217
+ /**
218
+ * Builds a single-key accessor map for simpler one-field sorting cases.
219
+ */
220
+ declare const singleKeyAccessors: <TItem, const TKey extends string>(key: TKey, accessor: (item: TItem) => SortPrimitive$1) => Record<TKey, (item: TItem) => SortPrimitive$1>;
221
+
222
+ export { type ExsortedSorter, type SortAccessors, type SortController, type SortDirection, type SortPrimitive$1 as SortPrimitive, type SortState, type UseSortedListOptions, type UseSortedListResult, singleKeyAccessors, useSortedList };
@@ -0,0 +1,222 @@
1
+ import { CompareFn } from 'exsorted/types';
2
+
3
+ /**
4
+ * Direction used when applying a sort operation.
5
+ */
6
+ type SortDirection = "asc" | "desc";
7
+ /**
8
+ * Primitive values supported by the default comparator.
9
+ */
10
+ type SortPrimitive$1 = string | number | boolean | Date | null | undefined;
11
+ /**
12
+ * Current sort state containing the active key and direction.
13
+ *
14
+ * @template TKey key union for sortable fields
15
+ */
16
+ interface SortState<TKey extends PropertyKey = string> {
17
+ key: TKey;
18
+ direction: SortDirection;
19
+ }
20
+ /**
21
+ * Map of sort keys to accessor functions.
22
+ *
23
+ * Each accessor extracts a comparable primitive from an item.
24
+ *
25
+ * @template TItem item type in the source list
26
+ * @template TKey union of valid sort keys
27
+ */
28
+ type SortAccessors<TItem, TKey extends PropertyKey> = Record<TKey, (item: TItem) => SortPrimitive$1>;
29
+ /**
30
+ * External controller contract for controlled sorting mode.
31
+ *
32
+ * @template TKey key union for sortable fields
33
+ */
34
+ interface SortController<TKey extends PropertyKey> {
35
+ sort: SortState<TKey>;
36
+ setSort: (next: SortState<TKey> | ((previous: SortState<TKey>) => SortState<TKey>)) => void;
37
+ reset: () => void;
38
+ }
39
+ /**
40
+ * Exsorted-compatible sorting function signature.
41
+ *
42
+ * Accepts only strict comparator-based sorters.
43
+ *
44
+ * Excluded intentionally:
45
+ * - non-compare family from `exsorted/non-compare`:
46
+ * `countingSort`, `radixSort`, `bucketSort`, `pigeonholeSort`
47
+ * - `introSort` from `exsorted/standard` (overload includes threshold variants)
48
+ *
49
+ * The trailing `...extra: never[]` prevents passing sorters with additional
50
+ * non-compatible parameters.
51
+ */
52
+ type ExsortedSorter<TItem> = (arr: TItem[], compareFn?: CompareFn<TItem>, ...extra: never[]) => TItem[];
53
+ /**
54
+ * Configuration object for useSortedList.
55
+ *
56
+ * Supports three modes:
57
+ * - standalone mode: no external state/controller, optional initialKey/initialDirection
58
+ * - controller mode: provide `sort` to fully control state and actions
59
+ * - state mode: provide `state` for read-only external state observation
60
+ *
61
+ * @template TItem item type in the source list
62
+ * @template TKey union of valid sort keys
63
+ */
64
+ type UseSortedListOptions<TItem, TKey extends PropertyKey> = {
65
+ /**
66
+ * Accessors used to derive sortable values from each item.
67
+ */
68
+ accessors: SortAccessors<TItem, TKey>;
69
+ /**
70
+ * Custom comparator used for all sort keys.
71
+ */
72
+ comparator?: CompareFn<TItem>;
73
+ /**
74
+ * Custom Exsorted sorter (defaults to mergeSort in the hook).
75
+ */
76
+ sorter?: ExsortedSorter<TItem>;
77
+ } & ({
78
+ /**
79
+ * Controlled mode: external controller with state + mutators.
80
+ */
81
+ sort: SortController<TKey>;
82
+ state?: never;
83
+ initialKey?: never;
84
+ initialDirection?: never;
85
+ } | {
86
+ /**
87
+ * Read-only external state mode.
88
+ */
89
+ state: SortState<TKey>;
90
+ sort?: never;
91
+ initialKey?: never;
92
+ initialDirection?: never;
93
+ } | {
94
+ sort?: never;
95
+ state?: never;
96
+ /**
97
+ * Standalone mode initial key.
98
+ *
99
+ * If omitted, the first accessor key is used.
100
+ */
101
+ initialKey?: TKey;
102
+ /**
103
+ * Standalone mode initial direction.
104
+ *
105
+ * Defaults to `asc`.
106
+ */
107
+ initialDirection?: SortDirection;
108
+ });
109
+ /**
110
+ * Return shape for useSortedList.
111
+ *
112
+ * @template TItem item type in the source list
113
+ * @template TKey union of valid sort keys
114
+ */
115
+ interface UseSortedListResult<TItem, TKey extends PropertyKey = string> {
116
+ /**
117
+ * Current visible list, which may be sorted or original depending on mode/actions.
118
+ */
119
+ items: TItem[];
120
+ /**
121
+ * Last visible list snapshot prior to the most recent sorted update.
122
+ */
123
+ previousItems: TItem[];
124
+ /**
125
+ * Original source list reference from hook input.
126
+ */
127
+ originalItems: readonly TItem[];
128
+ /**
129
+ * True while a deferred transition is applying a new sorted result.
130
+ */
131
+ isSorting: boolean;
132
+ /**
133
+ * True when sorted view is active and no transition is pending.
134
+ */
135
+ isSorted: boolean;
136
+ /**
137
+ * Current effective sort state.
138
+ */
139
+ sort: SortState<TKey>;
140
+ /**
141
+ * Shortcut to `sort.key`.
142
+ */
143
+ sortKey: TKey;
144
+ /**
145
+ * Shortcut to `sort.direction`.
146
+ */
147
+ direction: SortDirection;
148
+ /**
149
+ * Updates only the active sort key.
150
+ */
151
+ setSortKey: (nextKey: TKey) => void;
152
+ /**
153
+ * Updates only the active sort direction.
154
+ */
155
+ setDirection: (nextDirection: SortDirection) => void;
156
+ /**
157
+ * Toggles direction between `asc` and `desc`.
158
+ */
159
+ toggleDirection: () => void;
160
+ /**
161
+ * Replaces sort state directly or via an updater callback.
162
+ */
163
+ setSort: (next: SortState<TKey> | ((previous: SortState<TKey>) => SortState<TKey>)) => void;
164
+ /**
165
+ * Resets sorting state to the initial standalone/controller state.
166
+ */
167
+ reset: () => void;
168
+ /**
169
+ * Shows original input order in `items`.
170
+ */
171
+ restoreOriginal: () => void;
172
+ /**
173
+ * Switches back to sorted view in `items`.
174
+ */
175
+ restoreSorted: () => void;
176
+ }
177
+
178
+ /**
179
+ * Internal primitive union used by accessor and comparator helpers.
180
+ */
181
+ type SortPrimitive = string | number | boolean | Date | null | undefined;
182
+ /**
183
+ * Internal accessor map used to infer key unions for hook APIs.
184
+ */
185
+ type SortAccessorRecord<TItem> = Record<string, (item: TItem) => SortPrimitive>;
186
+ /**
187
+ * Extracts string keys from accessor maps for stable public API typing.
188
+ */
189
+ type SortKey<TAccessors extends Record<string, (...args: any[]) => unknown>> = Extract<keyof TAccessors, string>;
190
+
191
+ /**
192
+ * Sorts a readonly list using Exsorted with stable, non-mutating behavior.
193
+ *
194
+ * Supports 3 modes:
195
+ * - standalone mode: pass initialKey/initialDirection (or neither) and the hook owns sort state
196
+ * - controller mode: pass sort controller from outside for fully controlled state/actions
197
+ * - state mode: pass state only for read-only external state (actions become no-ops)
198
+ *
199
+ * @template TItem item type in the source list
200
+ * @template TAccessors accessor map used for sort keys and inferred key union
201
+ * @param items source list (never mutated)
202
+ * @param options sorting config, accessors, and state mode controls
203
+ * @returns visible items, previous/original snapshots, status flags, and sort actions
204
+ * @example
205
+ * const sorted = useSortedList(products, {
206
+ * accessors: {
207
+ * name: (item) => item.name,
208
+ * price: (item) => item.price,
209
+ * },
210
+ * initialKey: "price",
211
+ * });
212
+ */
213
+ declare function useSortedList<TItem, const TAccessors extends SortAccessorRecord<TItem>>(items: readonly TItem[], options: UseSortedListOptions<TItem, SortKey<TAccessors>> & {
214
+ accessors: TAccessors;
215
+ }): UseSortedListResult<TItem, SortKey<TAccessors>>;
216
+
217
+ /**
218
+ * Builds a single-key accessor map for simpler one-field sorting cases.
219
+ */
220
+ declare const singleKeyAccessors: <TItem, const TKey extends string>(key: TKey, accessor: (item: TItem) => SortPrimitive$1) => Record<TKey, (item: TItem) => SortPrimitive$1>;
221
+
222
+ export { type ExsortedSorter, type SortAccessors, type SortController, type SortDirection, type SortPrimitive$1 as SortPrimitive, type SortState, type UseSortedListOptions, type UseSortedListResult, singleKeyAccessors, useSortedList };
package/dist/index.js ADDED
@@ -0,0 +1,266 @@
1
+ // src/hooks/useSortedList.ts
2
+ import {
3
+ useCallback as useCallback2,
4
+ useDeferredValue,
5
+ useEffect,
6
+ useMemo as useMemo2,
7
+ useRef,
8
+ useState as useState2,
9
+ useTransition
10
+ } from "react";
11
+
12
+ // src/hooks/useSortState.ts
13
+ import { useCallback, useMemo, useState } from "react";
14
+ var defaultGetNextDirection = (current) => current === "asc" ? "desc" : "asc";
15
+ var resolveInitialKey = (options) => {
16
+ if ("keys" in options) {
17
+ if (options.initialKey !== void 0) {
18
+ return options.initialKey;
19
+ }
20
+ const firstKey = options.keys[0];
21
+ if (firstKey === void 0) {
22
+ throw new Error(
23
+ "useSortState expected at least one key in options.keys when initialKey is omitted."
24
+ );
25
+ }
26
+ return firstKey;
27
+ }
28
+ return options.initialKey;
29
+ };
30
+ function useSortState(options) {
31
+ const initialDirection = options.initialDirection ?? "asc";
32
+ const initialKey = useMemo(() => resolveInitialKey(options), [options]);
33
+ const initialSort = useMemo(
34
+ () => ({ key: initialKey, direction: initialDirection }),
35
+ [initialDirection, initialKey]
36
+ );
37
+ const [sort, setSort] = useState(initialSort);
38
+ const setSortKey = useCallback((nextKey) => {
39
+ setSort((previous) => ({ ...previous, key: nextKey }));
40
+ }, []);
41
+ const setDirection = useCallback((nextDirection) => {
42
+ setSort((previous) => ({ ...previous, direction: nextDirection }));
43
+ }, []);
44
+ const toggleDirection = useCallback(() => {
45
+ const getNextDirection = options.getNextDirection ?? defaultGetNextDirection;
46
+ setSort((previous) => ({
47
+ ...previous,
48
+ direction: getNextDirection(previous.direction)
49
+ }));
50
+ }, [options]);
51
+ const reset = useCallback(() => {
52
+ setSort(initialSort);
53
+ }, [initialSort]);
54
+ return {
55
+ sort,
56
+ sortKey: sort.key,
57
+ direction: sort.direction,
58
+ setSortKey,
59
+ setDirection,
60
+ toggleDirection,
61
+ setSort,
62
+ reset
63
+ };
64
+ }
65
+
66
+ // src/utils/useSortedList.utils.ts
67
+ import { mergeSort } from "exsorted/merge";
68
+ var collator = new Intl.Collator(void 0, {
69
+ numeric: true,
70
+ sensitivity: "base"
71
+ });
72
+ var comparePrimitiveValues = (left, right) => {
73
+ if (left === right) {
74
+ return 0;
75
+ }
76
+ if (left == null) {
77
+ return 1;
78
+ }
79
+ if (right == null) {
80
+ return -1;
81
+ }
82
+ if (left instanceof Date && right instanceof Date) {
83
+ return left.getTime() - right.getTime();
84
+ }
85
+ if (typeof left === "boolean" && typeof right === "boolean") {
86
+ return Number(left) - Number(right);
87
+ }
88
+ if (typeof left === "number" && typeof right === "number") {
89
+ return left - right;
90
+ }
91
+ return collator.compare(String(left), String(right));
92
+ };
93
+ var withDirection = (comparison, direction) => direction === "asc" ? comparison : -comparison;
94
+ var singleKeyAccessors = (key, accessor) => ({ [key]: accessor });
95
+ var toggleSortDirection = (direction) => direction === "asc" ? "desc" : "asc";
96
+ var buildComparator = (accessor, state, customComparator) => {
97
+ const compareBase = customComparator ?? ((left, right) => comparePrimitiveValues(accessor(left), accessor(right)));
98
+ return (left, right) => withDirection(compareBase(left, right), state.direction);
99
+ };
100
+ var sortList = (items, state, accessor, comparator, sorter) => {
101
+ if (!accessor) {
102
+ return [...items];
103
+ }
104
+ const selectedSorter = sorter ?? mergeSort;
105
+ const directionalComparator = buildComparator(accessor, state, comparator);
106
+ return selectedSorter([...items], directionalComparator);
107
+ };
108
+ var resolveDefaultKey = (accessors) => {
109
+ const firstKey = Object.keys(accessors)[0];
110
+ if (firstKey === void 0) {
111
+ throw new Error("useSortedList expected at least one accessor key.");
112
+ }
113
+ return firstKey;
114
+ };
115
+ var resolveStandaloneInitialDirection = (options) => {
116
+ if ("initialDirection" in options && options.initialDirection !== void 0) {
117
+ return options.initialDirection;
118
+ }
119
+ return "asc";
120
+ };
121
+ var resolveStandaloneInitialKey = (options, fallbackKey) => {
122
+ if ("initialKey" in options && options.initialKey !== void 0) {
123
+ return options.initialKey;
124
+ }
125
+ return fallbackKey;
126
+ };
127
+ var resolveAccessorWithFallback = (accessors, key, fallbackKey) => {
128
+ const directAccessor = accessors[key];
129
+ if (directAccessor !== void 0) {
130
+ return { accessor: directAccessor, resolvedKey: key };
131
+ }
132
+ return { accessor: accessors[fallbackKey], resolvedKey: fallbackKey };
133
+ };
134
+ var resolveExternalController = (options) => "sort" in options ? options.sort : void 0;
135
+ var resolveExternalState = (options) => "state" in options ? options.state : void 0;
136
+ var areArraysShallowEqual = (left, right) => {
137
+ if (left.length !== right.length) {
138
+ return false;
139
+ }
140
+ for (let index = 0; index < left.length; index += 1) {
141
+ if (!Object.is(left[index], right[index])) {
142
+ return false;
143
+ }
144
+ }
145
+ return true;
146
+ };
147
+ var resolveVisibleItems = (params) => {
148
+ const { isOriginalMode, sourceItems, isPending, previousItems, sortedItems } = params;
149
+ if (isOriginalMode) {
150
+ return [...sourceItems];
151
+ }
152
+ if (isPending) {
153
+ return previousItems;
154
+ }
155
+ return sortedItems;
156
+ };
157
+
158
+ // src/hooks/useSortedList.ts
159
+ function useSortedList(items, options) {
160
+ const { accessors, comparator, sorter } = options;
161
+ const [isPending, startTransition] = useTransition();
162
+ const defaultKey = useMemo2(() => resolveDefaultKey(accessors), [accessors]);
163
+ const externalSortController = resolveExternalController(options);
164
+ const externalState = resolveExternalState(options);
165
+ const standaloneInitialDirection = resolveStandaloneInitialDirection(options);
166
+ const standaloneInitialKey = resolveStandaloneInitialKey(options, defaultKey);
167
+ const localSortController = useSortState({
168
+ initialKey: standaloneInitialKey,
169
+ initialDirection: standaloneInitialDirection
170
+ });
171
+ const activeSortController = externalSortController ?? (externalState === void 0 ? localSortController : void 0);
172
+ const activeSort = externalState ?? activeSortController?.sort ?? localSortController.sort;
173
+ const setSort = useCallback2(
174
+ (next) => {
175
+ activeSortController?.setSort(next);
176
+ },
177
+ [activeSortController]
178
+ );
179
+ const setSortKey = useCallback2(
180
+ (nextKey) => {
181
+ setSort((previous) => ({ ...previous, key: nextKey }));
182
+ },
183
+ [setSort]
184
+ );
185
+ const setDirection = useCallback2(
186
+ (nextDirection) => {
187
+ setSort((previous) => ({ ...previous, direction: nextDirection }));
188
+ },
189
+ [setSort]
190
+ );
191
+ const toggleDirection = useCallback2(() => {
192
+ setSort((previous) => ({
193
+ ...previous,
194
+ direction: toggleSortDirection(previous.direction)
195
+ }));
196
+ }, [setSort]);
197
+ const reset = useCallback2(() => {
198
+ activeSortController?.reset();
199
+ }, [activeSortController]);
200
+ const deferredItems = useDeferredValue(items);
201
+ const deferredState = useDeferredValue(activeSort);
202
+ const effectiveItems = deferredItems;
203
+ const effectiveState = deferredState;
204
+ const effectiveComparator = comparator;
205
+ const { accessor, resolvedKey } = resolveAccessorWithFallback(
206
+ accessors,
207
+ effectiveState.key,
208
+ defaultKey
209
+ );
210
+ const effectiveSort = resolvedKey === effectiveState.key ? effectiveState : { ...effectiveState, key: resolvedKey };
211
+ const nextSorted = useMemo2(
212
+ () => sortList(effectiveItems, effectiveSort, accessor, effectiveComparator, sorter),
213
+ [accessor, effectiveComparator, effectiveItems, effectiveSort, sorter]
214
+ );
215
+ const [sorted, setSorted] = useState2(nextSorted);
216
+ const [isOriginalMode, setIsOriginalMode] = useState2(false);
217
+ const previousItemsRef = useRef(nextSorted);
218
+ const restoreOriginal = useCallback2(() => {
219
+ setIsOriginalMode(true);
220
+ }, []);
221
+ const restoreSorted = useCallback2(() => {
222
+ setIsOriginalMode(false);
223
+ }, []);
224
+ useEffect(() => {
225
+ if (areArraysShallowEqual(sorted, nextSorted)) {
226
+ return;
227
+ }
228
+ startTransition(() => {
229
+ setSorted((current) => {
230
+ if (areArraysShallowEqual(current, nextSorted)) {
231
+ return current;
232
+ }
233
+ previousItemsRef.current = current;
234
+ return nextSorted;
235
+ });
236
+ });
237
+ }, [nextSorted, sorted, startTransition]);
238
+ const visibleItems = resolveVisibleItems({
239
+ isOriginalMode,
240
+ sourceItems: items,
241
+ isPending,
242
+ previousItems: previousItemsRef.current,
243
+ sortedItems: sorted
244
+ });
245
+ return {
246
+ items: visibleItems,
247
+ previousItems: previousItemsRef.current,
248
+ originalItems: items,
249
+ isSorting: isPending,
250
+ isSorted: !isPending && !isOriginalMode,
251
+ sort: activeSort,
252
+ sortKey: activeSort.key,
253
+ direction: activeSort.direction,
254
+ setSortKey,
255
+ setDirection,
256
+ toggleDirection,
257
+ setSort,
258
+ reset,
259
+ restoreOriginal,
260
+ restoreSorted
261
+ };
262
+ }
263
+ export {
264
+ singleKeyAccessors,
265
+ useSortedList
266
+ };
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "exsorted-react",
3
+ "version": "1.0.3",
4
+ "description": "React hooks for sorting with Exsorted and TypeScript-first inference",
5
+ "keywords": [
6
+ "custom-hook",
7
+ "exsorted",
8
+ "react",
9
+ "typescript"
10
+ ],
11
+ "homepage": "https://github.com/Ink01101011/exsorted-react#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/Ink01101011/exsorted-react/issues"
14
+ },
15
+ "license": "MIT",
16
+ "author": "Ink01101011",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/Ink01101011/exsorted-react.git"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.cjs"
36
+ }
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
43
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
44
+ "lint": "pnpm dlx oxlint@latest .",
45
+ "lint:fix": "pnpm dlx oxlint@latest --fix .",
46
+ "format": "pnpm dlx oxfmt@latest --write .",
47
+ "format:check": "pnpm dlx oxfmt@latest --check .",
48
+ "typecheck": "tsc --noEmit",
49
+ "test": "jest --runInBand",
50
+ "test:watch": "jest --watch",
51
+ "size": "node ./scripts/check-bundle-size.mjs",
52
+ "audit:cve": "pnpm audit --audit-level=high",
53
+ "check": "pnpm run lint && pnpm run format:check && pnpm run typecheck && pnpm run build && pnpm run size",
54
+ "prepare": "husky",
55
+ "precommit:check": "pnpm run lint && pnpm run format:check && pnpm run typecheck",
56
+ "prepublishOnly": "pnpm run build"
57
+ },
58
+ "dependencies": {
59
+ "exsorted": "^1.1.0"
60
+ },
61
+ "devDependencies": {
62
+ "@testing-library/react": "^16.2.0",
63
+ "@types/jest": "^29.5.14",
64
+ "@types/react": "^18.0.0 || ^19.0.0",
65
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
66
+ "husky": "^9.1.7",
67
+ "jest": "^29.7.0",
68
+ "jest-environment-jsdom": "^29.7.0",
69
+ "jsdom": "^25.0.1",
70
+ "react": "^18.2.0 || ^19.0.0",
71
+ "react-dom": "^18.2.0 || ^19.0.0",
72
+ "ts-jest": "^29.2.5",
73
+ "tsup": "^8.3.5",
74
+ "typescript": "^5.6.3"
75
+ },
76
+ "peerDependencies": {
77
+ "exsorted": "^1.1.0",
78
+ "react": "^18.2.0 || ^19.0.0"
79
+ },
80
+ "peerDependenciesMeta": {
81
+ "exsorted": {
82
+ "optional": true
83
+ }
84
+ },
85
+ "engines": {
86
+ "node": ">=18"
87
+ }
88
+ }