@himanshu-sorathiya/react-kit 1.0.32 → 1.0.34

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/dist/state.d.ts CHANGED
@@ -1,22 +1,261 @@
1
1
  // Generated by dts-bundle-generator v9.5.1
2
2
 
3
+ export type Primitive = string | number | boolean | bigint | symbol | null | undefined;
4
+ export type PathImpl<K extends string | number, V> = V extends Primitive ? `${K}` : V extends readonly unknown[] ? `${K}` | `${K}.${number}` | (V[number] extends Primitive ? never : `${K}.${number}.${Path<V[number]>}`) : `${K}` | `${K}.${Path<V>}`;
5
+ export type Path<T> = T extends object ? {
6
+ [K in keyof T & (string | number)]: PathImpl<K, T[K]>;
7
+ }[keyof T & (string | number)] : never;
8
+ export type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? PathValue<T[K], Rest> : unknown : P extends keyof T ? T[P] : unknown;
9
+ /** The id type `useExpansion` operates on by default - a raw `string` or `number`. */
10
+ export type ExpansionId = string | number;
11
+ /**
12
+ * Options for {@link useExpansion}, in either of its two modes - id-only
13
+ * (`T` defaults to `TId`) or object mode (`T` is a distinct item shape,
14
+ * `TId` its resolved id type).
15
+ *
16
+ * @remarks
17
+ * The conditional shape is what makes `field` required in object mode and
18
+ * disallowed in id-only mode, entirely at the type level - see
19
+ * {@link useExpansion}'s remarks for why.
20
+ *
21
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
22
+ * @typeParam TId - The id type.
23
+ */
24
+ export type UseExpansionOptions<T = ExpansionId, TId extends ExpansionId = ExpansionId> = T extends TId ? {
25
+ /** The full list of expandable items - backs `expandedItems`/`collapsedItems`, and is the default target for `expandAll` when it's called with no argument. */
26
+ items?: readonly T[];
27
+ /** Ids expanded from the start - frozen at mount, see {@link useExpansion}'s remarks. */
28
+ initialExpandedIds?: readonly TId[];
29
+ /** Whether more than one item can be expanded at once. `false` (the default) gives accordion-style behavior, where expanding one item collapses whatever was previously expanded. */
30
+ multiple?: boolean;
31
+ /**
32
+ * In single (`multiple: false`) mode, whether the one expanded item
33
+ * can be collapsed back down to nothing.
34
+ *
35
+ * @remarks
36
+ * Ignored when `multiple` is `true` - "can everything be collapsed"
37
+ * isn't a meaningful constraint once more than one item can be open
38
+ * at a time. Mirrors Radix UI Accordion's `collapsible` prop, though
39
+ * the default here is the opposite of Radix's: `true`, matching what
40
+ * this hook already did before `collapsible` existed, so leaving it
41
+ * unset doesn't change any existing behavior.
42
+ *
43
+ * @defaultValue `true`
44
+ */
45
+ collapsible?: boolean;
46
+ } : {
47
+ /** The full list of expandable items - backs `expandedItems`/`collapsedItems`, and is the default target for `expandAll` when it's called with no argument. */
48
+ items: readonly T[];
49
+ /** A dot-path into `T`, used to resolve an item's id whenever a target is given as an item rather than a raw id. */
50
+ field: Path<T> | (string & {});
51
+ /** Ids expanded from the start - frozen at mount, see {@link useExpansion}'s remarks. */
52
+ initialExpandedIds?: readonly TId[];
53
+ /** Whether more than one item can be expanded at once. `false` (the default) gives accordion-style behavior, where expanding one item collapses whatever was previously expanded. */
54
+ multiple?: boolean;
55
+ /**
56
+ * In single (`multiple: false`) mode, whether the one expanded item
57
+ * can be collapsed back down to nothing.
58
+ *
59
+ * @remarks
60
+ * Ignored when `multiple` is `true` - "can everything be collapsed"
61
+ * isn't a meaningful constraint once more than one item can be open
62
+ * at a time. Mirrors Radix UI Accordion's `collapsible` prop, though
63
+ * the default here is the opposite of Radix's: `true`, matching what
64
+ * this hook already did before `collapsible` existed, so leaving it
65
+ * unset doesn't change any existing behavior.
66
+ *
67
+ * @defaultValue `true`
68
+ */
69
+ collapsible?: boolean;
70
+ };
71
+ /**
72
+ * Return value of {@link useExpansion}.
73
+ *
74
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
75
+ * @typeParam TId - The id type.
76
+ */
77
+ export interface UseExpansionReturn<T = ExpansionId, TId extends ExpansionId = ExpansionId> {
78
+ /** The currently expanded ids. */
79
+ expandedIds: readonly TId[];
80
+ /** The subset of `items` that are currently expanded. */
81
+ expandedItems: readonly T[];
82
+ /** The subset of `items` that are not currently expanded. */
83
+ collapsedItems: readonly T[];
84
+ /** `expandedIds.length`. */
85
+ expandedCount: number;
86
+ /** Whether anything at all is expanded. */
87
+ hasExpanded: boolean;
88
+ /** Whether `itemOrId` is currently expanded. */
89
+ isExpanded: (itemOrId: TId | T) => boolean;
90
+ /**
91
+ * Expands `itemOrId`.
92
+ * @remarks No-ops if it's already expanded. In single mode, also collapses whatever was previously expanded.
93
+ */
94
+ expand: (itemOrId: TId | T) => void;
95
+ /**
96
+ * Collapses `itemOrId`.
97
+ * @remarks No-ops if it isn't currently expanded, or (in single mode) if `collapsible` is `false`.
98
+ */
99
+ collapse: (itemOrId: TId | T) => void;
100
+ /**
101
+ * Expands `itemOrId` if it isn't expanded, collapses it if it is.
102
+ * @remarks In single mode, expanding also collapses whatever was previously expanded; collapsing is blocked when `collapsible` is `false`, same as {@link UseExpansionReturn.collapse}.
103
+ */
104
+ toggleExpansion: (itemOrId: TId | T) => void;
105
+ /**
106
+ * Expands every id/item given, or every item in `items` if called with
107
+ * no argument. Replaces the current expanded set rather than adding to
108
+ * it.
109
+ * @remarks No-ops (with a dev warning) when `multiple` is `false` - expanding "all" is meaningless in a mode that only ever allows one item open.
110
+ */
111
+ expandAll: (itemsArray?: readonly TId[] | readonly T[]) => void;
112
+ /** Collapses every id/item given, or collapses everything if called with no argument. Unlike {@link UseExpansionReturn.expandAll}, not gated by `multiple` - collapsing to nothing is always meaningful. */
113
+ collapseAll: (itemsArray?: readonly TId[] | readonly T[]) => void;
114
+ /** Restores the expanded set to the value `initialExpandedIds` had at mount - see {@link useExpansion}'s remarks. Re-truncated against the *current* `multiple` value, not whatever it was at mount. */
115
+ resetExpansion: () => void;
116
+ /** Replaces the entire expanded set with exactly these ids/items, truncated to just the first if `multiple` is `false` and more than one is given. */
117
+ replaceExpansion: (newExpandedItems: readonly TId[] | readonly T[]) => void;
118
+ }
119
+ /**
120
+ * Manages which item(s) in a list are expanded - accordions, expandable
121
+ * table rows, collapsible sections.
122
+ *
123
+ * @remarks
124
+ * - Uncontrolled only for now - controlled mode is planned separately and
125
+ * will be added without breaking this signature.
126
+ * - SSR-safe: performs no DOM/window access; `initialExpandedIds` must be
127
+ * deterministic between server and client renders to avoid hydration
128
+ * mismatches.
129
+ * - All returned callbacks are manually memoized with `useCallback` so this
130
+ * hook is safe to use even in codebases **without** the React Compiler.
131
+ * - Two modes, picked by whether `T` is assignable to `TId`: id-only
132
+ * (default) - `itemOrId` parameters only ever receive raw ids, `field`
133
+ * is disallowed; or object mode - `<Row, string>` plus a required
134
+ * `field`, letting `itemOrId` parameters take a full item too. See
135
+ * {@link UseExpansionOptions}.
136
+ * - The single/multiple invariant (at most one expanded id when `multiple`
137
+ * is `false`) is enforced at every entry point that can introduce more
138
+ * than one id at once - the initial mount, `resetExpansion`, and
139
+ * `replaceExpansion` - not just `expand`/`toggleExpansion`, which only
140
+ * ever add one id at a time by construction.
141
+ * - `collapsible` (single mode only) specifically gates the *interactive*
142
+ * collapse path - `collapse`/`toggleExpansion` closing the one expanded
143
+ * item. It does not gate `collapseAll` or `replaceExpansion([])`: those
144
+ * are explicit, deliberate "set state directly" calls, treated the same
145
+ * as `clearPins`/`replacePins` in `usePin` not respecting `maxPins`
146
+ * either - a soft interactive constraint doesn't override an explicit
147
+ * caller instruction.
148
+ *
149
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
150
+ * @typeParam TId - The id type.
151
+ * @param options - See {@link UseExpansionOptions}.
152
+ * @returns The current expansion state and the actions to change it. See {@link UseExpansionReturn}.
153
+ *
154
+ * @example
155
+ * Accordion (single, non-collapsible - always exactly one open):
156
+ * ```tsx
157
+ * const { isExpanded, toggleExpansion } = useExpansion({
158
+ * multiple: false,
159
+ * collapsible: false,
160
+ * initialExpandedIds: ["section-1"],
161
+ * });
162
+ * ```
163
+ *
164
+ * @example
165
+ * Object mode, multiple expandable rows:
166
+ * ```tsx
167
+ * interface Row { id: string; label: string }
168
+ * const { expandedItems, expand, expandAll } = useExpansion<Row, string>({
169
+ * items: rows,
170
+ * field: "id",
171
+ * multiple: true,
172
+ * });
173
+ * ```
174
+ */
175
+ export declare function useExpansion<T = ExpansionId, TId extends ExpansionId = ExpansionId>(options?: UseExpansionOptions<T, TId>): UseExpansionReturn<T, TId>;
176
+ /**
177
+ * `Omit<T, K>`, applied per union member instead of to the flattened union
178
+ * as a whole - plain `Omit` over a union loses the correlation between
179
+ * `type`/`operator` and that arm's own `value` shape, which is exactly the
180
+ * information {@link FilterConfigUpdate} needs to preserve.
181
+ */
182
+ export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
183
+ /** `Partial<T>`, applied per union member - same distributive reasoning as {@link DistributiveOmit}. */
184
+ export type DistributivePartial<T> = T extends unknown ? Partial<T> : never;
185
+ /** The comparison strategy for a filter - selects which operator set (`text`, `number`, etc.) applies, or `custom` for a caller-supplied predicate. */
3
186
  export type FilterType = "text" | "number" | "boolean" | "date" | "select" | "multiselect" | "custom";
187
+ /** Operators available on a `text`-type filter. */
4
188
  export type TextOperator = "contains" | "equals" | "startsWith" | "endsWith" | "notContains";
189
+ /** Operators available on a `number`-type filter. */
5
190
  export type NumberOperator = "equals" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual" | "between";
6
- export type BooleanOperator = "equals";
191
+ /** Operators available on a `boolean`-type filter. */
192
+ export type BooleanOperator = "equals" | "notEquals";
193
+ /** Operators available on a `date`-type filter. */
7
194
  export type DateOperator = "equals" | "before" | "after" | "between";
8
- export type SelectOperator = "equals" | "notEquals";
195
+ /** Operators available on a `select`-type filter (single value against a scalar field). */
196
+ export type SelectOperator = "equals" | "notEquals" | "in" | "notIn";
197
+ /** Operators available on a `multiselect`-type filter (against an array-valued field). */
9
198
  export type MultiselectOperator = "in" | "notIn" | "intersects";
199
+ /** The sole operator for a `custom`-type filter - present for structural symmetry with the other operator unions, not because there's a real choice to make. */
10
200
  export type CustomOperator = "custom";
201
+ /** Options shared across filter types, though not every option is meaningful for every type. */
11
202
  export interface FilterOptions<T = unknown> {
203
+ /**
204
+ * Case sensitivity for `text` comparisons. Ignored by every other
205
+ * filter type - `select` deliberately has no equivalent, since select
206
+ * values are enumerated tokens (a fixed options list, an HTML
207
+ * `<select>`), not free-typed input that could differ only by case.
208
+ * @defaultValue `false`
209
+ */
12
210
  caseSensitive?: boolean;
211
+ /**
212
+ * Comparison granularity for `date` filters.
213
+ * `"day"` ignores time-of-day (calendar-date comparison); `"instant"`
214
+ * compares exact milliseconds. Ignored by every other filter type.
215
+ * @defaultValue `"day"`
216
+ */
217
+ dateGranularity?: "day" | "instant";
218
+ /** Required for `type: "custom"` - the predicate deciding whether an item matches. Ignored by every other filter type. */
13
219
  compare?: (itemValue: unknown, filterValue: unknown, item: T) => boolean;
14
220
  }
221
+ /** Options for {@link useFilter}. */
222
+ export interface UseFilterOptions {
223
+ /**
224
+ * Defers the filtering recomputation (via `useDeferredValue`) so
225
+ * changing `filters` doesn't block a more urgent update (e.g. the
226
+ * keystroke that triggered the change). Only `filters` is deferred -
227
+ * `data` is not.
228
+ * @defaultValue `false`
229
+ */
230
+ defer?: boolean;
231
+ }
232
+ /** Fields common to every {@link FilterConfig}, regardless of `type`/`operator`. */
15
233
  export type BaseFilterConfig<T> = FilterOptions<T> & {
234
+ /** Unique identifier for this filter - used for lookups (`getFilter`, `isFilterActive`, etc.) and to distinguish filters in the `filters` array. */
16
235
  id: string;
17
- field: string;
236
+ /** Dot-separated path into each item, e.g. `"address.city"`. Falls back to `id` when omitted - so `id` alone is often enough if it already matches the field name. */
237
+ field?: string;
238
+ /**
239
+ * Whether this filter currently participates in filtering. An inactive
240
+ * filter stays in the `filters` array (so its configuration - operator,
241
+ * value, etc. - is preserved) but is skipped during evaluation.
242
+ * @defaultValue `true`
243
+ */
18
244
  isActive?: boolean;
19
245
  };
246
+ /**
247
+ * A single filter's full configuration.
248
+ *
249
+ * @remarks
250
+ * Each `type` has its own dedicated arm(s) - `number`/`date`/`select` each
251
+ * split further by operator (a `between` arm with a `{min,max}` value,
252
+ * separate from every other operator's scalar/array value) - rather than
253
+ * grouping multiple type literals under one shared arm. This is what lets
254
+ * TypeScript actually narrow `value`'s type based on `type`+`operator`,
255
+ * and what makes {@link FilterOptionsForType}-style per-type extraction
256
+ * (and IDE autocomplete while constructing a filter) work correctly -
257
+ * grouping literals under one arm silently breaks both.
258
+ */
20
259
  export type FilterConfig<T = unknown> = BaseFilterConfig<T> & ({
21
260
  type: "text";
22
261
  operator: TextOperator;
@@ -49,8 +288,12 @@ export type FilterConfig<T = unknown> = BaseFilterConfig<T> & ({
49
288
  value: string | number | Date;
50
289
  } | {
51
290
  type: "select";
52
- operator: SelectOperator;
291
+ operator: Exclude<SelectOperator, "in" | "notIn">;
53
292
  value: string | number;
293
+ } | {
294
+ type: "select";
295
+ operator: "in" | "notIn";
296
+ value: (string | number)[];
54
297
  } | {
55
298
  type: "multiselect";
56
299
  operator: MultiselectOperator;
@@ -60,175 +303,1598 @@ export type FilterConfig<T = unknown> = BaseFilterConfig<T> & ({
60
303
  operator: CustomOperator;
61
304
  value: unknown;
62
305
  });
306
+ /** All configured filters. Order has no effect on filtering (every active filter is AND-combined), only on iteration order of `filters`/`groupedRecord`-style outputs elsewhere in this library. */
63
307
  export type FilterState<T> = FilterConfig<T>[];
308
+ /**
309
+ * The update payload type for `updateFilterConfig`.
310
+ *
311
+ * @remarks
312
+ * Best-effort compile-time guidance, not a full guarantee: a *merge* of an
313
+ * update into an existing filter can still land on a structurally invalid
314
+ * `type`/`operator`/`value` combination that the type system alone can't
315
+ * catch (partial updates flatten across a union in ways whole-object
316
+ * construction doesn't). `isValueShapeValid`, run on the merged result
317
+ * inside `applyFilterUpdate`, is the actual runtime backstop.
318
+ */
319
+ export type FilterConfigUpdate<T> = DistributivePartial<DistributiveOmit<FilterConfig<T>, "id">>;
320
+ /** Return value of {@link useFilter}. */
64
321
  export interface UseFilterReturn<T> {
322
+ /** `data`, filtered by every active entry in `filters` (AND-combined). Same reference as `data` (no copy, no filter) when there are no active filters. */
65
323
  filteredItems: T[];
324
+ /** Every configured filter, active or not. */
66
325
  filters: FilterState<T>;
326
+ /** Count of filters with `isActive !== false`. */
327
+ activeFilterCount: number;
328
+ /** `activeFilterCount > 0`. */
329
+ hasActiveFilters: boolean;
330
+ /** Adds a new filter, or replaces the existing one with the same `id`. */
67
331
  upsertFilter: (filter: FilterConfig<T>) => void;
332
+ /** Removes one filter by `id`, or several at once by passing an array of ids. */
68
333
  removeFilter: (id: string | string[]) => void;
334
+ /** Clears every filter entirely - equivalent to `replaceFilters([])`. */
69
335
  clearFilters: () => void;
336
+ /** Restores `filters` to the value passed as `initialFilters` at mount. Later changes to that argument have no effect - the reset target is frozen at mount. */
70
337
  resetFilters: () => void;
338
+ /** Replaces the entire `filters` array at once. */
71
339
  replaceFilters: (filters: FilterConfig<T>[]) => void;
340
+ /** Flips a filter's `isActive` (defaulting to `true` if the filter doesn't exist yet, in which case it's added). Configuration (operator, value, etc.) is preserved either way. */
72
341
  toggleFilter: (filter: FilterConfig<T>) => void;
73
- updateFilterConfig: (id: string, partialConfig: Partial<Omit<FilterConfig<T>, "id">>) => void;
342
+ /**
343
+ * Partially updates an existing filter's config by `id`.
344
+ * @remarks An unknown `id`, or an update that would produce a value
345
+ * incompatible with the filter's `type`/`operator`, is rejected - see
346
+ * {@link isValueShapeValid}.
347
+ */
348
+ updateFilterConfig: (id: string, partialConfig: FilterConfigUpdate<T>) => void;
349
+ /** Looks up a filter's full config by `id`. `undefined` if no such filter exists. */
350
+ getFilter: (id: string) => FilterConfig<T> | undefined;
351
+ /** Just a filter's `value`, by `id`. `undefined` if no such filter exists. */
74
352
  getFilterValue: (id: string) => unknown;
353
+ /** Sugar for `updateFilterConfig(id, { value })` - updates only a filter's value, leaving its type/operator/other options untouched. */
75
354
  updateFilterValue: (id: string, value: unknown) => void;
355
+ /** Whether a filter (by `id`) currently participates in filtering. `false` for a nonexistent `id`, same as an explicitly inactive one. */
76
356
  isFilterActive: (id: string) => boolean;
77
357
  }
78
- export declare function useFilter<T>(data?: T[], initialFilters?: FilterState<T>): UseFilterReturn<T>;
79
- export interface FuzzySearchOptions {
80
- threshold?: number;
81
- caseSensitive?: boolean;
82
- matchStrategy?: "any" | "all";
83
- exactPhraseBonus?: boolean;
84
- }
85
- export interface IndexedToken {
86
- text: string;
87
- weight: number;
88
- }
89
- export interface FlatIndexedItem<T> {
90
- item: T;
91
- index: number;
92
- tokens: IndexedToken[];
93
- combinedFlatText: string;
94
- }
95
- export interface ScoredItem<T> {
96
- item: T;
97
- score: number;
98
- index: number;
99
- }
100
- export type UseFuzzySearchFields<T> = {
101
- field: Extract<keyof T, string>;
102
- weight: number;
103
- }[];
104
- export type UseFuzzySearchReturn<T> = T[];
105
- export declare function useFuzzySearch<T>(data: T[], query: string, fields: UseFuzzySearchFields<T>, options?: FuzzySearchOptions): UseFuzzySearchReturn<T>;
358
+ /**
359
+ * Filters an array against one or more configured conditions - text
360
+ * matching, numeric/date comparisons and ranges, boolean/select/multiselect
361
+ * matching, or a fully custom predicate.
362
+ *
363
+ * @remarks
364
+ * - **Every active filter is AND-combined** - an item must satisfy all of
365
+ * them to be included. There's no built-in OR-across-filters; use a
366
+ * `custom` filter for that if needed.
367
+ * - **Missing/incomparable data always excludes the item**, uniformly
368
+ * across every built-in operator - including the "negative" ones like
369
+ * `notContains`/`notEquals`/`notIn`. A row with no value for the field is
370
+ * treated as "unknown," never as a confident non-match. See
371
+ * `FILTER_STRATEGIES`'s file-level `@remarks` for the full reasoning.
372
+ * - **Validation is dev/prod-split**, same convention as this library's
373
+ * other hooks: an unknown `type`/`operator` combination, or a `custom`
374
+ * filter missing its `compare` function, throws immediately in
375
+ * development, but is silently excluded (that filter matches nothing) in
376
+ * production.
377
+ * - **`field` falls back to `id`** when omitted - so a filter whose `id`
378
+ * already matches the data's field name doesn't need `field` set
379
+ * separately.
380
+ * - **Id lookups are O(1)**, backed by a `Map` built once per `filters`
381
+ * change, not a linear scan per call.
382
+ *
383
+ * @typeParam T - The type of each item in `data`.
384
+ * @param data - The items to filter. Defaults to `[]`.
385
+ * @param initialFilters - Filters applied at mount. Defaults to `[]` (no filtering).
386
+ * @param options - See {@link UseFilterOptions}.
387
+ * @returns The filtered items and the current filter state, plus the
388
+ * actions to change it. See {@link UseFilterReturn}.
389
+ *
390
+ * @example
391
+ * ```tsx
392
+ * const { filteredItems, upsertFilter } = useFilter(products, [
393
+ * { id: "category", type: "select", operator: "equals", value: "Books" },
394
+ * ]);
395
+ *
396
+ * upsertFilter({
397
+ * id: "price",
398
+ * type: "number",
399
+ * operator: "between",
400
+ * value: { min: 10, max: 50 },
401
+ * });
402
+ * ```
403
+ */
404
+ export declare function useFilter<T>(data?: T[], initialFilters?: FilterState<T>, options?: UseFilterOptions): UseFilterReturn<T>;
405
+ /** Bucket granularity for a `date`-type group level - see {@link GroupByDateLevel.bucket}. */
406
+ export type DateBucketGranularity = "day" | "month" | "year";
407
+ /**
408
+ * Groups by a field's raw value.
409
+ * @remarks If the resolved value is an array (e.g. a `tags` field), the
410
+ * item fans out into a separate group per element instead of being
411
+ * String()-coerced into one combined key - see {@link useGrouping}'s
412
+ * `@remarks` for details.
413
+ */
414
+ export type GroupByFieldLevel = {
415
+ type: "field";
416
+ /** Dot-separated path into each item, e.g. `"address.city"`. */
417
+ field: string;
418
+ };
419
+ /** Groups by a `Date`-valued field, bucketed to day/month/year rather than exact timestamp - avoids one group per unique millisecond. */
420
+ export type GroupByDateLevel = {
421
+ type: "date";
422
+ /** Dot-separated path into each item. */
423
+ field: string;
424
+ /**
425
+ * Bucket granularity.
426
+ * @defaultValue `"day"`
427
+ */
428
+ bucket?: DateBucketGranularity;
429
+ };
430
+ /** Groups by a caller-supplied key function - for keys that can't be expressed as a plain field path (computed buckets, merging otherwise-distinct values into one group, etc.). */
431
+ export type GroupByCustomLevel<T> = {
432
+ type: "custom";
433
+ /** Optional; used only to identify this level in dev warnings. */
434
+ id?: string;
435
+ /**
436
+ * Computes this item's group key(s).
437
+ * @remarks Return an array to fan the item out into multiple groups at
438
+ * this level, mirroring array-valued {@link GroupByFieldLevel}s.
439
+ * `null`/`undefined` places the item in the Unknown bucket. Throwing,
440
+ * or returning anything other than `string | string[] | null | undefined`,
441
+ * is treated as level misconfiguration - see {@link useGrouping}'s
442
+ * `@remarks`.
443
+ */
444
+ getKey: (item: T) => string | string[] | null | undefined;
445
+ };
446
+ /** A single grouping level - a plain string is shorthand for `{ type: "field", field: theString }`. */
447
+ export type GroupByLevel<T> = string | GroupByFieldLevel | GroupByDateLevel | GroupByCustomLevel<T>;
448
+ /** Same union as {@link GroupByLevel} with the string shorthand already expanded - what {@link useGrouping} stores internally and returns via `activeGroupBy`. */
449
+ export type NormalizedGroupByLevel<T> = GroupByFieldLevel | GroupByDateLevel | GroupByCustomLevel<T>;
450
+ /** A single group, as produced by {@link useGrouping}'s `groupedArray`. */
106
451
  export interface Group<T> {
452
+ /** Stable identifier for this group - e.g. the field's stringified value, or a date bucket like `"2026-08"`. Unique among sibling groups at the same level. */
107
453
  key: string;
454
+ /** Human-readable display text. Usually equal to `key`, except for date buckets (e.g. key `"2026-08"`, label `"August 2026"`). */
455
+ label: string;
456
+ /** ALL items under this group, flattened across any deeper levels. */
108
457
  items: T[];
458
+ /** Present only when there's another grouping level below this one. */
459
+ subGroups?: Group<T>[];
460
+ }
461
+ /** Options for {@link useGrouping}. */
462
+ export interface UseGroupingOptions {
463
+ /**
464
+ * Defers the grouping recomputation (via `useDeferredValue`) so changing
465
+ * `activeGroupBy` doesn't block a more urgent update, e.g. the UI
466
+ * interaction that triggered the change. Only `activeGroupBy` is
467
+ * deferred - `items` is not.
468
+ *
469
+ * @defaultValue `false`
470
+ */
471
+ defer?: boolean;
472
+ /**
473
+ * Display label for the synthetic bucket holding items whose value at a
474
+ * given level is missing, blank, or otherwise unresolvable.
475
+ *
476
+ * @defaultValue `"Unknown"`
477
+ */
478
+ unknownGroupLabel?: string;
479
+ /**
480
+ * Display label for the single synthetic group returned when
481
+ * `activeGroupBy` is empty (no grouping applied).
482
+ *
483
+ * @defaultValue `"Ungrouped"`
484
+ */
485
+ ungroupedGroupLabel?: string;
109
486
  }
487
+ /** Return value of {@link useGrouping}. */
110
488
  export interface UseGroupingReturn<T> {
111
- groupedRecord: Record<string, T[]>;
489
+ /** All groups, hierarchically - top-level groups from `activeGroupBy[0]`, each optionally holding `subGroups` from deeper levels. Render this for anything beyond a single flat level. */
112
490
  groupedArray: Group<T>[];
491
+ /** Top-level groups only, flattened -- Record<key, items>. For deeper
492
+ * levels, traverse a group's `subGroups` directly. */
493
+ groupedRecord: Record<string, T[]>;
494
+ /** Top-level group keys only, in the same order as `groupedArray`. */
113
495
  groupKeys: string[];
496
+ /** Count of top-level groups only - `groupKeys.length`. */
114
497
  totalGroups: number;
115
- activeGroupBy: string | undefined;
116
- changeGroupBy: (newField: string) => void;
498
+ /** The currently-applied grouping levels, normalized (string shorthand already expanded). Empty when no grouping is applied. */
499
+ activeGroupBy: NormalizedGroupByLevel<T>[];
500
+ /** `activeGroupBy.length > 0`. */
501
+ hasActiveGrouping: boolean;
502
+ /** The actual label in use for the "missing/unresolvable value" bucket - reflects `options.unknownGroupLabel` if customized, otherwise the default. Compare a group's `key` against this rather than hardcoding `"Unknown"`. */
503
+ unknownGroupKey: string;
504
+ /** The actual label in use for the "no grouping applied" bucket - reflects `options.ungroupedGroupLabel` if customized. Compare a group's `key` against this rather than hardcoding `"Ungrouped"`. */
505
+ ungroupedGroupKey: string;
506
+ /**
507
+ * Replaces `activeGroupBy` with one level or an array of levels (for
508
+ * hierarchical grouping - order determines nesting, `newGroupBy[0]`
509
+ * becomes the top level).
510
+ * @remarks A malformed level (missing `field`, non-function `getKey`,
511
+ * etc.) is rejected - see {@link useGrouping}'s `@remarks`.
512
+ */
513
+ changeGroupBy: (newGroupBy: GroupByLevel<T> | GroupByLevel<T>[]) => void;
514
+ /** Clears `activeGroupBy` entirely - equivalent to `changeGroupBy([])`. */
117
515
  clearGrouping: () => void;
516
+ /** Restores `activeGroupBy` to the value passed as `initialGroupBy` at mount. Later changes to that argument have no effect - the reset target is frozen at mount. */
118
517
  resetGrouping: () => void;
518
+ /** Looks up a single top-level group by its `key`. `undefined` if no such group exists.
519
+ * @remarks Top-level lookup only, matching `groupedRecord`'s scope - use `groupedArray`/`subGroups` directly for a nested group. */
520
+ getGroup: (groupKey: string) => Group<T> | undefined;
521
+ /** `getGroup(groupKey)?.items ?? []` - the items in a top-level group, or an empty array if it doesn't exist. */
119
522
  getGroupItems: (groupKey: string) => T[];
120
523
  }
121
- export declare function useGrouping<T = unknown>(options?: {
122
- items?: T[];
123
- initialGroupBy?: string;
124
- }): UseGroupingReturn<T>;
524
+ /**
525
+ * Partitions an array into groups by one or more field values, date
526
+ * buckets, or a custom key function - single-level or hierarchical
527
+ * (subgroups within groups).
528
+ *
529
+ * @remarks
530
+ * - **Fan-out, not partitioning.** If a level's resolved value for an item
531
+ * is an array (e.g. a `tags` field, or a `custom` level's `getKey`
532
+ * returning multiple keys), the item is placed in *every* matching group
533
+ * at that level, not just one. Summed item counts across sibling groups
534
+ * can therefore exceed the original array length - this reflects genuine
535
+ * multi-group membership, not a bug.
536
+ * - **Two distinct synthetic buckets**, both customizable via
537
+ * {@link UseGroupingOptions}: `unknownGroupLabel` (default `"Unknown"`)
538
+ * for items whose value at a level is missing, blank, or otherwise
539
+ * unresolvable; `ungroupedGroupLabel` (default `"Ungrouped"`) for the
540
+ * single group returned when no grouping is applied at all. The actual
541
+ * labels in use are returned as `unknownGroupKey`/`ungroupedGroupKey` -
542
+ * compare against those rather than hardcoding the default strings, in
543
+ * case they've been customized.
544
+ * - **Validation is dev/prod-split**, same convention as this library's
545
+ * other hooks: a malformed level (missing/empty `field`, a non-function
546
+ * `getKey`, an invalid `bucket`) throws immediately in development, but
547
+ * is rejected silently (falling back to the previous/empty grouping) in
548
+ * production. A `custom` level's `getKey` throwing, or returning
549
+ * something other than `string | string[] | null | undefined`, follows
550
+ * the same split - see {@link GroupByCustomLevel.getKey}.
551
+ * - **`groupedRecord`/`groupKeys`/`totalGroups`/`getGroup`/`getGroupItems`
552
+ * are all top-level only** - for anything below the first grouping
553
+ * level, traverse a `Group`'s `subGroups` directly via `groupedArray`.
554
+ * - Order of top-level (and each nested level's) groups follows first
555
+ * appearance in `items`, not any particular sort - re-sort `groupedArray`
556
+ * yourself if a specific order is needed.
557
+ *
558
+ * @typeParam T - The type of each item in `data`.
559
+ * @param data - The items to group. Defaults to `[]`.
560
+ * @param initialGroupBy - Grouping level(s) applied at mount. Omit for no
561
+ * initial grouping.
562
+ * @param options - See {@link UseGroupingOptions}.
563
+ * @returns The current grouping and the actions to change it. See
564
+ * {@link UseGroupingReturn}.
565
+ *
566
+ * @example
567
+ * Single-level, by a plain field:
568
+ * ```tsx
569
+ * const { groupedArray } = useGrouping(users, "department");
570
+ * // groupedArray: [{ key: "Engineering", items: [...] }, { key: "Sales", items: [...] }, ...]
571
+ * ```
572
+ *
573
+ * @example
574
+ * Hierarchical, by department then a date bucket:
575
+ * ```tsx
576
+ * const { groupedArray, changeGroupBy } = useGrouping(orders);
577
+ *
578
+ * changeGroupBy([
579
+ * "region",
580
+ * { type: "date", field: "placedAt", bucket: "month" },
581
+ * ]);
582
+ * // groupedArray: [{ key: "EMEA", items: [...], subGroups: [{ key: "2026-08", ... }] }, ...]
583
+ * ```
584
+ */
585
+ export declare function useGrouping<T = unknown>(data?: T[], initialGroupBy?: GroupByLevel<T> | GroupByLevel<T>[], options?: UseGroupingOptions): UseGroupingReturn<T>;
125
586
  export type SelectionId = string | number;
126
- export interface UseMultipleSelectionReturn<T> {
127
- selectedIds: SelectionId[];
587
+ /**
588
+ * All dot-notation paths of `T` whose resolved value is assignable to `TId`.
589
+ *
590
+ * @remarks
591
+ * This is what makes `field` type-safe: a path pointing at a boolean, an
592
+ * object, or a non-existent property simply isn't a member of this type, so
593
+ * passing it is a compile-time error rather than a silent `undefined` at
594
+ * runtime. Built on top of the `Path`/`PathValue` machinery already exported
595
+ * by `getValue`'s module.
596
+ *
597
+ * @template T The item shape.
598
+ * @template TId The id type the resolved value must be assignable to.
599
+ */
600
+ export type SelectionIdPath<T, TId extends SelectionId> = {
601
+ [P in Path<T> & string]: PathValue<T, P> extends TId ? P : never;
602
+ }[Path<T> & string];
603
+ /**
604
+ * The options every {@link useMultipleSelection} call accepts regardless of
605
+ * whether `T` is a raw id type or an object type.
606
+ *
607
+ * @typeParam T - The item shape (or `TId` itself, for id-only mode).
608
+ * @typeParam TId - The id type used internally for `Set`/comparison purposes.
609
+ */
610
+ export interface UseMultipleSelectionBaseOptions<T, TId extends SelectionId> {
611
+ /**
612
+ * The items this selection is over. Determines `selectedItems`, and is
613
+ * what `selectAll`/`toggleAll`/`invertSelection` operate against.
614
+ *
615
+ * @remarks
616
+ * Pass a stable/memoized array. An inline `items={data.filter(...)}` gets
617
+ * a new reference every render, which cascades into every memoized value
618
+ * derived from `items` recomputing every render too — this hook can't
619
+ * cheaply detect "same contents, different reference" for you.
620
+ */
621
+ items?: readonly T[];
622
+ /**
623
+ * The ids selected on mount, and what {@link UseMultipleSelectionReturn.reset}
624
+ * returns the selection to.
625
+ *
626
+ * @remarks
627
+ * Only the value present on the **first render** seeds state (standard
628
+ * `useState` initializer semantics). `reset()` always reads the **latest**
629
+ * `defaultSelectedIds` at call time, so if this changes across renders,
630
+ * `reset()` restores to the newest default, not the original mount-time one.
631
+ */
632
+ defaultSelectedIds?: readonly TId[];
633
+ /**
634
+ * Predicate that marks certain ids as non-selectable.
635
+ *
636
+ * @remarks
637
+ * Enforced on every operation that **adds** to the selection (`select`,
638
+ * `toggle`'s select-direction, `selectMultiple`, `selectAll`, `toggleAll`,
639
+ * `invertSelection`, `replaceSelection`). It never blocks **removal**
640
+ * (`deselect`, `deselectMultiple`, `retainOnly`, `toggle`'s deselect-direction,
641
+ * `deselectAll`) — if an already-selected id becomes disabled later, you can
642
+ * always deselect it, just not re-select it.
643
+ *
644
+ * @param id - The id being checked.
645
+ * @returns `true` if the id must not be added to the selection.
646
+ */
647
+ isDisabled?: (id: TId) => boolean;
648
+ }
649
+ /**
650
+ * The `field` requirement, resolved conditionally on whether `T` is already
651
+ * an id (`[T] extends [TId]`) or a full object.
652
+ *
653
+ * @remarks
654
+ * - id-only mode (`T` is assignable to `TId`, e.g. the default `T = SelectionId`):
655
+ * `field` is forbidden — there's nothing to extract a path from.
656
+ * - object mode: `field` is **required**, and restricted to
657
+ * {@link SelectionIdPath} — a path that doesn't exist on `T`, or whose
658
+ * resolved value isn't assignable to `TId`, is a compile-time error rather
659
+ * than a silent `undefined` id at runtime.
660
+ */
661
+ export type UseMultipleSelectionFieldOptions<T, TId extends SelectionId> = [
662
+ T
663
+ ] extends [
664
+ TId
665
+ ] ? {
666
+ field?: never;
667
+ } : {
668
+ field: SelectionIdPath<T, TId>;
669
+ };
670
+ /**
671
+ * Combined options for {@link useMultipleSelection}. See
672
+ * {@link UseMultipleSelectionBaseOptions} and {@link UseMultipleSelectionFieldOptions}.
673
+ *
674
+ * @typeParam T - The item shape. Defaults to `SelectionId` (id-only mode: pass
675
+ * raw ids as "items", no `field` needed).
676
+ * @typeParam TId - The id type. Defaults to `SelectionId`; narrow it (e.g. to a
677
+ * branded `UserId`) for stronger inference.
678
+ */
679
+ export type UseMultipleSelectionOptions<T = SelectionId, TId extends SelectionId = SelectionId> = UseMultipleSelectionBaseOptions<T, TId> & UseMultipleSelectionFieldOptions<T, TId>;
680
+ /**
681
+ * Return shape of {@link useMultipleSelection}.
682
+ *
683
+ * @typeParam T - The item shape.
684
+ * @typeParam TId - The id type.
685
+ */
686
+ export interface UseMultipleSelectionReturn<T = SelectionId, TId extends SelectionId = SelectionId> {
687
+ /**
688
+ * The currently selected ids.
689
+ *
690
+ * @remarks
691
+ * Ordered to match `items`' order whenever every selected id is present in
692
+ * `items`. If some selected ids aren't in the current `items` array (e.g.
693
+ * a previously-selected item that's since been filtered out, or an id
694
+ * selected directly without ever appearing in `items`), those "orphaned"
695
+ * ids are preserved and appended at the end, rather than silently dropped —
696
+ * this hook never discards selection state you didn't ask it to discard.
697
+ */
698
+ selectedIds: readonly TId[];
699
+ /** The number of currently selected ids (including any orphaned ones — see {@link UseMultipleSelectionReturn.selectedIds}). */
128
700
  selectedCount: number;
129
- selectedItems: T[];
701
+ /** The subset of `items` that are currently selected, in `items`' order. */
702
+ selectedItems: readonly T[];
703
+ /** Whether nothing at all is selected. */
130
704
  isEmpty: boolean;
131
- select: (item: SelectionId | T) => void;
132
- deselect: (item: SelectionId | T) => void;
133
- toggle: (item: SelectionId | T) => void;
134
- isSelected: (item: SelectionId | T) => boolean;
135
- resetSelection: () => void;
136
- replaceSelection: (newSelectedItems: SelectionId[] | T[]) => void;
705
+ /**
706
+ * Whether every *selectable* (non-disabled) item in `items` is currently
707
+ * selected. `false` when `items` is empty. Intended for a "select all"
708
+ * checkbox's checked state.
709
+ */
710
+ isAllSelected: boolean;
711
+ /**
712
+ * Whether some, but not all, selectable items in `items` are selected.
713
+ * Intended for a "select all" checkbox's indeterminate state.
714
+ */
715
+ isPartiallySelected: boolean;
716
+ /**
717
+ * Adds `item` to the selection.
718
+ * @remarks No-ops if `item` resolves to a disabled id.
719
+ * @param item - A raw id, or a full item (requires `field` to have been configured).
720
+ */
721
+ select: (item: TId | T) => void;
722
+ /**
723
+ * Removes `item` from the selection. Always allowed, even for disabled ids.
724
+ * @param item - A raw id, or a full item.
725
+ */
726
+ deselect: (item: TId | T) => void;
727
+ /**
728
+ * Adds `item` if not selected, removes it if selected.
729
+ * @remarks The add-direction is blocked for disabled ids; the remove-direction never is.
730
+ * @param item - A raw id, or a full item.
731
+ */
732
+ toggle: (item: TId | T) => void;
733
+ /**
734
+ * Checks whether `item` is currently selected.
735
+ * @param item - A raw id, or a full item.
736
+ * @returns `true` if currently selected.
737
+ */
738
+ isSelected: (item: TId | T) => boolean;
739
+ /** Restores the selection to the current `defaultSelectedIds` (or empty, if none was provided). */
740
+ reset: () => void;
741
+ /**
742
+ * Replaces the entire selection with exactly these ids/items.
743
+ * @remarks Disabled ids are filtered out of the replacement set.
744
+ */
745
+ replaceSelection: (newSelectedItems: readonly TId[] | readonly T[]) => void;
746
+ /** Selects every selectable (non-disabled) item in `items`. */
137
747
  selectAll: () => void;
748
+ /** Clears the entire selection, including any disabled-but-selected or orphaned ids. */
138
749
  deselectAll: () => void;
750
+ /**
751
+ * If every selectable item in `items` is currently selected, clears the
752
+ * whole selection; otherwise selects every selectable item in `items`.
753
+ *
754
+ * @remarks
755
+ * Determined by actual set membership against every selectable item, not
756
+ * a size comparison — this stays correct even if `items` contains duplicate
757
+ * resolved ids, or the current selection contains ids no longer present in
758
+ * `items` (both of which would silently misfire a naive `prev.size === items.length` check).
759
+ */
139
760
  toggleAll: () => void;
761
+ /** Selects every currently-unselected selectable item, and deselects everything else. */
140
762
  invertSelection: () => void;
141
- selectMultiple: (newItems: SelectionId[] | T[]) => void;
142
- deselectMultiple: (itemsToRemove: SelectionId[] | T[]) => void;
143
- retainOnly: (itemsToRetain: SelectionId[] | T[]) => void;
763
+ /**
764
+ * Adds multiple ids/items to the selection at once.
765
+ * @remarks Disabled ids are filtered out.
766
+ */
767
+ selectMultiple: (newItems: readonly TId[] | readonly T[]) => void;
768
+ /** Removes multiple ids/items from the selection at once. Always allowed. */
769
+ deselectMultiple: (itemsToRemove: readonly TId[] | readonly T[]) => void;
770
+ /** Keeps only the ids/items in `itemsToRetain` that are already selected (an intersection); never adds anything new. */
771
+ retainOnly: (itemsToRetain: readonly TId[] | readonly T[]) => void;
144
772
  }
145
- export declare function useMultipleSelection<T = unknown>(options?: {
146
- items?: T[];
147
- field?: string;
148
- initialSelectedIds?: SelectionId[];
149
- }): UseMultipleSelectionReturn<T>;
150
- export interface UseOrderReturn<T> {
151
- orderedItems: T[];
152
- moveUp: (index: number) => void;
153
- moveDown: (index: number) => void;
154
- canMoveUp: (index: number) => boolean;
155
- canMoveDown: (index: number) => boolean;
156
- moveToTop: (index: number) => void;
157
- moveToBottom: (index: number) => void;
158
- move: (fromIndex: number, toIndex: number) => void;
159
- swap: (indexA: number, indexB: number) => void;
773
+ /**
774
+ * Manages multi-item selection state (e.g. checkboxes in a table, a
775
+ * multi-select list, bulk-action UI).
776
+ *
777
+ * @remarks
778
+ * - Uncontrolled only for now — controlled mode is planned separately and
779
+ * will be added without breaking this signature.
780
+ * - SSR-safe: no DOM/window access; `defaultSelectedIds` must be deterministic
781
+ * between server and client renders to avoid hydration mismatches.
782
+ * - All returned callbacks are manually memoized with `useCallback`/`useMemo`
783
+ * so this hook is safe to use even in codebases **without** the React Compiler.
784
+ * - Works in two modes, picked by whether `T` is assignable to `TId`:
785
+ * - **id-only mode** (default): `useMultipleSelection()` "items" are raw
786
+ * ids, no `field` needed.
787
+ * - **object mode**: `useMultipleSelection<Item, string>({ items, field: "id" })`
788
+ * `field` is a type-checked dot-path into `Item` and is required.
789
+ *
790
+ * @typeParam T - The item shape (or `TId` itself, for id-only mode).
791
+ * @typeParam TId - The id type. Defaults to `SelectionId`; narrow it for branded-id inference.
792
+ * @param options - See {@link UseMultipleSelectionOptions}.
793
+ * @returns The current selection state and the actions to mutate it. See {@link UseMultipleSelectionReturn}.
794
+ *
795
+ * @example
796
+ * ```tsx
797
+ * // id-only mode
798
+ * const { selectedIds, toggle } = useMultipleSelection({ defaultSelectedIds: ["a"] });
799
+ * ```
800
+ *
801
+ * @example
802
+ * ```tsx
803
+ * // object mode, with a nested field path and disabled rows
804
+ * interface Row { id: string; locked: boolean }
805
+ * const { selectedItems, toggleAll, isAllSelected, isPartiallySelected } = useMultipleSelection<Row, string>({
806
+ * items: rows,
807
+ * field: "id",
808
+ * isDisabled: (id) => rows.find((r) => r.id === id)?.locked ?? false,
809
+ * });
810
+ * ```
811
+ */
812
+ export declare function useMultipleSelection<T = SelectionId, TId extends SelectionId = SelectionId>(options?: UseMultipleSelectionOptions<T, TId>): UseMultipleSelectionReturn<T, TId>;
813
+ /**
814
+ * The shape of every `target` parameter across {@link UseOrderReturn}'s
815
+ * methods - a plain array index when no `field` is configured, or either an
816
+ * index *or* a full item once one is (letting callers resolve an item's
817
+ * current position for themselves instead of tracking indices by hand).
818
+ *
819
+ * @remarks
820
+ * Deliberately gated behind `WithField` rather than always allowing `T`:
821
+ * `useOrder`'s base (no-`field`) overload can be called with `T = number`
822
+ * (ordering a plain array of numbers), where allowing an item argument too
823
+ * would make a bare `number` genuinely ambiguous - "index `5`" or "the item
824
+ * `5`"? Restricting item-based targeting to the `field`-configured overload
825
+ * (see {@link UseOrderFieldOptions}) sidesteps that ambiguity entirely,
826
+ * since `field` only makes sense for object items in the first place.
827
+ *
828
+ * @typeParam T - The item shape.
829
+ * @typeParam WithField - Whether `field` was configured - see {@link UseOrderFieldOptions}.
830
+ */
831
+ export type OrderTarget<T, WithField extends boolean> = WithField extends true ? number | T : number;
832
+ /**
833
+ * Options shared by both {@link useOrder} overloads, with or without `field`
834
+ * configured.
835
+ *
836
+ * @typeParam T - The item shape.
837
+ */
838
+ export interface UseOrderBaseOptions<T> {
839
+ /**
840
+ * Marks an item as un-movable and un-displaceable - see
841
+ * {@link UseOrderReturn} for exactly which operations this blocks, and
842
+ * how.
843
+ *
844
+ * @param item - The item to check.
845
+ * @param index - That item's current index.
846
+ * @returns `true` if `item` should be locked in place.
847
+ */
848
+ isDisabled?: (item: T, index: number) => boolean;
849
+ }
850
+ /**
851
+ * Options for the `field`-configured overload of {@link useOrder}, which
852
+ * additionally accepts full items (not just indices) as move targets.
853
+ *
854
+ * @typeParam T - The item shape.
855
+ */
856
+ export interface UseOrderFieldOptions<T> extends UseOrderBaseOptions<T> {
857
+ /**
858
+ * A dot-path into `T`, used to resolve an item's id whenever a target is
859
+ * given as an item rather than a raw index - see {@link OrderTarget}.
860
+ */
861
+ field: Path<T> | (string & {});
862
+ }
863
+ /**
864
+ * Return value of {@link useOrder}.
865
+ *
866
+ * @typeParam T - The item shape.
867
+ * @typeParam WithField - Whether `field` was configured - see {@link OrderTarget}.
868
+ */
869
+ export interface UseOrderReturn<T, WithField extends boolean = false> {
870
+ /** The items in their current order. */
871
+ orderedItems: readonly T[];
872
+ /**
873
+ * Moves the item at `target` one position earlier (toward index `0`).
874
+ * @remarks No-ops at the start of the list, or if `target`'s item or the item before it is disabled.
875
+ */
876
+ movePrevious: (target: OrderTarget<T, WithField>) => void;
877
+ /**
878
+ * Moves the item at `target` one position later (toward the end).
879
+ * @remarks No-ops at the end of the list, or if `target`'s item or the item after it is disabled.
880
+ */
881
+ moveNext: (target: OrderTarget<T, WithField>) => void;
882
+ /** Whether {@link UseOrderReturn.movePrevious} would currently have an effect for `target`. */
883
+ canMovePrevious: (target: OrderTarget<T, WithField>) => boolean;
884
+ /** Whether {@link UseOrderReturn.moveNext} would currently have an effect for `target`. */
885
+ canMoveNext: (target: OrderTarget<T, WithField>) => boolean;
886
+ /**
887
+ * Moves the item at `target` to the very start of the list.
888
+ * @remarks No-ops if it's already first, or if it's disabled. Items it moves past are shifted by one slot but not themselves checked - see {@link useOrder}'s remarks.
889
+ */
890
+ moveToStart: (target: OrderTarget<T, WithField>) => void;
891
+ /**
892
+ * Moves the item at `target` to the very end of the list.
893
+ * @remarks No-ops if it's already last, or if it's disabled. Items it moves past are shifted by one slot but not themselves checked - see {@link useOrder}'s remarks.
894
+ */
895
+ moveToEnd: (target: OrderTarget<T, WithField>) => void;
896
+ /**
897
+ * Moves the item at `from` to the position at `to`, shifting everything
898
+ * in between by one slot.
899
+ * @remarks No-ops if `from` and `to` resolve to the same index, or if the item at `from` is disabled. Items shifted in between are not themselves checked - see {@link useOrder}'s remarks.
900
+ */
901
+ move: (from: OrderTarget<T, WithField>, to: OrderTarget<T, WithField>) => void;
902
+ /**
903
+ * Exchanges the positions of the items at `a` and `b`.
904
+ * @remarks No-ops if `a` and `b` resolve to the same index, or if either item is disabled.
905
+ */
906
+ swap: (a: OrderTarget<T, WithField>, b: OrderTarget<T, WithField>) => void;
907
+ /** Restores the order to the value `initialItems` had at mount - see {@link useOrder}'s remarks. */
160
908
  resetOrder: () => void;
161
- replaceOrder: (newOrderedItems: T[]) => void;
909
+ /** Replaces the entire order with exactly these items. */
910
+ replaceOrder: (newOrderedItems: readonly T[]) => void;
162
911
  }
163
- export declare function useOrder<T>(initialItems?: T[]): UseOrderReturn<T>;
912
+ /**
913
+ * Manages the order of a list of items - drag-to-reorder UIs, "move
914
+ * up"/"move down" controls, manual sort overrides layered on top of a base
915
+ * sort.
916
+ *
917
+ * @remarks
918
+ * - Uncontrolled only for now - controlled mode is planned separately and
919
+ * will be added without breaking this signature.
920
+ * - SSR-safe: performs no DOM/window access; `initialItems` must be
921
+ * deterministic between server and client renders to avoid hydration
922
+ * mismatches.
923
+ * - All returned callbacks are manually memoized with `useCallback` so this
924
+ * hook is safe to use even in codebases **without** the React Compiler.
925
+ * - Two overloads, picked by whether `field` is configured: without it,
926
+ * every method's `target` parameter is a plain `number` index; with it,
927
+ * `target` also accepts a full item, resolved to its current index via
928
+ * `field`. See {@link OrderTarget} for why this is gated behind `field`
929
+ * rather than always allowed.
930
+ * - `isDisabled` blocks differently depending on the operation. For the
931
+ * pairwise operations (`movePrevious`, `moveNext`, `swap`), *both* items
932
+ * involved must be movable, since both change position. For the
933
+ * repositioning operations (`moveToStart`, `moveToEnd`, `move`), only the
934
+ * item being moved is checked - items it displaces shift by one slot but
935
+ * are never asked to swap places, so their own `isDisabled` state isn't
936
+ * consulted for the move to proceed.
937
+ * - `resetOrder` restores the value `initialItems` had at mount, not
938
+ * whatever it is on the current render - a later change to the
939
+ * `initialItems` prop doesn't retroactively change what `resetOrder`
940
+ * restores to.
941
+ *
942
+ * @typeParam T - The item shape.
943
+ * @param initialItems - The items to manage, in their starting order.
944
+ * @param options - See {@link UseOrderBaseOptions} / {@link UseOrderFieldOptions}.
945
+ * @returns The current order and the actions to change it. See {@link UseOrderReturn}.
946
+ *
947
+ * @example
948
+ * Index-only:
949
+ * ```tsx
950
+ * const { orderedItems, movePrevious, moveNext } = useOrder(["a", "b", "c"]);
951
+ * ```
952
+ *
953
+ * @example
954
+ * With `field`, so items themselves can be passed as move targets:
955
+ * ```tsx
956
+ * interface Row { id: string; label: string }
957
+ * const { orderedItems, movePrevious } = useOrder<Row>(rows, { field: "id" });
958
+ * // movePrevious(rows[2]) works directly - no manual index lookup needed
959
+ * ```
960
+ */
961
+ export declare function useOrder<T extends Record<string, unknown>>(initialItems: readonly T[] | undefined, options: UseOrderFieldOptions<T>): UseOrderReturn<T, true>;
962
+ export declare function useOrder<T>(initialItems?: readonly T[], options?: UseOrderBaseOptions<T>): UseOrderReturn<T, false>;
963
+ /** Return value of {@link usePagination}. */
164
964
  export interface UsePaginationReturn<T> {
965
+ /** The items for the current page - a slice of `data`, `pageSize` items long (fewer on the last page if `totalCount` doesn't divide evenly). */
165
966
  pageItems: T[];
967
+ /**
968
+ * Current page, 1-based.
969
+ * @remarks Always clamped into `[1, totalPages]` for display, even if
970
+ * the underlying position becomes momentarily out of range (e.g. `data`
971
+ * shrinks). The clamp is display-only - see the `currentPageIndex` note
972
+ * in the implementation for why the real position isn't lost.
973
+ */
974
+ pageNumber: number;
975
+ /** Items per page. */
166
976
  pageSize: number;
167
- pageIndex: number;
977
+ /** `Math.max(1, Math.ceil(totalCount / pageSize))` - always at least `1`, even for an empty `data`. */
168
978
  totalPages: number;
169
- canPrevious: boolean;
170
- canNext: boolean;
979
+ /** `data.length` (after the array-safety check) - the un-paginated item count. */
980
+ totalCount: number;
981
+ /** 1-based index of the first item on the current page, for a "Showing X-Y of Z" display. `0` when `totalCount` is `0`. */
982
+ startIndex: number;
983
+ /** 1-based index of the last item on the current page. `0` when `totalCount` is `0`. */
984
+ endIndex: number;
985
+ /** Whether {@link UsePaginationReturn.previousPage} would move anywhere. */
986
+ canPreviousPage: boolean;
987
+ /** Whether {@link UsePaginationReturn.nextPage} would move anywhere. */
988
+ canNextPage: boolean;
989
+ /** Moves to the next page, if any. No-op (not a warning) on the last page - this is routine UI usage, not caller error. */
171
990
  nextPage: () => void;
991
+ /** Moves to the previous page, if any. No-op on the first page. */
172
992
  previousPage: () => void;
993
+ /** Jumps to page `1`. */
173
994
  goToFirstPage: () => void;
995
+ /** Jumps to the last page. */
174
996
  goToLastPage: () => void;
175
- goToPage: (newPageIndex: number) => void;
997
+ /**
998
+ * Jumps to a specific page.
999
+ * @remarks A well-formed but out-of-range page (e.g. `999` on a
1000
+ * 5-page list) is clamped to the nearest valid page, not rejected -
1001
+ * only a malformed value (non-integer, `< 1`, `NaN`, etc.) is treated
1002
+ * as caller error. See {@link isPositiveInteger}.
1003
+ */
1004
+ goToPage: (newPageNumber: number) => void;
1005
+ /**
1006
+ * Changes `pageSize`.
1007
+ * @remarks `pageNumber` is left untouched here - it re-bounds itself
1008
+ * automatically against the new `totalPages` on the next render.
1009
+ */
176
1010
  changePageSize: (newPageSize: number) => void;
177
- resetPageIndex: () => void;
1011
+ /** Restores `pageNumber` to the value passed as `initialPageNumber` at mount. Later changes to that argument have no effect - the reset target is frozen at mount, same as `pageSize`/full pagination reset below. */
1012
+ resetPageNumber: () => void;
1013
+ /** Restores `pageSize` to the value passed as `initialPageSize` at mount. */
178
1014
  resetPageSize: () => void;
1015
+ /** Restores both `pageNumber` and `pageSize` to their mount-time initial values, in one update. */
179
1016
  resetPagination: () => void;
180
1017
  }
181
- export declare function usePagination<T>(data: T[] | undefined, initialPageSize: number, initialPageIndex?: number): UsePaginationReturn<T>;
182
- export interface UseSingleSelectionReturn {
183
- selectedId: SelectionId | undefined;
1018
+ /**
1019
+ * Paginates an array client-side - slices `data` into pages and exposes the
1020
+ * navigation state and actions to move between them.
1021
+ *
1022
+ * @remarks
1023
+ * - **Validation is dev/prod-split**: passing a malformed page number or
1024
+ * page size (non-integer, `< 1`, `NaN`, etc.) to the constructor or any
1025
+ * mutator throws immediately in development (surfacing the bug fast), but
1026
+ * silently falls back to the last valid value in production, rather than
1027
+ * crashing a real user's session over a caller mistake. See
1028
+ * {@link isPositiveInteger}.
1029
+ * - **Out-of-range is different from malformed.** `goToPage(999)` on a
1030
+ * 5-page list isn't an error - it's clamped to the last page. Only
1031
+ * structurally invalid input (see above) is treated as a mistake.
1032
+ * - **`pageNumber` is 1-based** in this public API; page count/index math
1033
+ * is kept 0-based internally.
1034
+ * - **Reset targets are frozen at mount.** `resetPageNumber`/`resetPageSize`/
1035
+ * `resetPagination` always restore the `initialPageNumber`/`initialPageSize`
1036
+ * values as they were on the very first render - passing different values
1037
+ * to `usePagination` on a later render does not change what reset
1038
+ * restores to.
1039
+ * - **No manual/server-side pagination mode** - `data` is always assumed to
1040
+ * be the complete, un-paginated dataset, sliced client-side.
1041
+ *
1042
+ * @typeParam T - The type of each item in `data`.
1043
+ * @param data - The full, un-paginated array. Defaults to `[]`.
1044
+ * @param initialPageSize - Items per page at mount.
1045
+ * @defaultValue initialPageSize `10`
1046
+ * @param initialPageNumber - Starting page (1-based) at mount.
1047
+ * @defaultValue initialPageNumber `1`
1048
+ * @returns The current page's items, position, and the actions to
1049
+ * navigate/resize/reset. See {@link UsePaginationReturn}.
1050
+ *
1051
+ * @example
1052
+ * ```tsx
1053
+ * const { pageItems, pageNumber, totalPages, nextPage, previousPage } =
1054
+ * usePagination(rows, 20);
1055
+ *
1056
+ * return (
1057
+ * <>
1058
+ * {pageItems.map((row) => <Row key={row.id} {...row} />)}
1059
+ * <button onClick={previousPage}>Prev</button>
1060
+ * <span>{pageNumber} / {totalPages}</span>
1061
+ * <button onClick={nextPage}>Next</button>
1062
+ * </>
1063
+ * );
1064
+ * ```
1065
+ */
1066
+ export declare function usePagination<T>(data?: T[], initialPageSize?: number, initialPageNumber?: number): UsePaginationReturn<T>;
1067
+ /** The id type `usePin` operates on by default - a raw `string` or `number`. */
1068
+ export type PinId = string | number;
1069
+ /**
1070
+ * Options for {@link usePin}, in either of its two modes - id-only (`T`
1071
+ * defaults to `TId`) or object mode (`T` is a distinct item shape, `TId`
1072
+ * its resolved id type).
1073
+ *
1074
+ * @remarks
1075
+ * The conditional shape is what makes `field` required in object mode and
1076
+ * disallowed in id-only mode, entirely at the type level - see
1077
+ * {@link usePin}'s remarks for why.
1078
+ *
1079
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
1080
+ * @typeParam TId - The id type.
1081
+ */
1082
+ export type UsePinOptions<T = PinId, TId extends PinId = PinId> = T extends TId ? {
1083
+ /** The full list of pinnable items - backs `pinnedItems`/`unpinnedItems`, and is the default target for `pinAll` when it's called with no argument. */
1084
+ items?: readonly T[];
1085
+ /** Ids pinned from the start - frozen at mount, see {@link usePin}'s remarks. */
1086
+ initialPinnedIds?: readonly TId[];
1087
+ /** The maximum number of ids that can be pinned at once. `undefined` means unlimited. */
1088
+ maxPins?: number;
1089
+ } : {
1090
+ /** The full list of pinnable items - backs `pinnedItems`/`unpinnedItems`, and is the default target for `pinAll` when it's called with no argument. */
1091
+ items: readonly T[];
1092
+ /** A dot-path into `T`, used to resolve an item's id whenever a target is given as an item rather than a raw id. */
1093
+ field: Path<T> | (string & {});
1094
+ /** Ids pinned from the start - frozen at mount, see {@link usePin}'s remarks. */
1095
+ initialPinnedIds?: readonly TId[];
1096
+ /** The maximum number of ids that can be pinned at once. `undefined` means unlimited. */
1097
+ maxPins?: number;
1098
+ };
1099
+ /**
1100
+ * Return value of {@link usePin}.
1101
+ *
1102
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
1103
+ * @typeParam TId - The id type.
1104
+ */
1105
+ export interface UsePinReturn<T = PinId, TId extends PinId = PinId> {
1106
+ /** The currently pinned ids. */
1107
+ pinnedIds: readonly TId[];
1108
+ /** The subset of `items` that are currently pinned. */
1109
+ pinnedItems: readonly T[];
1110
+ /** The subset of `items` that are not currently pinned. */
1111
+ unpinnedItems: readonly T[];
1112
+ /** `pinnedIds.length`. */
1113
+ pinnedCount: number;
1114
+ /** Whether anything at all is pinned. */
1115
+ hasPins: boolean;
1116
+ /** Whether `pinnedCount` has reached `maxPins`. */
1117
+ isAtMaxLimit: boolean;
1118
+ /** The current pin limit - `Number.MAX_SAFE_INTEGER` when unset/unlimited. */
1119
+ maxPins: number;
1120
+ /**
1121
+ * Pins `itemOrId`.
1122
+ * @remarks No-ops if it's already pinned, or if `maxPins` has been reached.
1123
+ */
1124
+ pin: (itemOrId: TId | T) => void;
1125
+ /** Unpins `itemOrId`. Always allowed, even past `maxPins` - removal never needs to check the limit. */
1126
+ unpin: (itemOrId: TId | T) => void;
1127
+ /**
1128
+ * Pins `itemOrId` if it isn't pinned, unpins it if it is.
1129
+ * @remarks The pin-direction is blocked once `maxPins` is reached; the unpin-direction never is.
1130
+ */
1131
+ togglePin: (itemOrId: TId | T) => void;
1132
+ /** Whether `itemOrId` is currently pinned. */
1133
+ isPinned: (itemOrId: TId | T) => boolean;
1134
+ /** Whether {@link UsePinReturn.pin} would currently have an effect for `itemOrId` - `true` if it's already pinned (a no-op call still "succeeds") or there's room under `maxPins`. */
1135
+ canPin: (itemOrId: TId | T) => boolean;
1136
+ /** Unpins everything. */
1137
+ clearPins: () => void;
1138
+ /** Restores the pinned set to the value `initialPinnedIds` had at mount - see {@link usePin}'s remarks. */
1139
+ resetPins: () => void;
1140
+ /** Replaces the entire pinned set with exactly these ids/items, truncated to the first `maxPins` if it exceeds the current limit. */
1141
+ replacePins: (newPinnedItems: readonly TId[] | readonly T[]) => void;
1142
+ /**
1143
+ * Pins every id/item given, or every item in `items` if called with no
1144
+ * argument.
1145
+ * @remarks If `maxPins` is reached partway through, whatever already fit stays pinned rather than the whole call being rejected.
1146
+ */
1147
+ pinAll: (itemsArray?: readonly TId[] | readonly T[]) => void;
1148
+ /** Unpins every id/item given, or unpins everything if called with no argument. */
1149
+ unpinAll: (itemsArray?: readonly TId[] | readonly T[]) => void;
1150
+ /** Changes `maxPins` to a new value. Does not retroactively unpin anything already over the new limit. */
1151
+ changeMaxPins: (newMaxPins: number) => void;
1152
+ /** Restores `maxPins` to the value it had at mount. */
1153
+ resetMaxPins: () => void;
1154
+ }
1155
+ /**
1156
+ * Manages a pinned/starred subset of a list - pinned rows in a table,
1157
+ * favorited items, "keep at top" behavior.
1158
+ *
1159
+ * @remarks
1160
+ * - Uncontrolled only for now - controlled mode is planned separately and
1161
+ * will be added without breaking this signature.
1162
+ * - SSR-safe: performs no DOM/window access; `initialPinnedIds` must be
1163
+ * deterministic between server and client renders to avoid hydration
1164
+ * mismatches.
1165
+ * - All returned callbacks are manually memoized with `useCallback` so this
1166
+ * hook is safe to use even in codebases **without** the React Compiler.
1167
+ * - Two modes, picked by whether `T` is assignable to `TId`: id-only
1168
+ * (default) - `itemOrId` parameters only ever receive raw ids, `field`
1169
+ * is disallowed; or object mode - `<Row, string>` plus a required
1170
+ * `field`, letting `itemOrId` parameters take a full item too. See
1171
+ * {@link UsePinOptions}.
1172
+ * - `maxPins` truncation always keeps the *first* ids/items and warns in
1173
+ * dev about the rest - this applies at mount (`initialPinnedIds`),
1174
+ * `replacePins`, and `pinAll`.
1175
+ * - `resetPins` and `resetMaxPins` are independent - resetting one doesn't
1176
+ * touch the other, and each restores exactly the value its own option
1177
+ * had at mount, not whatever it is on the current render.
1178
+ *
1179
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
1180
+ * @typeParam TId - The id type.
1181
+ * @param options - See {@link UsePinOptions}.
1182
+ * @returns The current pinned state and the actions to change it. See {@link UsePinReturn}.
1183
+ *
1184
+ * @example
1185
+ * Id-only:
1186
+ * ```tsx
1187
+ * const { pinnedIds, pin, isPinned } = usePin({ initialPinnedIds: ["row-1"] });
1188
+ * ```
1189
+ *
1190
+ * @example
1191
+ * Object mode, with a pin limit:
1192
+ * ```tsx
1193
+ * interface Row { id: string; label: string }
1194
+ * const { pinnedItems, pin, canPin } = usePin<Row, string>({
1195
+ * items: rows,
1196
+ * field: "id",
1197
+ * maxPins: 5,
1198
+ * });
1199
+ * ```
1200
+ */
1201
+ export declare function usePin<T = PinId, TId extends PinId = PinId>(options?: UsePinOptions<T, TId>): UsePinReturn<T, TId>;
1202
+ /**
1203
+ * Configuration options for {@link useSingleSelection}.
1204
+ *
1205
+ * @typeParam TId - The concrete id type used by this selection instance.
1206
+ * Must be assignable to {@link SelectionId} (`string | number`), but can be
1207
+ * a narrower/branded type (e.g. `UserId`) for stronger inference at call sites.
1208
+ */
1209
+ export interface UseSingleSelectionOptions<TId extends SelectionId = SelectionId> {
1210
+ /**
1211
+ * The id that is selected on mount, and the id that {@link UseSingleSelectionReturn.reset}
1212
+ * returns the selection to.
1213
+ *
1214
+ * @remarks
1215
+ * Only the value present on the **first render** is used to seed state
1216
+ * (standard `useState` initializer semantics — later changes to this value
1217
+ * do not retroactively change the current selection). However, `reset()`
1218
+ * always reads the **latest** `defaultSelectedId` at the time it's called,
1219
+ * so if this value changes across renders, `reset()` will restore to the
1220
+ * newest default, not the original mount-time one.
1221
+ */
1222
+ defaultSelectedId?: TId;
1223
+ /**
1224
+ * Predicate that marks certain ids as non-selectable.
1225
+ *
1226
+ * @remarks
1227
+ * This guard is only enforced inside {@link UseSingleSelectionReturn.select}
1228
+ * and {@link UseSingleSelectionReturn.toggle}. It does **not** retroactively
1229
+ * clear a selection if an already-selected id later becomes disabled — the
1230
+ * hook has no way to know `isDisabled`'s result changed unless you call
1231
+ * `select`/`toggle` again. Reconcile that case yourself (e.g. via an effect)
1232
+ * if it matters for your use case.
1233
+ *
1234
+ * @param id - The id being checked before selection.
1235
+ * @returns `true` if the id must not be selectable.
1236
+ */
1237
+ isDisabled?: (id: TId) => boolean;
1238
+ }
1239
+ /**
1240
+ * Return shape of {@link useSingleSelection}.
1241
+ *
1242
+ * @typeParam TId - The concrete id type used by this selection instance.
1243
+ */
1244
+ export interface UseSingleSelectionReturn<TId extends SelectionId = SelectionId> {
1245
+ /** The currently selected id, or `undefined` if nothing is selected. */
1246
+ selectedId: TId | undefined;
1247
+ /**
1248
+ * Whether any id is currently selected.
1249
+ *
1250
+ * @remarks
1251
+ * Correctly distinguishes "nothing selected" from a falsy-but-valid id
1252
+ * such as `0` or `""` — this is `selectedId !== undefined`, not `!!selectedId`.
1253
+ */
184
1254
  hasSelection: boolean;
185
- select: (id: SelectionId) => void;
1255
+ /**
1256
+ * Selects the given id, replacing any current selection.
1257
+ *
1258
+ * @remarks No-ops if `id` is disabled per `isDisabled`.
1259
+ * @param id - The id to select.
1260
+ */
1261
+ select: (id: TId) => void;
1262
+ /** Clears the current selection (sets it to `undefined`). */
186
1263
  deselect: () => void;
187
- toggle: (id: SelectionId) => void;
188
- isSelected: (id: SelectionId) => boolean;
189
- resetSelection: () => void;
1264
+ /**
1265
+ * Selects `id` if it isn't already selected; deselects it if it is.
1266
+ *
1267
+ * @remarks No-ops if `id` is disabled per `isDisabled`.
1268
+ * @param id - The id to toggle.
1269
+ */
1270
+ toggle: (id: TId) => void;
1271
+ /**
1272
+ * Checks whether the given id is the currently selected one.
1273
+ *
1274
+ * @param id - The id to check.
1275
+ * @returns `true` if `id` is currently selected.
1276
+ */
1277
+ isSelected: (id: TId) => boolean;
1278
+ /**
1279
+ * Restores the selection to the current `defaultSelectedId`
1280
+ * (or `undefined` if none was provided).
1281
+ */
1282
+ reset: () => void;
190
1283
  }
191
- export declare function useSingleSelection(initialSelectedId?: SelectionId): UseSingleSelectionReturn;
1284
+ /**
1285
+ * Manages single-item selection state (e.g. a radio group, a single-select
1286
+ * list, an active tab/row).
1287
+ *
1288
+ * @remarks
1289
+ * - Uncontrolled only for now — controlled mode (`selectedId` + `onSelectionChange`)
1290
+ * is planned separately and will be added without breaking this signature.
1291
+ * - SSR-safe: performs no DOM/window access; `defaultSelectedId` must be
1292
+ * deterministic between server and client renders to avoid hydration mismatches.
1293
+ * - All returned callbacks are manually memoized with `useCallback` so this
1294
+ * hook is safe to use even in codebases **without** the React Compiler.
1295
+ *
1296
+ * @typeParam TId - The concrete id type used by this selection instance.
1297
+ * @param options - Optional configuration. See {@link UseSingleSelectionOptions}.
1298
+ * @returns The current selection state and the actions to mutate it. See {@link UseSingleSelectionReturn}.
1299
+ *
1300
+ * @example
1301
+ * ```tsx
1302
+ * const { selectedId, select, isSelected } = useSingleSelection<string>({
1303
+ * defaultSelectedId: "row-1",
1304
+ * });
1305
+ * ```
1306
+ */
1307
+ export declare function useSingleSelection<TId extends SelectionId = SelectionId>(options?: UseSingleSelectionOptions<TId>): UseSingleSelectionReturn<TId>;
1308
+ /** The comparison strategy for a sort key - selects which built-in comparator is used, or `custom` for a caller-supplied one. */
192
1309
  export type SortType = "numeric" | "alphabetical" | "alphanumeric" | "boolean" | "date" | "basic" | "custom";
1310
+ /**
1311
+ * How a missing value (per each sort type's own definition of "missing" -
1312
+ * e.g. non-numeric for `numeric`, unparseable for `date`) is positioned
1313
+ * relative to present values.
1314
+ *
1315
+ * @remarks
1316
+ * - `"first"`/`"last"` are **absolute positions** - they never flip with
1317
+ * sort direction, which is the entire reason to choose them over `-1`/`1`.
1318
+ * - `-1`/`1` **simulate a real extreme value** (very small / very large
1319
+ * respectively) - unlike `"first"`/`"last"`, these DO flip with
1320
+ * direction, exactly as a genuine value at that extreme would.
1321
+ *
1322
+ * @defaultValue `"last"`
1323
+ */
1324
+ export type SortUndefinedOption = "first" | "last" | -1 | 1;
1325
+ /** Options shared by every sort type, regardless of comparator. */
193
1326
  export interface BaseSortOptions {
1327
+ /**
1328
+ * Sort direction.
1329
+ * @defaultValue `false` (ascending)
1330
+ */
194
1331
  desc?: boolean;
1332
+ /**
1333
+ * When set, `toggleSort` cycles asc -> desc -> asc, skipping the
1334
+ * "remove this sort key" step it would otherwise land on after desc.
1335
+ * @defaultValue `false`
1336
+ */
195
1337
  disableSortRemoval?: boolean;
1338
+ /**
1339
+ * Flips the effective sort direction without changing `desc` itself -
1340
+ * useful for a column whose "natural" order is descending (e.g. a
1341
+ * priority field where higher should sort first by default).
1342
+ * @defaultValue `false`
1343
+ */
196
1344
  invertSorting?: boolean;
197
- sortUndefined?: "first" | "last" | -1 | 1;
1345
+ /** See {@link SortUndefinedOption}. */
1346
+ sortUndefined?: SortUndefinedOption;
198
1347
  }
1348
+ /** Fields common to every {@link SortConfig}, regardless of `type`. */
199
1349
  export interface BaseSortConfig extends BaseSortOptions {
1350
+ /** Unique identifier for this sort key - used for lookups (`getSort`, `getSortDirection`, etc.) and to distinguish sort keys in a multi-key `sorts` array. */
200
1351
  id: string;
1352
+ /** Dot-separated path into each item, e.g. `"address.city"`. Falls back to `id` when omitted - so `id` alone is often enough if it already matches the field name. */
201
1353
  field?: string;
202
1354
  }
1355
+ /**
1356
+ * A single sort key's full configuration.
1357
+ *
1358
+ * @remarks
1359
+ * Each `type` has its own dedicated union arm (rather than several type
1360
+ * literals sharing one arm) so that {@link SortOptionsForType} - which
1361
+ * extracts a single type's own options via `Extract<SortConfig, { type: T }>`
1362
+ * - resolves correctly per type. Grouping literals under one arm would
1363
+ * make that extraction silently fail for every type in the group.
1364
+ */
203
1365
  export type SortConfig = BaseSortConfig & ({
204
- type: "numeric" | "boolean" | "date" | "basic";
1366
+ type: "numeric";
1367
+ } | {
1368
+ type: "boolean";
1369
+ } | {
1370
+ type: "date";
1371
+ dateGranularity?: "day" | "instant";
205
1372
  } | {
206
- type: "alphabetical" | "alphanumeric";
1373
+ type: "basic";
1374
+ } | {
1375
+ type: "alphabetical";
1376
+ caseSensitive?: boolean;
1377
+ } | {
1378
+ type: "alphanumeric";
207
1379
  caseSensitive?: boolean;
208
1380
  } | {
209
1381
  type: "custom";
210
1382
  compare: (a: unknown, b: unknown) => number;
211
1383
  });
1384
+ /** All active sort keys, in priority order - `sorts[0]` is the primary sort, later entries only break ties left by earlier ones. */
212
1385
  export type SortState = SortConfig[];
1386
+ /**
1387
+ * A single sort type's own options, with `id`/`type`/`field` (which every
1388
+ * type shares, and which `toggleSort` already takes as separate arguments)
1389
+ * stripped out. Used to type `toggleSort`'s `options` parameter, narrowed
1390
+ * to only the options relevant to the specific `type` being toggled.
1391
+ */
213
1392
  export type SortOptionsForType<T extends SortType> = Omit<Extract<SortConfig, {
214
1393
  type: T;
215
1394
  }>, "id" | "type" | "field">;
1395
+ /**
1396
+ * The update payload type for `updateSortConfig`.
1397
+ *
1398
+ * @remarks
1399
+ * Preserves per-type shape correlation at the type level - e.g. TypeScript
1400
+ * will reject `{ dateGranularity: "day" }` paired with a `type` that isn't
1401
+ * `"date"`. This is best-effort compile-time guidance, not a full
1402
+ * guarantee: a *merge* of an update into an existing config can still land
1403
+ * on a structurally invalid combination that the type system alone can't
1404
+ * catch (partial updates flatten across a union in ways whole-object
1405
+ * construction doesn't). {@link isSortConfigShapeValid}, run on the merged
1406
+ * result inside `applySortUpdate`, is the actual runtime backstop.
1407
+ */
1408
+ export type SortConfigUpdate = DistributivePartial<DistributiveOmit<SortConfig, "id">>;
1409
+ /** Options for {@link useSort}. */
1410
+ export interface UseSortOptions {
1411
+ /**
1412
+ * Defers the sort recomputation (via `useDeferredValue`) so changing
1413
+ * `sorts` doesn't block a more urgent update. Only `sorts` is deferred -
1414
+ * `data` is not.
1415
+ * @defaultValue `false`
1416
+ */
1417
+ defer?: boolean;
1418
+ }
1419
+ /** Return value of {@link useSort}. */
216
1420
  export interface UseSortReturn<T> {
1421
+ /** `data`, sorted by every entry in `sorts`, applied in priority order. Same reference as `data` (no copy, no sort) when `sorts` is empty. */
217
1422
  sortedItems: T[];
1423
+ /** The currently-active sort keys, in priority order. */
218
1424
  sorts: SortState;
1425
+ /** `sorts.length`. */
1426
+ sortCount: number;
1427
+ /** `sortCount > 0`. */
1428
+ hasSorts: boolean;
1429
+ /** Adds a new sort key, or replaces the existing one with the same `id`. */
219
1430
  upsertSorts: (sort: SortConfig) => void;
1431
+ /** Removes one sort key by `id`, or several at once by passing an array of ids. */
220
1432
  removeSort: (id: string | string[]) => void;
1433
+ /** Clears every sort key - equivalent to `replaceSorts([])`. */
221
1434
  clearSorts: () => void;
1435
+ /** Restores `sorts` to the value passed as `initialSorts` at mount. Later changes to that argument have no effect - the reset target is frozen at mount. */
222
1436
  resetSorts: () => void;
1437
+ /** Replaces the entire `sorts` array at once. */
223
1438
  replaceSorts: (sorts: SortState) => void;
1439
+ /**
1440
+ * Cycles a sort key through asc -> desc -> (reset to asc, or remove
1441
+ * entirely) - the standard three-state table-header sort interaction.
1442
+ *
1443
+ * @remarks
1444
+ * - Whether the last step resets to asc or removes the sort key
1445
+ * entirely depends on `disableSortRemoval` (checked on both the
1446
+ * existing config and the newly-passed `options`).
1447
+ * - `options.multi` (`false` by default) controls whether toggling
1448
+ * this key adds/updates it alongside any other active sort keys
1449
+ * (`true`), or replaces the entire `sorts` array with just this one
1450
+ * key (`false`) - the usual "click a column to sort by only that
1451
+ * column, shift-click to add a secondary sort" pattern.
1452
+ * - `type` must be supplied on every call, even for an already-active
1453
+ * sort key - this hook has no independent memory of a key's type
1454
+ * beyond what's in `sorts` itself.
1455
+ */
224
1456
  toggleSort: <TType extends SortType>(id: string, type: TType, options?: SortOptionsForType<TType> & {
225
1457
  multi?: boolean;
226
1458
  field?: string;
227
1459
  }) => void;
1460
+ /**
1461
+ * Partially updates an existing sort key's config by `id`.
1462
+ * @remarks An unknown `id`, or an update that would produce a
1463
+ * structurally invalid config for its `type`, is rejected - see
1464
+ * {@link isSortConfigShapeValid}.
1465
+ */
1466
+ updateSortConfig: (id: string, partialConfig: SortConfigUpdate) => void;
1467
+ /** Looks up a sort key's full config by `id`. `undefined` if no such key exists. */
1468
+ getSort: (id: string) => SortConfig | undefined;
1469
+ /** `"asc"`/`"desc"` for an active sort key, `undefined` if `id` isn't currently sorted. */
228
1470
  getSortDirection: (id: string) => "asc" | "desc" | undefined;
1471
+ /** What `toggleSort(id, ...)` would transition `id` to next, without actually calling it - useful for rendering the right sort-direction icon before the user clicks. */
229
1472
  getNextSortingOrder: (id: string) => "asc" | "desc" | "none";
1473
+ /** This sort key's 1-based priority among active sort keys (`1` = primary), or `undefined` if `id` isn't currently sorted. */
230
1474
  getSortIndex: (id: string) => number | undefined;
231
1475
  }
232
- export declare function useSort<T>(data?: T[], initialSorts?: SortState): UseSortReturn<T>;
1476
+ /**
1477
+ * Sorts an array by one or more keys - single-column or multi-column,
1478
+ * priority determined by array order.
1479
+ *
1480
+ * @remarks
1481
+ * - **Multi-key sort**: `sorts[0]` is the primary sort; later entries in
1482
+ * the array only break ties left unresolved by earlier ones - the same
1483
+ * convention as `Array.prototype.sort` with a compound comparator, or a
1484
+ * spreadsheet's "sort by, then by" dialog.
1485
+ * - **Validation is dev/prod-split**, uniformly across every mutator that
1486
+ * can produce a structurally invalid config (`toggleSort`,
1487
+ * `updateSortConfig`), and across an unresolvable sort key encountered
1488
+ * during sorting itself (unknown `type`, or a `custom` sort missing its
1489
+ * `compare` function): throws immediately in development, but degrades
1490
+ * gracefully in production (an invalid mutation is rejected/no-op; an
1491
+ * unresolvable sort key at compute time is simply skipped, later keys
1492
+ * still apply).
1493
+ * - **`sortUndefined`'s `"first"`/`"last"` are absolute positions**, never
1494
+ * affected by `desc`/`invertSorting`; `-1`/`1` simulate a real extreme
1495
+ * value and do flip with direction - see {@link SortUndefinedOption}.
1496
+ * - **`resetSorts` is frozen at mount** - it restores `initialSorts` as it
1497
+ * was on the very first render, not whatever value that argument holds
1498
+ * on a later render.
1499
+ * - **Id lookups are O(1)**, backed by a `Map` built once per `sorts`
1500
+ * change, not a linear scan per call - safe to call `getSort`/
1501
+ * `getSortDirection`/etc. once per rendered column header without a
1502
+ * performance concern.
1503
+ *
1504
+ * @typeParam T - The type of each item in `data`.
1505
+ * @param data - The items to sort. Defaults to `[]`.
1506
+ * @param initialSorts - Sort keys applied at mount. Defaults to `[]` (no sorting).
1507
+ * @param options - See {@link UseSortOptions}.
1508
+ * @returns The sorted items and the current sort state, plus the actions
1509
+ * to change it. See {@link UseSortReturn}.
1510
+ *
1511
+ * @example
1512
+ * Single column, via a table header click handler:
1513
+ * ```tsx
1514
+ * const { sortedItems, getSortDirection, toggleSort } = useSort(rows);
1515
+ *
1516
+ * <th onClick={() => toggleSort("name", "alphabetical")}>
1517
+ * Name {getSortDirection("name") === "asc" ? "▲" : "▼"}
1518
+ * </th>
1519
+ * ```
1520
+ *
1521
+ * @example
1522
+ * Multi-key, set directly:
1523
+ * ```tsx
1524
+ * const { sortedItems, replaceSorts } = useSort(orders, [
1525
+ * { id: "status", type: "alphabetical" },
1526
+ * { id: "placedAt", type: "date", desc: true },
1527
+ * ]);
1528
+ * // sorted by status first; same-status orders sorted by placedAt, newest first
1529
+ * ```
1530
+ */
1531
+ export declare function useSort<T>(data?: T[], initialSorts?: SortState, options?: UseSortOptions): UseSortReturn<T>;
1532
+ /**
1533
+ * Restricts `field` to a **top-level** key of `T` whose value is assignable to `TId`.
1534
+ *
1535
+ * @remarks
1536
+ * Deliberately not the recursive dot-path style used by `useMultipleSelection`'s
1537
+ * `SelectionIdPath<T, TId>`. Tree nodes are self-referential (a `children` property
1538
+ * whose value is `T[]`, pointing back to `T`), and running the recursive `Path<T>`
1539
+ * machinery over a self-referential type produces a genuine TypeScript circular-reference
1540
+ * error. A tree node's id is virtually always a direct top-level property anyway — a
1541
+ * dot-path id nested inside a tree node would be an unusual shape — so this simpler,
1542
+ * non-recursive key filter is both the fix and the more correct constraint. The function
1543
+ * form below remains available for anything this doesn't cover.
1544
+ */
1545
+ export type FieldKey<T, TId extends SelectionId> = {
1546
+ [K in keyof T]: T[K] extends TId ? K : never;
1547
+ }[keyof T] & string;
1548
+ /** Restricts `childrenField` to a top-level key of `T` whose value is `T[]` (or `undefined`). */
1549
+ export type ChildrenKey<T> = {
1550
+ [K in keyof T]: T[K] extends readonly T[] | undefined ? K : never;
1551
+ }[keyof T] & string;
1552
+ /** Either a type-checked property key, or a function, for extracting a node's id. */
1553
+ export type FieldAccessor<T, TId extends SelectionId> = FieldKey<T, TId> | ((item: T) => TId);
1554
+ /** Either a type-checked property key, or a function, for extracting a node's children. */
1555
+ export type ChildrenAccessor<T> = ChildrenKey<T> | ((item: T) => readonly T[] | undefined);
1556
+ /** The three states a tree node can be in with respect to the current selection. */
1557
+ export type TreeNodeState = "selected" | "indeterminate" | "unselected";
1558
+ /** One flattened tree entry, produced by {@link flattenForest}. */
1559
+ export interface FlatTreeEntry<T, TId extends SelectionId> {
1560
+ item: T;
1561
+ id: TId;
1562
+ parentId: TId | undefined;
1563
+ depth: number;
1564
+ }
1565
+ /** The precomputed structures every tree-selection operation is built on. */
1566
+ export interface FlattenedForest<T, TId extends SelectionId> {
1567
+ /** Every node in the forest, in DFS pre-order. */
1568
+ entries: readonly FlatTreeEntry<T, TId>[];
1569
+ /** O(1) lookup from id to its flattened entry. */
1570
+ entryById: ReadonlyMap<TId, FlatTreeEntry<T, TId>>;
1571
+ /** O(1) lookup from a node's id to its direct children's ids. Roots are keyed under `undefined`. */
1572
+ childIdsByParentId: ReadonlyMap<TId | undefined, readonly TId[]>;
1573
+ }
1574
+ export interface UseTreeSelectionOptions<T, TId extends SelectionId> {
1575
+ /** The forest — an array of root nodes. Each node's children are found via `childrenField`. */
1576
+ items: readonly T[];
1577
+ /** How to extract a node's id. A type-checked top-level key, or a function. */
1578
+ field: FieldAccessor<T, TId>;
1579
+ /** How to extract a node's children. A type-checked top-level key, or a function. */
1580
+ childrenField: ChildrenAccessor<T>;
1581
+ /**
1582
+ * The ids selected on mount, and what {@link UseTreeSelectionReturn.reset} returns to.
1583
+ *
1584
+ * @remarks
1585
+ * Not trusted as already cascade-consistent — always run through the same
1586
+ * full bottom-up normalization used by `reset`/`selectAll`/etc, so passing
1587
+ * e.g. only a leaf (without its ancestors) still produces correct indeterminate
1588
+ * ancestor state from the start.
1589
+ */
1590
+ defaultSelectedIds?: readonly TId[];
1591
+ /**
1592
+ * Predicate marking certain ids as non-selectable.
1593
+ *
1594
+ * @remarks
1595
+ * A disabled node is skipped during cascade (never force-toggled) and excluded
1596
+ * from every "are all children selected" computation, but disabling a node does
1597
+ * **not** auto-disable its descendants, and does not retroactively clear it if
1598
+ * it was already selected before becoming disabled — same philosophy as the
1599
+ * flat selection hooks.
1600
+ */
1601
+ isDisabled?: (id: TId) => boolean;
1602
+ /**
1603
+ * Independently controls whether selecting a node propagates to its descendants
1604
+ * (`down`) and/or its ancestors (`up`). Both default to `true`.
1605
+ *
1606
+ * @remarks
1607
+ * `{ down: false, up: false }` degenerates into flat, non-hierarchical
1608
+ * selection over tree-rendered nodes (no relationship between a node's
1609
+ * selection and its parent/children) — the same shape react-arborist/MUI's
1610
+ * default tree multi-select uses, as opposed to checkbox-tree cascading.
1611
+ */
1612
+ cascade?: {
1613
+ down?: boolean;
1614
+ up?: boolean;
1615
+ };
1616
+ /**
1617
+ * When `true`, {@link UseTreeSelectionReturn.selectedIds} and
1618
+ * {@link UseTreeSelectionReturn.selectedItems} only include leaf nodes,
1619
+ * even if a fully-covered branch is internally tracked as selected.
1620
+ *
1621
+ * @remarks
1622
+ * This does not change `select`/`toggle`/cascade behavior at all — you can
1623
+ * still call `select` on a branch and it cascades normally, and
1624
+ * {@link UseTreeSelectionReturn.getNodeState} still reports branches correctly
1625
+ * as `"selected"`/`"indeterminate"` for display regardless of this option. It
1626
+ * only changes what the flat `selectedIds`/`selectedItems` arrays report — useful
1627
+ * when branches are just a UI grouping and the "real" selected resources are
1628
+ * always the leaves (e.g. sending a set of leaf resource ids to a backend).
1629
+ * {@link UseTreeSelectionReturn.selectedLeafIds} gives you the leaf subset
1630
+ * regardless of this option, if you want both views at once.
1631
+ */
1632
+ leafOnly?: boolean;
1633
+ }
1634
+ export interface UseTreeSelectionReturn<T, TId extends SelectionId> {
1635
+ /**
1636
+ * Every fully-selected node's id, in forest (DFS pre-order) order.
1637
+ * @remarks Includes branch ids too, unless `leafOnly` is set. See {@link UseTreeSelectionOptions.leafOnly}.
1638
+ */
1639
+ selectedIds: readonly TId[];
1640
+ /** Just the leaf ids among the current selection, regardless of `leafOnly`. */
1641
+ selectedLeafIds: readonly TId[];
1642
+ /** Every node currently in a partial (some-but-not-all-descendants-selected) state. */
1643
+ indeterminateIds: readonly TId[];
1644
+ /** The item objects corresponding to {@link UseTreeSelectionReturn.selectedIds}. */
1645
+ selectedItems: readonly T[];
1646
+ /** `selectedIds.length`. */
1647
+ selectedCount: number;
1648
+ /** Whether nothing at all is selected (not even indeterminate). */
1649
+ isEmpty: boolean;
1650
+ /** Whether every selectable root (and by construction, everything under it) is selected. */
1651
+ isAllSelected: boolean;
1652
+ /** Whether some, but not all, of the forest is selected or indeterminate. */
1653
+ isPartiallySelected: boolean;
1654
+ /**
1655
+ * The full tri-state read for a node — the primitive the rest of the boolean
1656
+ * getters below are built from.
1657
+ * @param item - A raw id, or a full item.
1658
+ */
1659
+ getNodeState: (item: TId | T) => TreeNodeState;
1660
+ /** `getNodeState(item) === "selected"`. */
1661
+ isSelected: (item: TId | T) => boolean;
1662
+ /** `getNodeState(item) === "indeterminate"`. */
1663
+ isIndeterminate: (item: TId | T) => boolean;
1664
+ /**
1665
+ * Selects `item`, cascading per the `cascade` option.
1666
+ * @remarks No-ops if `item` itself resolves to a disabled id.
1667
+ */
1668
+ select: (item: TId | T) => void;
1669
+ /** Deselects `item`, cascading per the `cascade` option. Always allowed, even for disabled ids. */
1670
+ deselect: (item: TId | T) => void;
1671
+ /**
1672
+ * Selects `item` if not selected, deselects it if selected, cascading per the `cascade` option.
1673
+ * @remarks The select-direction is blocked for disabled ids; the deselect-direction never is.
1674
+ */
1675
+ toggle: (item: TId | T) => void;
1676
+ /** Restores the selection to the current `defaultSelectedIds` (normalized), or empty if none was given. */
1677
+ reset: () => void;
1678
+ /** Replaces the entire selection with exactly these ids/items (normalized — always internally consistent afterward). */
1679
+ replaceSelection: (newSelectedItems: readonly TId[] | readonly T[]) => void;
1680
+ /** Selects every selectable node in the forest. */
1681
+ selectAll: () => void;
1682
+ /** Clears the entire selection. */
1683
+ deselectAll: () => void;
1684
+ /** If everything selectable is currently selected, clears the selection; otherwise selects everything selectable. */
1685
+ toggleAll: () => void;
1686
+ /**
1687
+ * Selects multiple ids/items at once, cascading each per the `cascade` option.
1688
+ *
1689
+ * @remarks
1690
+ * More efficient than calling {@link UseTreeSelectionReturn.select} in a loop:
1691
+ * shared ancestors of multiple targets are only recomputed once each, not once
1692
+ * per target that shares them.
1693
+ */
1694
+ selectMultiple: (newItems: readonly TId[] | readonly T[]) => void;
1695
+ /** Deselects multiple ids/items at once, cascading each per the `cascade` option. Always allowed, even for disabled ids. */
1696
+ deselectMultiple: (itemsToRemove: readonly TId[] | readonly T[]) => void;
1697
+ /**
1698
+ * Keeps only the ids/items in `itemsToRetain` that are already selected (an
1699
+ * intersection); never adds anything new. The result is re-normalized, so
1700
+ * ancestor indeterminate state stays correct after the shrink.
1701
+ */
1702
+ retainOnly: (itemsToRetain: readonly TId[] | readonly T[]) => void;
1703
+ /**
1704
+ * Inverts the selection at the leaf level: every currently-unselected
1705
+ * selectable leaf becomes selected, every currently-selected one becomes
1706
+ * unselected. Branch/indeterminate state is re-derived from the result.
1707
+ * @remarks A disabled leaf that was selected before this call does not survive
1708
+ * it — like `useMultipleSelection`'s `invertSelection`, this is a full replace,
1709
+ * not a merge.
1710
+ */
1711
+ invertSelection: () => void;
1712
+ /** The id of `item`'s direct parent, or `undefined` if it's a root. */
1713
+ getParentId: (item: TId | T) => TId | undefined;
1714
+ /** Every ancestor id of `item`, nearest first, root last. Empty if `item` is a root. */
1715
+ getAncestorIds: (item: TId | T) => readonly TId[];
1716
+ /** Every descendant id of `item`. Empty if `item` is a leaf. */
1717
+ getDescendantIds: (item: TId | T) => readonly TId[];
1718
+ }
1719
+ /**
1720
+ * Manages hierarchical (tree/forest) selection state — checkbox trees, nested
1721
+ * category pickers, permission trees, file explorers.
1722
+ *
1723
+ * @remarks
1724
+ * - Uncontrolled only for now, SSR-safe (no DOM access), all callbacks manually
1725
+ * memoized — same guarantees as the other selection hooks in this library.
1726
+ * - Expand/collapse state is explicitly **not** managed here — it's an orthogonal
1727
+ * view concern, not a selection concern (this hook only ever reads the full
1728
+ * `children` structure, regardless of what's currently expanded in the UI).
1729
+ * - **Performance**: a single `select`/`deselect`/`toggle` call is
1730
+ * O(affected subtree) for the cascade-down step plus O(depth × branching factor)
1731
+ * for the cascade-up step — it never re-walks the whole tree. Only whole-forest
1732
+ * operations (`selectAll`, `toggleAll`, `reset`, `replaceSelection`, and the
1733
+ * initial mount) do a full O(n) pass, which is the right complexity for
1734
+ * something that touches every node anyway. Verified independent of selection
1735
+ * order (a known bug class in at least one production tree-selection library
1736
+ * is indeterminate state differing based on the order nodes were selected in).
1737
+ * - If `items` (the tree **structure**) changes after mount — nodes added, removed,
1738
+ * or moved — the existing selection/indeterminate state is **not** automatically
1739
+ * re-normalized against the new shape (to avoid surprise O(n) work on every data
1740
+ * refresh). It stays correct for anything the change didn't touch, but the newly
1741
+ * changed area may need an explicit `reset()` or `replaceSelection()` to
1742
+ * guarantee full consistency again.
1743
+ *
1744
+ * @typeParam T - The tree node shape.
1745
+ * @typeParam TId - The id type. Defaults to `SelectionId`.
1746
+ * @param options - See {@link UseTreeSelectionOptions}.
1747
+ * @returns The current selection state and the actions to mutate it. See {@link UseTreeSelectionReturn}.
1748
+ *
1749
+ * @example
1750
+ * ```tsx
1751
+ * interface Category { id: string; name: string; children?: Category[] }
1752
+ *
1753
+ * const { getNodeState, toggle, isAllSelected, toggleAll } = useTreeSelection<Category, string>({
1754
+ * items: categoryTree,
1755
+ * field: "id",
1756
+ * childrenField: "children",
1757
+ * });
1758
+ * ```
1759
+ */
1760
+ export declare function useTreeSelection<T, TId extends SelectionId = SelectionId>(options: UseTreeSelectionOptions<T, TId>): UseTreeSelectionReturn<T, TId>;
1761
+ /** The id type `useVisibility` operates on by default - a raw `string` or `number`. */
1762
+ export type VisibilityId = string | number;
1763
+ /**
1764
+ * Options for {@link useVisibility}, in either of its two modes - id-only
1765
+ * (`T` defaults to `TId`) or object mode (`T` is a distinct item shape,
1766
+ * `TId` its resolved id type).
1767
+ *
1768
+ * @remarks
1769
+ * The conditional shape is what makes `field` required in object mode and
1770
+ * disallowed in id-only mode, entirely at the type level - see
1771
+ * {@link useVisibility}'s remarks for why.
1772
+ *
1773
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
1774
+ * @typeParam TId - The id type.
1775
+ */
1776
+ export type UseVisibilityOptions<T = VisibilityId, TId extends VisibilityId = VisibilityId> = T extends TId ? {
1777
+ /** The full list of items whose visibility is being tracked - backs `visibleItems`/`hiddenItems`, and is the default target for `showAll` when it's called with no argument. */
1778
+ items?: readonly T[];
1779
+ /** Ids visible from the start - frozen at mount, see {@link useVisibility}'s remarks. */
1780
+ initialVisibleIds?: readonly TId[];
1781
+ } : {
1782
+ /** The full list of items whose visibility is being tracked - backs `visibleItems`/`hiddenItems`, and is the default target for `showAll` when it's called with no argument. */
1783
+ items: readonly T[];
1784
+ /** A dot-path into `T`, used to resolve an item's id whenever a target is given as an item rather than a raw id. */
1785
+ field: Path<T> | (string & {});
1786
+ /** Ids visible from the start - frozen at mount, see {@link useVisibility}'s remarks. */
1787
+ initialVisibleIds?: readonly TId[];
1788
+ };
1789
+ /**
1790
+ * Return value of {@link useVisibility}.
1791
+ *
1792
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
1793
+ * @typeParam TId - The id type.
1794
+ */
1795
+ export interface UseVisibilityReturn<T = VisibilityId, TId extends VisibilityId = VisibilityId> {
1796
+ /** The currently visible ids. */
1797
+ visibleIds: readonly TId[];
1798
+ /** The subset of `items` that are currently visible. */
1799
+ visibleItems: readonly T[];
1800
+ /** The subset of `items` that are currently hidden. */
1801
+ hiddenItems: readonly T[];
1802
+ /** `visibleIds.length`. */
1803
+ visibleCount: number;
1804
+ /** `hiddenItems.length`. */
1805
+ hiddenCount: number;
1806
+ /** Whether anything at all is visible. */
1807
+ hasVisible: boolean;
1808
+ /** Whether `itemOrId` is currently visible. */
1809
+ isVisible: (itemOrId: TId | T) => boolean;
1810
+ /** Shows `itemOrId`. No-ops if it's already visible. */
1811
+ show: (itemOrId: TId | T) => void;
1812
+ /** Hides `itemOrId`. No-ops if it's already hidden. */
1813
+ hide: (itemOrId: TId | T) => void;
1814
+ /** Shows `itemOrId` if it's hidden, hides it if it's visible. */
1815
+ toggleVisibility: (itemOrId: TId | T) => void;
1816
+ /** Shows every id/item given, or every item in `items` if called with no argument. Adds to the current visible set rather than replacing it. */
1817
+ showAll: (itemsArray?: readonly TId[] | readonly T[]) => void;
1818
+ /**
1819
+ * Hides every id/item given, or hides everything if called with no
1820
+ * argument.
1821
+ * @remarks Unlike {@link UseVisibilityReturn.showAll}, "no argument" means a blanket clear, not "act on the hook's own `items`" - see {@link useVisibility}'s remarks.
1822
+ */
1823
+ hideAll: (itemsArray?: readonly TId[] | readonly T[]) => void;
1824
+ /** Restores the visible set to the value `initialVisibleIds` had at mount - see {@link useVisibility}'s remarks. */
1825
+ resetVisibility: () => void;
1826
+ /** Replaces the entire visible set with exactly these ids/items. */
1827
+ replaceVisibility: (newVisibleItems: readonly TId[] | readonly T[]) => void;
1828
+ }
1829
+ /**
1830
+ * Manages which item(s) in a list are visible - show/hide toggles, column
1831
+ * visibility, filterable chip lists.
1832
+ *
1833
+ * @remarks
1834
+ * - Uncontrolled only for now - controlled mode is planned separately and
1835
+ * will be added without breaking this signature.
1836
+ * - SSR-safe: performs no DOM/window access; `initialVisibleIds` must be
1837
+ * deterministic between server and client renders to avoid hydration
1838
+ * mismatches.
1839
+ * - All returned callbacks are manually memoized with `useCallback` so this
1840
+ * hook is safe to use even in codebases **without** the React Compiler.
1841
+ * - Two modes, picked by whether `T` is assignable to `TId`: id-only
1842
+ * (default) - `itemOrId` parameters only ever receive raw ids, `field`
1843
+ * is disallowed; or object mode - `<Row, string>` plus a required
1844
+ * `field`, letting `itemOrId` parameters take a full item too. See
1845
+ * {@link UseVisibilityOptions}.
1846
+ * - `showAll`/`hideAll` intentionally differ in what "no argument" means:
1847
+ * `showAll()` acts on the hook's own `items` (additive - anything
1848
+ * already visible but no longer in `items` is left alone), while
1849
+ * `hideAll()` is a blanket clear regardless of `items` (so a
1850
+ * previously-visible id that's since fallen out of `items` doesn't stay
1851
+ * visible forever). Passing an explicit array to either scopes it to
1852
+ * just that subset.
1853
+ *
1854
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
1855
+ * @typeParam TId - The id type.
1856
+ * @param options - See {@link UseVisibilityOptions}.
1857
+ * @returns The current visibility state and the actions to change it. See {@link UseVisibilityReturn}.
1858
+ *
1859
+ * @example
1860
+ * Id-only:
1861
+ * ```tsx
1862
+ * const { isVisible, toggleVisibility } = useVisibility({
1863
+ * initialVisibleIds: ["col-name", "col-email"],
1864
+ * });
1865
+ * ```
1866
+ *
1867
+ * @example
1868
+ * Object mode:
1869
+ * ```tsx
1870
+ * interface Column { id: string; label: string }
1871
+ * const { visibleItems, hide, showAll } = useVisibility<Column, string>({
1872
+ * items: columns,
1873
+ * field: "id",
1874
+ * });
1875
+ * ```
1876
+ */
1877
+ export declare function useVisibility<T = VisibilityId, TId extends VisibilityId = VisibilityId>(options?: UseVisibilityOptions<T, TId>): UseVisibilityReturn<T, TId>;
1878
+ export interface FuzzySearchOptions {
1879
+ threshold?: number;
1880
+ caseSensitive?: boolean;
1881
+ matchStrategy?: "any" | "all";
1882
+ exactPhraseBonus?: boolean;
1883
+ }
1884
+ export interface IndexedToken {
1885
+ text: string;
1886
+ weight: number;
1887
+ }
1888
+ export interface FlatIndexedItem<T> {
1889
+ item: T;
1890
+ index: number;
1891
+ tokens: IndexedToken[];
1892
+ combinedFlatText: string;
1893
+ }
1894
+ export interface ScoredItem<T> {
1895
+ item: T;
1896
+ score: number;
1897
+ index: number;
1898
+ }
233
1899
 
234
1900
  export {};