@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/index.d.ts CHANGED
@@ -2721,23 +2721,262 @@ export interface UseVirtualListReturn {
2721
2721
  * ```
2722
2722
  */
2723
2723
  export declare function useVirtualList<T = unknown>(options: UseVirtualListOptions<T>): UseVirtualListReturn;
2724
+ export type Primitive = string | number | boolean | bigint | symbol | null | undefined;
2725
+ 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>}`;
2726
+ export type Path<T> = T extends object ? {
2727
+ [K in keyof T & (string | number)]: PathImpl<K, T[K]>;
2728
+ }[keyof T & (string | number)] : never;
2729
+ 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;
2730
+ /** The id type `useExpansion` operates on by default - a raw `string` or `number`. */
2731
+ export type ExpansionId = string | number;
2732
+ /**
2733
+ * Options for {@link useExpansion}, in either of its two modes - id-only
2734
+ * (`T` defaults to `TId`) or object mode (`T` is a distinct item shape,
2735
+ * `TId` its resolved id type).
2736
+ *
2737
+ * @remarks
2738
+ * The conditional shape is what makes `field` required in object mode and
2739
+ * disallowed in id-only mode, entirely at the type level - see
2740
+ * {@link useExpansion}'s remarks for why.
2741
+ *
2742
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
2743
+ * @typeParam TId - The id type.
2744
+ */
2745
+ export type UseExpansionOptions<T = ExpansionId, TId extends ExpansionId = ExpansionId> = T extends TId ? {
2746
+ /** The full list of expandable items - backs `expandedItems`/`collapsedItems`, and is the default target for `expandAll` when it's called with no argument. */
2747
+ items?: readonly T[];
2748
+ /** Ids expanded from the start - frozen at mount, see {@link useExpansion}'s remarks. */
2749
+ initialExpandedIds?: readonly TId[];
2750
+ /** 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. */
2751
+ multiple?: boolean;
2752
+ /**
2753
+ * In single (`multiple: false`) mode, whether the one expanded item
2754
+ * can be collapsed back down to nothing.
2755
+ *
2756
+ * @remarks
2757
+ * Ignored when `multiple` is `true` - "can everything be collapsed"
2758
+ * isn't a meaningful constraint once more than one item can be open
2759
+ * at a time. Mirrors Radix UI Accordion's `collapsible` prop, though
2760
+ * the default here is the opposite of Radix's: `true`, matching what
2761
+ * this hook already did before `collapsible` existed, so leaving it
2762
+ * unset doesn't change any existing behavior.
2763
+ *
2764
+ * @defaultValue `true`
2765
+ */
2766
+ collapsible?: boolean;
2767
+ } : {
2768
+ /** The full list of expandable items - backs `expandedItems`/`collapsedItems`, and is the default target for `expandAll` when it's called with no argument. */
2769
+ items: readonly T[];
2770
+ /** 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. */
2771
+ field: Path<T> | (string & {});
2772
+ /** Ids expanded from the start - frozen at mount, see {@link useExpansion}'s remarks. */
2773
+ initialExpandedIds?: readonly TId[];
2774
+ /** 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. */
2775
+ multiple?: boolean;
2776
+ /**
2777
+ * In single (`multiple: false`) mode, whether the one expanded item
2778
+ * can be collapsed back down to nothing.
2779
+ *
2780
+ * @remarks
2781
+ * Ignored when `multiple` is `true` - "can everything be collapsed"
2782
+ * isn't a meaningful constraint once more than one item can be open
2783
+ * at a time. Mirrors Radix UI Accordion's `collapsible` prop, though
2784
+ * the default here is the opposite of Radix's: `true`, matching what
2785
+ * this hook already did before `collapsible` existed, so leaving it
2786
+ * unset doesn't change any existing behavior.
2787
+ *
2788
+ * @defaultValue `true`
2789
+ */
2790
+ collapsible?: boolean;
2791
+ };
2792
+ /**
2793
+ * Return value of {@link useExpansion}.
2794
+ *
2795
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
2796
+ * @typeParam TId - The id type.
2797
+ */
2798
+ export interface UseExpansionReturn<T = ExpansionId, TId extends ExpansionId = ExpansionId> {
2799
+ /** The currently expanded ids. */
2800
+ expandedIds: readonly TId[];
2801
+ /** The subset of `items` that are currently expanded. */
2802
+ expandedItems: readonly T[];
2803
+ /** The subset of `items` that are not currently expanded. */
2804
+ collapsedItems: readonly T[];
2805
+ /** `expandedIds.length`. */
2806
+ expandedCount: number;
2807
+ /** Whether anything at all is expanded. */
2808
+ hasExpanded: boolean;
2809
+ /** Whether `itemOrId` is currently expanded. */
2810
+ isExpanded: (itemOrId: TId | T) => boolean;
2811
+ /**
2812
+ * Expands `itemOrId`.
2813
+ * @remarks No-ops if it's already expanded. In single mode, also collapses whatever was previously expanded.
2814
+ */
2815
+ expand: (itemOrId: TId | T) => void;
2816
+ /**
2817
+ * Collapses `itemOrId`.
2818
+ * @remarks No-ops if it isn't currently expanded, or (in single mode) if `collapsible` is `false`.
2819
+ */
2820
+ collapse: (itemOrId: TId | T) => void;
2821
+ /**
2822
+ * Expands `itemOrId` if it isn't expanded, collapses it if it is.
2823
+ * @remarks In single mode, expanding also collapses whatever was previously expanded; collapsing is blocked when `collapsible` is `false`, same as {@link UseExpansionReturn.collapse}.
2824
+ */
2825
+ toggleExpansion: (itemOrId: TId | T) => void;
2826
+ /**
2827
+ * Expands every id/item given, or every item in `items` if called with
2828
+ * no argument. Replaces the current expanded set rather than adding to
2829
+ * it.
2830
+ * @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.
2831
+ */
2832
+ expandAll: (itemsArray?: readonly TId[] | readonly T[]) => void;
2833
+ /** 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. */
2834
+ collapseAll: (itemsArray?: readonly TId[] | readonly T[]) => void;
2835
+ /** 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. */
2836
+ resetExpansion: () => void;
2837
+ /** 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. */
2838
+ replaceExpansion: (newExpandedItems: readonly TId[] | readonly T[]) => void;
2839
+ }
2840
+ /**
2841
+ * Manages which item(s) in a list are expanded - accordions, expandable
2842
+ * table rows, collapsible sections.
2843
+ *
2844
+ * @remarks
2845
+ * - Uncontrolled only for now - controlled mode is planned separately and
2846
+ * will be added without breaking this signature.
2847
+ * - SSR-safe: performs no DOM/window access; `initialExpandedIds` must be
2848
+ * deterministic between server and client renders to avoid hydration
2849
+ * mismatches.
2850
+ * - All returned callbacks are manually memoized with `useCallback` so this
2851
+ * hook is safe to use even in codebases **without** the React Compiler.
2852
+ * - Two modes, picked by whether `T` is assignable to `TId`: id-only
2853
+ * (default) - `itemOrId` parameters only ever receive raw ids, `field`
2854
+ * is disallowed; or object mode - `<Row, string>` plus a required
2855
+ * `field`, letting `itemOrId` parameters take a full item too. See
2856
+ * {@link UseExpansionOptions}.
2857
+ * - The single/multiple invariant (at most one expanded id when `multiple`
2858
+ * is `false`) is enforced at every entry point that can introduce more
2859
+ * than one id at once - the initial mount, `resetExpansion`, and
2860
+ * `replaceExpansion` - not just `expand`/`toggleExpansion`, which only
2861
+ * ever add one id at a time by construction.
2862
+ * - `collapsible` (single mode only) specifically gates the *interactive*
2863
+ * collapse path - `collapse`/`toggleExpansion` closing the one expanded
2864
+ * item. It does not gate `collapseAll` or `replaceExpansion([])`: those
2865
+ * are explicit, deliberate "set state directly" calls, treated the same
2866
+ * as `clearPins`/`replacePins` in `usePin` not respecting `maxPins`
2867
+ * either - a soft interactive constraint doesn't override an explicit
2868
+ * caller instruction.
2869
+ *
2870
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
2871
+ * @typeParam TId - The id type.
2872
+ * @param options - See {@link UseExpansionOptions}.
2873
+ * @returns The current expansion state and the actions to change it. See {@link UseExpansionReturn}.
2874
+ *
2875
+ * @example
2876
+ * Accordion (single, non-collapsible - always exactly one open):
2877
+ * ```tsx
2878
+ * const { isExpanded, toggleExpansion } = useExpansion({
2879
+ * multiple: false,
2880
+ * collapsible: false,
2881
+ * initialExpandedIds: ["section-1"],
2882
+ * });
2883
+ * ```
2884
+ *
2885
+ * @example
2886
+ * Object mode, multiple expandable rows:
2887
+ * ```tsx
2888
+ * interface Row { id: string; label: string }
2889
+ * const { expandedItems, expand, expandAll } = useExpansion<Row, string>({
2890
+ * items: rows,
2891
+ * field: "id",
2892
+ * multiple: true,
2893
+ * });
2894
+ * ```
2895
+ */
2896
+ export declare function useExpansion<T = ExpansionId, TId extends ExpansionId = ExpansionId>(options?: UseExpansionOptions<T, TId>): UseExpansionReturn<T, TId>;
2897
+ /**
2898
+ * `Omit<T, K>`, applied per union member instead of to the flattened union
2899
+ * as a whole - plain `Omit` over a union loses the correlation between
2900
+ * `type`/`operator` and that arm's own `value` shape, which is exactly the
2901
+ * information {@link FilterConfigUpdate} needs to preserve.
2902
+ */
2903
+ export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
2904
+ /** `Partial<T>`, applied per union member - same distributive reasoning as {@link DistributiveOmit}. */
2905
+ export type DistributivePartial<T> = T extends unknown ? Partial<T> : never;
2906
+ /** The comparison strategy for a filter - selects which operator set (`text`, `number`, etc.) applies, or `custom` for a caller-supplied predicate. */
2724
2907
  export type FilterType = "text" | "number" | "boolean" | "date" | "select" | "multiselect" | "custom";
2908
+ /** Operators available on a `text`-type filter. */
2725
2909
  export type TextOperator = "contains" | "equals" | "startsWith" | "endsWith" | "notContains";
2910
+ /** Operators available on a `number`-type filter. */
2726
2911
  export type NumberOperator = "equals" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual" | "between";
2727
- export type BooleanOperator = "equals";
2912
+ /** Operators available on a `boolean`-type filter. */
2913
+ export type BooleanOperator = "equals" | "notEquals";
2914
+ /** Operators available on a `date`-type filter. */
2728
2915
  export type DateOperator = "equals" | "before" | "after" | "between";
2729
- export type SelectOperator = "equals" | "notEquals";
2916
+ /** Operators available on a `select`-type filter (single value against a scalar field). */
2917
+ export type SelectOperator = "equals" | "notEquals" | "in" | "notIn";
2918
+ /** Operators available on a `multiselect`-type filter (against an array-valued field). */
2730
2919
  export type MultiselectOperator = "in" | "notIn" | "intersects";
2920
+ /** 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. */
2731
2921
  export type CustomOperator = "custom";
2922
+ /** Options shared across filter types, though not every option is meaningful for every type. */
2732
2923
  export interface FilterOptions<T = unknown> {
2924
+ /**
2925
+ * Case sensitivity for `text` comparisons. Ignored by every other
2926
+ * filter type - `select` deliberately has no equivalent, since select
2927
+ * values are enumerated tokens (a fixed options list, an HTML
2928
+ * `<select>`), not free-typed input that could differ only by case.
2929
+ * @defaultValue `false`
2930
+ */
2733
2931
  caseSensitive?: boolean;
2932
+ /**
2933
+ * Comparison granularity for `date` filters.
2934
+ * `"day"` ignores time-of-day (calendar-date comparison); `"instant"`
2935
+ * compares exact milliseconds. Ignored by every other filter type.
2936
+ * @defaultValue `"day"`
2937
+ */
2938
+ dateGranularity?: "day" | "instant";
2939
+ /** Required for `type: "custom"` - the predicate deciding whether an item matches. Ignored by every other filter type. */
2734
2940
  compare?: (itemValue: unknown, filterValue: unknown, item: T) => boolean;
2735
2941
  }
2942
+ /** Options for {@link useFilter}. */
2943
+ export interface UseFilterOptions {
2944
+ /**
2945
+ * Defers the filtering recomputation (via `useDeferredValue`) so
2946
+ * changing `filters` doesn't block a more urgent update (e.g. the
2947
+ * keystroke that triggered the change). Only `filters` is deferred -
2948
+ * `data` is not.
2949
+ * @defaultValue `false`
2950
+ */
2951
+ defer?: boolean;
2952
+ }
2953
+ /** Fields common to every {@link FilterConfig}, regardless of `type`/`operator`. */
2736
2954
  export type BaseFilterConfig<T> = FilterOptions<T> & {
2955
+ /** Unique identifier for this filter - used for lookups (`getFilter`, `isFilterActive`, etc.) and to distinguish filters in the `filters` array. */
2737
2956
  id: string;
2738
- field: string;
2957
+ /** 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. */
2958
+ field?: string;
2959
+ /**
2960
+ * Whether this filter currently participates in filtering. An inactive
2961
+ * filter stays in the `filters` array (so its configuration - operator,
2962
+ * value, etc. - is preserved) but is skipped during evaluation.
2963
+ * @defaultValue `true`
2964
+ */
2739
2965
  isActive?: boolean;
2740
2966
  };
2967
+ /**
2968
+ * A single filter's full configuration.
2969
+ *
2970
+ * @remarks
2971
+ * Each `type` has its own dedicated arm(s) - `number`/`date`/`select` each
2972
+ * split further by operator (a `between` arm with a `{min,max}` value,
2973
+ * separate from every other operator's scalar/array value) - rather than
2974
+ * grouping multiple type literals under one shared arm. This is what lets
2975
+ * TypeScript actually narrow `value`'s type based on `type`+`operator`,
2976
+ * and what makes {@link FilterOptionsForType}-style per-type extraction
2977
+ * (and IDE autocomplete while constructing a filter) work correctly -
2978
+ * grouping literals under one arm silently breaks both.
2979
+ */
2741
2980
  export type FilterConfig<T = unknown> = BaseFilterConfig<T> & ({
2742
2981
  type: "text";
2743
2982
  operator: TextOperator;
@@ -2770,8 +3009,12 @@ export type FilterConfig<T = unknown> = BaseFilterConfig<T> & ({
2770
3009
  value: string | number | Date;
2771
3010
  } | {
2772
3011
  type: "select";
2773
- operator: SelectOperator;
3012
+ operator: Exclude<SelectOperator, "in" | "notIn">;
2774
3013
  value: string | number;
3014
+ } | {
3015
+ type: "select";
3016
+ operator: "in" | "notIn";
3017
+ value: (string | number)[];
2775
3018
  } | {
2776
3019
  type: "multiselect";
2777
3020
  operator: MultiselectOperator;
@@ -2781,176 +3024,1599 @@ export type FilterConfig<T = unknown> = BaseFilterConfig<T> & ({
2781
3024
  operator: CustomOperator;
2782
3025
  value: unknown;
2783
3026
  });
3027
+ /** 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. */
2784
3028
  export type FilterState<T> = FilterConfig<T>[];
3029
+ /**
3030
+ * The update payload type for `updateFilterConfig`.
3031
+ *
3032
+ * @remarks
3033
+ * Best-effort compile-time guidance, not a full guarantee: a *merge* of an
3034
+ * update into an existing filter can still land on a structurally invalid
3035
+ * `type`/`operator`/`value` combination that the type system alone can't
3036
+ * catch (partial updates flatten across a union in ways whole-object
3037
+ * construction doesn't). `isValueShapeValid`, run on the merged result
3038
+ * inside `applyFilterUpdate`, is the actual runtime backstop.
3039
+ */
3040
+ export type FilterConfigUpdate<T> = DistributivePartial<DistributiveOmit<FilterConfig<T>, "id">>;
3041
+ /** Return value of {@link useFilter}. */
2785
3042
  export interface UseFilterReturn<T> {
3043
+ /** `data`, filtered by every active entry in `filters` (AND-combined). Same reference as `data` (no copy, no filter) when there are no active filters. */
2786
3044
  filteredItems: T[];
3045
+ /** Every configured filter, active or not. */
2787
3046
  filters: FilterState<T>;
3047
+ /** Count of filters with `isActive !== false`. */
3048
+ activeFilterCount: number;
3049
+ /** `activeFilterCount > 0`. */
3050
+ hasActiveFilters: boolean;
3051
+ /** Adds a new filter, or replaces the existing one with the same `id`. */
2788
3052
  upsertFilter: (filter: FilterConfig<T>) => void;
3053
+ /** Removes one filter by `id`, or several at once by passing an array of ids. */
2789
3054
  removeFilter: (id: string | string[]) => void;
3055
+ /** Clears every filter entirely - equivalent to `replaceFilters([])`. */
2790
3056
  clearFilters: () => void;
3057
+ /** 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. */
2791
3058
  resetFilters: () => void;
3059
+ /** Replaces the entire `filters` array at once. */
2792
3060
  replaceFilters: (filters: FilterConfig<T>[]) => void;
3061
+ /** 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. */
2793
3062
  toggleFilter: (filter: FilterConfig<T>) => void;
2794
- updateFilterConfig: (id: string, partialConfig: Partial<Omit<FilterConfig<T>, "id">>) => void;
3063
+ /**
3064
+ * Partially updates an existing filter's config by `id`.
3065
+ * @remarks An unknown `id`, or an update that would produce a value
3066
+ * incompatible with the filter's `type`/`operator`, is rejected - see
3067
+ * {@link isValueShapeValid}.
3068
+ */
3069
+ updateFilterConfig: (id: string, partialConfig: FilterConfigUpdate<T>) => void;
3070
+ /** Looks up a filter's full config by `id`. `undefined` if no such filter exists. */
3071
+ getFilter: (id: string) => FilterConfig<T> | undefined;
3072
+ /** Just a filter's `value`, by `id`. `undefined` if no such filter exists. */
2795
3073
  getFilterValue: (id: string) => unknown;
3074
+ /** Sugar for `updateFilterConfig(id, { value })` - updates only a filter's value, leaving its type/operator/other options untouched. */
2796
3075
  updateFilterValue: (id: string, value: unknown) => void;
3076
+ /** Whether a filter (by `id`) currently participates in filtering. `false` for a nonexistent `id`, same as an explicitly inactive one. */
2797
3077
  isFilterActive: (id: string) => boolean;
2798
3078
  }
2799
- export declare function useFilter<T>(data?: T[], initialFilters?: FilterState<T>): UseFilterReturn<T>;
2800
- export interface FuzzySearchOptions {
2801
- threshold?: number;
2802
- caseSensitive?: boolean;
2803
- matchStrategy?: "any" | "all";
2804
- exactPhraseBonus?: boolean;
2805
- }
2806
- export interface IndexedToken {
2807
- text: string;
2808
- weight: number;
2809
- }
2810
- export interface FlatIndexedItem<T> {
2811
- item: T;
2812
- index: number;
2813
- tokens: IndexedToken[];
2814
- combinedFlatText: string;
2815
- }
2816
- export interface ScoredItem<T> {
2817
- item: T;
2818
- score: number;
2819
- index: number;
2820
- }
2821
- export type UseFuzzySearchFields<T> = {
2822
- field: Extract<keyof T, string>;
2823
- weight: number;
2824
- }[];
2825
- export type UseFuzzySearchReturn<T> = T[];
2826
- export declare function useFuzzySearch<T>(data: T[], query: string, fields: UseFuzzySearchFields<T>, options?: FuzzySearchOptions): UseFuzzySearchReturn<T>;
3079
+ /**
3080
+ * Filters an array against one or more configured conditions - text
3081
+ * matching, numeric/date comparisons and ranges, boolean/select/multiselect
3082
+ * matching, or a fully custom predicate.
3083
+ *
3084
+ * @remarks
3085
+ * - **Every active filter is AND-combined** - an item must satisfy all of
3086
+ * them to be included. There's no built-in OR-across-filters; use a
3087
+ * `custom` filter for that if needed.
3088
+ * - **Missing/incomparable data always excludes the item**, uniformly
3089
+ * across every built-in operator - including the "negative" ones like
3090
+ * `notContains`/`notEquals`/`notIn`. A row with no value for the field is
3091
+ * treated as "unknown," never as a confident non-match. See
3092
+ * `FILTER_STRATEGIES`'s file-level `@remarks` for the full reasoning.
3093
+ * - **Validation is dev/prod-split**, same convention as this library's
3094
+ * other hooks: an unknown `type`/`operator` combination, or a `custom`
3095
+ * filter missing its `compare` function, throws immediately in
3096
+ * development, but is silently excluded (that filter matches nothing) in
3097
+ * production.
3098
+ * - **`field` falls back to `id`** when omitted - so a filter whose `id`
3099
+ * already matches the data's field name doesn't need `field` set
3100
+ * separately.
3101
+ * - **Id lookups are O(1)**, backed by a `Map` built once per `filters`
3102
+ * change, not a linear scan per call.
3103
+ *
3104
+ * @typeParam T - The type of each item in `data`.
3105
+ * @param data - The items to filter. Defaults to `[]`.
3106
+ * @param initialFilters - Filters applied at mount. Defaults to `[]` (no filtering).
3107
+ * @param options - See {@link UseFilterOptions}.
3108
+ * @returns The filtered items and the current filter state, plus the
3109
+ * actions to change it. See {@link UseFilterReturn}.
3110
+ *
3111
+ * @example
3112
+ * ```tsx
3113
+ * const { filteredItems, upsertFilter } = useFilter(products, [
3114
+ * { id: "category", type: "select", operator: "equals", value: "Books" },
3115
+ * ]);
3116
+ *
3117
+ * upsertFilter({
3118
+ * id: "price",
3119
+ * type: "number",
3120
+ * operator: "between",
3121
+ * value: { min: 10, max: 50 },
3122
+ * });
3123
+ * ```
3124
+ */
3125
+ export declare function useFilter<T>(data?: T[], initialFilters?: FilterState<T>, options?: UseFilterOptions): UseFilterReturn<T>;
3126
+ /** Bucket granularity for a `date`-type group level - see {@link GroupByDateLevel.bucket}. */
3127
+ export type DateBucketGranularity = "day" | "month" | "year";
3128
+ /**
3129
+ * Groups by a field's raw value.
3130
+ * @remarks If the resolved value is an array (e.g. a `tags` field), the
3131
+ * item fans out into a separate group per element instead of being
3132
+ * String()-coerced into one combined key - see {@link useGrouping}'s
3133
+ * `@remarks` for details.
3134
+ */
3135
+ export type GroupByFieldLevel = {
3136
+ type: "field";
3137
+ /** Dot-separated path into each item, e.g. `"address.city"`. */
3138
+ field: string;
3139
+ };
3140
+ /** Groups by a `Date`-valued field, bucketed to day/month/year rather than exact timestamp - avoids one group per unique millisecond. */
3141
+ export type GroupByDateLevel = {
3142
+ type: "date";
3143
+ /** Dot-separated path into each item. */
3144
+ field: string;
3145
+ /**
3146
+ * Bucket granularity.
3147
+ * @defaultValue `"day"`
3148
+ */
3149
+ bucket?: DateBucketGranularity;
3150
+ };
3151
+ /** 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.). */
3152
+ export type GroupByCustomLevel<T> = {
3153
+ type: "custom";
3154
+ /** Optional; used only to identify this level in dev warnings. */
3155
+ id?: string;
3156
+ /**
3157
+ * Computes this item's group key(s).
3158
+ * @remarks Return an array to fan the item out into multiple groups at
3159
+ * this level, mirroring array-valued {@link GroupByFieldLevel}s.
3160
+ * `null`/`undefined` places the item in the Unknown bucket. Throwing,
3161
+ * or returning anything other than `string | string[] | null | undefined`,
3162
+ * is treated as level misconfiguration - see {@link useGrouping}'s
3163
+ * `@remarks`.
3164
+ */
3165
+ getKey: (item: T) => string | string[] | null | undefined;
3166
+ };
3167
+ /** A single grouping level - a plain string is shorthand for `{ type: "field", field: theString }`. */
3168
+ export type GroupByLevel<T> = string | GroupByFieldLevel | GroupByDateLevel | GroupByCustomLevel<T>;
3169
+ /** Same union as {@link GroupByLevel} with the string shorthand already expanded - what {@link useGrouping} stores internally and returns via `activeGroupBy`. */
3170
+ export type NormalizedGroupByLevel<T> = GroupByFieldLevel | GroupByDateLevel | GroupByCustomLevel<T>;
3171
+ /** A single group, as produced by {@link useGrouping}'s `groupedArray`. */
2827
3172
  export interface Group<T> {
3173
+ /** 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. */
2828
3174
  key: string;
3175
+ /** Human-readable display text. Usually equal to `key`, except for date buckets (e.g. key `"2026-08"`, label `"August 2026"`). */
3176
+ label: string;
3177
+ /** ALL items under this group, flattened across any deeper levels. */
2829
3178
  items: T[];
3179
+ /** Present only when there's another grouping level below this one. */
3180
+ subGroups?: Group<T>[];
3181
+ }
3182
+ /** Options for {@link useGrouping}. */
3183
+ export interface UseGroupingOptions {
3184
+ /**
3185
+ * Defers the grouping recomputation (via `useDeferredValue`) so changing
3186
+ * `activeGroupBy` doesn't block a more urgent update, e.g. the UI
3187
+ * interaction that triggered the change. Only `activeGroupBy` is
3188
+ * deferred - `items` is not.
3189
+ *
3190
+ * @defaultValue `false`
3191
+ */
3192
+ defer?: boolean;
3193
+ /**
3194
+ * Display label for the synthetic bucket holding items whose value at a
3195
+ * given level is missing, blank, or otherwise unresolvable.
3196
+ *
3197
+ * @defaultValue `"Unknown"`
3198
+ */
3199
+ unknownGroupLabel?: string;
3200
+ /**
3201
+ * Display label for the single synthetic group returned when
3202
+ * `activeGroupBy` is empty (no grouping applied).
3203
+ *
3204
+ * @defaultValue `"Ungrouped"`
3205
+ */
3206
+ ungroupedGroupLabel?: string;
2830
3207
  }
3208
+ /** Return value of {@link useGrouping}. */
2831
3209
  export interface UseGroupingReturn<T> {
2832
- groupedRecord: Record<string, T[]>;
3210
+ /** 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. */
2833
3211
  groupedArray: Group<T>[];
3212
+ /** Top-level groups only, flattened -- Record<key, items>. For deeper
3213
+ * levels, traverse a group's `subGroups` directly. */
3214
+ groupedRecord: Record<string, T[]>;
3215
+ /** Top-level group keys only, in the same order as `groupedArray`. */
2834
3216
  groupKeys: string[];
3217
+ /** Count of top-level groups only - `groupKeys.length`. */
2835
3218
  totalGroups: number;
2836
- activeGroupBy: string | undefined;
2837
- changeGroupBy: (newField: string) => void;
3219
+ /** The currently-applied grouping levels, normalized (string shorthand already expanded). Empty when no grouping is applied. */
3220
+ activeGroupBy: NormalizedGroupByLevel<T>[];
3221
+ /** `activeGroupBy.length > 0`. */
3222
+ hasActiveGrouping: boolean;
3223
+ /** 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"`. */
3224
+ unknownGroupKey: string;
3225
+ /** 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"`. */
3226
+ ungroupedGroupKey: string;
3227
+ /**
3228
+ * Replaces `activeGroupBy` with one level or an array of levels (for
3229
+ * hierarchical grouping - order determines nesting, `newGroupBy[0]`
3230
+ * becomes the top level).
3231
+ * @remarks A malformed level (missing `field`, non-function `getKey`,
3232
+ * etc.) is rejected - see {@link useGrouping}'s `@remarks`.
3233
+ */
3234
+ changeGroupBy: (newGroupBy: GroupByLevel<T> | GroupByLevel<T>[]) => void;
3235
+ /** Clears `activeGroupBy` entirely - equivalent to `changeGroupBy([])`. */
2838
3236
  clearGrouping: () => void;
3237
+ /** 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. */
2839
3238
  resetGrouping: () => void;
3239
+ /** Looks up a single top-level group by its `key`. `undefined` if no such group exists.
3240
+ * @remarks Top-level lookup only, matching `groupedRecord`'s scope - use `groupedArray`/`subGroups` directly for a nested group. */
3241
+ getGroup: (groupKey: string) => Group<T> | undefined;
3242
+ /** `getGroup(groupKey)?.items ?? []` - the items in a top-level group, or an empty array if it doesn't exist. */
2840
3243
  getGroupItems: (groupKey: string) => T[];
2841
3244
  }
2842
- export declare function useGrouping<T = unknown>(options?: {
2843
- items?: T[];
2844
- initialGroupBy?: string;
2845
- }): UseGroupingReturn<T>;
3245
+ /**
3246
+ * Partitions an array into groups by one or more field values, date
3247
+ * buckets, or a custom key function - single-level or hierarchical
3248
+ * (subgroups within groups).
3249
+ *
3250
+ * @remarks
3251
+ * - **Fan-out, not partitioning.** If a level's resolved value for an item
3252
+ * is an array (e.g. a `tags` field, or a `custom` level's `getKey`
3253
+ * returning multiple keys), the item is placed in *every* matching group
3254
+ * at that level, not just one. Summed item counts across sibling groups
3255
+ * can therefore exceed the original array length - this reflects genuine
3256
+ * multi-group membership, not a bug.
3257
+ * - **Two distinct synthetic buckets**, both customizable via
3258
+ * {@link UseGroupingOptions}: `unknownGroupLabel` (default `"Unknown"`)
3259
+ * for items whose value at a level is missing, blank, or otherwise
3260
+ * unresolvable; `ungroupedGroupLabel` (default `"Ungrouped"`) for the
3261
+ * single group returned when no grouping is applied at all. The actual
3262
+ * labels in use are returned as `unknownGroupKey`/`ungroupedGroupKey` -
3263
+ * compare against those rather than hardcoding the default strings, in
3264
+ * case they've been customized.
3265
+ * - **Validation is dev/prod-split**, same convention as this library's
3266
+ * other hooks: a malformed level (missing/empty `field`, a non-function
3267
+ * `getKey`, an invalid `bucket`) throws immediately in development, but
3268
+ * is rejected silently (falling back to the previous/empty grouping) in
3269
+ * production. A `custom` level's `getKey` throwing, or returning
3270
+ * something other than `string | string[] | null | undefined`, follows
3271
+ * the same split - see {@link GroupByCustomLevel.getKey}.
3272
+ * - **`groupedRecord`/`groupKeys`/`totalGroups`/`getGroup`/`getGroupItems`
3273
+ * are all top-level only** - for anything below the first grouping
3274
+ * level, traverse a `Group`'s `subGroups` directly via `groupedArray`.
3275
+ * - Order of top-level (and each nested level's) groups follows first
3276
+ * appearance in `items`, not any particular sort - re-sort `groupedArray`
3277
+ * yourself if a specific order is needed.
3278
+ *
3279
+ * @typeParam T - The type of each item in `data`.
3280
+ * @param data - The items to group. Defaults to `[]`.
3281
+ * @param initialGroupBy - Grouping level(s) applied at mount. Omit for no
3282
+ * initial grouping.
3283
+ * @param options - See {@link UseGroupingOptions}.
3284
+ * @returns The current grouping and the actions to change it. See
3285
+ * {@link UseGroupingReturn}.
3286
+ *
3287
+ * @example
3288
+ * Single-level, by a plain field:
3289
+ * ```tsx
3290
+ * const { groupedArray } = useGrouping(users, "department");
3291
+ * // groupedArray: [{ key: "Engineering", items: [...] }, { key: "Sales", items: [...] }, ...]
3292
+ * ```
3293
+ *
3294
+ * @example
3295
+ * Hierarchical, by department then a date bucket:
3296
+ * ```tsx
3297
+ * const { groupedArray, changeGroupBy } = useGrouping(orders);
3298
+ *
3299
+ * changeGroupBy([
3300
+ * "region",
3301
+ * { type: "date", field: "placedAt", bucket: "month" },
3302
+ * ]);
3303
+ * // groupedArray: [{ key: "EMEA", items: [...], subGroups: [{ key: "2026-08", ... }] }, ...]
3304
+ * ```
3305
+ */
3306
+ export declare function useGrouping<T = unknown>(data?: T[], initialGroupBy?: GroupByLevel<T> | GroupByLevel<T>[], options?: UseGroupingOptions): UseGroupingReturn<T>;
2846
3307
  export type SelectionId = string | number;
2847
- export interface UseMultipleSelectionReturn<T> {
2848
- selectedIds: SelectionId[];
3308
+ /**
3309
+ * All dot-notation paths of `T` whose resolved value is assignable to `TId`.
3310
+ *
3311
+ * @remarks
3312
+ * This is what makes `field` type-safe: a path pointing at a boolean, an
3313
+ * object, or a non-existent property simply isn't a member of this type, so
3314
+ * passing it is a compile-time error rather than a silent `undefined` at
3315
+ * runtime. Built on top of the `Path`/`PathValue` machinery already exported
3316
+ * by `getValue`'s module.
3317
+ *
3318
+ * @template T The item shape.
3319
+ * @template TId The id type the resolved value must be assignable to.
3320
+ */
3321
+ export type SelectionIdPath<T, TId extends SelectionId> = {
3322
+ [P in Path<T> & string]: PathValue<T, P> extends TId ? P : never;
3323
+ }[Path<T> & string];
3324
+ /**
3325
+ * The options every {@link useMultipleSelection} call accepts regardless of
3326
+ * whether `T` is a raw id type or an object type.
3327
+ *
3328
+ * @typeParam T - The item shape (or `TId` itself, for id-only mode).
3329
+ * @typeParam TId - The id type used internally for `Set`/comparison purposes.
3330
+ */
3331
+ export interface UseMultipleSelectionBaseOptions<T, TId extends SelectionId> {
3332
+ /**
3333
+ * The items this selection is over. Determines `selectedItems`, and is
3334
+ * what `selectAll`/`toggleAll`/`invertSelection` operate against.
3335
+ *
3336
+ * @remarks
3337
+ * Pass a stable/memoized array. An inline `items={data.filter(...)}` gets
3338
+ * a new reference every render, which cascades into every memoized value
3339
+ * derived from `items` recomputing every render too — this hook can't
3340
+ * cheaply detect "same contents, different reference" for you.
3341
+ */
3342
+ items?: readonly T[];
3343
+ /**
3344
+ * The ids selected on mount, and what {@link UseMultipleSelectionReturn.reset}
3345
+ * returns the selection to.
3346
+ *
3347
+ * @remarks
3348
+ * Only the value present on the **first render** seeds state (standard
3349
+ * `useState` initializer semantics). `reset()` always reads the **latest**
3350
+ * `defaultSelectedIds` at call time, so if this changes across renders,
3351
+ * `reset()` restores to the newest default, not the original mount-time one.
3352
+ */
3353
+ defaultSelectedIds?: readonly TId[];
3354
+ /**
3355
+ * Predicate that marks certain ids as non-selectable.
3356
+ *
3357
+ * @remarks
3358
+ * Enforced on every operation that **adds** to the selection (`select`,
3359
+ * `toggle`'s select-direction, `selectMultiple`, `selectAll`, `toggleAll`,
3360
+ * `invertSelection`, `replaceSelection`). It never blocks **removal**
3361
+ * (`deselect`, `deselectMultiple`, `retainOnly`, `toggle`'s deselect-direction,
3362
+ * `deselectAll`) — if an already-selected id becomes disabled later, you can
3363
+ * always deselect it, just not re-select it.
3364
+ *
3365
+ * @param id - The id being checked.
3366
+ * @returns `true` if the id must not be added to the selection.
3367
+ */
3368
+ isDisabled?: (id: TId) => boolean;
3369
+ }
3370
+ /**
3371
+ * The `field` requirement, resolved conditionally on whether `T` is already
3372
+ * an id (`[T] extends [TId]`) or a full object.
3373
+ *
3374
+ * @remarks
3375
+ * - id-only mode (`T` is assignable to `TId`, e.g. the default `T = SelectionId`):
3376
+ * `field` is forbidden — there's nothing to extract a path from.
3377
+ * - object mode: `field` is **required**, and restricted to
3378
+ * {@link SelectionIdPath} — a path that doesn't exist on `T`, or whose
3379
+ * resolved value isn't assignable to `TId`, is a compile-time error rather
3380
+ * than a silent `undefined` id at runtime.
3381
+ */
3382
+ export type UseMultipleSelectionFieldOptions<T, TId extends SelectionId> = [
3383
+ T
3384
+ ] extends [
3385
+ TId
3386
+ ] ? {
3387
+ field?: never;
3388
+ } : {
3389
+ field: SelectionIdPath<T, TId>;
3390
+ };
3391
+ /**
3392
+ * Combined options for {@link useMultipleSelection}. See
3393
+ * {@link UseMultipleSelectionBaseOptions} and {@link UseMultipleSelectionFieldOptions}.
3394
+ *
3395
+ * @typeParam T - The item shape. Defaults to `SelectionId` (id-only mode: pass
3396
+ * raw ids as "items", no `field` needed).
3397
+ * @typeParam TId - The id type. Defaults to `SelectionId`; narrow it (e.g. to a
3398
+ * branded `UserId`) for stronger inference.
3399
+ */
3400
+ export type UseMultipleSelectionOptions<T = SelectionId, TId extends SelectionId = SelectionId> = UseMultipleSelectionBaseOptions<T, TId> & UseMultipleSelectionFieldOptions<T, TId>;
3401
+ /**
3402
+ * Return shape of {@link useMultipleSelection}.
3403
+ *
3404
+ * @typeParam T - The item shape.
3405
+ * @typeParam TId - The id type.
3406
+ */
3407
+ export interface UseMultipleSelectionReturn<T = SelectionId, TId extends SelectionId = SelectionId> {
3408
+ /**
3409
+ * The currently selected ids.
3410
+ *
3411
+ * @remarks
3412
+ * Ordered to match `items`' order whenever every selected id is present in
3413
+ * `items`. If some selected ids aren't in the current `items` array (e.g.
3414
+ * a previously-selected item that's since been filtered out, or an id
3415
+ * selected directly without ever appearing in `items`), those "orphaned"
3416
+ * ids are preserved and appended at the end, rather than silently dropped —
3417
+ * this hook never discards selection state you didn't ask it to discard.
3418
+ */
3419
+ selectedIds: readonly TId[];
3420
+ /** The number of currently selected ids (including any orphaned ones — see {@link UseMultipleSelectionReturn.selectedIds}). */
2849
3421
  selectedCount: number;
2850
- selectedItems: T[];
3422
+ /** The subset of `items` that are currently selected, in `items`' order. */
3423
+ selectedItems: readonly T[];
3424
+ /** Whether nothing at all is selected. */
2851
3425
  isEmpty: boolean;
2852
- select: (item: SelectionId | T) => void;
2853
- deselect: (item: SelectionId | T) => void;
2854
- toggle: (item: SelectionId | T) => void;
2855
- isSelected: (item: SelectionId | T) => boolean;
2856
- resetSelection: () => void;
2857
- replaceSelection: (newSelectedItems: SelectionId[] | T[]) => void;
3426
+ /**
3427
+ * Whether every *selectable* (non-disabled) item in `items` is currently
3428
+ * selected. `false` when `items` is empty. Intended for a "select all"
3429
+ * checkbox's checked state.
3430
+ */
3431
+ isAllSelected: boolean;
3432
+ /**
3433
+ * Whether some, but not all, selectable items in `items` are selected.
3434
+ * Intended for a "select all" checkbox's indeterminate state.
3435
+ */
3436
+ isPartiallySelected: boolean;
3437
+ /**
3438
+ * Adds `item` to the selection.
3439
+ * @remarks No-ops if `item` resolves to a disabled id.
3440
+ * @param item - A raw id, or a full item (requires `field` to have been configured).
3441
+ */
3442
+ select: (item: TId | T) => void;
3443
+ /**
3444
+ * Removes `item` from the selection. Always allowed, even for disabled ids.
3445
+ * @param item - A raw id, or a full item.
3446
+ */
3447
+ deselect: (item: TId | T) => void;
3448
+ /**
3449
+ * Adds `item` if not selected, removes it if selected.
3450
+ * @remarks The add-direction is blocked for disabled ids; the remove-direction never is.
3451
+ * @param item - A raw id, or a full item.
3452
+ */
3453
+ toggle: (item: TId | T) => void;
3454
+ /**
3455
+ * Checks whether `item` is currently selected.
3456
+ * @param item - A raw id, or a full item.
3457
+ * @returns `true` if currently selected.
3458
+ */
3459
+ isSelected: (item: TId | T) => boolean;
3460
+ /** Restores the selection to the current `defaultSelectedIds` (or empty, if none was provided). */
3461
+ reset: () => void;
3462
+ /**
3463
+ * Replaces the entire selection with exactly these ids/items.
3464
+ * @remarks Disabled ids are filtered out of the replacement set.
3465
+ */
3466
+ replaceSelection: (newSelectedItems: readonly TId[] | readonly T[]) => void;
3467
+ /** Selects every selectable (non-disabled) item in `items`. */
2858
3468
  selectAll: () => void;
3469
+ /** Clears the entire selection, including any disabled-but-selected or orphaned ids. */
2859
3470
  deselectAll: () => void;
3471
+ /**
3472
+ * If every selectable item in `items` is currently selected, clears the
3473
+ * whole selection; otherwise selects every selectable item in `items`.
3474
+ *
3475
+ * @remarks
3476
+ * Determined by actual set membership against every selectable item, not
3477
+ * a size comparison — this stays correct even if `items` contains duplicate
3478
+ * resolved ids, or the current selection contains ids no longer present in
3479
+ * `items` (both of which would silently misfire a naive `prev.size === items.length` check).
3480
+ */
2860
3481
  toggleAll: () => void;
3482
+ /** Selects every currently-unselected selectable item, and deselects everything else. */
2861
3483
  invertSelection: () => void;
2862
- selectMultiple: (newItems: SelectionId[] | T[]) => void;
2863
- deselectMultiple: (itemsToRemove: SelectionId[] | T[]) => void;
2864
- retainOnly: (itemsToRetain: SelectionId[] | T[]) => void;
3484
+ /**
3485
+ * Adds multiple ids/items to the selection at once.
3486
+ * @remarks Disabled ids are filtered out.
3487
+ */
3488
+ selectMultiple: (newItems: readonly TId[] | readonly T[]) => void;
3489
+ /** Removes multiple ids/items from the selection at once. Always allowed. */
3490
+ deselectMultiple: (itemsToRemove: readonly TId[] | readonly T[]) => void;
3491
+ /** Keeps only the ids/items in `itemsToRetain` that are already selected (an intersection); never adds anything new. */
3492
+ retainOnly: (itemsToRetain: readonly TId[] | readonly T[]) => void;
2865
3493
  }
2866
- export declare function useMultipleSelection<T = unknown>(options?: {
2867
- items?: T[];
2868
- field?: string;
2869
- initialSelectedIds?: SelectionId[];
2870
- }): UseMultipleSelectionReturn<T>;
2871
- export interface UseOrderReturn<T> {
2872
- orderedItems: T[];
2873
- moveUp: (index: number) => void;
2874
- moveDown: (index: number) => void;
2875
- canMoveUp: (index: number) => boolean;
2876
- canMoveDown: (index: number) => boolean;
2877
- moveToTop: (index: number) => void;
2878
- moveToBottom: (index: number) => void;
2879
- move: (fromIndex: number, toIndex: number) => void;
2880
- swap: (indexA: number, indexB: number) => void;
3494
+ /**
3495
+ * Manages multi-item selection state (e.g. checkboxes in a table, a
3496
+ * multi-select list, bulk-action UI).
3497
+ *
3498
+ * @remarks
3499
+ * - Uncontrolled only for now — controlled mode is planned separately and
3500
+ * will be added without breaking this signature.
3501
+ * - SSR-safe: no DOM/window access; `defaultSelectedIds` must be deterministic
3502
+ * between server and client renders to avoid hydration mismatches.
3503
+ * - All returned callbacks are manually memoized with `useCallback`/`useMemo`
3504
+ * so this hook is safe to use even in codebases **without** the React Compiler.
3505
+ * - Works in two modes, picked by whether `T` is assignable to `TId`:
3506
+ * - **id-only mode** (default): `useMultipleSelection()` "items" are raw
3507
+ * ids, no `field` needed.
3508
+ * - **object mode**: `useMultipleSelection<Item, string>({ items, field: "id" })`
3509
+ * `field` is a type-checked dot-path into `Item` and is required.
3510
+ *
3511
+ * @typeParam T - The item shape (or `TId` itself, for id-only mode).
3512
+ * @typeParam TId - The id type. Defaults to `SelectionId`; narrow it for branded-id inference.
3513
+ * @param options - See {@link UseMultipleSelectionOptions}.
3514
+ * @returns The current selection state and the actions to mutate it. See {@link UseMultipleSelectionReturn}.
3515
+ *
3516
+ * @example
3517
+ * ```tsx
3518
+ * // id-only mode
3519
+ * const { selectedIds, toggle } = useMultipleSelection({ defaultSelectedIds: ["a"] });
3520
+ * ```
3521
+ *
3522
+ * @example
3523
+ * ```tsx
3524
+ * // object mode, with a nested field path and disabled rows
3525
+ * interface Row { id: string; locked: boolean }
3526
+ * const { selectedItems, toggleAll, isAllSelected, isPartiallySelected } = useMultipleSelection<Row, string>({
3527
+ * items: rows,
3528
+ * field: "id",
3529
+ * isDisabled: (id) => rows.find((r) => r.id === id)?.locked ?? false,
3530
+ * });
3531
+ * ```
3532
+ */
3533
+ export declare function useMultipleSelection<T = SelectionId, TId extends SelectionId = SelectionId>(options?: UseMultipleSelectionOptions<T, TId>): UseMultipleSelectionReturn<T, TId>;
3534
+ /**
3535
+ * The shape of every `target` parameter across {@link UseOrderReturn}'s
3536
+ * methods - a plain array index when no `field` is configured, or either an
3537
+ * index *or* a full item once one is (letting callers resolve an item's
3538
+ * current position for themselves instead of tracking indices by hand).
3539
+ *
3540
+ * @remarks
3541
+ * Deliberately gated behind `WithField` rather than always allowing `T`:
3542
+ * `useOrder`'s base (no-`field`) overload can be called with `T = number`
3543
+ * (ordering a plain array of numbers), where allowing an item argument too
3544
+ * would make a bare `number` genuinely ambiguous - "index `5`" or "the item
3545
+ * `5`"? Restricting item-based targeting to the `field`-configured overload
3546
+ * (see {@link UseOrderFieldOptions}) sidesteps that ambiguity entirely,
3547
+ * since `field` only makes sense for object items in the first place.
3548
+ *
3549
+ * @typeParam T - The item shape.
3550
+ * @typeParam WithField - Whether `field` was configured - see {@link UseOrderFieldOptions}.
3551
+ */
3552
+ export type OrderTarget<T, WithField extends boolean> = WithField extends true ? number | T : number;
3553
+ /**
3554
+ * Options shared by both {@link useOrder} overloads, with or without `field`
3555
+ * configured.
3556
+ *
3557
+ * @typeParam T - The item shape.
3558
+ */
3559
+ export interface UseOrderBaseOptions<T> {
3560
+ /**
3561
+ * Marks an item as un-movable and un-displaceable - see
3562
+ * {@link UseOrderReturn} for exactly which operations this blocks, and
3563
+ * how.
3564
+ *
3565
+ * @param item - The item to check.
3566
+ * @param index - That item's current index.
3567
+ * @returns `true` if `item` should be locked in place.
3568
+ */
3569
+ isDisabled?: (item: T, index: number) => boolean;
3570
+ }
3571
+ /**
3572
+ * Options for the `field`-configured overload of {@link useOrder}, which
3573
+ * additionally accepts full items (not just indices) as move targets.
3574
+ *
3575
+ * @typeParam T - The item shape.
3576
+ */
3577
+ export interface UseOrderFieldOptions<T> extends UseOrderBaseOptions<T> {
3578
+ /**
3579
+ * A dot-path into `T`, used to resolve an item's id whenever a target is
3580
+ * given as an item rather than a raw index - see {@link OrderTarget}.
3581
+ */
3582
+ field: Path<T> | (string & {});
3583
+ }
3584
+ /**
3585
+ * Return value of {@link useOrder}.
3586
+ *
3587
+ * @typeParam T - The item shape.
3588
+ * @typeParam WithField - Whether `field` was configured - see {@link OrderTarget}.
3589
+ */
3590
+ export interface UseOrderReturn<T, WithField extends boolean = false> {
3591
+ /** The items in their current order. */
3592
+ orderedItems: readonly T[];
3593
+ /**
3594
+ * Moves the item at `target` one position earlier (toward index `0`).
3595
+ * @remarks No-ops at the start of the list, or if `target`'s item or the item before it is disabled.
3596
+ */
3597
+ movePrevious: (target: OrderTarget<T, WithField>) => void;
3598
+ /**
3599
+ * Moves the item at `target` one position later (toward the end).
3600
+ * @remarks No-ops at the end of the list, or if `target`'s item or the item after it is disabled.
3601
+ */
3602
+ moveNext: (target: OrderTarget<T, WithField>) => void;
3603
+ /** Whether {@link UseOrderReturn.movePrevious} would currently have an effect for `target`. */
3604
+ canMovePrevious: (target: OrderTarget<T, WithField>) => boolean;
3605
+ /** Whether {@link UseOrderReturn.moveNext} would currently have an effect for `target`. */
3606
+ canMoveNext: (target: OrderTarget<T, WithField>) => boolean;
3607
+ /**
3608
+ * Moves the item at `target` to the very start of the list.
3609
+ * @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.
3610
+ */
3611
+ moveToStart: (target: OrderTarget<T, WithField>) => void;
3612
+ /**
3613
+ * Moves the item at `target` to the very end of the list.
3614
+ * @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.
3615
+ */
3616
+ moveToEnd: (target: OrderTarget<T, WithField>) => void;
3617
+ /**
3618
+ * Moves the item at `from` to the position at `to`, shifting everything
3619
+ * in between by one slot.
3620
+ * @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.
3621
+ */
3622
+ move: (from: OrderTarget<T, WithField>, to: OrderTarget<T, WithField>) => void;
3623
+ /**
3624
+ * Exchanges the positions of the items at `a` and `b`.
3625
+ * @remarks No-ops if `a` and `b` resolve to the same index, or if either item is disabled.
3626
+ */
3627
+ swap: (a: OrderTarget<T, WithField>, b: OrderTarget<T, WithField>) => void;
3628
+ /** Restores the order to the value `initialItems` had at mount - see {@link useOrder}'s remarks. */
2881
3629
  resetOrder: () => void;
2882
- replaceOrder: (newOrderedItems: T[]) => void;
3630
+ /** Replaces the entire order with exactly these items. */
3631
+ replaceOrder: (newOrderedItems: readonly T[]) => void;
3632
+ }
3633
+ /**
3634
+ * Manages the order of a list of items - drag-to-reorder UIs, "move
3635
+ * up"/"move down" controls, manual sort overrides layered on top of a base
3636
+ * sort.
3637
+ *
3638
+ * @remarks
3639
+ * - Uncontrolled only for now - controlled mode is planned separately and
3640
+ * will be added without breaking this signature.
3641
+ * - SSR-safe: performs no DOM/window access; `initialItems` must be
3642
+ * deterministic between server and client renders to avoid hydration
3643
+ * mismatches.
3644
+ * - All returned callbacks are manually memoized with `useCallback` so this
3645
+ * hook is safe to use even in codebases **without** the React Compiler.
3646
+ * - Two overloads, picked by whether `field` is configured: without it,
3647
+ * every method's `target` parameter is a plain `number` index; with it,
3648
+ * `target` also accepts a full item, resolved to its current index via
3649
+ * `field`. See {@link OrderTarget} for why this is gated behind `field`
3650
+ * rather than always allowed.
3651
+ * - `isDisabled` blocks differently depending on the operation. For the
3652
+ * pairwise operations (`movePrevious`, `moveNext`, `swap`), *both* items
3653
+ * involved must be movable, since both change position. For the
3654
+ * repositioning operations (`moveToStart`, `moveToEnd`, `move`), only the
3655
+ * item being moved is checked - items it displaces shift by one slot but
3656
+ * are never asked to swap places, so their own `isDisabled` state isn't
3657
+ * consulted for the move to proceed.
3658
+ * - `resetOrder` restores the value `initialItems` had at mount, not
3659
+ * whatever it is on the current render - a later change to the
3660
+ * `initialItems` prop doesn't retroactively change what `resetOrder`
3661
+ * restores to.
3662
+ *
3663
+ * @typeParam T - The item shape.
3664
+ * @param initialItems - The items to manage, in their starting order.
3665
+ * @param options - See {@link UseOrderBaseOptions} / {@link UseOrderFieldOptions}.
3666
+ * @returns The current order and the actions to change it. See {@link UseOrderReturn}.
3667
+ *
3668
+ * @example
3669
+ * Index-only:
3670
+ * ```tsx
3671
+ * const { orderedItems, movePrevious, moveNext } = useOrder(["a", "b", "c"]);
3672
+ * ```
3673
+ *
3674
+ * @example
3675
+ * With `field`, so items themselves can be passed as move targets:
3676
+ * ```tsx
3677
+ * interface Row { id: string; label: string }
3678
+ * const { orderedItems, movePrevious } = useOrder<Row>(rows, { field: "id" });
3679
+ * // movePrevious(rows[2]) works directly - no manual index lookup needed
3680
+ * ```
3681
+ */
3682
+ export declare function useOrder<T extends Record<string, unknown>>(initialItems: readonly T[] | undefined, options: UseOrderFieldOptions<T>): UseOrderReturn<T, true>;
3683
+ export declare function useOrder<T>(initialItems?: readonly T[], options?: UseOrderBaseOptions<T>): UseOrderReturn<T, false>;
3684
+ /** Return value of {@link usePagination}. */
3685
+ export interface UsePaginationReturn<T> {
3686
+ /** The items for the current page - a slice of `data`, `pageSize` items long (fewer on the last page if `totalCount` doesn't divide evenly). */
3687
+ pageItems: T[];
3688
+ /**
3689
+ * Current page, 1-based.
3690
+ * @remarks Always clamped into `[1, totalPages]` for display, even if
3691
+ * the underlying position becomes momentarily out of range (e.g. `data`
3692
+ * shrinks). The clamp is display-only - see the `currentPageIndex` note
3693
+ * in the implementation for why the real position isn't lost.
3694
+ */
3695
+ pageNumber: number;
3696
+ /** Items per page. */
3697
+ pageSize: number;
3698
+ /** `Math.max(1, Math.ceil(totalCount / pageSize))` - always at least `1`, even for an empty `data`. */
3699
+ totalPages: number;
3700
+ /** `data.length` (after the array-safety check) - the un-paginated item count. */
3701
+ totalCount: number;
3702
+ /** 1-based index of the first item on the current page, for a "Showing X-Y of Z" display. `0` when `totalCount` is `0`. */
3703
+ startIndex: number;
3704
+ /** 1-based index of the last item on the current page. `0` when `totalCount` is `0`. */
3705
+ endIndex: number;
3706
+ /** Whether {@link UsePaginationReturn.previousPage} would move anywhere. */
3707
+ canPreviousPage: boolean;
3708
+ /** Whether {@link UsePaginationReturn.nextPage} would move anywhere. */
3709
+ canNextPage: boolean;
3710
+ /** Moves to the next page, if any. No-op (not a warning) on the last page - this is routine UI usage, not caller error. */
3711
+ nextPage: () => void;
3712
+ /** Moves to the previous page, if any. No-op on the first page. */
3713
+ previousPage: () => void;
3714
+ /** Jumps to page `1`. */
3715
+ goToFirstPage: () => void;
3716
+ /** Jumps to the last page. */
3717
+ goToLastPage: () => void;
3718
+ /**
3719
+ * Jumps to a specific page.
3720
+ * @remarks A well-formed but out-of-range page (e.g. `999` on a
3721
+ * 5-page list) is clamped to the nearest valid page, not rejected -
3722
+ * only a malformed value (non-integer, `< 1`, `NaN`, etc.) is treated
3723
+ * as caller error. See {@link isPositiveInteger}.
3724
+ */
3725
+ goToPage: (newPageNumber: number) => void;
3726
+ /**
3727
+ * Changes `pageSize`.
3728
+ * @remarks `pageNumber` is left untouched here - it re-bounds itself
3729
+ * automatically against the new `totalPages` on the next render.
3730
+ */
3731
+ changePageSize: (newPageSize: number) => void;
3732
+ /** 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. */
3733
+ resetPageNumber: () => void;
3734
+ /** Restores `pageSize` to the value passed as `initialPageSize` at mount. */
3735
+ resetPageSize: () => void;
3736
+ /** Restores both `pageNumber` and `pageSize` to their mount-time initial values, in one update. */
3737
+ resetPagination: () => void;
3738
+ }
3739
+ /**
3740
+ * Paginates an array client-side - slices `data` into pages and exposes the
3741
+ * navigation state and actions to move between them.
3742
+ *
3743
+ * @remarks
3744
+ * - **Validation is dev/prod-split**: passing a malformed page number or
3745
+ * page size (non-integer, `< 1`, `NaN`, etc.) to the constructor or any
3746
+ * mutator throws immediately in development (surfacing the bug fast), but
3747
+ * silently falls back to the last valid value in production, rather than
3748
+ * crashing a real user's session over a caller mistake. See
3749
+ * {@link isPositiveInteger}.
3750
+ * - **Out-of-range is different from malformed.** `goToPage(999)` on a
3751
+ * 5-page list isn't an error - it's clamped to the last page. Only
3752
+ * structurally invalid input (see above) is treated as a mistake.
3753
+ * - **`pageNumber` is 1-based** in this public API; page count/index math
3754
+ * is kept 0-based internally.
3755
+ * - **Reset targets are frozen at mount.** `resetPageNumber`/`resetPageSize`/
3756
+ * `resetPagination` always restore the `initialPageNumber`/`initialPageSize`
3757
+ * values as they were on the very first render - passing different values
3758
+ * to `usePagination` on a later render does not change what reset
3759
+ * restores to.
3760
+ * - **No manual/server-side pagination mode** - `data` is always assumed to
3761
+ * be the complete, un-paginated dataset, sliced client-side.
3762
+ *
3763
+ * @typeParam T - The type of each item in `data`.
3764
+ * @param data - The full, un-paginated array. Defaults to `[]`.
3765
+ * @param initialPageSize - Items per page at mount.
3766
+ * @defaultValue initialPageSize `10`
3767
+ * @param initialPageNumber - Starting page (1-based) at mount.
3768
+ * @defaultValue initialPageNumber `1`
3769
+ * @returns The current page's items, position, and the actions to
3770
+ * navigate/resize/reset. See {@link UsePaginationReturn}.
3771
+ *
3772
+ * @example
3773
+ * ```tsx
3774
+ * const { pageItems, pageNumber, totalPages, nextPage, previousPage } =
3775
+ * usePagination(rows, 20);
3776
+ *
3777
+ * return (
3778
+ * <>
3779
+ * {pageItems.map((row) => <Row key={row.id} {...row} />)}
3780
+ * <button onClick={previousPage}>Prev</button>
3781
+ * <span>{pageNumber} / {totalPages}</span>
3782
+ * <button onClick={nextPage}>Next</button>
3783
+ * </>
3784
+ * );
3785
+ * ```
3786
+ */
3787
+ export declare function usePagination<T>(data?: T[], initialPageSize?: number, initialPageNumber?: number): UsePaginationReturn<T>;
3788
+ /** The id type `usePin` operates on by default - a raw `string` or `number`. */
3789
+ export type PinId = string | number;
3790
+ /**
3791
+ * Options for {@link usePin}, in either of its two modes - id-only (`T`
3792
+ * defaults to `TId`) or object mode (`T` is a distinct item shape, `TId`
3793
+ * its resolved id type).
3794
+ *
3795
+ * @remarks
3796
+ * The conditional shape is what makes `field` required in object mode and
3797
+ * disallowed in id-only mode, entirely at the type level - see
3798
+ * {@link usePin}'s remarks for why.
3799
+ *
3800
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
3801
+ * @typeParam TId - The id type.
3802
+ */
3803
+ export type UsePinOptions<T = PinId, TId extends PinId = PinId> = T extends TId ? {
3804
+ /** The full list of pinnable items - backs `pinnedItems`/`unpinnedItems`, and is the default target for `pinAll` when it's called with no argument. */
3805
+ items?: readonly T[];
3806
+ /** Ids pinned from the start - frozen at mount, see {@link usePin}'s remarks. */
3807
+ initialPinnedIds?: readonly TId[];
3808
+ /** The maximum number of ids that can be pinned at once. `undefined` means unlimited. */
3809
+ maxPins?: number;
3810
+ } : {
3811
+ /** The full list of pinnable items - backs `pinnedItems`/`unpinnedItems`, and is the default target for `pinAll` when it's called with no argument. */
3812
+ items: readonly T[];
3813
+ /** 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. */
3814
+ field: Path<T> | (string & {});
3815
+ /** Ids pinned from the start - frozen at mount, see {@link usePin}'s remarks. */
3816
+ initialPinnedIds?: readonly TId[];
3817
+ /** The maximum number of ids that can be pinned at once. `undefined` means unlimited. */
3818
+ maxPins?: number;
3819
+ };
3820
+ /**
3821
+ * Return value of {@link usePin}.
3822
+ *
3823
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
3824
+ * @typeParam TId - The id type.
3825
+ */
3826
+ export interface UsePinReturn<T = PinId, TId extends PinId = PinId> {
3827
+ /** The currently pinned ids. */
3828
+ pinnedIds: readonly TId[];
3829
+ /** The subset of `items` that are currently pinned. */
3830
+ pinnedItems: readonly T[];
3831
+ /** The subset of `items` that are not currently pinned. */
3832
+ unpinnedItems: readonly T[];
3833
+ /** `pinnedIds.length`. */
3834
+ pinnedCount: number;
3835
+ /** Whether anything at all is pinned. */
3836
+ hasPins: boolean;
3837
+ /** Whether `pinnedCount` has reached `maxPins`. */
3838
+ isAtMaxLimit: boolean;
3839
+ /** The current pin limit - `Number.MAX_SAFE_INTEGER` when unset/unlimited. */
3840
+ maxPins: number;
3841
+ /**
3842
+ * Pins `itemOrId`.
3843
+ * @remarks No-ops if it's already pinned, or if `maxPins` has been reached.
3844
+ */
3845
+ pin: (itemOrId: TId | T) => void;
3846
+ /** Unpins `itemOrId`. Always allowed, even past `maxPins` - removal never needs to check the limit. */
3847
+ unpin: (itemOrId: TId | T) => void;
3848
+ /**
3849
+ * Pins `itemOrId` if it isn't pinned, unpins it if it is.
3850
+ * @remarks The pin-direction is blocked once `maxPins` is reached; the unpin-direction never is.
3851
+ */
3852
+ togglePin: (itemOrId: TId | T) => void;
3853
+ /** Whether `itemOrId` is currently pinned. */
3854
+ isPinned: (itemOrId: TId | T) => boolean;
3855
+ /** 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`. */
3856
+ canPin: (itemOrId: TId | T) => boolean;
3857
+ /** Unpins everything. */
3858
+ clearPins: () => void;
3859
+ /** Restores the pinned set to the value `initialPinnedIds` had at mount - see {@link usePin}'s remarks. */
3860
+ resetPins: () => void;
3861
+ /** Replaces the entire pinned set with exactly these ids/items, truncated to the first `maxPins` if it exceeds the current limit. */
3862
+ replacePins: (newPinnedItems: readonly TId[] | readonly T[]) => void;
3863
+ /**
3864
+ * Pins every id/item given, or every item in `items` if called with no
3865
+ * argument.
3866
+ * @remarks If `maxPins` is reached partway through, whatever already fit stays pinned rather than the whole call being rejected.
3867
+ */
3868
+ pinAll: (itemsArray?: readonly TId[] | readonly T[]) => void;
3869
+ /** Unpins every id/item given, or unpins everything if called with no argument. */
3870
+ unpinAll: (itemsArray?: readonly TId[] | readonly T[]) => void;
3871
+ /** Changes `maxPins` to a new value. Does not retroactively unpin anything already over the new limit. */
3872
+ changeMaxPins: (newMaxPins: number) => void;
3873
+ /** Restores `maxPins` to the value it had at mount. */
3874
+ resetMaxPins: () => void;
2883
3875
  }
2884
- export declare function useOrder<T>(initialItems?: T[]): UseOrderReturn<T>;
2885
- export interface UsePaginationReturn<T> {
2886
- pageItems: T[];
2887
- pageSize: number;
2888
- pageIndex: number;
2889
- totalPages: number;
2890
- canPrevious: boolean;
2891
- canNext: boolean;
2892
- nextPage: () => void;
2893
- previousPage: () => void;
2894
- goToFirstPage: () => void;
2895
- goToLastPage: () => void;
2896
- goToPage: (newPageIndex: number) => void;
2897
- changePageSize: (newPageSize: number) => void;
2898
- resetPageIndex: () => void;
2899
- resetPageSize: () => void;
2900
- resetPagination: () => void;
3876
+ /**
3877
+ * Manages a pinned/starred subset of a list - pinned rows in a table,
3878
+ * favorited items, "keep at top" behavior.
3879
+ *
3880
+ * @remarks
3881
+ * - Uncontrolled only for now - controlled mode is planned separately and
3882
+ * will be added without breaking this signature.
3883
+ * - SSR-safe: performs no DOM/window access; `initialPinnedIds` must be
3884
+ * deterministic between server and client renders to avoid hydration
3885
+ * mismatches.
3886
+ * - All returned callbacks are manually memoized with `useCallback` so this
3887
+ * hook is safe to use even in codebases **without** the React Compiler.
3888
+ * - Two modes, picked by whether `T` is assignable to `TId`: id-only
3889
+ * (default) - `itemOrId` parameters only ever receive raw ids, `field`
3890
+ * is disallowed; or object mode - `<Row, string>` plus a required
3891
+ * `field`, letting `itemOrId` parameters take a full item too. See
3892
+ * {@link UsePinOptions}.
3893
+ * - `maxPins` truncation always keeps the *first* ids/items and warns in
3894
+ * dev about the rest - this applies at mount (`initialPinnedIds`),
3895
+ * `replacePins`, and `pinAll`.
3896
+ * - `resetPins` and `resetMaxPins` are independent - resetting one doesn't
3897
+ * touch the other, and each restores exactly the value its own option
3898
+ * had at mount, not whatever it is on the current render.
3899
+ *
3900
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
3901
+ * @typeParam TId - The id type.
3902
+ * @param options - See {@link UsePinOptions}.
3903
+ * @returns The current pinned state and the actions to change it. See {@link UsePinReturn}.
3904
+ *
3905
+ * @example
3906
+ * Id-only:
3907
+ * ```tsx
3908
+ * const { pinnedIds, pin, isPinned } = usePin({ initialPinnedIds: ["row-1"] });
3909
+ * ```
3910
+ *
3911
+ * @example
3912
+ * Object mode, with a pin limit:
3913
+ * ```tsx
3914
+ * interface Row { id: string; label: string }
3915
+ * const { pinnedItems, pin, canPin } = usePin<Row, string>({
3916
+ * items: rows,
3917
+ * field: "id",
3918
+ * maxPins: 5,
3919
+ * });
3920
+ * ```
3921
+ */
3922
+ export declare function usePin<T = PinId, TId extends PinId = PinId>(options?: UsePinOptions<T, TId>): UsePinReturn<T, TId>;
3923
+ /**
3924
+ * Configuration options for {@link useSingleSelection}.
3925
+ *
3926
+ * @typeParam TId - The concrete id type used by this selection instance.
3927
+ * Must be assignable to {@link SelectionId} (`string | number`), but can be
3928
+ * a narrower/branded type (e.g. `UserId`) for stronger inference at call sites.
3929
+ */
3930
+ export interface UseSingleSelectionOptions<TId extends SelectionId = SelectionId> {
3931
+ /**
3932
+ * The id that is selected on mount, and the id that {@link UseSingleSelectionReturn.reset}
3933
+ * returns the selection to.
3934
+ *
3935
+ * @remarks
3936
+ * Only the value present on the **first render** is used to seed state
3937
+ * (standard `useState` initializer semantics — later changes to this value
3938
+ * do not retroactively change the current selection). However, `reset()`
3939
+ * always reads the **latest** `defaultSelectedId` at the time it's called,
3940
+ * so if this value changes across renders, `reset()` will restore to the
3941
+ * newest default, not the original mount-time one.
3942
+ */
3943
+ defaultSelectedId?: TId;
3944
+ /**
3945
+ * Predicate that marks certain ids as non-selectable.
3946
+ *
3947
+ * @remarks
3948
+ * This guard is only enforced inside {@link UseSingleSelectionReturn.select}
3949
+ * and {@link UseSingleSelectionReturn.toggle}. It does **not** retroactively
3950
+ * clear a selection if an already-selected id later becomes disabled — the
3951
+ * hook has no way to know `isDisabled`'s result changed unless you call
3952
+ * `select`/`toggle` again. Reconcile that case yourself (e.g. via an effect)
3953
+ * if it matters for your use case.
3954
+ *
3955
+ * @param id - The id being checked before selection.
3956
+ * @returns `true` if the id must not be selectable.
3957
+ */
3958
+ isDisabled?: (id: TId) => boolean;
2901
3959
  }
2902
- export declare function usePagination<T>(data: T[] | undefined, initialPageSize: number, initialPageIndex?: number): UsePaginationReturn<T>;
2903
- export interface UseSingleSelectionReturn {
2904
- selectedId: SelectionId | undefined;
3960
+ /**
3961
+ * Return shape of {@link useSingleSelection}.
3962
+ *
3963
+ * @typeParam TId - The concrete id type used by this selection instance.
3964
+ */
3965
+ export interface UseSingleSelectionReturn<TId extends SelectionId = SelectionId> {
3966
+ /** The currently selected id, or `undefined` if nothing is selected. */
3967
+ selectedId: TId | undefined;
3968
+ /**
3969
+ * Whether any id is currently selected.
3970
+ *
3971
+ * @remarks
3972
+ * Correctly distinguishes "nothing selected" from a falsy-but-valid id
3973
+ * such as `0` or `""` — this is `selectedId !== undefined`, not `!!selectedId`.
3974
+ */
2905
3975
  hasSelection: boolean;
2906
- select: (id: SelectionId) => void;
3976
+ /**
3977
+ * Selects the given id, replacing any current selection.
3978
+ *
3979
+ * @remarks No-ops if `id` is disabled per `isDisabled`.
3980
+ * @param id - The id to select.
3981
+ */
3982
+ select: (id: TId) => void;
3983
+ /** Clears the current selection (sets it to `undefined`). */
2907
3984
  deselect: () => void;
2908
- toggle: (id: SelectionId) => void;
2909
- isSelected: (id: SelectionId) => boolean;
2910
- resetSelection: () => void;
3985
+ /**
3986
+ * Selects `id` if it isn't already selected; deselects it if it is.
3987
+ *
3988
+ * @remarks No-ops if `id` is disabled per `isDisabled`.
3989
+ * @param id - The id to toggle.
3990
+ */
3991
+ toggle: (id: TId) => void;
3992
+ /**
3993
+ * Checks whether the given id is the currently selected one.
3994
+ *
3995
+ * @param id - The id to check.
3996
+ * @returns `true` if `id` is currently selected.
3997
+ */
3998
+ isSelected: (id: TId) => boolean;
3999
+ /**
4000
+ * Restores the selection to the current `defaultSelectedId`
4001
+ * (or `undefined` if none was provided).
4002
+ */
4003
+ reset: () => void;
2911
4004
  }
2912
- export declare function useSingleSelection(initialSelectedId?: SelectionId): UseSingleSelectionReturn;
4005
+ /**
4006
+ * Manages single-item selection state (e.g. a radio group, a single-select
4007
+ * list, an active tab/row).
4008
+ *
4009
+ * @remarks
4010
+ * - Uncontrolled only for now — controlled mode (`selectedId` + `onSelectionChange`)
4011
+ * is planned separately and will be added without breaking this signature.
4012
+ * - SSR-safe: performs no DOM/window access; `defaultSelectedId` must be
4013
+ * deterministic between server and client renders to avoid hydration mismatches.
4014
+ * - All returned callbacks are manually memoized with `useCallback` so this
4015
+ * hook is safe to use even in codebases **without** the React Compiler.
4016
+ *
4017
+ * @typeParam TId - The concrete id type used by this selection instance.
4018
+ * @param options - Optional configuration. See {@link UseSingleSelectionOptions}.
4019
+ * @returns The current selection state and the actions to mutate it. See {@link UseSingleSelectionReturn}.
4020
+ *
4021
+ * @example
4022
+ * ```tsx
4023
+ * const { selectedId, select, isSelected } = useSingleSelection<string>({
4024
+ * defaultSelectedId: "row-1",
4025
+ * });
4026
+ * ```
4027
+ */
4028
+ export declare function useSingleSelection<TId extends SelectionId = SelectionId>(options?: UseSingleSelectionOptions<TId>): UseSingleSelectionReturn<TId>;
4029
+ /** The comparison strategy for a sort key - selects which built-in comparator is used, or `custom` for a caller-supplied one. */
2913
4030
  export type SortType = "numeric" | "alphabetical" | "alphanumeric" | "boolean" | "date" | "basic" | "custom";
4031
+ /**
4032
+ * How a missing value (per each sort type's own definition of "missing" -
4033
+ * e.g. non-numeric for `numeric`, unparseable for `date`) is positioned
4034
+ * relative to present values.
4035
+ *
4036
+ * @remarks
4037
+ * - `"first"`/`"last"` are **absolute positions** - they never flip with
4038
+ * sort direction, which is the entire reason to choose them over `-1`/`1`.
4039
+ * - `-1`/`1` **simulate a real extreme value** (very small / very large
4040
+ * respectively) - unlike `"first"`/`"last"`, these DO flip with
4041
+ * direction, exactly as a genuine value at that extreme would.
4042
+ *
4043
+ * @defaultValue `"last"`
4044
+ */
4045
+ export type SortUndefinedOption = "first" | "last" | -1 | 1;
4046
+ /** Options shared by every sort type, regardless of comparator. */
2914
4047
  export interface BaseSortOptions {
4048
+ /**
4049
+ * Sort direction.
4050
+ * @defaultValue `false` (ascending)
4051
+ */
2915
4052
  desc?: boolean;
4053
+ /**
4054
+ * When set, `toggleSort` cycles asc -> desc -> asc, skipping the
4055
+ * "remove this sort key" step it would otherwise land on after desc.
4056
+ * @defaultValue `false`
4057
+ */
2916
4058
  disableSortRemoval?: boolean;
4059
+ /**
4060
+ * Flips the effective sort direction without changing `desc` itself -
4061
+ * useful for a column whose "natural" order is descending (e.g. a
4062
+ * priority field where higher should sort first by default).
4063
+ * @defaultValue `false`
4064
+ */
2917
4065
  invertSorting?: boolean;
2918
- sortUndefined?: "first" | "last" | -1 | 1;
4066
+ /** See {@link SortUndefinedOption}. */
4067
+ sortUndefined?: SortUndefinedOption;
2919
4068
  }
4069
+ /** Fields common to every {@link SortConfig}, regardless of `type`. */
2920
4070
  export interface BaseSortConfig extends BaseSortOptions {
4071
+ /** Unique identifier for this sort key - used for lookups (`getSort`, `getSortDirection`, etc.) and to distinguish sort keys in a multi-key `sorts` array. */
2921
4072
  id: string;
4073
+ /** 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. */
2922
4074
  field?: string;
2923
4075
  }
4076
+ /**
4077
+ * A single sort key's full configuration.
4078
+ *
4079
+ * @remarks
4080
+ * Each `type` has its own dedicated union arm (rather than several type
4081
+ * literals sharing one arm) so that {@link SortOptionsForType} - which
4082
+ * extracts a single type's own options via `Extract<SortConfig, { type: T }>`
4083
+ * - resolves correctly per type. Grouping literals under one arm would
4084
+ * make that extraction silently fail for every type in the group.
4085
+ */
2924
4086
  export type SortConfig = BaseSortConfig & ({
2925
- type: "numeric" | "boolean" | "date" | "basic";
4087
+ type: "numeric";
4088
+ } | {
4089
+ type: "boolean";
4090
+ } | {
4091
+ type: "date";
4092
+ dateGranularity?: "day" | "instant";
2926
4093
  } | {
2927
- type: "alphabetical" | "alphanumeric";
4094
+ type: "basic";
4095
+ } | {
4096
+ type: "alphabetical";
4097
+ caseSensitive?: boolean;
4098
+ } | {
4099
+ type: "alphanumeric";
2928
4100
  caseSensitive?: boolean;
2929
4101
  } | {
2930
4102
  type: "custom";
2931
4103
  compare: (a: unknown, b: unknown) => number;
2932
4104
  });
4105
+ /** All active sort keys, in priority order - `sorts[0]` is the primary sort, later entries only break ties left by earlier ones. */
2933
4106
  export type SortState = SortConfig[];
4107
+ /**
4108
+ * A single sort type's own options, with `id`/`type`/`field` (which every
4109
+ * type shares, and which `toggleSort` already takes as separate arguments)
4110
+ * stripped out. Used to type `toggleSort`'s `options` parameter, narrowed
4111
+ * to only the options relevant to the specific `type` being toggled.
4112
+ */
2934
4113
  export type SortOptionsForType<T extends SortType> = Omit<Extract<SortConfig, {
2935
4114
  type: T;
2936
4115
  }>, "id" | "type" | "field">;
4116
+ /**
4117
+ * The update payload type for `updateSortConfig`.
4118
+ *
4119
+ * @remarks
4120
+ * Preserves per-type shape correlation at the type level - e.g. TypeScript
4121
+ * will reject `{ dateGranularity: "day" }` paired with a `type` that isn't
4122
+ * `"date"`. This is best-effort compile-time guidance, not a full
4123
+ * guarantee: a *merge* of an update into an existing config can still land
4124
+ * on a structurally invalid combination that the type system alone can't
4125
+ * catch (partial updates flatten across a union in ways whole-object
4126
+ * construction doesn't). {@link isSortConfigShapeValid}, run on the merged
4127
+ * result inside `applySortUpdate`, is the actual runtime backstop.
4128
+ */
4129
+ export type SortConfigUpdate = DistributivePartial<DistributiveOmit<SortConfig, "id">>;
4130
+ /** Options for {@link useSort}. */
4131
+ export interface UseSortOptions {
4132
+ /**
4133
+ * Defers the sort recomputation (via `useDeferredValue`) so changing
4134
+ * `sorts` doesn't block a more urgent update. Only `sorts` is deferred -
4135
+ * `data` is not.
4136
+ * @defaultValue `false`
4137
+ */
4138
+ defer?: boolean;
4139
+ }
4140
+ /** Return value of {@link useSort}. */
2937
4141
  export interface UseSortReturn<T> {
4142
+ /** `data`, sorted by every entry in `sorts`, applied in priority order. Same reference as `data` (no copy, no sort) when `sorts` is empty. */
2938
4143
  sortedItems: T[];
4144
+ /** The currently-active sort keys, in priority order. */
2939
4145
  sorts: SortState;
4146
+ /** `sorts.length`. */
4147
+ sortCount: number;
4148
+ /** `sortCount > 0`. */
4149
+ hasSorts: boolean;
4150
+ /** Adds a new sort key, or replaces the existing one with the same `id`. */
2940
4151
  upsertSorts: (sort: SortConfig) => void;
4152
+ /** Removes one sort key by `id`, or several at once by passing an array of ids. */
2941
4153
  removeSort: (id: string | string[]) => void;
4154
+ /** Clears every sort key - equivalent to `replaceSorts([])`. */
2942
4155
  clearSorts: () => void;
4156
+ /** 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. */
2943
4157
  resetSorts: () => void;
4158
+ /** Replaces the entire `sorts` array at once. */
2944
4159
  replaceSorts: (sorts: SortState) => void;
4160
+ /**
4161
+ * Cycles a sort key through asc -> desc -> (reset to asc, or remove
4162
+ * entirely) - the standard three-state table-header sort interaction.
4163
+ *
4164
+ * @remarks
4165
+ * - Whether the last step resets to asc or removes the sort key
4166
+ * entirely depends on `disableSortRemoval` (checked on both the
4167
+ * existing config and the newly-passed `options`).
4168
+ * - `options.multi` (`false` by default) controls whether toggling
4169
+ * this key adds/updates it alongside any other active sort keys
4170
+ * (`true`), or replaces the entire `sorts` array with just this one
4171
+ * key (`false`) - the usual "click a column to sort by only that
4172
+ * column, shift-click to add a secondary sort" pattern.
4173
+ * - `type` must be supplied on every call, even for an already-active
4174
+ * sort key - this hook has no independent memory of a key's type
4175
+ * beyond what's in `sorts` itself.
4176
+ */
2945
4177
  toggleSort: <TType extends SortType>(id: string, type: TType, options?: SortOptionsForType<TType> & {
2946
4178
  multi?: boolean;
2947
4179
  field?: string;
2948
4180
  }) => void;
4181
+ /**
4182
+ * Partially updates an existing sort key's config by `id`.
4183
+ * @remarks An unknown `id`, or an update that would produce a
4184
+ * structurally invalid config for its `type`, is rejected - see
4185
+ * {@link isSortConfigShapeValid}.
4186
+ */
4187
+ updateSortConfig: (id: string, partialConfig: SortConfigUpdate) => void;
4188
+ /** Looks up a sort key's full config by `id`. `undefined` if no such key exists. */
4189
+ getSort: (id: string) => SortConfig | undefined;
4190
+ /** `"asc"`/`"desc"` for an active sort key, `undefined` if `id` isn't currently sorted. */
2949
4191
  getSortDirection: (id: string) => "asc" | "desc" | undefined;
4192
+ /** What `toggleSort(id, ...)` would transition `id` to next, without actually calling it - useful for rendering the right sort-direction icon before the user clicks. */
2950
4193
  getNextSortingOrder: (id: string) => "asc" | "desc" | "none";
4194
+ /** This sort key's 1-based priority among active sort keys (`1` = primary), or `undefined` if `id` isn't currently sorted. */
2951
4195
  getSortIndex: (id: string) => number | undefined;
2952
4196
  }
2953
- export declare function useSort<T>(data?: T[], initialSorts?: SortState): UseSortReturn<T>;
4197
+ /**
4198
+ * Sorts an array by one or more keys - single-column or multi-column,
4199
+ * priority determined by array order.
4200
+ *
4201
+ * @remarks
4202
+ * - **Multi-key sort**: `sorts[0]` is the primary sort; later entries in
4203
+ * the array only break ties left unresolved by earlier ones - the same
4204
+ * convention as `Array.prototype.sort` with a compound comparator, or a
4205
+ * spreadsheet's "sort by, then by" dialog.
4206
+ * - **Validation is dev/prod-split**, uniformly across every mutator that
4207
+ * can produce a structurally invalid config (`toggleSort`,
4208
+ * `updateSortConfig`), and across an unresolvable sort key encountered
4209
+ * during sorting itself (unknown `type`, or a `custom` sort missing its
4210
+ * `compare` function): throws immediately in development, but degrades
4211
+ * gracefully in production (an invalid mutation is rejected/no-op; an
4212
+ * unresolvable sort key at compute time is simply skipped, later keys
4213
+ * still apply).
4214
+ * - **`sortUndefined`'s `"first"`/`"last"` are absolute positions**, never
4215
+ * affected by `desc`/`invertSorting`; `-1`/`1` simulate a real extreme
4216
+ * value and do flip with direction - see {@link SortUndefinedOption}.
4217
+ * - **`resetSorts` is frozen at mount** - it restores `initialSorts` as it
4218
+ * was on the very first render, not whatever value that argument holds
4219
+ * on a later render.
4220
+ * - **Id lookups are O(1)**, backed by a `Map` built once per `sorts`
4221
+ * change, not a linear scan per call - safe to call `getSort`/
4222
+ * `getSortDirection`/etc. once per rendered column header without a
4223
+ * performance concern.
4224
+ *
4225
+ * @typeParam T - The type of each item in `data`.
4226
+ * @param data - The items to sort. Defaults to `[]`.
4227
+ * @param initialSorts - Sort keys applied at mount. Defaults to `[]` (no sorting).
4228
+ * @param options - See {@link UseSortOptions}.
4229
+ * @returns The sorted items and the current sort state, plus the actions
4230
+ * to change it. See {@link UseSortReturn}.
4231
+ *
4232
+ * @example
4233
+ * Single column, via a table header click handler:
4234
+ * ```tsx
4235
+ * const { sortedItems, getSortDirection, toggleSort } = useSort(rows);
4236
+ *
4237
+ * <th onClick={() => toggleSort("name", "alphabetical")}>
4238
+ * Name {getSortDirection("name") === "asc" ? "▲" : "▼"}
4239
+ * </th>
4240
+ * ```
4241
+ *
4242
+ * @example
4243
+ * Multi-key, set directly:
4244
+ * ```tsx
4245
+ * const { sortedItems, replaceSorts } = useSort(orders, [
4246
+ * { id: "status", type: "alphabetical" },
4247
+ * { id: "placedAt", type: "date", desc: true },
4248
+ * ]);
4249
+ * // sorted by status first; same-status orders sorted by placedAt, newest first
4250
+ * ```
4251
+ */
4252
+ export declare function useSort<T>(data?: T[], initialSorts?: SortState, options?: UseSortOptions): UseSortReturn<T>;
4253
+ /**
4254
+ * Restricts `field` to a **top-level** key of `T` whose value is assignable to `TId`.
4255
+ *
4256
+ * @remarks
4257
+ * Deliberately not the recursive dot-path style used by `useMultipleSelection`'s
4258
+ * `SelectionIdPath<T, TId>`. Tree nodes are self-referential (a `children` property
4259
+ * whose value is `T[]`, pointing back to `T`), and running the recursive `Path<T>`
4260
+ * machinery over a self-referential type produces a genuine TypeScript circular-reference
4261
+ * error. A tree node's id is virtually always a direct top-level property anyway — a
4262
+ * dot-path id nested inside a tree node would be an unusual shape — so this simpler,
4263
+ * non-recursive key filter is both the fix and the more correct constraint. The function
4264
+ * form below remains available for anything this doesn't cover.
4265
+ */
4266
+ export type FieldKey<T, TId extends SelectionId> = {
4267
+ [K in keyof T]: T[K] extends TId ? K : never;
4268
+ }[keyof T] & string;
4269
+ /** Restricts `childrenField` to a top-level key of `T` whose value is `T[]` (or `undefined`). */
4270
+ export type ChildrenKey<T> = {
4271
+ [K in keyof T]: T[K] extends readonly T[] | undefined ? K : never;
4272
+ }[keyof T] & string;
4273
+ /** Either a type-checked property key, or a function, for extracting a node's id. */
4274
+ export type FieldAccessor<T, TId extends SelectionId> = FieldKey<T, TId> | ((item: T) => TId);
4275
+ /** Either a type-checked property key, or a function, for extracting a node's children. */
4276
+ export type ChildrenAccessor<T> = ChildrenKey<T> | ((item: T) => readonly T[] | undefined);
4277
+ /** The three states a tree node can be in with respect to the current selection. */
4278
+ export type TreeNodeState = "selected" | "indeterminate" | "unselected";
4279
+ /** One flattened tree entry, produced by {@link flattenForest}. */
4280
+ export interface FlatTreeEntry<T, TId extends SelectionId> {
4281
+ item: T;
4282
+ id: TId;
4283
+ parentId: TId | undefined;
4284
+ depth: number;
4285
+ }
4286
+ /** The precomputed structures every tree-selection operation is built on. */
4287
+ export interface FlattenedForest<T, TId extends SelectionId> {
4288
+ /** Every node in the forest, in DFS pre-order. */
4289
+ entries: readonly FlatTreeEntry<T, TId>[];
4290
+ /** O(1) lookup from id to its flattened entry. */
4291
+ entryById: ReadonlyMap<TId, FlatTreeEntry<T, TId>>;
4292
+ /** O(1) lookup from a node's id to its direct children's ids. Roots are keyed under `undefined`. */
4293
+ childIdsByParentId: ReadonlyMap<TId | undefined, readonly TId[]>;
4294
+ }
4295
+ export interface UseTreeSelectionOptions<T, TId extends SelectionId> {
4296
+ /** The forest — an array of root nodes. Each node's children are found via `childrenField`. */
4297
+ items: readonly T[];
4298
+ /** How to extract a node's id. A type-checked top-level key, or a function. */
4299
+ field: FieldAccessor<T, TId>;
4300
+ /** How to extract a node's children. A type-checked top-level key, or a function. */
4301
+ childrenField: ChildrenAccessor<T>;
4302
+ /**
4303
+ * The ids selected on mount, and what {@link UseTreeSelectionReturn.reset} returns to.
4304
+ *
4305
+ * @remarks
4306
+ * Not trusted as already cascade-consistent — always run through the same
4307
+ * full bottom-up normalization used by `reset`/`selectAll`/etc, so passing
4308
+ * e.g. only a leaf (without its ancestors) still produces correct indeterminate
4309
+ * ancestor state from the start.
4310
+ */
4311
+ defaultSelectedIds?: readonly TId[];
4312
+ /**
4313
+ * Predicate marking certain ids as non-selectable.
4314
+ *
4315
+ * @remarks
4316
+ * A disabled node is skipped during cascade (never force-toggled) and excluded
4317
+ * from every "are all children selected" computation, but disabling a node does
4318
+ * **not** auto-disable its descendants, and does not retroactively clear it if
4319
+ * it was already selected before becoming disabled — same philosophy as the
4320
+ * flat selection hooks.
4321
+ */
4322
+ isDisabled?: (id: TId) => boolean;
4323
+ /**
4324
+ * Independently controls whether selecting a node propagates to its descendants
4325
+ * (`down`) and/or its ancestors (`up`). Both default to `true`.
4326
+ *
4327
+ * @remarks
4328
+ * `{ down: false, up: false }` degenerates into flat, non-hierarchical
4329
+ * selection over tree-rendered nodes (no relationship between a node's
4330
+ * selection and its parent/children) — the same shape react-arborist/MUI's
4331
+ * default tree multi-select uses, as opposed to checkbox-tree cascading.
4332
+ */
4333
+ cascade?: {
4334
+ down?: boolean;
4335
+ up?: boolean;
4336
+ };
4337
+ /**
4338
+ * When `true`, {@link UseTreeSelectionReturn.selectedIds} and
4339
+ * {@link UseTreeSelectionReturn.selectedItems} only include leaf nodes,
4340
+ * even if a fully-covered branch is internally tracked as selected.
4341
+ *
4342
+ * @remarks
4343
+ * This does not change `select`/`toggle`/cascade behavior at all — you can
4344
+ * still call `select` on a branch and it cascades normally, and
4345
+ * {@link UseTreeSelectionReturn.getNodeState} still reports branches correctly
4346
+ * as `"selected"`/`"indeterminate"` for display regardless of this option. It
4347
+ * only changes what the flat `selectedIds`/`selectedItems` arrays report — useful
4348
+ * when branches are just a UI grouping and the "real" selected resources are
4349
+ * always the leaves (e.g. sending a set of leaf resource ids to a backend).
4350
+ * {@link UseTreeSelectionReturn.selectedLeafIds} gives you the leaf subset
4351
+ * regardless of this option, if you want both views at once.
4352
+ */
4353
+ leafOnly?: boolean;
4354
+ }
4355
+ export interface UseTreeSelectionReturn<T, TId extends SelectionId> {
4356
+ /**
4357
+ * Every fully-selected node's id, in forest (DFS pre-order) order.
4358
+ * @remarks Includes branch ids too, unless `leafOnly` is set. See {@link UseTreeSelectionOptions.leafOnly}.
4359
+ */
4360
+ selectedIds: readonly TId[];
4361
+ /** Just the leaf ids among the current selection, regardless of `leafOnly`. */
4362
+ selectedLeafIds: readonly TId[];
4363
+ /** Every node currently in a partial (some-but-not-all-descendants-selected) state. */
4364
+ indeterminateIds: readonly TId[];
4365
+ /** The item objects corresponding to {@link UseTreeSelectionReturn.selectedIds}. */
4366
+ selectedItems: readonly T[];
4367
+ /** `selectedIds.length`. */
4368
+ selectedCount: number;
4369
+ /** Whether nothing at all is selected (not even indeterminate). */
4370
+ isEmpty: boolean;
4371
+ /** Whether every selectable root (and by construction, everything under it) is selected. */
4372
+ isAllSelected: boolean;
4373
+ /** Whether some, but not all, of the forest is selected or indeterminate. */
4374
+ isPartiallySelected: boolean;
4375
+ /**
4376
+ * The full tri-state read for a node — the primitive the rest of the boolean
4377
+ * getters below are built from.
4378
+ * @param item - A raw id, or a full item.
4379
+ */
4380
+ getNodeState: (item: TId | T) => TreeNodeState;
4381
+ /** `getNodeState(item) === "selected"`. */
4382
+ isSelected: (item: TId | T) => boolean;
4383
+ /** `getNodeState(item) === "indeterminate"`. */
4384
+ isIndeterminate: (item: TId | T) => boolean;
4385
+ /**
4386
+ * Selects `item`, cascading per the `cascade` option.
4387
+ * @remarks No-ops if `item` itself resolves to a disabled id.
4388
+ */
4389
+ select: (item: TId | T) => void;
4390
+ /** Deselects `item`, cascading per the `cascade` option. Always allowed, even for disabled ids. */
4391
+ deselect: (item: TId | T) => void;
4392
+ /**
4393
+ * Selects `item` if not selected, deselects it if selected, cascading per the `cascade` option.
4394
+ * @remarks The select-direction is blocked for disabled ids; the deselect-direction never is.
4395
+ */
4396
+ toggle: (item: TId | T) => void;
4397
+ /** Restores the selection to the current `defaultSelectedIds` (normalized), or empty if none was given. */
4398
+ reset: () => void;
4399
+ /** Replaces the entire selection with exactly these ids/items (normalized — always internally consistent afterward). */
4400
+ replaceSelection: (newSelectedItems: readonly TId[] | readonly T[]) => void;
4401
+ /** Selects every selectable node in the forest. */
4402
+ selectAll: () => void;
4403
+ /** Clears the entire selection. */
4404
+ deselectAll: () => void;
4405
+ /** If everything selectable is currently selected, clears the selection; otherwise selects everything selectable. */
4406
+ toggleAll: () => void;
4407
+ /**
4408
+ * Selects multiple ids/items at once, cascading each per the `cascade` option.
4409
+ *
4410
+ * @remarks
4411
+ * More efficient than calling {@link UseTreeSelectionReturn.select} in a loop:
4412
+ * shared ancestors of multiple targets are only recomputed once each, not once
4413
+ * per target that shares them.
4414
+ */
4415
+ selectMultiple: (newItems: readonly TId[] | readonly T[]) => void;
4416
+ /** Deselects multiple ids/items at once, cascading each per the `cascade` option. Always allowed, even for disabled ids. */
4417
+ deselectMultiple: (itemsToRemove: readonly TId[] | readonly T[]) => void;
4418
+ /**
4419
+ * Keeps only the ids/items in `itemsToRetain` that are already selected (an
4420
+ * intersection); never adds anything new. The result is re-normalized, so
4421
+ * ancestor indeterminate state stays correct after the shrink.
4422
+ */
4423
+ retainOnly: (itemsToRetain: readonly TId[] | readonly T[]) => void;
4424
+ /**
4425
+ * Inverts the selection at the leaf level: every currently-unselected
4426
+ * selectable leaf becomes selected, every currently-selected one becomes
4427
+ * unselected. Branch/indeterminate state is re-derived from the result.
4428
+ * @remarks A disabled leaf that was selected before this call does not survive
4429
+ * it — like `useMultipleSelection`'s `invertSelection`, this is a full replace,
4430
+ * not a merge.
4431
+ */
4432
+ invertSelection: () => void;
4433
+ /** The id of `item`'s direct parent, or `undefined` if it's a root. */
4434
+ getParentId: (item: TId | T) => TId | undefined;
4435
+ /** Every ancestor id of `item`, nearest first, root last. Empty if `item` is a root. */
4436
+ getAncestorIds: (item: TId | T) => readonly TId[];
4437
+ /** Every descendant id of `item`. Empty if `item` is a leaf. */
4438
+ getDescendantIds: (item: TId | T) => readonly TId[];
4439
+ }
4440
+ /**
4441
+ * Manages hierarchical (tree/forest) selection state — checkbox trees, nested
4442
+ * category pickers, permission trees, file explorers.
4443
+ *
4444
+ * @remarks
4445
+ * - Uncontrolled only for now, SSR-safe (no DOM access), all callbacks manually
4446
+ * memoized — same guarantees as the other selection hooks in this library.
4447
+ * - Expand/collapse state is explicitly **not** managed here — it's an orthogonal
4448
+ * view concern, not a selection concern (this hook only ever reads the full
4449
+ * `children` structure, regardless of what's currently expanded in the UI).
4450
+ * - **Performance**: a single `select`/`deselect`/`toggle` call is
4451
+ * O(affected subtree) for the cascade-down step plus O(depth × branching factor)
4452
+ * for the cascade-up step — it never re-walks the whole tree. Only whole-forest
4453
+ * operations (`selectAll`, `toggleAll`, `reset`, `replaceSelection`, and the
4454
+ * initial mount) do a full O(n) pass, which is the right complexity for
4455
+ * something that touches every node anyway. Verified independent of selection
4456
+ * order (a known bug class in at least one production tree-selection library
4457
+ * is indeterminate state differing based on the order nodes were selected in).
4458
+ * - If `items` (the tree **structure**) changes after mount — nodes added, removed,
4459
+ * or moved — the existing selection/indeterminate state is **not** automatically
4460
+ * re-normalized against the new shape (to avoid surprise O(n) work on every data
4461
+ * refresh). It stays correct for anything the change didn't touch, but the newly
4462
+ * changed area may need an explicit `reset()` or `replaceSelection()` to
4463
+ * guarantee full consistency again.
4464
+ *
4465
+ * @typeParam T - The tree node shape.
4466
+ * @typeParam TId - The id type. Defaults to `SelectionId`.
4467
+ * @param options - See {@link UseTreeSelectionOptions}.
4468
+ * @returns The current selection state and the actions to mutate it. See {@link UseTreeSelectionReturn}.
4469
+ *
4470
+ * @example
4471
+ * ```tsx
4472
+ * interface Category { id: string; name: string; children?: Category[] }
4473
+ *
4474
+ * const { getNodeState, toggle, isAllSelected, toggleAll } = useTreeSelection<Category, string>({
4475
+ * items: categoryTree,
4476
+ * field: "id",
4477
+ * childrenField: "children",
4478
+ * });
4479
+ * ```
4480
+ */
4481
+ export declare function useTreeSelection<T, TId extends SelectionId = SelectionId>(options: UseTreeSelectionOptions<T, TId>): UseTreeSelectionReturn<T, TId>;
4482
+ /** The id type `useVisibility` operates on by default - a raw `string` or `number`. */
4483
+ export type VisibilityId = string | number;
4484
+ /**
4485
+ * Options for {@link useVisibility}, in either of its two modes - id-only
4486
+ * (`T` defaults to `TId`) or object mode (`T` is a distinct item shape,
4487
+ * `TId` its resolved id type).
4488
+ *
4489
+ * @remarks
4490
+ * The conditional shape is what makes `field` required in object mode and
4491
+ * disallowed in id-only mode, entirely at the type level - see
4492
+ * {@link useVisibility}'s remarks for why.
4493
+ *
4494
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
4495
+ * @typeParam TId - The id type.
4496
+ */
4497
+ export type UseVisibilityOptions<T = VisibilityId, TId extends VisibilityId = VisibilityId> = T extends TId ? {
4498
+ /** 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. */
4499
+ items?: readonly T[];
4500
+ /** Ids visible from the start - frozen at mount, see {@link useVisibility}'s remarks. */
4501
+ initialVisibleIds?: readonly TId[];
4502
+ } : {
4503
+ /** 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. */
4504
+ items: readonly T[];
4505
+ /** 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. */
4506
+ field: Path<T> | (string & {});
4507
+ /** Ids visible from the start - frozen at mount, see {@link useVisibility}'s remarks. */
4508
+ initialVisibleIds?: readonly TId[];
4509
+ };
4510
+ /**
4511
+ * Return value of {@link useVisibility}.
4512
+ *
4513
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
4514
+ * @typeParam TId - The id type.
4515
+ */
4516
+ export interface UseVisibilityReturn<T = VisibilityId, TId extends VisibilityId = VisibilityId> {
4517
+ /** The currently visible ids. */
4518
+ visibleIds: readonly TId[];
4519
+ /** The subset of `items` that are currently visible. */
4520
+ visibleItems: readonly T[];
4521
+ /** The subset of `items` that are currently hidden. */
4522
+ hiddenItems: readonly T[];
4523
+ /** `visibleIds.length`. */
4524
+ visibleCount: number;
4525
+ /** `hiddenItems.length`. */
4526
+ hiddenCount: number;
4527
+ /** Whether anything at all is visible. */
4528
+ hasVisible: boolean;
4529
+ /** Whether `itemOrId` is currently visible. */
4530
+ isVisible: (itemOrId: TId | T) => boolean;
4531
+ /** Shows `itemOrId`. No-ops if it's already visible. */
4532
+ show: (itemOrId: TId | T) => void;
4533
+ /** Hides `itemOrId`. No-ops if it's already hidden. */
4534
+ hide: (itemOrId: TId | T) => void;
4535
+ /** Shows `itemOrId` if it's hidden, hides it if it's visible. */
4536
+ toggleVisibility: (itemOrId: TId | T) => void;
4537
+ /** 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. */
4538
+ showAll: (itemsArray?: readonly TId[] | readonly T[]) => void;
4539
+ /**
4540
+ * Hides every id/item given, or hides everything if called with no
4541
+ * argument.
4542
+ * @remarks Unlike {@link UseVisibilityReturn.showAll}, "no argument" means a blanket clear, not "act on the hook's own `items`" - see {@link useVisibility}'s remarks.
4543
+ */
4544
+ hideAll: (itemsArray?: readonly TId[] | readonly T[]) => void;
4545
+ /** Restores the visible set to the value `initialVisibleIds` had at mount - see {@link useVisibility}'s remarks. */
4546
+ resetVisibility: () => void;
4547
+ /** Replaces the entire visible set with exactly these ids/items. */
4548
+ replaceVisibility: (newVisibleItems: readonly TId[] | readonly T[]) => void;
4549
+ }
4550
+ /**
4551
+ * Manages which item(s) in a list are visible - show/hide toggles, column
4552
+ * visibility, filterable chip lists.
4553
+ *
4554
+ * @remarks
4555
+ * - Uncontrolled only for now - controlled mode is planned separately and
4556
+ * will be added without breaking this signature.
4557
+ * - SSR-safe: performs no DOM/window access; `initialVisibleIds` must be
4558
+ * deterministic between server and client renders to avoid hydration
4559
+ * mismatches.
4560
+ * - All returned callbacks are manually memoized with `useCallback` so this
4561
+ * hook is safe to use even in codebases **without** the React Compiler.
4562
+ * - Two modes, picked by whether `T` is assignable to `TId`: id-only
4563
+ * (default) - `itemOrId` parameters only ever receive raw ids, `field`
4564
+ * is disallowed; or object mode - `<Row, string>` plus a required
4565
+ * `field`, letting `itemOrId` parameters take a full item too. See
4566
+ * {@link UseVisibilityOptions}.
4567
+ * - `showAll`/`hideAll` intentionally differ in what "no argument" means:
4568
+ * `showAll()` acts on the hook's own `items` (additive - anything
4569
+ * already visible but no longer in `items` is left alone), while
4570
+ * `hideAll()` is a blanket clear regardless of `items` (so a
4571
+ * previously-visible id that's since fallen out of `items` doesn't stay
4572
+ * visible forever). Passing an explicit array to either scopes it to
4573
+ * just that subset.
4574
+ *
4575
+ * @typeParam T - The item shape, or `TId` itself for id-only mode.
4576
+ * @typeParam TId - The id type.
4577
+ * @param options - See {@link UseVisibilityOptions}.
4578
+ * @returns The current visibility state and the actions to change it. See {@link UseVisibilityReturn}.
4579
+ *
4580
+ * @example
4581
+ * Id-only:
4582
+ * ```tsx
4583
+ * const { isVisible, toggleVisibility } = useVisibility({
4584
+ * initialVisibleIds: ["col-name", "col-email"],
4585
+ * });
4586
+ * ```
4587
+ *
4588
+ * @example
4589
+ * Object mode:
4590
+ * ```tsx
4591
+ * interface Column { id: string; label: string }
4592
+ * const { visibleItems, hide, showAll } = useVisibility<Column, string>({
4593
+ * items: columns,
4594
+ * field: "id",
4595
+ * });
4596
+ * ```
4597
+ */
4598
+ export declare function useVisibility<T = VisibilityId, TId extends VisibilityId = VisibilityId>(options?: UseVisibilityOptions<T, TId>): UseVisibilityReturn<T, TId>;
4599
+ export interface FuzzySearchOptions {
4600
+ threshold?: number;
4601
+ caseSensitive?: boolean;
4602
+ matchStrategy?: "any" | "all";
4603
+ exactPhraseBonus?: boolean;
4604
+ }
4605
+ export interface IndexedToken {
4606
+ text: string;
4607
+ weight: number;
4608
+ }
4609
+ export interface FlatIndexedItem<T> {
4610
+ item: T;
4611
+ index: number;
4612
+ tokens: IndexedToken[];
4613
+ combinedFlatText: string;
4614
+ }
4615
+ export interface ScoredItem<T> {
4616
+ item: T;
4617
+ score: number;
4618
+ index: number;
4619
+ }
2954
4620
  /**
2955
4621
  * Defines how a value of type `T` is converted to and from the string
2956
4622
  * format that `localStorage`/`sessionStorage` can actually store — the Web
@@ -3248,69 +4914,6 @@ export interface FuzzyHighlighterProps {
3248
4914
  caseSensitive?: boolean;
3249
4915
  }
3250
4916
  export declare function FuzzyHighlighter({ text, query, className, caseSensitive, }: FuzzyHighlighterProps): React$1.JSX.Element;
3251
- export type ExpansionId = string | number;
3252
- export interface UseExpansionReturn<T> {
3253
- expandedIds: ExpansionId[];
3254
- expandedItems: T[];
3255
- isExpanded: (itemOrId: ExpansionId | T) => boolean;
3256
- expand: (itemOrId: ExpansionId | T) => void;
3257
- collapse: (itemOrId: ExpansionId | T) => void;
3258
- toggle: (itemOrId: ExpansionId | T) => void;
3259
- expandAll: () => void;
3260
- collapseAll: () => void;
3261
- resetExpansion: () => void;
3262
- replaceExpansion: (newExpandedItems: ExpansionId[] | T[]) => void;
3263
- }
3264
- export declare function useExpansion<T = unknown>(options?: {
3265
- items?: T[];
3266
- field?: string;
3267
- initialExpandedIds?: ExpansionId[];
3268
- multiple?: boolean;
3269
- }): UseExpansionReturn<T>;
3270
- export type PinId = string | number;
3271
- export interface UsePinReturn<T> {
3272
- pinnedIds: PinId[];
3273
- pinnedItems: T[];
3274
- unpinnedItems: T[];
3275
- pinnedCount: number;
3276
- hasPins: boolean;
3277
- isAtMaxLimit: boolean;
3278
- pin: (itemOrId: PinId | T) => void;
3279
- unpin: (itemOrId: PinId | T) => void;
3280
- togglePin: (itemOrId: PinId | T) => void;
3281
- isPinned: (itemOrId: PinId | T) => boolean;
3282
- clearPins: () => void;
3283
- resetPins: () => void;
3284
- replacePins: (newPinnedItems: PinId[] | T[]) => void;
3285
- pinMultiple: (newItems: PinId[] | T[]) => void;
3286
- unpinMultiple: (itemsToRemove: PinId[] | T[]) => void;
3287
- }
3288
- export declare function usePin<T = unknown>(options?: {
3289
- items?: T[];
3290
- field?: string;
3291
- initialPinnedIds?: PinId[];
3292
- maxPins?: number;
3293
- }): UsePinReturn<T>;
3294
- export type VisibilityId = number | string;
3295
- export interface UseVisibilityReturn<T> {
3296
- visibleIds: VisibilityId[];
3297
- visibleItems: T[];
3298
- hiddenItems: T[];
3299
- visibleCount: number;
3300
- isVisible: (itemOrId: VisibilityId | T) => boolean;
3301
- show: (itemOrId: VisibilityId | T) => void;
3302
- hide: (itemOrId: VisibilityId | T) => void;
3303
- toggleVisibility: (itemOrId: VisibilityId | T) => void;
3304
- showAll: (itemsArray?: VisibilityId[] | T[]) => void;
3305
- hideAll: (itemsArray?: VisibilityId[] | T[]) => void;
3306
- resetVisibility: () => void;
3307
- replaceVisibility: (newVisibleItems: VisibilityId[] | T[]) => void;
3308
- }
3309
- export declare function useVisibility<T = unknown>(options?: {
3310
- items?: T[];
3311
- field?: string;
3312
- initialVisibleIds?: VisibilityId[];
3313
- }): UseVisibilityReturn<T>;
3314
4917
 
3315
4918
  export {
3316
4919
  UseStorageEngineReturn as UseLocalStorageReturn,