@himanshu-sorathiya/react-kit 1.0.32 → 1.0.33

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
@@ -123,30 +123,238 @@ export declare function useGrouping<T = unknown>(options?: {
123
123
  initialGroupBy?: string;
124
124
  }): UseGroupingReturn<T>;
125
125
  export type SelectionId = string | number;
126
- export interface UseMultipleSelectionReturn<T> {
127
- selectedIds: SelectionId[];
126
+ export type Primitive = string | number | boolean | bigint | symbol | null | undefined;
127
+ 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>}`;
128
+ export type Path<T> = T extends object ? {
129
+ [K in keyof T & (string | number)]: PathImpl<K, T[K]>;
130
+ }[keyof T & (string | number)] : never;
131
+ 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;
132
+ /**
133
+ * All dot-notation paths of `T` whose resolved value is assignable to `TId`.
134
+ *
135
+ * @remarks
136
+ * This is what makes `field` type-safe: a path pointing at a boolean, an
137
+ * object, or a non-existent property simply isn't a member of this type, so
138
+ * passing it is a compile-time error rather than a silent `undefined` at
139
+ * runtime. Built on top of the `Path`/`PathValue` machinery already exported
140
+ * by `getValue`'s module.
141
+ *
142
+ * @template T The item shape.
143
+ * @template TId The id type the resolved value must be assignable to.
144
+ */
145
+ export type SelectionIdPath<T, TId extends SelectionId> = {
146
+ [P in Path<T> & string]: PathValue<T, P> extends TId ? P : never;
147
+ }[Path<T> & string];
148
+ /**
149
+ * The options every {@link useMultipleSelection} call accepts regardless of
150
+ * whether `T` is a raw id type or an object type.
151
+ *
152
+ * @typeParam T - The item shape (or `TId` itself, for id-only mode).
153
+ * @typeParam TId - The id type used internally for `Set`/comparison purposes.
154
+ */
155
+ export interface UseMultipleSelectionBaseOptions<T, TId extends SelectionId> {
156
+ /**
157
+ * The items this selection is over. Determines `selectedItems`, and is
158
+ * what `selectAll`/`toggleAll`/`invertSelection` operate against.
159
+ *
160
+ * @remarks
161
+ * Pass a stable/memoized array. An inline `items={data.filter(...)}` gets
162
+ * a new reference every render, which cascades into every memoized value
163
+ * derived from `items` recomputing every render too — this hook can't
164
+ * cheaply detect "same contents, different reference" for you.
165
+ */
166
+ items?: readonly T[];
167
+ /**
168
+ * The ids selected on mount, and what {@link UseMultipleSelectionReturn.reset}
169
+ * returns the selection to.
170
+ *
171
+ * @remarks
172
+ * Only the value present on the **first render** seeds state (standard
173
+ * `useState` initializer semantics). `reset()` always reads the **latest**
174
+ * `defaultSelectedIds` at call time, so if this changes across renders,
175
+ * `reset()` restores to the newest default, not the original mount-time one.
176
+ */
177
+ defaultSelectedIds?: readonly TId[];
178
+ /**
179
+ * Predicate that marks certain ids as non-selectable.
180
+ *
181
+ * @remarks
182
+ * Enforced on every operation that **adds** to the selection (`select`,
183
+ * `toggle`'s select-direction, `selectMultiple`, `selectAll`, `toggleAll`,
184
+ * `invertSelection`, `replaceSelection`). It never blocks **removal**
185
+ * (`deselect`, `deselectMultiple`, `retainOnly`, `toggle`'s deselect-direction,
186
+ * `deselectAll`) — if an already-selected id becomes disabled later, you can
187
+ * always deselect it, just not re-select it.
188
+ *
189
+ * @param id - The id being checked.
190
+ * @returns `true` if the id must not be added to the selection.
191
+ */
192
+ isDisabled?: (id: TId) => boolean;
193
+ }
194
+ /**
195
+ * The `field` requirement, resolved conditionally on whether `T` is already
196
+ * an id (`[T] extends [TId]`) or a full object.
197
+ *
198
+ * @remarks
199
+ * - id-only mode (`T` is assignable to `TId`, e.g. the default `T = SelectionId`):
200
+ * `field` is forbidden — there's nothing to extract a path from.
201
+ * - object mode: `field` is **required**, and restricted to
202
+ * {@link SelectionIdPath} — a path that doesn't exist on `T`, or whose
203
+ * resolved value isn't assignable to `TId`, is a compile-time error rather
204
+ * than a silent `undefined` id at runtime.
205
+ */
206
+ export type UseMultipleSelectionFieldOptions<T, TId extends SelectionId> = [
207
+ T
208
+ ] extends [
209
+ TId
210
+ ] ? {
211
+ field?: never;
212
+ } : {
213
+ field: SelectionIdPath<T, TId>;
214
+ };
215
+ /**
216
+ * Combined options for {@link useMultipleSelection}. See
217
+ * {@link UseMultipleSelectionBaseOptions} and {@link UseMultipleSelectionFieldOptions}.
218
+ *
219
+ * @typeParam T - The item shape. Defaults to `SelectionId` (id-only mode: pass
220
+ * raw ids as "items", no `field` needed).
221
+ * @typeParam TId - The id type. Defaults to `SelectionId`; narrow it (e.g. to a
222
+ * branded `UserId`) for stronger inference.
223
+ */
224
+ export type UseMultipleSelectionOptions<T = SelectionId, TId extends SelectionId = SelectionId> = UseMultipleSelectionBaseOptions<T, TId> & UseMultipleSelectionFieldOptions<T, TId>;
225
+ /**
226
+ * Return shape of {@link useMultipleSelection}.
227
+ *
228
+ * @typeParam T - The item shape.
229
+ * @typeParam TId - The id type.
230
+ */
231
+ export interface UseMultipleSelectionReturn<T = SelectionId, TId extends SelectionId = SelectionId> {
232
+ /**
233
+ * The currently selected ids.
234
+ *
235
+ * @remarks
236
+ * Ordered to match `items`' order whenever every selected id is present in
237
+ * `items`. If some selected ids aren't in the current `items` array (e.g.
238
+ * a previously-selected item that's since been filtered out, or an id
239
+ * selected directly without ever appearing in `items`), those "orphaned"
240
+ * ids are preserved and appended at the end, rather than silently dropped —
241
+ * this hook never discards selection state you didn't ask it to discard.
242
+ */
243
+ selectedIds: readonly TId[];
244
+ /** The number of currently selected ids (including any orphaned ones — see {@link UseMultipleSelectionReturn.selectedIds}). */
128
245
  selectedCount: number;
129
- selectedItems: T[];
246
+ /** The subset of `items` that are currently selected, in `items`' order. */
247
+ selectedItems: readonly T[];
248
+ /** Whether nothing at all is selected. */
130
249
  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;
250
+ /**
251
+ * Whether every *selectable* (non-disabled) item in `items` is currently
252
+ * selected. `false` when `items` is empty. Intended for a "select all"
253
+ * checkbox's checked state.
254
+ */
255
+ isAllSelected: boolean;
256
+ /**
257
+ * Whether some, but not all, selectable items in `items` are selected.
258
+ * Intended for a "select all" checkbox's indeterminate state.
259
+ */
260
+ isPartiallySelected: boolean;
261
+ /**
262
+ * Adds `item` to the selection.
263
+ * @remarks No-ops if `item` resolves to a disabled id.
264
+ * @param item - A raw id, or a full item (requires `field` to have been configured).
265
+ */
266
+ select: (item: TId | T) => void;
267
+ /**
268
+ * Removes `item` from the selection. Always allowed, even for disabled ids.
269
+ * @param item - A raw id, or a full item.
270
+ */
271
+ deselect: (item: TId | T) => void;
272
+ /**
273
+ * Adds `item` if not selected, removes it if selected.
274
+ * @remarks The add-direction is blocked for disabled ids; the remove-direction never is.
275
+ * @param item - A raw id, or a full item.
276
+ */
277
+ toggle: (item: TId | T) => void;
278
+ /**
279
+ * Checks whether `item` is currently selected.
280
+ * @param item - A raw id, or a full item.
281
+ * @returns `true` if currently selected.
282
+ */
283
+ isSelected: (item: TId | T) => boolean;
284
+ /** Restores the selection to the current `defaultSelectedIds` (or empty, if none was provided). */
285
+ reset: () => void;
286
+ /**
287
+ * Replaces the entire selection with exactly these ids/items.
288
+ * @remarks Disabled ids are filtered out of the replacement set.
289
+ */
290
+ replaceSelection: (newSelectedItems: readonly TId[] | readonly T[]) => void;
291
+ /** Selects every selectable (non-disabled) item in `items`. */
137
292
  selectAll: () => void;
293
+ /** Clears the entire selection, including any disabled-but-selected or orphaned ids. */
138
294
  deselectAll: () => void;
295
+ /**
296
+ * If every selectable item in `items` is currently selected, clears the
297
+ * whole selection; otherwise selects every selectable item in `items`.
298
+ *
299
+ * @remarks
300
+ * Determined by actual set membership against every selectable item, not
301
+ * a size comparison — this stays correct even if `items` contains duplicate
302
+ * resolved ids, or the current selection contains ids no longer present in
303
+ * `items` (both of which would silently misfire a naive `prev.size === items.length` check).
304
+ */
139
305
  toggleAll: () => void;
306
+ /** Selects every currently-unselected selectable item, and deselects everything else. */
140
307
  invertSelection: () => void;
141
- selectMultiple: (newItems: SelectionId[] | T[]) => void;
142
- deselectMultiple: (itemsToRemove: SelectionId[] | T[]) => void;
143
- retainOnly: (itemsToRetain: SelectionId[] | T[]) => void;
308
+ /**
309
+ * Adds multiple ids/items to the selection at once.
310
+ * @remarks Disabled ids are filtered out.
311
+ */
312
+ selectMultiple: (newItems: readonly TId[] | readonly T[]) => void;
313
+ /** Removes multiple ids/items from the selection at once. Always allowed. */
314
+ deselectMultiple: (itemsToRemove: readonly TId[] | readonly T[]) => void;
315
+ /** Keeps only the ids/items in `itemsToRetain` that are already selected (an intersection); never adds anything new. */
316
+ retainOnly: (itemsToRetain: readonly TId[] | readonly T[]) => void;
144
317
  }
145
- export declare function useMultipleSelection<T = unknown>(options?: {
146
- items?: T[];
147
- field?: string;
148
- initialSelectedIds?: SelectionId[];
149
- }): UseMultipleSelectionReturn<T>;
318
+ /**
319
+ * Manages multi-item selection state (e.g. checkboxes in a table, a
320
+ * multi-select list, bulk-action UI).
321
+ *
322
+ * @remarks
323
+ * - Uncontrolled only for now — controlled mode is planned separately and
324
+ * will be added without breaking this signature.
325
+ * - SSR-safe: no DOM/window access; `defaultSelectedIds` must be deterministic
326
+ * between server and client renders to avoid hydration mismatches.
327
+ * - All returned callbacks are manually memoized with `useCallback`/`useMemo`
328
+ * so this hook is safe to use even in codebases **without** the React Compiler.
329
+ * - Works in two modes, picked by whether `T` is assignable to `TId`:
330
+ * - **id-only mode** (default): `useMultipleSelection()` — "items" are raw
331
+ * ids, no `field` needed.
332
+ * - **object mode**: `useMultipleSelection<Item, string>({ items, field: "id" })` —
333
+ * `field` is a type-checked dot-path into `Item` and is required.
334
+ *
335
+ * @typeParam T - The item shape (or `TId` itself, for id-only mode).
336
+ * @typeParam TId - The id type. Defaults to `SelectionId`; narrow it for branded-id inference.
337
+ * @param options - See {@link UseMultipleSelectionOptions}.
338
+ * @returns The current selection state and the actions to mutate it. See {@link UseMultipleSelectionReturn}.
339
+ *
340
+ * @example
341
+ * ```tsx
342
+ * // id-only mode
343
+ * const { selectedIds, toggle } = useMultipleSelection({ defaultSelectedIds: ["a"] });
344
+ * ```
345
+ *
346
+ * @example
347
+ * ```tsx
348
+ * // object mode, with a nested field path and disabled rows
349
+ * interface Row { id: string; locked: boolean }
350
+ * const { selectedItems, toggleAll, isAllSelected, isPartiallySelected } = useMultipleSelection<Row, string>({
351
+ * items: rows,
352
+ * field: "id",
353
+ * isDisabled: (id) => rows.find((r) => r.id === id)?.locked ?? false,
354
+ * });
355
+ * ```
356
+ */
357
+ export declare function useMultipleSelection<T = SelectionId, TId extends SelectionId = SelectionId>(options?: UseMultipleSelectionOptions<T, TId>): UseMultipleSelectionReturn<T, TId>;
150
358
  export interface UseOrderReturn<T> {
151
359
  orderedItems: T[];
152
360
  moveUp: (index: number) => void;
@@ -179,16 +387,112 @@ export interface UsePaginationReturn<T> {
179
387
  resetPagination: () => void;
180
388
  }
181
389
  export declare function usePagination<T>(data: T[] | undefined, initialPageSize: number, initialPageIndex?: number): UsePaginationReturn<T>;
182
- export interface UseSingleSelectionReturn {
183
- selectedId: SelectionId | undefined;
390
+ /**
391
+ * Configuration options for {@link useSingleSelection}.
392
+ *
393
+ * @typeParam TId - The concrete id type used by this selection instance.
394
+ * Must be assignable to {@link SelectionId} (`string | number`), but can be
395
+ * a narrower/branded type (e.g. `UserId`) for stronger inference at call sites.
396
+ */
397
+ export interface UseSingleSelectionOptions<TId extends SelectionId = SelectionId> {
398
+ /**
399
+ * The id that is selected on mount, and the id that {@link UseSingleSelectionReturn.reset}
400
+ * returns the selection to.
401
+ *
402
+ * @remarks
403
+ * Only the value present on the **first render** is used to seed state
404
+ * (standard `useState` initializer semantics — later changes to this value
405
+ * do not retroactively change the current selection). However, `reset()`
406
+ * always reads the **latest** `defaultSelectedId` at the time it's called,
407
+ * so if this value changes across renders, `reset()` will restore to the
408
+ * newest default, not the original mount-time one.
409
+ */
410
+ defaultSelectedId?: TId;
411
+ /**
412
+ * Predicate that marks certain ids as non-selectable.
413
+ *
414
+ * @remarks
415
+ * This guard is only enforced inside {@link UseSingleSelectionReturn.select}
416
+ * and {@link UseSingleSelectionReturn.toggle}. It does **not** retroactively
417
+ * clear a selection if an already-selected id later becomes disabled — the
418
+ * hook has no way to know `isDisabled`'s result changed unless you call
419
+ * `select`/`toggle` again. Reconcile that case yourself (e.g. via an effect)
420
+ * if it matters for your use case.
421
+ *
422
+ * @param id - The id being checked before selection.
423
+ * @returns `true` if the id must not be selectable.
424
+ */
425
+ isDisabled?: (id: TId) => boolean;
426
+ }
427
+ /**
428
+ * Return shape of {@link useSingleSelection}.
429
+ *
430
+ * @typeParam TId - The concrete id type used by this selection instance.
431
+ */
432
+ export interface UseSingleSelectionReturn<TId extends SelectionId = SelectionId> {
433
+ /** The currently selected id, or `undefined` if nothing is selected. */
434
+ selectedId: TId | undefined;
435
+ /**
436
+ * Whether any id is currently selected.
437
+ *
438
+ * @remarks
439
+ * Correctly distinguishes "nothing selected" from a falsy-but-valid id
440
+ * such as `0` or `""` — this is `selectedId !== undefined`, not `!!selectedId`.
441
+ */
184
442
  hasSelection: boolean;
185
- select: (id: SelectionId) => void;
443
+ /**
444
+ * Selects the given id, replacing any current selection.
445
+ *
446
+ * @remarks No-ops if `id` is disabled per `isDisabled`.
447
+ * @param id - The id to select.
448
+ */
449
+ select: (id: TId) => void;
450
+ /** Clears the current selection (sets it to `undefined`). */
186
451
  deselect: () => void;
187
- toggle: (id: SelectionId) => void;
188
- isSelected: (id: SelectionId) => boolean;
189
- resetSelection: () => void;
452
+ /**
453
+ * Selects `id` if it isn't already selected; deselects it if it is.
454
+ *
455
+ * @remarks No-ops if `id` is disabled per `isDisabled`.
456
+ * @param id - The id to toggle.
457
+ */
458
+ toggle: (id: TId) => void;
459
+ /**
460
+ * Checks whether the given id is the currently selected one.
461
+ *
462
+ * @param id - The id to check.
463
+ * @returns `true` if `id` is currently selected.
464
+ */
465
+ isSelected: (id: TId) => boolean;
466
+ /**
467
+ * Restores the selection to the current `defaultSelectedId`
468
+ * (or `undefined` if none was provided).
469
+ */
470
+ reset: () => void;
190
471
  }
191
- export declare function useSingleSelection(initialSelectedId?: SelectionId): UseSingleSelectionReturn;
472
+ /**
473
+ * Manages single-item selection state (e.g. a radio group, a single-select
474
+ * list, an active tab/row).
475
+ *
476
+ * @remarks
477
+ * - Uncontrolled only for now — controlled mode (`selectedId` + `onSelectionChange`)
478
+ * is planned separately and will be added without breaking this signature.
479
+ * - SSR-safe: performs no DOM/window access; `defaultSelectedId` must be
480
+ * deterministic between server and client renders to avoid hydration mismatches.
481
+ * - All returned callbacks are manually memoized with `useCallback` so this
482
+ * hook is safe to use even in codebases **without** the React Compiler.
483
+ *
484
+ * @typeParam TId - The concrete id type used by this selection instance.
485
+ * @param options - Optional configuration. See {@link UseSingleSelectionOptions}.
486
+ * @returns The current selection state and the actions to mutate it. See {@link UseSingleSelectionReturn}.
487
+ *
488
+ * @example
489
+ * ```tsx
490
+ * const { selectedId, select, isSelected } = useSingleSelection<string>({
491
+ * defaultSelectedId: "row-1",
492
+ * });
493
+ * ```
494
+ */
495
+ export declare function useSingleSelection<TId extends SelectionId = SelectionId>(options?: UseSingleSelectionOptions<TId>): UseSingleSelectionReturn<TId>;
192
496
  export type SortType = "numeric" | "alphabetical" | "alphanumeric" | "boolean" | "date" | "basic" | "custom";
193
497
  export interface BaseSortOptions {
194
498
  desc?: boolean;
@@ -230,5 +534,234 @@ export interface UseSortReturn<T> {
230
534
  getSortIndex: (id: string) => number | undefined;
231
535
  }
232
536
  export declare function useSort<T>(data?: T[], initialSorts?: SortState): UseSortReturn<T>;
537
+ /**
538
+ * Restricts `field` to a **top-level** key of `T` whose value is assignable to `TId`.
539
+ *
540
+ * @remarks
541
+ * Deliberately not the recursive dot-path style used by `useMultipleSelection`'s
542
+ * `SelectionIdPath<T, TId>`. Tree nodes are self-referential (a `children` property
543
+ * whose value is `T[]`, pointing back to `T`), and running the recursive `Path<T>`
544
+ * machinery over a self-referential type produces a genuine TypeScript circular-reference
545
+ * error. A tree node's id is virtually always a direct top-level property anyway — a
546
+ * dot-path id nested inside a tree node would be an unusual shape — so this simpler,
547
+ * non-recursive key filter is both the fix and the more correct constraint. The function
548
+ * form below remains available for anything this doesn't cover.
549
+ */
550
+ export type FieldKey<T, TId extends SelectionId> = {
551
+ [K in keyof T]: T[K] extends TId ? K : never;
552
+ }[keyof T] & string;
553
+ /** Restricts `childrenField` to a top-level key of `T` whose value is `T[]` (or `undefined`). */
554
+ export type ChildrenKey<T> = {
555
+ [K in keyof T]: T[K] extends readonly T[] | undefined ? K : never;
556
+ }[keyof T] & string;
557
+ /** Either a type-checked property key, or a function, for extracting a node's id. */
558
+ export type FieldAccessor<T, TId extends SelectionId> = FieldKey<T, TId> | ((item: T) => TId);
559
+ /** Either a type-checked property key, or a function, for extracting a node's children. */
560
+ export type ChildrenAccessor<T> = ChildrenKey<T> | ((item: T) => readonly T[] | undefined);
561
+ /** The three states a tree node can be in with respect to the current selection. */
562
+ export type TreeNodeState = "selected" | "indeterminate" | "unselected";
563
+ /** One flattened tree entry, produced by {@link flattenForest}. */
564
+ export interface FlatTreeEntry<T, TId extends SelectionId> {
565
+ item: T;
566
+ id: TId;
567
+ parentId: TId | undefined;
568
+ depth: number;
569
+ }
570
+ /** The precomputed structures every tree-selection operation is built on. */
571
+ export interface FlattenedForest<T, TId extends SelectionId> {
572
+ /** Every node in the forest, in DFS pre-order. */
573
+ entries: readonly FlatTreeEntry<T, TId>[];
574
+ /** O(1) lookup from id to its flattened entry. */
575
+ entryById: ReadonlyMap<TId, FlatTreeEntry<T, TId>>;
576
+ /** O(1) lookup from a node's id to its direct children's ids. Roots are keyed under `undefined`. */
577
+ childIdsByParentId: ReadonlyMap<TId | undefined, readonly TId[]>;
578
+ }
579
+ export interface UseTreeSelectionOptions<T, TId extends SelectionId> {
580
+ /** The forest — an array of root nodes. Each node's children are found via `childrenField`. */
581
+ items: readonly T[];
582
+ /** How to extract a node's id. A type-checked top-level key, or a function. */
583
+ field: FieldAccessor<T, TId>;
584
+ /** How to extract a node's children. A type-checked top-level key, or a function. */
585
+ childrenField: ChildrenAccessor<T>;
586
+ /**
587
+ * The ids selected on mount, and what {@link UseTreeSelectionReturn.reset} returns to.
588
+ *
589
+ * @remarks
590
+ * Not trusted as already cascade-consistent — always run through the same
591
+ * full bottom-up normalization used by `reset`/`selectAll`/etc, so passing
592
+ * e.g. only a leaf (without its ancestors) still produces correct indeterminate
593
+ * ancestor state from the start.
594
+ */
595
+ defaultSelectedIds?: readonly TId[];
596
+ /**
597
+ * Predicate marking certain ids as non-selectable.
598
+ *
599
+ * @remarks
600
+ * A disabled node is skipped during cascade (never force-toggled) and excluded
601
+ * from every "are all children selected" computation, but disabling a node does
602
+ * **not** auto-disable its descendants, and does not retroactively clear it if
603
+ * it was already selected before becoming disabled — same philosophy as the
604
+ * flat selection hooks.
605
+ */
606
+ isDisabled?: (id: TId) => boolean;
607
+ /**
608
+ * Independently controls whether selecting a node propagates to its descendants
609
+ * (`down`) and/or its ancestors (`up`). Both default to `true`.
610
+ *
611
+ * @remarks
612
+ * `{ down: false, up: false }` degenerates into flat, non-hierarchical
613
+ * selection over tree-rendered nodes (no relationship between a node's
614
+ * selection and its parent/children) — the same shape react-arborist/MUI's
615
+ * default tree multi-select uses, as opposed to checkbox-tree cascading.
616
+ */
617
+ cascade?: {
618
+ down?: boolean;
619
+ up?: boolean;
620
+ };
621
+ /**
622
+ * When `true`, {@link UseTreeSelectionReturn.selectedIds} and
623
+ * {@link UseTreeSelectionReturn.selectedItems} only include leaf nodes,
624
+ * even if a fully-covered branch is internally tracked as selected.
625
+ *
626
+ * @remarks
627
+ * This does not change `select`/`toggle`/cascade behavior at all — you can
628
+ * still call `select` on a branch and it cascades normally, and
629
+ * {@link UseTreeSelectionReturn.getNodeState} still reports branches correctly
630
+ * as `"selected"`/`"indeterminate"` for display regardless of this option. It
631
+ * only changes what the flat `selectedIds`/`selectedItems` arrays report — useful
632
+ * when branches are just a UI grouping and the "real" selected resources are
633
+ * always the leaves (e.g. sending a set of leaf resource ids to a backend).
634
+ * {@link UseTreeSelectionReturn.selectedLeafIds} gives you the leaf subset
635
+ * regardless of this option, if you want both views at once.
636
+ */
637
+ leafOnly?: boolean;
638
+ }
639
+ export interface UseTreeSelectionReturn<T, TId extends SelectionId> {
640
+ /**
641
+ * Every fully-selected node's id, in forest (DFS pre-order) order.
642
+ * @remarks Includes branch ids too, unless `leafOnly` is set. See {@link UseTreeSelectionOptions.leafOnly}.
643
+ */
644
+ selectedIds: readonly TId[];
645
+ /** Just the leaf ids among the current selection, regardless of `leafOnly`. */
646
+ selectedLeafIds: readonly TId[];
647
+ /** Every node currently in a partial (some-but-not-all-descendants-selected) state. */
648
+ indeterminateIds: readonly TId[];
649
+ /** The item objects corresponding to {@link UseTreeSelectionReturn.selectedIds}. */
650
+ selectedItems: readonly T[];
651
+ /** `selectedIds.length`. */
652
+ selectedCount: number;
653
+ /** Whether nothing at all is selected (not even indeterminate). */
654
+ isEmpty: boolean;
655
+ /** Whether every selectable root (and by construction, everything under it) is selected. */
656
+ isAllSelected: boolean;
657
+ /** Whether some, but not all, of the forest is selected or indeterminate. */
658
+ isPartiallySelected: boolean;
659
+ /**
660
+ * The full tri-state read for a node — the primitive the rest of the boolean
661
+ * getters below are built from.
662
+ * @param item - A raw id, or a full item.
663
+ */
664
+ getNodeState: (item: TId | T) => TreeNodeState;
665
+ /** `getNodeState(item) === "selected"`. */
666
+ isSelected: (item: TId | T) => boolean;
667
+ /** `getNodeState(item) === "indeterminate"`. */
668
+ isIndeterminate: (item: TId | T) => boolean;
669
+ /**
670
+ * Selects `item`, cascading per the `cascade` option.
671
+ * @remarks No-ops if `item` itself resolves to a disabled id.
672
+ */
673
+ select: (item: TId | T) => void;
674
+ /** Deselects `item`, cascading per the `cascade` option. Always allowed, even for disabled ids. */
675
+ deselect: (item: TId | T) => void;
676
+ /**
677
+ * Selects `item` if not selected, deselects it if selected, cascading per the `cascade` option.
678
+ * @remarks The select-direction is blocked for disabled ids; the deselect-direction never is.
679
+ */
680
+ toggle: (item: TId | T) => void;
681
+ /** Restores the selection to the current `defaultSelectedIds` (normalized), or empty if none was given. */
682
+ reset: () => void;
683
+ /** Replaces the entire selection with exactly these ids/items (normalized — always internally consistent afterward). */
684
+ replaceSelection: (newSelectedItems: readonly TId[] | readonly T[]) => void;
685
+ /** Selects every selectable node in the forest. */
686
+ selectAll: () => void;
687
+ /** Clears the entire selection. */
688
+ deselectAll: () => void;
689
+ /** If everything selectable is currently selected, clears the selection; otherwise selects everything selectable. */
690
+ toggleAll: () => void;
691
+ /**
692
+ * Selects multiple ids/items at once, cascading each per the `cascade` option.
693
+ *
694
+ * @remarks
695
+ * More efficient than calling {@link UseTreeSelectionReturn.select} in a loop:
696
+ * shared ancestors of multiple targets are only recomputed once each, not once
697
+ * per target that shares them.
698
+ */
699
+ selectMultiple: (newItems: readonly TId[] | readonly T[]) => void;
700
+ /** Deselects multiple ids/items at once, cascading each per the `cascade` option. Always allowed, even for disabled ids. */
701
+ deselectMultiple: (itemsToRemove: readonly TId[] | readonly T[]) => void;
702
+ /**
703
+ * Keeps only the ids/items in `itemsToRetain` that are already selected (an
704
+ * intersection); never adds anything new. The result is re-normalized, so
705
+ * ancestor indeterminate state stays correct after the shrink.
706
+ */
707
+ retainOnly: (itemsToRetain: readonly TId[] | readonly T[]) => void;
708
+ /**
709
+ * Inverts the selection at the leaf level: every currently-unselected
710
+ * selectable leaf becomes selected, every currently-selected one becomes
711
+ * unselected. Branch/indeterminate state is re-derived from the result.
712
+ * @remarks A disabled leaf that was selected before this call does not survive
713
+ * it — like `useMultipleSelection`'s `invertSelection`, this is a full replace,
714
+ * not a merge.
715
+ */
716
+ invertSelection: () => void;
717
+ /** The id of `item`'s direct parent, or `undefined` if it's a root. */
718
+ getParentId: (item: TId | T) => TId | undefined;
719
+ /** Every ancestor id of `item`, nearest first, root last. Empty if `item` is a root. */
720
+ getAncestorIds: (item: TId | T) => readonly TId[];
721
+ /** Every descendant id of `item`. Empty if `item` is a leaf. */
722
+ getDescendantIds: (item: TId | T) => readonly TId[];
723
+ }
724
+ /**
725
+ * Manages hierarchical (tree/forest) selection state — checkbox trees, nested
726
+ * category pickers, permission trees, file explorers.
727
+ *
728
+ * @remarks
729
+ * - Uncontrolled only for now, SSR-safe (no DOM access), all callbacks manually
730
+ * memoized — same guarantees as the other selection hooks in this library.
731
+ * - Expand/collapse state is explicitly **not** managed here — it's an orthogonal
732
+ * view concern, not a selection concern (this hook only ever reads the full
733
+ * `children` structure, regardless of what's currently expanded in the UI).
734
+ * - **Performance**: a single `select`/`deselect`/`toggle` call is
735
+ * O(affected subtree) for the cascade-down step plus O(depth × branching factor)
736
+ * for the cascade-up step — it never re-walks the whole tree. Only whole-forest
737
+ * operations (`selectAll`, `toggleAll`, `reset`, `replaceSelection`, and the
738
+ * initial mount) do a full O(n) pass, which is the right complexity for
739
+ * something that touches every node anyway. Verified independent of selection
740
+ * order (a known bug class in at least one production tree-selection library
741
+ * is indeterminate state differing based on the order nodes were selected in).
742
+ * - If `items` (the tree **structure**) changes after mount — nodes added, removed,
743
+ * or moved — the existing selection/indeterminate state is **not** automatically
744
+ * re-normalized against the new shape (to avoid surprise O(n) work on every data
745
+ * refresh). It stays correct for anything the change didn't touch, but the newly
746
+ * changed area may need an explicit `reset()` or `replaceSelection()` to
747
+ * guarantee full consistency again.
748
+ *
749
+ * @typeParam T - The tree node shape.
750
+ * @typeParam TId - The id type. Defaults to `SelectionId`.
751
+ * @param options - See {@link UseTreeSelectionOptions}.
752
+ * @returns The current selection state and the actions to mutate it. See {@link UseTreeSelectionReturn}.
753
+ *
754
+ * @example
755
+ * ```tsx
756
+ * interface Category { id: string; name: string; children?: Category[] }
757
+ *
758
+ * const { getNodeState, toggle, isAllSelected, toggleAll } = useTreeSelection<Category, string>({
759
+ * items: categoryTree,
760
+ * field: "id",
761
+ * childrenField: "children",
762
+ * });
763
+ * ```
764
+ */
765
+ export declare function useTreeSelection<T, TId extends SelectionId = SelectionId>(options: UseTreeSelectionOptions<T, TId>): UseTreeSelectionReturn<T, TId>;
233
766
 
234
767
  export {};
package/dist/state.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as e, c as t, i as n, n as r, o as i, r as a, s as o, t as s } from "./state2.js";
2
- export { t as useFilter, o as useFuzzySearch, i as useGrouping, e as useMultipleSelection, n as useOrder, a as usePagination, r as useSingleSelection, s as useSort };
1
+ import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c } from "./state2.js";
2
+ export { r as useFilter, t as useFuzzySearch, s as useGrouping, a as useMultipleSelection, e as useOrder, n as usePagination, o as useSingleSelection, i as useSort, c as useTreeSelection };