@react-hive/honey-utils 4.2.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +56 -12
  2. package/dist/README.md +56 -12
  3. package/dist/a11y/focus/is-html-element-focusable.d.ts +8 -6
  4. package/dist/a11y/focus/move-focus-within-container.d.ts +1 -1
  5. package/dist/array/chunk.d.ts +4 -1
  6. package/dist/array/compose.d.ts +7 -3
  7. package/dist/array/get-adjacent-items.d.ts +1 -1
  8. package/dist/array/index.d.ts +1 -0
  9. package/dist/array/intersection.d.ts +2 -1
  10. package/dist/array/is-array.d.ts +6 -0
  11. package/dist/array/is-empty-array.d.ts +7 -0
  12. package/dist/array/pipe.d.ts +5 -2
  13. package/dist/array/to-map-by.d.ts +48 -0
  14. package/dist/async/async.d.ts +10 -1
  15. package/dist/async/retry.d.ts +4 -3
  16. package/dist/dom/data-transfer.d.ts +22 -4
  17. package/dist/dom/dom.d.ts +7 -5
  18. package/dist/dom/transform/parse-2d-matrix.d.ts +5 -0
  19. package/dist/encoding/decode-base64-json.d.ts +22 -1
  20. package/dist/encoding/decode-base64.d.ts +23 -1
  21. package/dist/encoding/encode-base64-json.d.ts +16 -0
  22. package/dist/encoding/encode-base64.d.ts +20 -0
  23. package/dist/env/storage/index.d.ts +1 -0
  24. package/dist/env/storage/local-storage-json.d.ts +29 -0
  25. package/dist/env/storage/local-storage.d.ts +1 -6
  26. package/dist/file.d.ts +3 -2
  27. package/dist/function/function.d.ts +12 -0
  28. package/dist/geometry/apply-inertia-step.d.ts +1 -1
  29. package/dist/geometry/layout/calculate-center-offset.d.ts +11 -1
  30. package/dist/geometry/layout/clamp-offset-to-overflow.d.ts +6 -0
  31. package/dist/geometry/resolve-bounded-delta.d.ts +1 -1
  32. package/dist/guards.d.ts +24 -3
  33. package/dist/index.cjs +1 -1
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.dev.cjs +474 -100
  36. package/dist/index.dev.cjs.map +1 -1
  37. package/dist/index.mjs +1 -1
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/intersection/get-dom-rect-intersection-ratio.d.ts +8 -1
  40. package/dist/intersection/resolve-axis-delta.d.ts +7 -0
  41. package/dist/json/parse-json.d.ts +22 -1
  42. package/dist/math/hash-string.d.ts +1 -1
  43. package/dist/math/math.d.ts +17 -1
  44. package/dist/object.d.ts +7 -1
  45. package/dist/string/for-each-char.d.ts +1 -1
  46. package/dist/string/is-nil-or-empty-string.d.ts +1 -1
  47. package/dist/string/parse-file-name.d.ts +1 -1
  48. package/dist/tree/flatten-tree.d.ts +2 -2
  49. package/dist/tree/get-tree-children.d.ts +2 -2
  50. package/dist/tree/search-tree.d.ts +2 -2
  51. package/dist/tree/tree.types.d.ts +1 -1
  52. package/package.json +8 -7
  53. package/dist/geometry/apply-inertia-step.spec.d.ts +0 -1
  54. package/dist/geometry/resolve-bounded-delta.spec.d.ts +0 -1
  55. package/dist/id/generate-ephemeral-id.spec.d.ts +0 -1
package/README.md CHANGED
@@ -99,7 +99,7 @@ splitStringIntoWords('hello world');
99
99
  * Generate a hash from a string
100
100
  */
101
101
  const hash = hashString('background-color: red;');
102
- // ➜ 'e4k1z0x'
102
+ // ➜ '1s0s409'
103
103
  ```
104
104
 
105
105
  ### Encoding Utilities
@@ -188,6 +188,36 @@ parseJson('{name:"Mike"}');
188
188
  // ➜ null
189
189
  ```
190
190
 
191
+ ### Storage Utilities
192
+
193
+ ```ts
194
+ import { readLocalStorageJson, writeLocalStorageJson } from '@react-hive/honey-utils';
195
+
196
+ /**
197
+ * Store a value in localStorage as JSON
198
+ */
199
+ writeLocalStorageJson('basket', [{ productId: 'p1', quantity: 2 }]);
200
+ // ➜ true
201
+
202
+ /**
203
+ * Read it back
204
+ */
205
+ readLocalStorageJson<{ productId: string; quantity: number }[]>('basket');
206
+ // ➜ [{ productId: 'p1', quantity: 2 }]
207
+
208
+ /**
209
+ * Nothing stored, invalid JSON, or storage that cannot be read returns null
210
+ */
211
+ readLocalStorageJson('missing');
212
+ // ➜ null
213
+
214
+ /**
215
+ * A write that fails (storage full, private mode, a circular value) returns false rather than throwing
216
+ */
217
+ writeLocalStorageJson('basket', circularValue);
218
+ // ➜ false
219
+ ```
220
+
191
221
  ### Array Utilities
192
222
 
193
223
  ```ts
@@ -200,6 +230,7 @@ import {
200
230
  intersection,
201
231
  difference,
202
232
  toggleArrayItem,
233
+ toMapBy,
203
234
  pipe,
204
235
  compose,
205
236
  } from '@react-hive/honey-utils';
@@ -260,6 +291,16 @@ toggleArrayItem(['a', 'b'], 'b', false);
260
291
  toggleArrayItem(['a', 'b'], 'a');
261
292
  // ➜ ['b']
262
293
 
294
+ /**
295
+ * Index an array by a key
296
+ */
297
+ const users = [{ id: 'u1', name: 'Ann' }, { id: 'u2', name: 'Bob' }];
298
+
299
+ toMapBy(users, user => user.id);
300
+ // ➜ Map { 'u1' => { id: 'u1', name: 'Ann' }, 'u2' => { id: 'u2', name: 'Bob' } }
301
+ toMapBy(users, user => user.id, user => user.name);
302
+ // ➜ Map { 'u1' => 'Ann', 'u2' => 'Bob' }
303
+
263
304
  /**
264
305
  * Compose functions from left to right
265
306
  */
@@ -679,7 +720,7 @@ function divide(a: number, b: number): number {
679
720
  ---
680
721
 
681
722
  - `isString(value: unknown): value is string` - Checks if a value is a `string`.
682
- - `isNilOrEmptyString(value: unknown): value is null | undefined` - Checks if a value is `null`, `undefined`, or an empty string.
723
+ - `isNilOrEmptyString(value: unknown): value is null | undefined | ''` - Checks if a value is `null`, `undefined`, or an empty string.
683
724
  - `toKebabCase(input: string): string` - Converts a string to kebab-case.
684
725
  - `camelToDashCase(input: string): string` - Converts camelCase to dash-case.
685
726
  - `splitStringIntoWords(input: string): string[]` - Splits a string into an array of words.
@@ -714,7 +755,7 @@ function divide(a: number, b: number): number {
714
755
 
715
756
  ---
716
757
 
717
- - `definedProps<T extends object>(obj: T): DefinedProps<T>` - Creates a new object by removing all properties whose values are `undefined`.
758
+ - `definedProps<T extends object>(obj: T): DefinedProps<T>` - Creates a new object by removing all properties whose values are `undefined`, symbol-keyed ones included. A key that may be `undefined` comes out optional.
718
759
 
719
760
  ### Array Utilities
720
761
 
@@ -724,8 +765,9 @@ function divide(a: number, b: number): number {
724
765
  - `isEmptyArray(value: unknown): value is []` - Checks if a value is an empty array.
725
766
  - `compact<T>(array: (T | Falsy)[]): T[]` – Returns a new array with all falsy values (false, null, undefined, 0, '', NaN) removed, preserving only truthy items of type `T`.
726
767
  - `unique<T>(array: T[]): T[]` - Returns a new array with all duplicate elements removed. Keeps only the first occurrence of each value.
727
- - `chunk<T>(array: T[], size: number): T[][]` - Splits an array into smaller arrays ("chunks") of the specified size.
728
- - `intersection<T>(...arrays: T[][]): T[]` - Returns an array of elements that exist in all provided arrays.
768
+ - `toMapBy<Item, Key, Value>(items: readonly Item[], getKey: (item: Item, index: number) => Key, getValue?: (item: Item, index: number) => Value): Map<Key, Item | Value>` - Indexes an array by a key: a `Map` from each item's key to the item, or to what `getValue` returns for it. Where two items share a key, the later one wins.
769
+ - `chunk<T>(array: T[], size: number): T[][]` - Splits an array into smaller arrays ("chunks") of the specified size. Throws unless `size` is a positive whole number.
770
+ - `intersection<T>(...arrays: T[][]): T[]` - Returns an array of elements that exist in all provided arrays, each once, in the order of the first array.
729
771
  - `difference<T>(array: T[], exclude: T[]): T[]` - Returns a new array that contains items from `array` that are not present in `exclude`.
730
772
  - `pipe(...fns: Function[]): Function` - Composes unary functions left-to-right. Returns a new function that applies all given functions in a sequence.
731
773
  - `compose(...fns: Function[]): Function` - Composes unary functions **right-to-left**. Same as `pipe`, but applies functions in reverse order.
@@ -740,7 +782,7 @@ function divide(a: number, b: number): number {
740
782
  - `noop(): void` - A no-operation function.
741
783
  - `not<Args extends any[]>(fn: (...args: Args) => any): (...args: Args) => boolean` - Creates a new function that negates the result of the given predicate function. Useful for logical inversions, e.g., turning `isEven` into `isOdd`.
742
784
  - `invokeIfFunction<Args extends any[], Result>(input: ((...args: Args) => Result) | Result, ...args: Args): Result` - Invokes the input if it's a function, otherwise returns it as-is.
743
- - `once<T extends (...args: any[]) => any>(fn: T): T` - Wraps a function so it can only be executed once. The result of the first invocation is cached and returned for all subsequent calls. Preserves both the original function’s parameter types and `this` binding.
785
+ - `once<T extends (...args: any[]) => any>(fn: T): T` - Wraps a function so it can only be executed once. The result of the first invocation is cached and returned for all subsequent calls; if it throws, every call throws that same error. Preserves both the original function’s parameter types and `this` binding.
744
786
 
745
787
  ### Type Guards
746
788
 
@@ -749,9 +791,9 @@ function divide(a: number, b: number): number {
749
791
  - `assert(condition: any, message: string): asserts condition` - Asserts that a condition is truthy, throwing an error with the provided message if it's not.
750
792
  - `isNumber(value: unknown): value is number` - Checks if a value is a `number`.
751
793
  - `isBool(value: unknown): value is boolean` - Checks if a value is a `boolean`.
752
- - `isObject(value: unknown): value is object` - Checks if a value is an `object`.
794
+ - `isObject(value: unknown): value is object` - Checks if a value is an `object`, `null` excluded.
753
795
  - `isNil(value: unknown): value is null | undefined` - Checks if a value is `null` or `undefined`.
754
- - `isEmptyObject(value: unknown): value is Record<string, never>` - Checks if a value is an empty object.
796
+ - `isEmptyObject(value: unknown): value is Record<string, never>` - Checks if a value is an empty object - no own enumerable properties, symbol-keyed ones included.
755
797
  - `isNull(value: unknown): value is null` - Checks if a value is `null`.
756
798
  - `isUndefined(value: unknown): value is undefined` - Checks if a value is `undefined`.
757
799
  - `isDefined<T>(value: T): value is NonNullable<T>` - Checks if a value is neither `null` nor `undefined`.
@@ -786,6 +828,8 @@ function divide(a: number, b: number): number {
786
828
 
787
829
  - `isLocalStorageReadable(): boolean` - Determines whether `localStorage` can be safely read from. This check works even when writes fail (e.g., due to `QuotaExceededError`) and ensures that calling `getItem()` does not throw in restricted environments.
788
830
  - `getLocalStorageCapabilities(): LocalStorageCapabilities` - Detects the browser's read and write capabilities for `localStorage`. Readability is determined by safe execution of `getItem()`, while writability requires successful `setItem()` and `removeItem()`.
831
+ - `readLocalStorageJson<Value>(key: string): Nullable<Value>` - Reads the value stored as JSON under a key in `localStorage`. Returns `null` when nothing is stored, the stored value is not valid JSON, or storage cannot be read (SSR, disabled storage, sandboxed iframes). Never throws. The value is cast to `Value`, not validated.
832
+ - `writeLocalStorageJson(key: string, value: unknown): boolean` - Stores a value under a key in `localStorage` as JSON. Returns `true` once stored, or `false` when storage cannot be written (`QuotaExceededError`, private mode, restricted contexts, SSR) or the value cannot be serialised. Never throws.
789
833
 
790
834
  ### Geometry
791
835
 
@@ -806,7 +850,7 @@ function divide(a: number, b: number): number {
806
850
  - `cloneBlob(blob: Blob): Blob` - Creates a clone of a Blob object with the same content and type as the original.
807
851
  - `getElementOffsetRect(element: HTMLElement): DOMRect` - Returns a `DOMRect` representing the element's layout position using `offsetLeft`, `offsetTop`, and `clientWidth`/`clientHeight`.
808
852
  - `isAnchorHtmlElement(element: HTMLElement): element is HTMLAnchorElement` - Determines whether the provided element is an `<a>` tag. Acts as a type guard that narrows the element to `HTMLAnchorElement`.
809
- - `isContentEditableHtmlElement(element: HTMLElement): boolean` - Returns `true` if the element has `contenteditable="true"`, making it user-editable and implicitly focusable.
853
+ - `isContentEditableHtmlElement(element: HTMLElement): boolean` - Returns `true` if the element's `contenteditable` is `""`, `"true"` or `"plaintext-only"` (in any case), making it user-editable and implicitly focusable.
810
854
 
811
855
  #### File
812
856
 
@@ -858,8 +902,8 @@ function divide(a: number, b: number): number {
858
902
  - `everyAsync<Item>(array: Item[], predicate): Promise<boolean>` - Returns `true` if **all** items in the array pass the async predicate.
859
903
  - `findAsync<Item>(array: Item[], predicate): Promise<Nullable<Item>>` - Returns the first array item that passes the async predicate, or `null` if no match is found.
860
904
  - `delay(delayMs: number): Promise<void>` - Creates a promise that resolves after the specified delay in milliseconds.
861
- - `timeout<T>(promise: Promise<T>, timeoutMs: number, message?: string): Promise<T>` - Wraps a promise with a timeout. If the promise does not settle within the given duration, it rejects with a timeout error.
862
- - `retry<Task, TaskResult>(task: Task, options?: RetryOptions): Function` - Wraps an asynchronous function with retry logic, with configurable max attempts, delay between retries, exponential backoff, and retry callbacks.
905
+ - `timeout<T>(promise: Promise<T>, timeoutMs: number, message?: string): Promise<T>` - Wraps a promise with a timeout. If the promise does not settle within the given duration, it rejects with a timeout error. Its timer is cleared however the race ends.
906
+ - `retry<Args, TaskResult>(task: (...args: Args) => Promise<TaskResult>, options?: RetryOptions): (...args: Args) => Promise<TaskResult>` - Wraps an asynchronous function with retry logic, with configurable max attempts (at least one), delay between retries, exponential backoff, and retry callbacks.
863
907
 
864
908
  ### Intersection Utilities
865
909
 
@@ -874,7 +918,7 @@ function divide(a: number, b: number): number {
874
918
 
875
919
  #### Focus
876
920
 
877
- - `isHtmlElementFocusable(element: Nullable<HTMLElement>): boolean` - Checks whether an element is considered focusable according to browser rules. Factors include: visibility, `display`, `disabled`, `tabindex`, native focusable tags, `contenteditable`, and presence of a non-null `tabindex`.
921
+ - `isHtmlElementFocusable(element: Nullable<HTMLElement>): boolean` - Checks whether an element is considered focusable according to browser rules. Factors include: visibility, `display` (an ancestor's too), `:disabled` (a disabled `<fieldset>`'s controls too), a valid `tabindex` (negative removes it, non-negative makes any element focusable), native focusable tags, and `contenteditable`.
878
922
  - `getFocusableHtmlElements(container: HTMLElement): HTMLElement[]` - Returns all focusable descendant elements within a container, using `isHtmlElementFocusable` to filter them.
879
923
  - `moveFocusWithinContainer(direction: FocusMoveDirection, container?: Nullable<HTMLElement>, options?: MoveFocusWithinContainerOptions): void` - Moves focus to the next or previous focusable element within a container. Supports cyclic navigation, optional wrapping control, and custom focus index resolution for advanced keyboard navigation patterns (e.g. roving tabindex).
880
924
 
package/dist/README.md CHANGED
@@ -99,7 +99,7 @@ splitStringIntoWords('hello world');
99
99
  * Generate a hash from a string
100
100
  */
101
101
  const hash = hashString('background-color: red;');
102
- // ➜ 'e4k1z0x'
102
+ // ➜ '1s0s409'
103
103
  ```
104
104
 
105
105
  ### Encoding Utilities
@@ -188,6 +188,36 @@ parseJson('{name:"Mike"}');
188
188
  // ➜ null
189
189
  ```
190
190
 
191
+ ### Storage Utilities
192
+
193
+ ```ts
194
+ import { readLocalStorageJson, writeLocalStorageJson } from '@react-hive/honey-utils';
195
+
196
+ /**
197
+ * Store a value in localStorage as JSON
198
+ */
199
+ writeLocalStorageJson('basket', [{ productId: 'p1', quantity: 2 }]);
200
+ // ➜ true
201
+
202
+ /**
203
+ * Read it back
204
+ */
205
+ readLocalStorageJson<{ productId: string; quantity: number }[]>('basket');
206
+ // ➜ [{ productId: 'p1', quantity: 2 }]
207
+
208
+ /**
209
+ * Nothing stored, invalid JSON, or storage that cannot be read returns null
210
+ */
211
+ readLocalStorageJson('missing');
212
+ // ➜ null
213
+
214
+ /**
215
+ * A write that fails (storage full, private mode, a circular value) returns false rather than throwing
216
+ */
217
+ writeLocalStorageJson('basket', circularValue);
218
+ // ➜ false
219
+ ```
220
+
191
221
  ### Array Utilities
192
222
 
193
223
  ```ts
@@ -200,6 +230,7 @@ import {
200
230
  intersection,
201
231
  difference,
202
232
  toggleArrayItem,
233
+ toMapBy,
203
234
  pipe,
204
235
  compose,
205
236
  } from '@react-hive/honey-utils';
@@ -260,6 +291,16 @@ toggleArrayItem(['a', 'b'], 'b', false);
260
291
  toggleArrayItem(['a', 'b'], 'a');
261
292
  // ➜ ['b']
262
293
 
294
+ /**
295
+ * Index an array by a key
296
+ */
297
+ const users = [{ id: 'u1', name: 'Ann' }, { id: 'u2', name: 'Bob' }];
298
+
299
+ toMapBy(users, user => user.id);
300
+ // ➜ Map { 'u1' => { id: 'u1', name: 'Ann' }, 'u2' => { id: 'u2', name: 'Bob' } }
301
+ toMapBy(users, user => user.id, user => user.name);
302
+ // ➜ Map { 'u1' => 'Ann', 'u2' => 'Bob' }
303
+
263
304
  /**
264
305
  * Compose functions from left to right
265
306
  */
@@ -679,7 +720,7 @@ function divide(a: number, b: number): number {
679
720
  ---
680
721
 
681
722
  - `isString(value: unknown): value is string` - Checks if a value is a `string`.
682
- - `isNilOrEmptyString(value: unknown): value is null | undefined` - Checks if a value is `null`, `undefined`, or an empty string.
723
+ - `isNilOrEmptyString(value: unknown): value is null | undefined | ''` - Checks if a value is `null`, `undefined`, or an empty string.
683
724
  - `toKebabCase(input: string): string` - Converts a string to kebab-case.
684
725
  - `camelToDashCase(input: string): string` - Converts camelCase to dash-case.
685
726
  - `splitStringIntoWords(input: string): string[]` - Splits a string into an array of words.
@@ -714,7 +755,7 @@ function divide(a: number, b: number): number {
714
755
 
715
756
  ---
716
757
 
717
- - `definedProps<T extends object>(obj: T): DefinedProps<T>` - Creates a new object by removing all properties whose values are `undefined`.
758
+ - `definedProps<T extends object>(obj: T): DefinedProps<T>` - Creates a new object by removing all properties whose values are `undefined`, symbol-keyed ones included. A key that may be `undefined` comes out optional.
718
759
 
719
760
  ### Array Utilities
720
761
 
@@ -724,8 +765,9 @@ function divide(a: number, b: number): number {
724
765
  - `isEmptyArray(value: unknown): value is []` - Checks if a value is an empty array.
725
766
  - `compact<T>(array: (T | Falsy)[]): T[]` – Returns a new array with all falsy values (false, null, undefined, 0, '', NaN) removed, preserving only truthy items of type `T`.
726
767
  - `unique<T>(array: T[]): T[]` - Returns a new array with all duplicate elements removed. Keeps only the first occurrence of each value.
727
- - `chunk<T>(array: T[], size: number): T[][]` - Splits an array into smaller arrays ("chunks") of the specified size.
728
- - `intersection<T>(...arrays: T[][]): T[]` - Returns an array of elements that exist in all provided arrays.
768
+ - `toMapBy<Item, Key, Value>(items: readonly Item[], getKey: (item: Item, index: number) => Key, getValue?: (item: Item, index: number) => Value): Map<Key, Item | Value>` - Indexes an array by a key: a `Map` from each item's key to the item, or to what `getValue` returns for it. Where two items share a key, the later one wins.
769
+ - `chunk<T>(array: T[], size: number): T[][]` - Splits an array into smaller arrays ("chunks") of the specified size. Throws unless `size` is a positive whole number.
770
+ - `intersection<T>(...arrays: T[][]): T[]` - Returns an array of elements that exist in all provided arrays, each once, in the order of the first array.
729
771
  - `difference<T>(array: T[], exclude: T[]): T[]` - Returns a new array that contains items from `array` that are not present in `exclude`.
730
772
  - `pipe(...fns: Function[]): Function` - Composes unary functions left-to-right. Returns a new function that applies all given functions in a sequence.
731
773
  - `compose(...fns: Function[]): Function` - Composes unary functions **right-to-left**. Same as `pipe`, but applies functions in reverse order.
@@ -740,7 +782,7 @@ function divide(a: number, b: number): number {
740
782
  - `noop(): void` - A no-operation function.
741
783
  - `not<Args extends any[]>(fn: (...args: Args) => any): (...args: Args) => boolean` - Creates a new function that negates the result of the given predicate function. Useful for logical inversions, e.g., turning `isEven` into `isOdd`.
742
784
  - `invokeIfFunction<Args extends any[], Result>(input: ((...args: Args) => Result) | Result, ...args: Args): Result` - Invokes the input if it's a function, otherwise returns it as-is.
743
- - `once<T extends (...args: any[]) => any>(fn: T): T` - Wraps a function so it can only be executed once. The result of the first invocation is cached and returned for all subsequent calls. Preserves both the original function’s parameter types and `this` binding.
785
+ - `once<T extends (...args: any[]) => any>(fn: T): T` - Wraps a function so it can only be executed once. The result of the first invocation is cached and returned for all subsequent calls; if it throws, every call throws that same error. Preserves both the original function’s parameter types and `this` binding.
744
786
 
745
787
  ### Type Guards
746
788
 
@@ -749,9 +791,9 @@ function divide(a: number, b: number): number {
749
791
  - `assert(condition: any, message: string): asserts condition` - Asserts that a condition is truthy, throwing an error with the provided message if it's not.
750
792
  - `isNumber(value: unknown): value is number` - Checks if a value is a `number`.
751
793
  - `isBool(value: unknown): value is boolean` - Checks if a value is a `boolean`.
752
- - `isObject(value: unknown): value is object` - Checks if a value is an `object`.
794
+ - `isObject(value: unknown): value is object` - Checks if a value is an `object`, `null` excluded.
753
795
  - `isNil(value: unknown): value is null | undefined` - Checks if a value is `null` or `undefined`.
754
- - `isEmptyObject(value: unknown): value is Record<string, never>` - Checks if a value is an empty object.
796
+ - `isEmptyObject(value: unknown): value is Record<string, never>` - Checks if a value is an empty object - no own enumerable properties, symbol-keyed ones included.
755
797
  - `isNull(value: unknown): value is null` - Checks if a value is `null`.
756
798
  - `isUndefined(value: unknown): value is undefined` - Checks if a value is `undefined`.
757
799
  - `isDefined<T>(value: T): value is NonNullable<T>` - Checks if a value is neither `null` nor `undefined`.
@@ -786,6 +828,8 @@ function divide(a: number, b: number): number {
786
828
 
787
829
  - `isLocalStorageReadable(): boolean` - Determines whether `localStorage` can be safely read from. This check works even when writes fail (e.g., due to `QuotaExceededError`) and ensures that calling `getItem()` does not throw in restricted environments.
788
830
  - `getLocalStorageCapabilities(): LocalStorageCapabilities` - Detects the browser's read and write capabilities for `localStorage`. Readability is determined by safe execution of `getItem()`, while writability requires successful `setItem()` and `removeItem()`.
831
+ - `readLocalStorageJson<Value>(key: string): Nullable<Value>` - Reads the value stored as JSON under a key in `localStorage`. Returns `null` when nothing is stored, the stored value is not valid JSON, or storage cannot be read (SSR, disabled storage, sandboxed iframes). Never throws. The value is cast to `Value`, not validated.
832
+ - `writeLocalStorageJson(key: string, value: unknown): boolean` - Stores a value under a key in `localStorage` as JSON. Returns `true` once stored, or `false` when storage cannot be written (`QuotaExceededError`, private mode, restricted contexts, SSR) or the value cannot be serialised. Never throws.
789
833
 
790
834
  ### Geometry
791
835
 
@@ -806,7 +850,7 @@ function divide(a: number, b: number): number {
806
850
  - `cloneBlob(blob: Blob): Blob` - Creates a clone of a Blob object with the same content and type as the original.
807
851
  - `getElementOffsetRect(element: HTMLElement): DOMRect` - Returns a `DOMRect` representing the element's layout position using `offsetLeft`, `offsetTop`, and `clientWidth`/`clientHeight`.
808
852
  - `isAnchorHtmlElement(element: HTMLElement): element is HTMLAnchorElement` - Determines whether the provided element is an `<a>` tag. Acts as a type guard that narrows the element to `HTMLAnchorElement`.
809
- - `isContentEditableHtmlElement(element: HTMLElement): boolean` - Returns `true` if the element has `contenteditable="true"`, making it user-editable and implicitly focusable.
853
+ - `isContentEditableHtmlElement(element: HTMLElement): boolean` - Returns `true` if the element's `contenteditable` is `""`, `"true"` or `"plaintext-only"` (in any case), making it user-editable and implicitly focusable.
810
854
 
811
855
  #### File
812
856
 
@@ -858,8 +902,8 @@ function divide(a: number, b: number): number {
858
902
  - `everyAsync<Item>(array: Item[], predicate): Promise<boolean>` - Returns `true` if **all** items in the array pass the async predicate.
859
903
  - `findAsync<Item>(array: Item[], predicate): Promise<Nullable<Item>>` - Returns the first array item that passes the async predicate, or `null` if no match is found.
860
904
  - `delay(delayMs: number): Promise<void>` - Creates a promise that resolves after the specified delay in milliseconds.
861
- - `timeout<T>(promise: Promise<T>, timeoutMs: number, message?: string): Promise<T>` - Wraps a promise with a timeout. If the promise does not settle within the given duration, it rejects with a timeout error.
862
- - `retry<Task, TaskResult>(task: Task, options?: RetryOptions): Function` - Wraps an asynchronous function with retry logic, with configurable max attempts, delay between retries, exponential backoff, and retry callbacks.
905
+ - `timeout<T>(promise: Promise<T>, timeoutMs: number, message?: string): Promise<T>` - Wraps a promise with a timeout. If the promise does not settle within the given duration, it rejects with a timeout error. Its timer is cleared however the race ends.
906
+ - `retry<Args, TaskResult>(task: (...args: Args) => Promise<TaskResult>, options?: RetryOptions): (...args: Args) => Promise<TaskResult>` - Wraps an asynchronous function with retry logic, with configurable max attempts (at least one), delay between retries, exponential backoff, and retry callbacks.
863
907
 
864
908
  ### Intersection Utilities
865
909
 
@@ -874,7 +918,7 @@ function divide(a: number, b: number): number {
874
918
 
875
919
  #### Focus
876
920
 
877
- - `isHtmlElementFocusable(element: Nullable<HTMLElement>): boolean` - Checks whether an element is considered focusable according to browser rules. Factors include: visibility, `display`, `disabled`, `tabindex`, native focusable tags, `contenteditable`, and presence of a non-null `tabindex`.
921
+ - `isHtmlElementFocusable(element: Nullable<HTMLElement>): boolean` - Checks whether an element is considered focusable according to browser rules. Factors include: visibility, `display` (an ancestor's too), `:disabled` (a disabled `<fieldset>`'s controls too), a valid `tabindex` (negative removes it, non-negative makes any element focusable), native focusable tags, and `contenteditable`.
878
922
  - `getFocusableHtmlElements(container: HTMLElement): HTMLElement[]` - Returns all focusable descendant elements within a container, using `isHtmlElementFocusable` to filter them.
879
923
  - `moveFocusWithinContainer(direction: FocusMoveDirection, container?: Nullable<HTMLElement>, options?: MoveFocusWithinContainerOptions): void` - Moves focus to the next or previous focusable element within a container. Supports cyclic navigation, optional wrapping control, and custom focus index resolution for advanced keyboard navigation patterns (e.g. roving tabindex).
880
924
 
@@ -1,15 +1,17 @@
1
- import type { Nullable } from '~/types';
1
+ import type { Nullable } from '../../types';
2
2
  export declare const FOCUSABLE_HTML_TAGS: string[];
3
3
  /**
4
4
  * Determines whether an HTMLElement is focusable under standard browser rules.
5
5
  *
6
6
  * The function checks a combination of factors:
7
- * - The element must be rendered (not `display: none` or `visibility: hidden`).
8
- * - Disabled form controls are never focusable.
9
- * - Elements with `tabindex="-1"` are intentionally removed from the focus order.
7
+ * - The element must be rendered: neither it nor an ancestor has `display: none`, and it is not
8
+ * `visibility: hidden`.
9
+ * - Disabled form controls are never focusable - including those inside a disabled `<fieldset>`.
10
+ * - Elements with a negative `tabindex` are intentionally removed from the focus order.
11
+ * - Any element with a valid, non-negative `tabindex` is focusable. A `tabindex` that is not an
12
+ * integer is ignored, as browsers ignore it.
10
13
  * - Certain native HTML elements are inherently focusable (e.g. inputs, buttons, anchors with `href`).
11
- * - Elements with `contenteditable="true"` are treated as focusable.
12
- * - Any element with a valid `tabindex` (not null) is considered focusable.
14
+ * - Editable elements (`contenteditable`) are treated as focusable.
13
15
  *
14
16
  * This logic approximates how browsers and the accessibility tree
15
17
  * determine real-world focusability—not just tabindex presence.
@@ -1,4 +1,4 @@
1
- import type { Nullable } from '~/types';
1
+ import type { Nullable } from '../../types';
2
2
  export type FocusMoveDirection = 'next' | 'previous';
3
3
  export interface MoveFocusWithinContainerOptions {
4
4
  /**
@@ -6,11 +6,14 @@
6
6
  * @template T - The type of the items in the array.
7
7
  *
8
8
  * @param array - The input array to be chunked.
9
- * @param size - The size of each chunk. Must be greater than 0.
9
+ * @param size - The size of each chunk. Must be a positive whole number.
10
10
  *
11
11
  * @returns An array of chunks, where each chunk is an array of the specified size
12
12
  * (except possibly the last chunk, which may be smaller).
13
13
  *
14
+ * @throws {Error} When `size` is not a positive whole number - a fractional size would give
15
+ * chunks of other sizes, and `Infinity` none at all.
16
+ *
14
17
  * @example
15
18
  * ```ts
16
19
  * chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
@@ -1,17 +1,21 @@
1
- type ComposeFn = (arg: unknown) => unknown;
1
+ /**
2
+ * Any unary function: one taking a typed argument is assignable here, where it is not to
3
+ * `ComposeFn`.
4
+ */
5
+ type AnyUnaryFn = (arg: never) => unknown;
2
6
  type Compose = {
3
7
  <A, R>(fn1: (a: A) => R): (a: A) => R;
4
8
  <A, B, R>(fn1: (b: B) => R, fn2: (a: A) => B): (a: A) => R;
5
9
  <A, B, C, R>(fn1: (c: C) => R, fn2: (b: B) => C, fn3: (a: A) => B): (a: A) => R;
6
10
  <A, B, C, D, R>(fn1: (d: D) => R, fn2: (c: C) => D, fn3: (b: B) => C, fn4: (a: A) => B): (a: A) => R;
7
11
  <A, B, C, D, E, R>(fn1: (e: E) => R, fn2: (d: D) => E, fn3: (c: C) => D, fn4: (b: B) => C, fn5: (a: A) => B): (a: A) => R;
8
- (...fns: ComposeFn[]): (arg: unknown) => unknown;
12
+ (...fns: AnyUnaryFn[]): (arg: unknown) => unknown;
9
13
  };
10
14
  /**
11
15
  * Composes multiple unary functions into a single function, applying them from **right to left**.
12
16
  *
13
17
  * Often used for building functional pipelines where the innermost function runs first.
14
- * Types are inferred up to 5 chained functions for full type safety.
18
+ * Types are inferred up to 5 chained functions for full type safety. Beyond that, it falls back to the unknown.
15
19
  *
16
20
  * @param fns - A list of unary functions to compose.
17
21
  *
@@ -1,4 +1,4 @@
1
- import type { Nullable } from '~/types';
1
+ import type { Nullable } from '../types';
2
2
  export interface AdjacentItems<T> {
3
3
  previousItem: Nullable<T>;
4
4
  currentItem: Nullable<T>;
@@ -9,3 +9,4 @@ export * from './is-array';
9
9
  export * from './is-empty-array';
10
10
  export * from './get-adjacent-items';
11
11
  export * from './toggle-array-item';
12
+ export * from './to-map-by';
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * @param arrays - Two or more arrays to find common elements from.
7
7
  *
8
- * @returns A new array containing only the elements that exist in all input arrays.
8
+ * @returns A new array containing only the elements that exist in all input arrays, each once, in
9
+ * the order of the first array.
9
10
  *
10
11
  * @example
11
12
  * ```ts
@@ -4,5 +4,11 @@
4
4
  * @param value - The value to check.
5
5
  *
6
6
  * @returns `true` if the value is an array; otherwise, `false`.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * isArray([1, 2]); // true
11
+ * isArray('12'); // false
12
+ * ```
7
13
  */
8
14
  export declare const isArray: (value: unknown) => value is unknown[];
@@ -4,5 +4,12 @@
4
4
  * @param value - The value to check.
5
5
  *
6
6
  * @returns `true` if the value is an empty array; otherwise, `false`.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * isEmptyArray([]); // true
11
+ * isEmptyArray([1]); // false
12
+ * isEmptyArray({}); // false
13
+ * ```
7
14
  */
8
15
  export declare const isEmptyArray: (value: unknown) => value is [];
@@ -1,11 +1,14 @@
1
- type PipeFn = (arg: unknown) => unknown;
1
+ /**
2
+ * Any unary function: one taking a typed argument is assignable here, where it is not to `PipeFn`.
3
+ */
4
+ type AnyUnaryFn = (arg: never) => unknown;
2
5
  type Pipe = {
3
6
  <A, B>(fn1: (a: A) => B): (a: A) => B;
4
7
  <A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C): (a: A) => C;
5
8
  <A, B, C, D>(fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D): (a: A) => D;
6
9
  <A, B, C, D, E>(fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E): (a: A) => E;
7
10
  <A, B, C, D, E, F>(fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F): (a: A) => F;
8
- (...fns: PipeFn[]): (arg: unknown) => unknown;
11
+ (...fns: AnyUnaryFn[]): (arg: unknown) => unknown;
9
12
  };
10
13
  /**
11
14
  * Composes multiple unary functions into a single function, applying them from left to right.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Indexes an array by a key: a `Map` from each item's key to the item itself.
3
+ *
4
+ * Items are added in order, so where two share a key the later one wins, as with
5
+ * `new Map(entries)`.
6
+ *
7
+ * @template Item - The type of the items in the array.
8
+ * @template Key - The type of the keys.
9
+ *
10
+ * @param items - The items to index.
11
+ * @param getKey - Returns the key an item is stored under.
12
+ *
13
+ * @returns A new `Map` from each key to its item.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const users = [{ id: 'u1', name: 'Ann' }, { id: 'u2', name: 'Bob' }];
18
+ *
19
+ * toMapBy(users, user => user.id);
20
+ * // Map { 'u1' => { id: 'u1', name: 'Ann' }, 'u2' => { id: 'u2', name: 'Bob' } }
21
+ * ```
22
+ */
23
+ export declare function toMapBy<Item, Key>(items: readonly Item[], getKey: (item: Item, index: number) => Key): Map<Key, Item>;
24
+ /**
25
+ * Indexes an array by a key: a `Map` from each item's key to what `getValue` returns for the item.
26
+ *
27
+ * Items are added in order, so where two share a key the later one wins, as with
28
+ * `new Map(entries)`.
29
+ *
30
+ * @template Item - The type of the items in the array.
31
+ * @template Key - The type of the keys.
32
+ * @template Value - The type of the values.
33
+ *
34
+ * @param items - The items to index.
35
+ * @param getKey - Returns the key an item is stored under.
36
+ * @param getValue - Returns the value stored for an item.
37
+ *
38
+ * @returns A new `Map` from each key to its value.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * const users = [{ id: 'u1', name: 'Ann' }, { id: 'u2', name: 'Bob' }];
43
+ *
44
+ * toMapBy(users, user => user.id, user => user.name);
45
+ * // Map { 'u1' => 'Ann', 'u2' => 'Bob' }
46
+ * ```
47
+ */
48
+ export declare function toMapBy<Item, Key, Value>(items: readonly Item[], getKey: (item: Item, index: number) => Key, getValue: (item: Item, index: number) => Value): Map<Key, Value>;
@@ -1,4 +1,4 @@
1
- import type { Nullable } from '~/types';
1
+ import type { Nullable } from '../types';
2
2
  /**
3
3
  * Checks if a value is a Promise.
4
4
  *
@@ -109,6 +109,9 @@ export declare const filterParallel: <Item>(array: Item[], predicate: (item: Ite
109
109
  /**
110
110
  * Asynchronously checks if at least one element in the array satisfies the async condition.
111
111
  *
112
+ * Runs one item at a time, in order, and stops at the first item that satisfies it - as
113
+ * `Array.prototype.some` does.
114
+ *
112
115
  * @param array - The array of items to check.
113
116
  * @param predicate - An async function that returns a boolean.
114
117
  *
@@ -118,6 +121,9 @@ export declare const someAsync: <Item>(array: Item[], predicate: (item: Item, in
118
121
  /**
119
122
  * Asynchronously checks if all elements in the array satisfy the async condition.
120
123
  *
124
+ * Runs one item at a time, in order, and stops at the first item that fails it - as
125
+ * `Array.prototype.every` does.
126
+ *
121
127
  * @param array - The array of items to check.
122
128
  * @param predicate - An async function that returns a boolean.
123
129
  *
@@ -140,6 +146,9 @@ export declare const reduceAsync: <Item, Accumulator>(array: Item[], fn: (accumu
140
146
  /**
141
147
  * Asynchronously finds the first element that satisfies the async condition.
142
148
  *
149
+ * Runs one item at a time, in order, and stops at the first match - as `Array.prototype.find`
150
+ * does.
151
+ *
143
152
  * @param array - The array of items to search.
144
153
  * @param predicate - An async function that returns a boolean.
145
154
  *
@@ -1,6 +1,7 @@
1
1
  interface RetryOptions {
2
2
  /**
3
- * Maximum number of retry attempts before failing.
3
+ * The total number of attempts, the first one included, before the last error is thrown. Taken
4
+ * as at least 1.
4
5
  *
5
6
  * @default 3
6
7
  */
@@ -35,7 +36,7 @@ interface RetryOptions {
35
36
  *
36
37
  * Useful for operations that may fail intermittently, such as network requests.
37
38
  *
38
- * @template Task - The type of the async function to wrap.
39
+ * @template Args - The arguments the async function takes.
39
40
  * @template TaskResult - The result type of the async function.
40
41
  *
41
42
  * @param task - The async function to wrap with retry logic.
@@ -68,5 +69,5 @@ interface RetryOptions {
68
69
  * .catch(error => console.error('Failed after retries:', error));
69
70
  * ```
70
71
  */
71
- export declare const retry: <Task extends (...args: unknown[]) => Promise<TaskResult>, TaskResult>(task: Task, { maxAttempts, delayMs, backoff, onRetry }?: RetryOptions) => ((...args: Parameters<Task>) => Promise<TaskResult>);
72
+ export declare const retry: <Args extends unknown[], TaskResult>(task: (...args: Args) => Promise<TaskResult>, { maxAttempts, delayMs, backoff, onRetry }?: RetryOptions) => ((...args: Args) => Promise<TaskResult>);
72
73
  export {};
@@ -1,19 +1,37 @@
1
- import type { Nullable } from '~/types';
1
+ import type { Nullable } from '../types';
2
2
  export declare const DATA_TRANSFER_JSON_MIME = "application/json";
3
3
  /**
4
4
  * Serializes and stores JSON data in a DataTransfer object.
5
5
  *
6
6
  * Useful for drag-and-drop communication between elements.
7
7
  *
8
- * @param dataTransfer DataTransfer instance from a drag event.
9
- * @param data Data to serialize.
8
+ * @template T - The type of the data.
9
+ *
10
+ * @param dataTransfer - DataTransfer instance from a drag event.
11
+ * @param data - Data to serialize.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * element.addEventListener('dragstart', event => {
16
+ * setDataTransferJson(event.dataTransfer, { id: 'card-1' });
17
+ * });
18
+ * ```
10
19
  */
11
20
  export declare const setDataTransferJson: <T>(dataTransfer: DataTransfer, data: T) => void;
12
21
  /**
13
22
  * Retrieves and parses JSON data from a DataTransfer object.
14
23
  *
15
- * @param dataTransfer DataTransfer instance from a drop event.
24
+ * @template Value - The type the JSON is expected to have.
25
+ *
26
+ * @param dataTransfer - DataTransfer instance from a drop event.
16
27
  *
17
28
  * @returns Parsed JSON value or null if not present or invalid.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * element.addEventListener('drop', event => {
33
+ * const card = getDataTransferJson<{ id: string }>(event.dataTransfer);
34
+ * });
35
+ * ```
18
36
  */
19
37
  export declare const getDataTransferJson: <Value>(dataTransfer: DataTransfer) => Nullable<Value>;