@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
@@ -12,7 +12,7 @@ __webpack_require__.r(__webpack_exports__);
12
12
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
13
13
  /* harmony export */ getFocusableHtmlElements: () => (/* binding */ getFocusableHtmlElements)
14
14
  /* harmony export */ });
15
- /* harmony import */ var _a11y__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ~/a11y */ "./src/a11y/index.ts");
15
+ /* harmony import */ var _is_html_element_focusable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./is-html-element-focusable */ "./src/a11y/focus/is-html-element-focusable.ts");
16
16
 
17
17
  /**
18
18
  * Collects all focusable descendant elements within a container.
@@ -25,7 +25,7 @@ __webpack_require__.r(__webpack_exports__);
25
25
  *
26
26
  * @returns An array of focusable HTMLElements in DOM order.
27
27
  */
28
- const getFocusableHtmlElements = (container) => Array.from(container.querySelectorAll('*')).filter(_a11y__WEBPACK_IMPORTED_MODULE_0__.isHtmlElementFocusable);
28
+ const getFocusableHtmlElements = (container) => Array.from(container.querySelectorAll('*')).filter(_is_html_element_focusable__WEBPACK_IMPORTED_MODULE_0__.isHtmlElementFocusable);
29
29
 
30
30
 
31
31
  /***/ },
@@ -67,16 +67,26 @@ __webpack_require__.r(__webpack_exports__);
67
67
  /* harmony import */ var _dom__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ~/dom */ "./src/dom/index.ts");
68
68
 
69
69
  const FOCUSABLE_HTML_TAGS = ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON', 'A'];
70
+ /**
71
+ * A `tabindex` attribute's value, read by the HTML rules for parsing integers - leading whitespace
72
+ * and a sign allowed, anything after the digits ignored - or `null` where there is none to read.
73
+ */
74
+ const parseTabIndex = (value) => {
75
+ const match = value === null ? null : /^\s*([+-]?\d+)/.exec(value);
76
+ return match ? parseInt(match[1], 10) : null;
77
+ };
70
78
  /**
71
79
  * Determines whether an HTMLElement is focusable under standard browser rules.
72
80
  *
73
81
  * The function checks a combination of factors:
74
- * - The element must be rendered (not `display: none` or `visibility: hidden`).
75
- * - Disabled form controls are never focusable.
76
- * - Elements with `tabindex="-1"` are intentionally removed from the focus order.
82
+ * - The element must be rendered: neither it nor an ancestor has `display: none`, and it is not
83
+ * `visibility: hidden`.
84
+ * - Disabled form controls are never focusable - including those inside a disabled `<fieldset>`.
85
+ * - Elements with a negative `tabindex` are intentionally removed from the focus order.
86
+ * - Any element with a valid, non-negative `tabindex` is focusable. A `tabindex` that is not an
87
+ * integer is ignored, as browsers ignore it.
77
88
  * - Certain native HTML elements are inherently focusable (e.g. inputs, buttons, anchors with `href`).
78
- * - Elements with `contenteditable="true"` are treated as focusable.
79
- * - Any element with a valid `tabindex` (not null) is considered focusable.
89
+ * - Editable elements (`contenteditable`) are treated as focusable.
80
90
  *
81
91
  * This logic approximates how browsers and the accessibility tree
82
92
  * determine real-world focusability—not just tabindex presence.
@@ -89,28 +99,30 @@ const isHtmlElementFocusable = (element) => {
89
99
  if (!element) {
90
100
  return false;
91
101
  }
92
- const style = window.getComputedStyle(element);
93
- if (style.visibility === 'hidden' || style.display === 'none') {
102
+ if (window.getComputedStyle(element).visibility === 'hidden') {
94
103
  return false;
95
104
  }
96
- if ('disabled' in element && element.disabled) {
97
- return false;
105
+ // `display: none` does not inherit, so a hidden ancestor has to be looked for
106
+ for (let node = element; node; node = node.parentElement) {
107
+ if (window.getComputedStyle(node).display === 'none') {
108
+ return false;
109
+ }
98
110
  }
99
- // Explicitly removed from tab order
100
- const tabIndex = element.getAttribute('tabindex');
101
- if (tabIndex === '-1') {
111
+ // `:disabled` covers a control inside a disabled `<fieldset>`, which `element.disabled` does not
112
+ if (element.matches(':disabled')) {
102
113
  return false;
103
114
  }
115
+ const tabIndex = parseTabIndex(element.getAttribute('tabindex'));
116
+ if (tabIndex !== null) {
117
+ return tabIndex >= 0;
118
+ }
104
119
  if (FOCUSABLE_HTML_TAGS.includes(element.tagName)) {
105
120
  if ((0,_dom__WEBPACK_IMPORTED_MODULE_0__.isAnchorHtmlElement)(element)) {
106
121
  return element.href !== '';
107
122
  }
108
123
  return true;
109
124
  }
110
- if ((0,_dom__WEBPACK_IMPORTED_MODULE_0__.isContentEditableHtmlElement)(element)) {
111
- return true;
112
- }
113
- return tabIndex !== null;
125
+ return (0,_dom__WEBPACK_IMPORTED_MODULE_0__.isContentEditableHtmlElement)(element);
114
126
  };
115
127
 
116
128
 
@@ -126,7 +138,7 @@ __webpack_require__.r(__webpack_exports__);
126
138
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
127
139
  /* harmony export */ moveFocusWithinContainer: () => (/* binding */ moveFocusWithinContainer)
128
140
  /* harmony export */ });
129
- /* harmony import */ var _a11y__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ~/a11y */ "./src/a11y/index.ts");
141
+ /* harmony import */ var _get_focusable_html_elements__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./get-focusable-html-elements */ "./src/a11y/focus/get-focusable-html-elements.ts");
130
142
 
131
143
  /**
132
144
  * Moves focus to the next or previous focusable element within a container.
@@ -158,7 +170,7 @@ const moveFocusWithinContainer = (direction, container = null, { wrap = true, ge
158
170
  if (!activeElement || !scope) {
159
171
  return;
160
172
  }
161
- const focusableElements = (0,_a11y__WEBPACK_IMPORTED_MODULE_0__.getFocusableHtmlElements)(scope);
173
+ const focusableElements = (0,_get_focusable_html_elements__WEBPACK_IMPORTED_MODULE_0__.getFocusableHtmlElements)(scope);
162
174
  if (focusableElements.length === 0) {
163
175
  return;
164
176
  }
@@ -232,11 +244,14 @@ __webpack_require__.r(__webpack_exports__);
232
244
  * @template T - The type of the items in the array.
233
245
  *
234
246
  * @param array - The input array to be chunked.
235
- * @param size - The size of each chunk. Must be greater than 0.
247
+ * @param size - The size of each chunk. Must be a positive whole number.
236
248
  *
237
249
  * @returns An array of chunks, where each chunk is an array of the specified size
238
250
  * (except possibly the last chunk, which may be smaller).
239
251
  *
252
+ * @throws {Error} When `size` is not a positive whole number - a fractional size would give
253
+ * chunks of other sizes, and `Infinity` none at all.
254
+ *
240
255
  * @example
241
256
  * ```ts
242
257
  * chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
@@ -244,7 +259,7 @@ __webpack_require__.r(__webpack_exports__);
244
259
  * ```
245
260
  */
246
261
  const chunk = (array, size) => {
247
- (0,_guards__WEBPACK_IMPORTED_MODULE_0__.assert)(size > 0, 'Chunk size must be greater than 0');
262
+ (0,_guards__WEBPACK_IMPORTED_MODULE_0__.assert)(Number.isInteger(size) && size > 0, 'Chunk size must be a positive whole number');
248
263
  return Array.from({ length: Math.ceil(array.length / size) }, (_, index) => array.slice(index * size, (index + 1) * size));
249
264
  };
250
265
 
@@ -298,7 +313,7 @@ __webpack_require__.r(__webpack_exports__);
298
313
  * Composes multiple unary functions into a single function, applying them from **right to left**.
299
314
  *
300
315
  * Often used for building functional pipelines where the innermost function runs first.
301
- * Types are inferred up to 5 chained functions for full type safety.
316
+ * Types are inferred up to 5 chained functions for full type safety. Beyond that, it falls back to the unknown.
302
317
  *
303
318
  * @param fns - A list of unary functions to compose.
304
319
  *
@@ -412,6 +427,7 @@ __webpack_require__.r(__webpack_exports__);
412
427
  /* harmony export */ isArray: () => (/* reexport safe */ _is_array__WEBPACK_IMPORTED_MODULE_7__.isArray),
413
428
  /* harmony export */ isEmptyArray: () => (/* reexport safe */ _is_empty_array__WEBPACK_IMPORTED_MODULE_8__.isEmptyArray),
414
429
  /* harmony export */ pipe: () => (/* reexport safe */ _pipe__WEBPACK_IMPORTED_MODULE_0__.pipe),
430
+ /* harmony export */ toMapBy: () => (/* reexport safe */ _to_map_by__WEBPACK_IMPORTED_MODULE_11__.toMapBy),
415
431
  /* harmony export */ toggleArrayItem: () => (/* reexport safe */ _toggle_array_item__WEBPACK_IMPORTED_MODULE_10__.toggleArrayItem),
416
432
  /* harmony export */ unique: () => (/* reexport safe */ _unique__WEBPACK_IMPORTED_MODULE_5__.unique)
417
433
  /* harmony export */ });
@@ -426,6 +442,8 @@ __webpack_require__.r(__webpack_exports__);
426
442
  /* harmony import */ var _is_empty_array__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./is-empty-array */ "./src/array/is-empty-array.ts");
427
443
  /* harmony import */ var _get_adjacent_items__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./get-adjacent-items */ "./src/array/get-adjacent-items.ts");
428
444
  /* harmony import */ var _toggle_array_item__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./toggle-array-item */ "./src/array/toggle-array-item.ts");
445
+ /* harmony import */ var _to_map_by__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./to-map-by */ "./src/array/to-map-by.ts");
446
+
429
447
 
430
448
 
431
449
 
@@ -451,7 +469,7 @@ __webpack_require__.r(__webpack_exports__);
451
469
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
452
470
  /* harmony export */ intersection: () => (/* binding */ intersection)
453
471
  /* harmony export */ });
454
- /* harmony import */ var _array__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ~/array */ "./src/array/index.ts");
472
+ /* harmony import */ var _unique__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./unique */ "./src/array/unique.ts");
455
473
 
456
474
  /**
457
475
  * Returns an array containing elements that exist in all provided arrays.
@@ -460,7 +478,8 @@ __webpack_require__.r(__webpack_exports__);
460
478
  *
461
479
  * @param arrays - Two or more arrays to find common elements from.
462
480
  *
463
- * @returns A new array containing only the elements that exist in all input arrays.
481
+ * @returns A new array containing only the elements that exist in all input arrays, each once, in
482
+ * the order of the first array.
464
483
  *
465
484
  * @example
466
485
  * ```ts
@@ -472,11 +491,8 @@ const intersection = (...arrays) => {
472
491
  if (arrays.length === 0) {
473
492
  return [];
474
493
  }
475
- if (arrays.length === 1) {
476
- return [...arrays[0]];
477
- }
478
494
  const [first, ...rest] = arrays;
479
- const uniqueFirst = (0,_array__WEBPACK_IMPORTED_MODULE_0__.unique)(first);
495
+ const uniqueFirst = (0,_unique__WEBPACK_IMPORTED_MODULE_0__.unique)(first);
480
496
  return uniqueFirst.filter(item => rest.every(array => array.includes(item)));
481
497
  };
482
498
 
@@ -499,6 +515,12 @@ __webpack_require__.r(__webpack_exports__);
499
515
  * @param value - The value to check.
500
516
  *
501
517
  * @returns `true` if the value is an array; otherwise, `false`.
518
+ *
519
+ * @example
520
+ * ```ts
521
+ * isArray([1, 2]); // true
522
+ * isArray('12'); // false
523
+ * ```
502
524
  */
503
525
  const isArray = (value) => Array.isArray(value);
504
526
 
@@ -515,7 +537,7 @@ __webpack_require__.r(__webpack_exports__);
515
537
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
516
538
  /* harmony export */ isEmptyArray: () => (/* binding */ isEmptyArray)
517
539
  /* harmony export */ });
518
- /* harmony import */ var _array__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ~/array */ "./src/array/index.ts");
540
+ /* harmony import */ var _is_array__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./is-array */ "./src/array/is-array.ts");
519
541
 
520
542
  /**
521
543
  * Checks if a value is an empty array.
@@ -523,8 +545,15 @@ __webpack_require__.r(__webpack_exports__);
523
545
  * @param value - The value to check.
524
546
  *
525
547
  * @returns `true` if the value is an empty array; otherwise, `false`.
548
+ *
549
+ * @example
550
+ * ```ts
551
+ * isEmptyArray([]); // true
552
+ * isEmptyArray([1]); // false
553
+ * isEmptyArray({}); // false
554
+ * ```
526
555
  */
527
- const isEmptyArray = (value) => (0,_array__WEBPACK_IMPORTED_MODULE_0__.isArray)(value) && value.length === 0;
556
+ const isEmptyArray = (value) => (0,_is_array__WEBPACK_IMPORTED_MODULE_0__.isArray)(value) && value.length === 0;
528
557
 
529
558
 
530
559
  /***/ },
@@ -563,6 +592,31 @@ __webpack_require__.r(__webpack_exports__);
563
592
  const pipe = (...fns) => (arg) => fns.reduce((prev, fn) => fn(prev), arg);
564
593
 
565
594
 
595
+ /***/ },
596
+
597
+ /***/ "./src/array/to-map-by.ts"
598
+ /*!********************************!*\
599
+ !*** ./src/array/to-map-by.ts ***!
600
+ \********************************/
601
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
602
+
603
+ __webpack_require__.r(__webpack_exports__);
604
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
605
+ /* harmony export */ toMapBy: () => (/* binding */ toMapBy)
606
+ /* harmony export */ });
607
+ /**
608
+ * Overloaded rather than given a default `Value`, which TypeScript would take from whatever type the
609
+ * result is assigned to - so a map of items could be declared a map of anything.
610
+ */
611
+ function toMapBy(items, getKey, getValue) {
612
+ const map = new Map();
613
+ items.forEach((item, index) => {
614
+ map.set(getKey(item, index), getValue ? getValue(item, index) : item);
615
+ });
616
+ return map;
617
+ }
618
+
619
+
566
620
  /***/ },
567
621
 
568
622
  /***/ "./src/array/toggle-array-item.ts"
@@ -663,9 +717,7 @@ __webpack_require__.r(__webpack_exports__);
663
717
  /* harmony export */ runSequential: () => (/* binding */ runSequential),
664
718
  /* harmony export */ someAsync: () => (/* binding */ someAsync)
665
719
  /* harmony export */ });
666
- /* harmony import */ var _array__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ~/array */ "./src/array/index.ts");
667
- /* harmony import */ var _function__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ~/function */ "./src/function/index.ts");
668
-
720
+ /* harmony import */ var _function__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ~/function */ "./src/function/index.ts");
669
721
 
670
722
  /**
671
723
  * Checks if a value is a Promise.
@@ -676,7 +728,7 @@ __webpack_require__.r(__webpack_exports__);
676
728
  *
677
729
  * @returns `true` if the value is a Promise; otherwise, `false`.
678
730
  */
679
- const isPromise = (value) => (0,_function__WEBPACK_IMPORTED_MODULE_1__.isFunction)(value?.then);
731
+ const isPromise = (value) => (0,_function__WEBPACK_IMPORTED_MODULE_0__.isFunction)(value?.then);
680
732
  /**
681
733
  * Asynchronously iterates over an array and executes an async function on each item sequentially,
682
734
  * collecting the results.
@@ -789,12 +841,15 @@ const filterSequential = async (array, predicate) => {
789
841
  * ```
790
842
  */
791
843
  const filterParallel = async (array, predicate) => {
792
- const results = await runParallel(array, async (item, index, array) => (await predicate(item, index, array)) ? item : false);
793
- return (0,_array__WEBPACK_IMPORTED_MODULE_0__.compact)(results);
844
+ const keep = await runParallel(array, predicate);
845
+ return array.filter((_, index) => keep[index]);
794
846
  };
795
847
  /**
796
848
  * Asynchronously checks if at least one element in the array satisfies the async condition.
797
849
  *
850
+ * Runs one item at a time, in order, and stops at the first item that satisfies it - as
851
+ * `Array.prototype.some` does.
852
+ *
798
853
  * @param array - The array of items to check.
799
854
  * @param predicate - An async function that returns a boolean.
800
855
  *
@@ -811,6 +866,9 @@ const someAsync = async (array, predicate) => {
811
866
  /**
812
867
  * Asynchronously checks if all elements in the array satisfy the async condition.
813
868
  *
869
+ * Runs one item at a time, in order, and stops at the first item that fails it - as
870
+ * `Array.prototype.every` does.
871
+ *
814
872
  * @param array - The array of items to check.
815
873
  * @param predicate - An async function that returns a boolean.
816
874
  *
@@ -846,6 +904,9 @@ const reduceAsync = async (array, fn, initialValue) => {
846
904
  /**
847
905
  * Asynchronously finds the first element that satisfies the async condition.
848
906
  *
907
+ * Runs one item at a time, in order, and stops at the first match - as `Array.prototype.find`
908
+ * does.
909
+ *
849
910
  * @param array - The array of items to search.
850
911
  * @param predicate - An async function that returns a boolean.
851
912
  *
@@ -946,7 +1007,7 @@ __webpack_require__.r(__webpack_exports__);
946
1007
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
947
1008
  /* harmony export */ retry: () => (/* binding */ retry)
948
1009
  /* harmony export */ });
949
- /* harmony import */ var _async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ~/async */ "./src/async/index.ts");
1010
+ /* harmony import */ var _delay__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./delay */ "./src/async/delay.ts");
950
1011
 
951
1012
  /**
952
1013
  * Wraps an asynchronous function with retry logic.
@@ -956,7 +1017,7 @@ __webpack_require__.r(__webpack_exports__);
956
1017
  *
957
1018
  * Useful for operations that may fail intermittently, such as network requests.
958
1019
  *
959
- * @template Task - The type of the async function to wrap.
1020
+ * @template Args - The arguments the async function takes.
960
1021
  * @template TaskResult - The result type of the async function.
961
1022
  *
962
1023
  * @param task - The async function to wrap with retry logic.
@@ -990,18 +1051,19 @@ __webpack_require__.r(__webpack_exports__);
990
1051
  * ```
991
1052
  */
992
1053
  const retry = (task, { maxAttempts = 3, delayMs = 300, backoff = true, onRetry } = {}) => {
1054
+ const attempts = Math.max(1, maxAttempts);
993
1055
  return async (...args) => {
994
1056
  let lastError;
995
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1057
+ for (let attempt = 1; attempt <= attempts; attempt++) {
996
1058
  try {
997
1059
  return await task(...args);
998
1060
  }
999
1061
  catch (e) {
1000
1062
  lastError = e;
1001
- if (attempt < maxAttempts) {
1063
+ if (attempt < attempts) {
1002
1064
  onRetry?.(attempt, e);
1003
1065
  const delayTime = backoff ? delayMs * 2 ** (attempt - 1) : delayMs;
1004
- await (0,_async__WEBPACK_IMPORTED_MODULE_0__.delay)(delayTime);
1066
+ await (0,_delay__WEBPACK_IMPORTED_MODULE_0__.delay)(delayTime);
1005
1067
  }
1006
1068
  }
1007
1069
  }
@@ -1022,8 +1084,6 @@ __webpack_require__.r(__webpack_exports__);
1022
1084
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1023
1085
  /* harmony export */ timeout: () => (/* binding */ timeout)
1024
1086
  /* harmony export */ });
1025
- /* harmony import */ var _async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ~/async */ "./src/async/index.ts");
1026
-
1027
1087
  /**
1028
1088
  * Wraps a promise with a timeout. If the promise does not settle within the specified time,
1029
1089
  * it will reject with a timeout error.
@@ -1047,15 +1107,18 @@ __webpack_require__.r(__webpack_exports__);
1047
1107
  * ```
1048
1108
  */
1049
1109
  const timeout = async (promise, timeoutMs, errorMessage = 'Operation timed out') => {
1050
- const timeoutId = null;
1110
+ let timeoutId = null;
1051
1111
  try {
1052
1112
  return await Promise.race([
1053
1113
  promise,
1054
- (0,_async__WEBPACK_IMPORTED_MODULE_0__.delay)(timeoutMs).then(() => Promise.reject(new Error(errorMessage))),
1114
+ new Promise((_, reject) => {
1115
+ timeoutId = setTimeout(() => reject(new Error(errorMessage)), timeoutMs);
1116
+ }),
1055
1117
  ]);
1056
1118
  }
1057
1119
  finally {
1058
- if (timeoutId) {
1120
+ // Cleared however the race ends, so a settled promise leaves no timer behind
1121
+ if (timeoutId !== null) {
1059
1122
  clearTimeout(timeoutId);
1060
1123
  }
1061
1124
  }
@@ -1157,8 +1220,17 @@ const DATA_TRANSFER_JSON_MIME = 'application/json';
1157
1220
  *
1158
1221
  * Useful for drag-and-drop communication between elements.
1159
1222
  *
1160
- * @param dataTransfer DataTransfer instance from a drag event.
1161
- * @param data Data to serialize.
1223
+ * @template T - The type of the data.
1224
+ *
1225
+ * @param dataTransfer - DataTransfer instance from a drag event.
1226
+ * @param data - Data to serialize.
1227
+ *
1228
+ * @example
1229
+ * ```ts
1230
+ * element.addEventListener('dragstart', event => {
1231
+ * setDataTransferJson(event.dataTransfer, { id: 'card-1' });
1232
+ * });
1233
+ * ```
1162
1234
  */
1163
1235
  const setDataTransferJson = (dataTransfer, data) => {
1164
1236
  dataTransfer.setData(DATA_TRANSFER_JSON_MIME, JSON.stringify(data));
@@ -1166,9 +1238,18 @@ const setDataTransferJson = (dataTransfer, data) => {
1166
1238
  /**
1167
1239
  * Retrieves and parses JSON data from a DataTransfer object.
1168
1240
  *
1169
- * @param dataTransfer DataTransfer instance from a drop event.
1241
+ * @template Value - The type the JSON is expected to have.
1242
+ *
1243
+ * @param dataTransfer - DataTransfer instance from a drop event.
1170
1244
  *
1171
1245
  * @returns Parsed JSON value or null if not present or invalid.
1246
+ *
1247
+ * @example
1248
+ * ```ts
1249
+ * element.addEventListener('drop', event => {
1250
+ * const card = getDataTransferJson<{ id: string }>(event.dataTransfer);
1251
+ * });
1252
+ * ```
1172
1253
  */
1173
1254
  const getDataTransferJson = (dataTransfer) => {
1174
1255
  const rawData = dataTransfer.getData(DATA_TRANSFER_JSON_MIME);
@@ -1205,8 +1286,9 @@ const cloneBlob = (blob) => new Blob([blob], { type: blob.type });
1205
1286
  /**
1206
1287
  * Returns the bounding DOMRect of an element based on offset and client dimensions.
1207
1288
  *
1208
- * This utility is useful when you need a stable, layout-based rect
1209
- * without triggering a reflow via `getBoundingClientRect()`.
1289
+ * Unlike `getBoundingClientRect()`, the rect is the element's layout box, before any CSS
1290
+ * transform - so it stays put while the element or its container is being translated or scaled.
1291
+ * Reading it still needs layout, as `getBoundingClientRect()` does.
1210
1292
  *
1211
1293
  * @param element - The target HTML element.
1212
1294
  * @returns A `DOMRect` representing the element’s offset position and size.
@@ -1228,14 +1310,18 @@ const isAnchorHtmlElement = (element) => element.tagName === 'A';
1228
1310
  /**
1229
1311
  * Checks whether an element is explicitly marked as contenteditable.
1230
1312
  *
1231
- * Browsers treat elements with `contenteditable="true"` as focusable,
1232
- * even if they are not normally keyboard-focusable.
1313
+ * Browsers treat editable elements as focusable, even if they are not normally
1314
+ * keyboard-focusable. An element is editable with `contenteditable` set to `""` (the bare
1315
+ * attribute), `"true"` or `"plaintext-only"`, in any case.
1233
1316
  *
1234
1317
  * @param element - The element to inspect.
1235
1318
  *
1236
- * @returns True if `contenteditable="true"` is set.
1319
+ * @returns True if the element's own `contenteditable` makes it editable.
1237
1320
  */
1238
- const isContentEditableHtmlElement = (element) => element.getAttribute('contenteditable') === 'true';
1321
+ const isContentEditableHtmlElement = (element) => {
1322
+ const value = element.getAttribute('contenteditable');
1323
+ return value !== null && ['', 'true', 'plaintext-only'].includes(value.toLowerCase());
1324
+ };
1239
1325
 
1240
1326
 
1241
1327
  /***/ },
@@ -1274,8 +1360,8 @@ __webpack_require__.r(__webpack_exports__);
1274
1360
  * @param options - Optional configuration controlling filename and link target.
1275
1361
  */
1276
1362
  const downloadFile = (file, { fileName, target } = {}) => {
1277
- // Browser guard (SSR / non-DOM environments)
1278
- if ((0,_guards__WEBPACK_IMPORTED_MODULE_1__.isUndefined)(document)) {
1363
+ // Browser guard (SSR / non-DOM environments) - `typeof`, as reading an undeclared `document` throws
1364
+ if (typeof document === 'undefined') {
1279
1365
  return;
1280
1366
  }
1281
1367
  const link = document.createElement('a');
@@ -1415,7 +1501,7 @@ __webpack_require__.r(__webpack_exports__);
1415
1501
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1416
1502
  /* harmony export */ centerElementInContainer: () => (/* binding */ centerElementInContainer)
1417
1503
  /* harmony export */ });
1418
- /* harmony import */ var _dom__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ~/dom */ "./src/dom/index.ts");
1504
+ /* harmony import */ var _overflow__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./overflow */ "./src/dom/layout/overflow.ts");
1419
1505
  /* harmony import */ var _geometry__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ~/geometry */ "./src/geometry/index.ts");
1420
1506
 
1421
1507
 
@@ -1447,7 +1533,7 @@ const centerElementInContainer = (containerElement, elementToCenter, { axis = 'b
1447
1533
  elementSize: elementToCenter.clientWidth,
1448
1534
  });
1449
1535
  translateX = shouldClamp
1450
- ? (0,_geometry__WEBPACK_IMPORTED_MODULE_1__.clampOffsetToOverflow)(offsetX, (0,_dom__WEBPACK_IMPORTED_MODULE_0__.getXOverflowWidth)(containerElement))
1536
+ ? (0,_geometry__WEBPACK_IMPORTED_MODULE_1__.clampOffsetToOverflow)(offsetX, (0,_overflow__WEBPACK_IMPORTED_MODULE_0__.getXOverflowWidth)(containerElement))
1451
1537
  : offsetX;
1452
1538
  }
1453
1539
  if (axis === 'y' || axis === 'both') {
@@ -1457,7 +1543,7 @@ const centerElementInContainer = (containerElement, elementToCenter, { axis = 'b
1457
1543
  elementSize: elementToCenter.clientHeight,
1458
1544
  });
1459
1545
  translateY = shouldClamp
1460
- ? (0,_geometry__WEBPACK_IMPORTED_MODULE_1__.clampOffsetToOverflow)(offsetY, (0,_dom__WEBPACK_IMPORTED_MODULE_0__.getYOverflowHeight)(containerElement))
1546
+ ? (0,_geometry__WEBPACK_IMPORTED_MODULE_1__.clampOffsetToOverflow)(offsetY, (0,_overflow__WEBPACK_IMPORTED_MODULE_0__.getYOverflowHeight)(containerElement))
1461
1547
  : offsetY;
1462
1548
  }
1463
1549
  containerElement.style.transform = `translate(${translateX}px, ${translateY}px)`;
@@ -1574,6 +1660,11 @@ __webpack_require__.r(__webpack_exports__);
1574
1660
  *
1575
1661
  * Only works with 2D transforms (i.e., `matrix(a, b, c, d, e, f)`).
1576
1662
  *
1663
+ * The values are the matrix's own entries rather than decomposed angles and factors: `scaleX` is
1664
+ * `a`, `skewY` is `b`, `skewX` is `c`, `scaleY` is `d`, and the translation is `e` and `f`. They read
1665
+ * as scale and skew only for a transform with no rotation - `rotate(90deg)` gives `scaleX` and
1666
+ * `scaleY` of 0 - and a skew is its tangent (`skewX(30deg)` gives `skewX` ≈ 0.577), not its angle.
1667
+ *
1577
1668
  * @param element - The element with a CSS transform applied.
1578
1669
  * @returns An object with parsed transformation values.
1579
1670
  *
@@ -1628,6 +1719,27 @@ __webpack_require__.r(__webpack_exports__);
1628
1719
  /* harmony import */ var _decode_base64__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./decode-base64 */ "./src/encoding/decode-base64.ts");
1629
1720
 
1630
1721
 
1722
+ /**
1723
+ * Decodes a base64 string and parses the JSON it holds - the reverse of `encodeBase64Json`.
1724
+ *
1725
+ * Never throws: a value that is not valid base64, or does not decode to valid JSON, gives `null`.
1726
+ * The result is only cast to `Value`, not checked against it.
1727
+ *
1728
+ * @template Value - The type the JSON is expected to have.
1729
+ *
1730
+ * @param value - The base64 string to decode.
1731
+ *
1732
+ * @returns The parsed value, or `null` when the value cannot be decoded or parsed.
1733
+ *
1734
+ * @example
1735
+ * ```ts
1736
+ * decodeBase64Json<{ name: string; age: number }>('eyJuYW1lIjoiTWlrZSIsImFnZSI6MzN9');
1737
+ * // ➜ { name: 'Mike', age: 33 }
1738
+ *
1739
+ * decodeBase64Json('SGVsbG8=');
1740
+ * // ➜ null - "Hello" is not JSON
1741
+ * ```
1742
+ */
1631
1743
  const decodeBase64Json = (value) => {
1632
1744
  const decodedValue = (0,_decode_base64__WEBPACK_IMPORTED_MODULE_1__.decodeBase64)(value);
1633
1745
  if (decodedValue === null) {
@@ -1664,12 +1776,37 @@ const decodeBase64Bytes = (value) => {
1664
1776
  }
1665
1777
  return bytes;
1666
1778
  };
1779
+ /**
1780
+ * Decodes a base64 string to the UTF-8 text it encodes - the reverse of `encodeBase64`.
1781
+ *
1782
+ * Never throws: a value that is not padded base64 of the standard alphabet, or whose bytes are not
1783
+ * valid UTF-8, decodes to `null`.
1784
+ *
1785
+ * Uses `Uint8Array.fromBase64` where the runtime has it, Node's `Buffer` otherwise, and `atob`
1786
+ * last.
1787
+ *
1788
+ * @param value - The base64 string to decode.
1789
+ *
1790
+ * @returns The decoded text, or `null` when the value is not valid base64 of UTF-8 text.
1791
+ *
1792
+ * @example
1793
+ * ```ts
1794
+ * decodeBase64('SGVsbG8=');
1795
+ * // ➜ 'Hello'
1796
+ *
1797
+ * decodeBase64('not base64!');
1798
+ * // ➜ null
1799
+ * ```
1800
+ */
1667
1801
  const decodeBase64 = (value) => {
1668
1802
  if (!BASE64_PATTERN.test(value)) {
1669
1803
  return null;
1670
1804
  }
1671
1805
  try {
1672
- return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(decodeBase64Bytes(value));
1806
+ return new TextDecoder('utf-8', {
1807
+ fatal: true,
1808
+ ignoreBOM: true,
1809
+ }).decode(decodeBase64Bytes(value));
1673
1810
  }
1674
1811
  catch {
1675
1812
  return null;
@@ -1691,6 +1828,22 @@ __webpack_require__.r(__webpack_exports__);
1691
1828
  /* harmony export */ });
1692
1829
  /* harmony import */ var _encode_base64__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./encode-base64 */ "./src/encoding/encode-base64.ts");
1693
1830
 
1831
+ /**
1832
+ * Serialises a value as JSON and encodes it as base64 - a structured value carried in a URL query
1833
+ * parameter, a cookie or a header.
1834
+ *
1835
+ * @template Value - The type of the value.
1836
+ *
1837
+ * @param value - Any JSON-serialisable value.
1838
+ *
1839
+ * @returns The base64 encoding of the value's JSON.
1840
+ *
1841
+ * @example
1842
+ * ```ts
1843
+ * encodeBase64Json({ name: 'Mike', age: 33 });
1844
+ * // ➜ 'eyJuYW1lIjoiTWlrZSIsImFnZSI6MzN9'
1845
+ * ```
1846
+ */
1694
1847
  const encodeBase64Json = (value) => (0,_encode_base64__WEBPACK_IMPORTED_MODULE_0__.encodeBase64)(JSON.stringify(value));
1695
1848
 
1696
1849
 
@@ -1719,6 +1872,26 @@ const encodeBase64Bytes = (bytes) => {
1719
1872
  }
1720
1873
  return btoa(binary);
1721
1874
  };
1875
+ /**
1876
+ * Encodes a string as base64, by its UTF-8 bytes - so text outside Latin-1 (accents, emoji, any
1877
+ * script), which `btoa` throws on, encodes as well.
1878
+ *
1879
+ * Uses `Uint8Array.prototype.toBase64` where the runtime has it, Node's `Buffer` otherwise, and
1880
+ * `btoa` last.
1881
+ *
1882
+ * @param value - The string to encode.
1883
+ *
1884
+ * @returns The base64 encoding of the string's UTF-8 bytes.
1885
+ *
1886
+ * @example
1887
+ * ```ts
1888
+ * encodeBase64('Hello');
1889
+ * // ➜ 'SGVsbG8='
1890
+ *
1891
+ * encodeBase64('Привіт');
1892
+ * // ➜ '0J/RgNC40LLRltGC'
1893
+ * ```
1894
+ */
1722
1895
  const encodeBase64 = (value) => encodeBase64Bytes(new TextEncoder().encode(value));
1723
1896
 
1724
1897
 
@@ -1758,7 +1931,9 @@ __webpack_require__.r(__webpack_exports__);
1758
1931
  __webpack_require__.r(__webpack_exports__);
1759
1932
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1760
1933
  /* harmony export */ getLocalStorageCapabilities: () => (/* reexport safe */ _storage__WEBPACK_IMPORTED_MODULE_0__.getLocalStorageCapabilities),
1761
- /* harmony export */ isLocalStorageReadable: () => (/* reexport safe */ _storage__WEBPACK_IMPORTED_MODULE_0__.isLocalStorageReadable)
1934
+ /* harmony export */ isLocalStorageReadable: () => (/* reexport safe */ _storage__WEBPACK_IMPORTED_MODULE_0__.isLocalStorageReadable),
1935
+ /* harmony export */ readLocalStorageJson: () => (/* reexport safe */ _storage__WEBPACK_IMPORTED_MODULE_0__.readLocalStorageJson),
1936
+ /* harmony export */ writeLocalStorageJson: () => (/* reexport safe */ _storage__WEBPACK_IMPORTED_MODULE_0__.writeLocalStorageJson)
1762
1937
  /* harmony export */ });
1763
1938
  /* harmony import */ var _storage__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./storage */ "./src/env/storage/index.ts");
1764
1939
 
@@ -1775,10 +1950,75 @@ __webpack_require__.r(__webpack_exports__);
1775
1950
  __webpack_require__.r(__webpack_exports__);
1776
1951
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1777
1952
  /* harmony export */ getLocalStorageCapabilities: () => (/* reexport safe */ _local_storage__WEBPACK_IMPORTED_MODULE_0__.getLocalStorageCapabilities),
1778
- /* harmony export */ isLocalStorageReadable: () => (/* reexport safe */ _local_storage__WEBPACK_IMPORTED_MODULE_0__.isLocalStorageReadable)
1953
+ /* harmony export */ isLocalStorageReadable: () => (/* reexport safe */ _local_storage__WEBPACK_IMPORTED_MODULE_0__.isLocalStorageReadable),
1954
+ /* harmony export */ readLocalStorageJson: () => (/* reexport safe */ _local_storage_json__WEBPACK_IMPORTED_MODULE_1__.readLocalStorageJson),
1955
+ /* harmony export */ writeLocalStorageJson: () => (/* reexport safe */ _local_storage_json__WEBPACK_IMPORTED_MODULE_1__.writeLocalStorageJson)
1779
1956
  /* harmony export */ });
1780
1957
  /* harmony import */ var _local_storage__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./local-storage */ "./src/env/storage/local-storage.ts");
1958
+ /* harmony import */ var _local_storage_json__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./local-storage-json */ "./src/env/storage/local-storage-json.ts");
1959
+
1960
+
1961
+
1962
+
1963
+ /***/ },
1964
+
1965
+ /***/ "./src/env/storage/local-storage-json.ts"
1966
+ /*!***********************************************!*\
1967
+ !*** ./src/env/storage/local-storage-json.ts ***!
1968
+ \***********************************************/
1969
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
1970
+
1971
+ __webpack_require__.r(__webpack_exports__);
1972
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1973
+ /* harmony export */ readLocalStorageJson: () => (/* binding */ readLocalStorageJson),
1974
+ /* harmony export */ writeLocalStorageJson: () => (/* binding */ writeLocalStorageJson)
1975
+ /* harmony export */ });
1976
+ /* harmony import */ var _json__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ~/json */ "./src/json/index.ts");
1781
1977
 
1978
+ /**
1979
+ * Reads the value stored as JSON under a key in `localStorage`.
1980
+ *
1981
+ * Never throws: storage that cannot be read at all (SSR, disabled storage, sandboxed iframes, strict
1982
+ * privacy modes) reads as nothing stored, as does a value that is not JSON.
1983
+ *
1984
+ * The value is only cast to `Value`, not checked against it - something stored by an older version
1985
+ * of the app, or by hand, can be of any shape, so check it before relying on it.
1986
+ *
1987
+ * @param key - The `localStorage` key to read.
1988
+ *
1989
+ * @returns The parsed value, or `null` when nothing is stored under the key, what is stored is not
1990
+ * valid JSON, or storage cannot be read.
1991
+ */
1992
+ const readLocalStorageJson = (key) => {
1993
+ try {
1994
+ const value = window.localStorage.getItem(key);
1995
+ return value === null ? null : (0,_json__WEBPACK_IMPORTED_MODULE_0__.parseJson)(value);
1996
+ }
1997
+ catch {
1998
+ return null;
1999
+ }
2000
+ };
2001
+ /**
2002
+ * Stores a value under a key in `localStorage`, as JSON.
2003
+ *
2004
+ * Never throws: a write that fails - storage full (`QuotaExceededError`), Safari Private Mode,
2005
+ * security-restricted contexts, SSR, or a value `JSON.stringify()` cannot serialise (a circular
2006
+ * reference, a `BigInt`) - reports `false` instead.
2007
+ *
2008
+ * @param key - The `localStorage` key to write.
2009
+ * @param value - Any JSON-serialisable value.
2010
+ *
2011
+ * @returns `true` once the value is stored, `false` when it could not be.
2012
+ */
2013
+ const writeLocalStorageJson = (key, value) => {
2014
+ try {
2015
+ window.localStorage.setItem(key, JSON.stringify(value));
2016
+ return true;
2017
+ }
2018
+ catch {
2019
+ return false;
2020
+ }
2021
+ };
1782
2022
 
1783
2023
 
1784
2024
  /***/ },
@@ -1806,10 +2046,14 @@ __webpack_require__.r(__webpack_exports__);
1806
2046
  * throw; otherwise `false`.
1807
2047
  */
1808
2048
  const isLocalStorageReadable = () => {
1809
- if (typeof window === 'undefined' || !window.localStorage) {
2049
+ if (typeof window === 'undefined') {
1810
2050
  return false;
1811
2051
  }
2052
+ // Inside the `try`: in a sandboxed iframe, or with storage blocked, reading the property throws
1812
2053
  try {
2054
+ if (!window.localStorage) {
2055
+ return false;
2056
+ }
1813
2057
  window.localStorage.getItem('__non_existing_key__');
1814
2058
  return true;
1815
2059
  }
@@ -1958,7 +2202,8 @@ const readFileSystemDirectoryEntries = async (directoryEntry) => {
1958
2202
  *
1959
2203
  * This function walks through all subdirectories, resolving each file into a `File` object.
1960
2204
  * Directories themselves are not returned. To avoid unnecessary noise, certain system or
1961
- * OS-generated files can be excluded via the `skipFiles` option.
2205
+ * OS-generated files and folders can be excluded via the `skipFiles` option - a folder named there
2206
+ * is not entered.
1962
2207
  *
1963
2208
  * @param directoryEntry - The starting directory entry to traverse.
1964
2209
  * @param options - Optional settings that control traversal behavior.
@@ -1978,18 +2223,18 @@ const traverseFileSystemDirectory = async (directoryEntry, { skipFiles = [
1978
2223
  const skipFilesSet = new Set(skipFiles);
1979
2224
  const entries = await readFileSystemDirectoryEntries(directoryEntry);
1980
2225
  const filePromises = await (0,_async__WEBPACK_IMPORTED_MODULE_0__.runParallel)(entries, async (entry) => {
2226
+ if (skipFilesSet.has(entry.name)) {
2227
+ return [];
2228
+ }
1981
2229
  if (entry.isDirectory) {
1982
2230
  return traverseFileSystemDirectory(entry, {
1983
2231
  skipFiles,
1984
2232
  });
1985
2233
  }
1986
- else if (!skipFilesSet.has(entry.name)) {
1987
- const file = await new Promise((resolve, reject) => {
1988
- entry.file(resolve, reject);
1989
- });
1990
- return [file];
1991
- }
1992
- return [];
2234
+ const file = await new Promise((resolve, reject) => {
2235
+ entry.file(resolve, reject);
2236
+ });
2237
+ return [file];
1993
2238
  });
1994
2239
  return filePromises.flat();
1995
2240
  };
@@ -2059,6 +2304,17 @@ __webpack_require__.r(__webpack_exports__);
2059
2304
  /* harmony export */ not: () => (/* binding */ not),
2060
2305
  /* harmony export */ once: () => (/* binding */ once)
2061
2306
  /* harmony export */ });
2307
+ /**
2308
+ * A function that does nothing and returns `undefined` - a default for an optional callback, so the
2309
+ * caller can call it without checking it is there.
2310
+ *
2311
+ * @example
2312
+ * ```ts
2313
+ * const { onClose = noop } = props;
2314
+ *
2315
+ * onClose();
2316
+ * ```
2317
+ */
2062
2318
  const noop = () => { };
2063
2319
  /**
2064
2320
  * Checks if a value is a function.
@@ -2067,6 +2323,7 @@ const noop = () => { };
2067
2323
  *
2068
2324
  * @returns `true` if the value is a function; otherwise, `false`.
2069
2325
  */
2326
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- what `typeof value === 'function'` narrows `unknown` to
2070
2327
  const isFunction = (value) => typeof value === 'function';
2071
2328
  /**
2072
2329
  * Creates a function that negates the result of the given predicate function.
@@ -2091,6 +2348,7 @@ const not = (fn) => (...args) => !fn(...args);
2091
2348
  * Wraps a function so that it can only be executed once.
2092
2349
  * The wrapped function remembers (caches) the result of the first invocation
2093
2350
  * and returns that same result for all subsequent calls, regardless of the arguments provided.
2351
+ * Where the first invocation throws, every call throws that same error - `fn` is not run again.
2094
2352
  *
2095
2353
  * Common use cases include:
2096
2354
  * - initializing singletons
@@ -2108,10 +2366,21 @@ const not = (fn) => (...args) => !fn(...args);
2108
2366
  const once = (fn) => {
2109
2367
  let called = false;
2110
2368
  let result;
2369
+ let hasThrown = false;
2370
+ let error;
2111
2371
  return function (...args) {
2112
2372
  if (!called) {
2113
2373
  called = true;
2114
- result = fn.apply(this, args);
2374
+ try {
2375
+ result = fn.apply(this, args);
2376
+ }
2377
+ catch (e) {
2378
+ hasThrown = true;
2379
+ error = e;
2380
+ }
2381
+ }
2382
+ if (hasThrown) {
2383
+ throw error;
2115
2384
  }
2116
2385
  return result;
2117
2386
  };
@@ -2179,7 +2448,7 @@ __webpack_require__.r(__webpack_exports__);
2179
2448
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
2180
2449
  /* harmony export */ applyInertiaStep: () => (/* binding */ applyInertiaStep)
2181
2450
  /* harmony export */ });
2182
- /* harmony import */ var _geometry__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ~/geometry */ "./src/geometry/index.ts");
2451
+ /* harmony import */ var _resolve_bounded_delta__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./resolve-bounded-delta */ "./src/geometry/resolve-bounded-delta.ts");
2183
2452
 
2184
2453
  /**
2185
2454
  * Applies a **single step** of bounded, velocity-based inertia.
@@ -2224,7 +2493,7 @@ const applyInertiaStep = ({ value, min, max, velocityPxMs, deltaTimeMs, friction
2224
2493
  if (Math.abs(velocityPxMs) < minVelocityPxMs) {
2225
2494
  return null;
2226
2495
  }
2227
- const nextValue = (0,_geometry__WEBPACK_IMPORTED_MODULE_0__.resolveBoundedDelta)({
2496
+ const nextValue = (0,_resolve_bounded_delta__WEBPACK_IMPORTED_MODULE_0__.resolveBoundedDelta)({
2228
2497
  delta: velocityPxMs * deltaTimeMs,
2229
2498
  value,
2230
2499
  min,
@@ -2289,13 +2558,24 @@ __webpack_require__.r(__webpack_exports__);
2289
2558
  /**
2290
2559
  * Calculates the raw offset required to center an element within a container along a single axis.
2291
2560
  *
2292
- * @returns A negative offset value suitable for use in a CSS `translate` transform.
2561
+ * @param options - The container's visible size, and the element's offset and size, along the axis.
2562
+ *
2563
+ * @returns The offset to translate the container by: negative for an element past the container's
2564
+ * centre, positive for one before it, and `0` for one already centred. Clamp it with
2565
+ * `clampOffsetToOverflow` to keep the container within its overflow.
2566
+ *
2567
+ * @example
2568
+ * ```ts
2569
+ * calculateCenterOffset({ containerSize: 100, elementOffset: 200, elementSize: 20 });
2570
+ * // -160
2571
+ * ```
2293
2572
  */
2294
2573
  const calculateCenterOffset = ({ containerSize, elementOffset, elementSize, }) => {
2295
2574
  const containerCenter = containerSize / 2;
2296
2575
  const elementCenter = elementOffset + elementSize / 2;
2297
2576
  const targetOffset = elementCenter - containerCenter;
2298
- return -targetOffset;
2577
+ // `|| 0` turns the `-0` of an element already centred into `0`
2578
+ return -targetOffset || 0;
2299
2579
  };
2300
2580
 
2301
2581
 
@@ -2318,6 +2598,12 @@ __webpack_require__.r(__webpack_exports__);
2318
2598
  * @param overflowSize - Maximum overflow size along the axis.
2319
2599
  *
2320
2600
  * @returns Clamped negative offset, or `0` when no overflow exists.
2601
+ *
2602
+ * @example
2603
+ * ```ts
2604
+ * clampOffsetToOverflow(-160, 100); // -100
2605
+ * clampOffsetToOverflow(40, 100); // 0
2606
+ * ```
2321
2607
  */
2322
2608
  const clampOffsetToOverflow = (offset, overflowSize) => {
2323
2609
  if (overflowSize <= 0) {
@@ -2455,6 +2741,26 @@ __webpack_require__.r(__webpack_exports__);
2455
2741
  /* harmony export */ isUndefined: () => (/* binding */ isUndefined),
2456
2742
  /* harmony export */ isValidDate: () => (/* binding */ isValidDate)
2457
2743
  /* harmony export */ });
2744
+ /**
2745
+ * Asserts that a condition holds, narrowing it for the code after the call.
2746
+ *
2747
+ * A `function` declaration rather than an arrow, which TypeScript needs for an `asserts` signature
2748
+ * to narrow at the call site.
2749
+ *
2750
+ * @param condition - The condition expected to be truthy.
2751
+ * @param message - The message of the error thrown when it is not.
2752
+ *
2753
+ * @throws {Error} When `condition` is falsy.
2754
+ *
2755
+ * @example
2756
+ * ```ts
2757
+ * const user = users.find(user => user.id === id);
2758
+ *
2759
+ * assert(user, `User ${id} not found`);
2760
+ *
2761
+ * user.name; // `user` is no longer `undefined` here
2762
+ * ```
2763
+ */
2458
2764
  function assert(condition, message) {
2459
2765
  if (!condition) {
2460
2766
  throw new Error(message);
@@ -2509,21 +2815,23 @@ const isNumber = (value) => typeof value === 'number';
2509
2815
  */
2510
2816
  const isBool = (value) => typeof value === 'boolean';
2511
2817
  /**
2512
- * Checks if a value is an object.
2818
+ * Checks if a value is an object - anything `typeof` calls `'object'` (plain objects, arrays,
2819
+ * dates, maps, class instances), except `null`.
2513
2820
  *
2514
2821
  * @param value - The value to check.
2515
2822
  *
2516
- * @returns `true` if the value is an object; otherwise, `false`.
2823
+ * @returns `true` if the value is a non-null object; otherwise, `false`.
2517
2824
  */
2518
- const isObject = (value) => typeof value === 'object';
2825
+ const isObject = (value) => typeof value === 'object' && value !== null;
2519
2826
  /**
2520
- * Checks if a value is an empty object (no own enumerable properties).
2827
+ * Checks if a value is an empty object (no own enumerable properties, symbol-keyed ones included).
2521
2828
  *
2522
2829
  * @param value - The value to check.
2523
2830
  *
2524
2831
  * @returns `true` if the value is an empty object; otherwise, `false`.
2525
2832
  */
2526
- const isEmptyObject = (value) => isObject(value) && !isNull(value) && Object.keys(value).length === 0;
2833
+ const isEmptyObject = (value) => isObject(value) &&
2834
+ Reflect.ownKeys(value).every(key => !Object.prototype.propertyIsEnumerable.call(value, key));
2527
2835
  /**
2528
2836
  * Checks if a value is a Date object.
2529
2837
  *
@@ -2762,13 +3070,23 @@ __webpack_require__.r(__webpack_exports__);
2762
3070
  * @param sourceRect - The rectangle used to measure overlap against the target.
2763
3071
  * @param targetRect - The rectangle whose covered area is measured.
2764
3072
  *
2765
- * @returns A number between `0` and `1` representing the intersection ratio.
3073
+ * @returns A number between `0` and `1` representing the intersection ratio - `0` for a target
3074
+ * with no area, which nothing can cover.
3075
+ *
3076
+ * @example
3077
+ * ```ts
3078
+ * getDOMRectIntersectionRatio(new DOMRect(0, 0, 100, 100), new DOMRect(50, 0, 100, 100));
3079
+ * // 0.5
3080
+ * ```
2766
3081
  */
2767
3082
  const getDOMRectIntersectionRatio = (sourceRect, targetRect) => {
2768
3083
  const xOverlap = Math.max(0, Math.min(sourceRect.right, targetRect.right) - Math.max(sourceRect.left, targetRect.left));
2769
3084
  const yOverlap = Math.max(0, Math.min(sourceRect.bottom, targetRect.bottom) - Math.max(sourceRect.top, targetRect.top));
2770
3085
  const intersectionArea = xOverlap * yOverlap;
2771
3086
  const targetArea = targetRect.width * targetRect.height;
3087
+ if (targetArea <= 0) {
3088
+ return 0;
3089
+ }
2772
3090
  return intersectionArea / targetArea;
2773
3091
  };
2774
3092
 
@@ -2830,26 +3148,35 @@ __webpack_require__.r(__webpack_exports__);
2830
3148
  * @param options - Resolution behavior configuration.
2831
3149
  *
2832
3150
  * @returns A normalized, axis-aligned delta suitable for further scroll or movement processing.
3151
+ *
3152
+ * @example
3153
+ * ```ts
3154
+ * // A vertical wheel scrolling a horizontal list
3155
+ * resolveAxisDelta({ deltaX: 0, deltaY: 40 }, 'x');
3156
+ * // { deltaX: -40, deltaY: 0 }
3157
+ * ```
2833
3158
  */
2834
3159
  const resolveAxisDelta = (delta, axis, { allowFallback = true, invert = true } = {}) => {
2835
3160
  const sign = invert ? -1 : 1;
3161
+ // `|| 0` keeps an inverted zero `0` rather than `-0`
3162
+ const resolve = (value) => sign * value || 0;
2836
3163
  switch (axis) {
2837
3164
  case 'x': {
2838
3165
  const value = delta.deltaX !== 0 ? delta.deltaX : allowFallback ? delta.deltaY : 0;
2839
3166
  return {
2840
- deltaX: sign * value,
3167
+ deltaX: resolve(value),
2841
3168
  deltaY: 0,
2842
3169
  };
2843
3170
  }
2844
3171
  case 'y':
2845
3172
  return {
2846
3173
  deltaX: 0,
2847
- deltaY: sign * delta.deltaY,
3174
+ deltaY: resolve(delta.deltaY),
2848
3175
  };
2849
3176
  default:
2850
3177
  return {
2851
- deltaX: sign * delta.deltaX,
2852
- deltaY: sign * delta.deltaY,
3178
+ deltaX: resolve(delta.deltaX),
3179
+ deltaY: resolve(delta.deltaY),
2853
3180
  };
2854
3181
  }
2855
3182
  };
@@ -2883,6 +3210,27 @@ __webpack_require__.r(__webpack_exports__);
2883
3210
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
2884
3211
  /* harmony export */ parseJson: () => (/* binding */ parseJson)
2885
3212
  /* harmony export */ });
3213
+ /**
3214
+ * Parses a JSON string without throwing.
3215
+ *
3216
+ * The result is only cast to `Value`, not checked against it - check its shape before relying on
3217
+ * it where the string comes from outside the program.
3218
+ *
3219
+ * @template Value - The type the JSON is expected to have.
3220
+ *
3221
+ * @param value - The JSON string to parse.
3222
+ *
3223
+ * @returns The parsed value, or `null` when the string is not valid JSON.
3224
+ *
3225
+ * @example
3226
+ * ```ts
3227
+ * parseJson<{ name: string }>('{"name":"Mike"}');
3228
+ * // ➜ { name: 'Mike' }
3229
+ *
3230
+ * parseJson('{name:"Mike"}');
3231
+ * // ➜ null
3232
+ * ```
3233
+ */
2886
3234
  const parseJson = (value) => {
2887
3235
  try {
2888
3236
  return JSON.parse(value);
@@ -2958,7 +3306,7 @@ __webpack_require__.r(__webpack_exports__);
2958
3306
  * @example
2959
3307
  * ```ts
2960
3308
  * const className = hashString('background-color: red;');
2961
- * // → 'e4k1z0x'
3309
+ * // → '1s0s409'
2962
3310
  * ```
2963
3311
  */
2964
3312
  const hashString = (input) => {
@@ -3017,6 +3365,11 @@ __webpack_require__.r(__webpack_exports__);
3017
3365
  * @param endY - The Y coordinate of the ending point.
3018
3366
  *
3019
3367
  * @returns The Euclidean distance between the two points.
3368
+ *
3369
+ * @example
3370
+ * ```ts
3371
+ * calculateEuclideanDistance(0, 0, 3, 4); // 5
3372
+ * ```
3020
3373
  */
3021
3374
  const calculateEuclideanDistance = (startX, startY, endX, endY) => {
3022
3375
  const deltaX = endX - startX;
@@ -3029,7 +3382,13 @@ const calculateEuclideanDistance = (startX, startY, endX, endY) => {
3029
3382
  * @param distance - The distance.
3030
3383
  * @param elapsedTime - The time taken to move the distance.
3031
3384
  *
3032
- * @returns The calculated speed, which is the absolute value of delta divided by elapsed time.
3385
+ * @returns The calculated speed, which is the absolute value of delta divided by elapsed time -
3386
+ * `Infinity` for a distance covered in no time, and `NaN` for none covered in none.
3387
+ *
3388
+ * @example
3389
+ * ```ts
3390
+ * calculateMovingSpeed(-120, 60); // 2
3391
+ * ```
3033
3392
  */
3034
3393
  const calculateMovingSpeed = (distance, elapsedTime) => Math.abs(distance / elapsedTime);
3035
3394
  /**
@@ -3039,6 +3398,11 @@ const calculateMovingSpeed = (distance, elapsedTime) => Math.abs(distance / elap
3039
3398
  * @param percentage - The percentage to calculate.
3040
3399
  *
3041
3400
  * @returns The calculated percentage of the value.
3401
+ *
3402
+ * @example
3403
+ * ```ts
3404
+ * calculatePercentage(200, 15); // 30
3405
+ * ```
3042
3406
  */
3043
3407
  const calculatePercentage = (value, percentage) => (value * percentage) / 100;
3044
3408
 
@@ -3070,12 +3434,19 @@ __webpack_require__.r(__webpack_exports__);
3070
3434
  *
3071
3435
  * @returns A new object containing only keys whose runtime values are not `undefined`.
3072
3436
  */
3073
- const definedProps = (obj) => Object.entries(obj).reduce((result, [key, value]) => {
3074
- if (value !== undefined) {
3075
- result[key] = value;
3076
- }
3437
+ const definedProps = (obj) => {
3438
+ const result = {};
3439
+ Reflect.ownKeys(obj).forEach(key => {
3440
+ if (!Object.prototype.propertyIsEnumerable.call(obj, key)) {
3441
+ return;
3442
+ }
3443
+ const value = obj[key];
3444
+ if (value !== undefined) {
3445
+ result[key] = value;
3446
+ }
3447
+ });
3077
3448
  return result;
3078
- }, {});
3449
+ };
3079
3450
 
3080
3451
 
3081
3452
  /***/ },
@@ -3116,7 +3487,7 @@ const camelToDashCase = (input) => {
3116
3487
  // Convert the first character to the lowercase without adding a hyphen
3117
3488
  const firstCharProcessed = firstChar.toLowerCase();
3118
3489
  // Process the rest of the string normally, adding hyphens before uppercase letters
3119
- const restProcessed = restOfString.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
3490
+ const restProcessed = restOfString.replace(/\p{Lu}/gu, letter => `-${letter.toLowerCase()}`);
3120
3491
  return firstCharProcessed + restProcessed;
3121
3492
  };
3122
3493
 
@@ -3151,7 +3522,7 @@ __webpack_require__.r(__webpack_exports__);
3151
3522
  * camelToWords('userIDNumber'); // → 'user ID Number'
3152
3523
  * ```
3153
3524
  */
3154
- const camelToWords = (input) => input.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
3525
+ const camelToWords = (input) => input.replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, '$1 $2').replace(/(\p{Lu})(\p{Lu}\p{Ll})/gu, '$1 $2');
3155
3526
 
3156
3527
 
3157
3528
  /***/ },
@@ -3268,7 +3639,7 @@ const getWordsInitials = (input, maxWords = Infinity) => {
3268
3639
  }
3269
3640
  return (0,_split_string_into_words__WEBPACK_IMPORTED_MODULE_0__.splitStringIntoWords)(input)
3270
3641
  .slice(0, maxWords)
3271
- .map(word => word[0])
3642
+ .map(word => String.fromCodePoint(word.codePointAt(0)))
3272
3643
  .join('')
3273
3644
  .toUpperCase();
3274
3645
  };
@@ -3515,7 +3886,7 @@ __webpack_require__.r(__webpack_exports__);
3515
3886
  * toKebabCase('hello123World'); // → 'hello123-world'
3516
3887
  * ```
3517
3888
  */
3518
- const toKebabCase = (input) => input.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
3889
+ const toKebabCase = (input) => input.replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, '$1-$2').toLowerCase();
3519
3890
 
3520
3891
 
3521
3892
  /***/ },
@@ -3982,6 +4353,7 @@ __webpack_require__.r(__webpack_exports__);
3982
4353
  /* harmony export */ parseJson: () => (/* reexport safe */ _json__WEBPACK_IMPORTED_MODULE_17__.parseJson),
3983
4354
  /* harmony export */ pipe: () => (/* reexport safe */ _array__WEBPACK_IMPORTED_MODULE_1__.pipe),
3984
4355
  /* harmony export */ readFilesFromDataTransfer: () => (/* reexport safe */ _file__WEBPACK_IMPORTED_MODULE_5__.readFilesFromDataTransfer),
4356
+ /* harmony export */ readLocalStorageJson: () => (/* reexport safe */ _env__WEBPACK_IMPORTED_MODULE_4__.readLocalStorageJson),
3985
4357
  /* harmony export */ reduceAsync: () => (/* reexport safe */ _async__WEBPACK_IMPORTED_MODULE_2__.reduceAsync),
3986
4358
  /* harmony export */ replaceHistorySearchParams: () => (/* reexport safe */ _dom__WEBPACK_IMPORTED_MODULE_3__.replaceHistorySearchParams),
3987
4359
  /* harmony export */ resolveAxisDelta: () => (/* reexport safe */ _intersection__WEBPACK_IMPORTED_MODULE_10__.resolveAxisDelta),
@@ -3996,9 +4368,11 @@ __webpack_require__.r(__webpack_exports__);
3996
4368
  /* harmony export */ splitStringIntoWords: () => (/* reexport safe */ _string__WEBPACK_IMPORTED_MODULE_13__.splitStringIntoWords),
3997
4369
  /* harmony export */ timeout: () => (/* reexport safe */ _async__WEBPACK_IMPORTED_MODULE_2__.timeout),
3998
4370
  /* harmony export */ toKebabCase: () => (/* reexport safe */ _string__WEBPACK_IMPORTED_MODULE_13__.toKebabCase),
4371
+ /* harmony export */ toMapBy: () => (/* reexport safe */ _array__WEBPACK_IMPORTED_MODULE_1__.toMapBy),
3999
4372
  /* harmony export */ toggleArrayItem: () => (/* reexport safe */ _array__WEBPACK_IMPORTED_MODULE_1__.toggleArrayItem),
4000
4373
  /* harmony export */ traverseFileSystemDirectory: () => (/* reexport safe */ _file__WEBPACK_IMPORTED_MODULE_5__.traverseFileSystemDirectory),
4001
- /* harmony export */ unique: () => (/* reexport safe */ _array__WEBPACK_IMPORTED_MODULE_1__.unique)
4374
+ /* harmony export */ unique: () => (/* reexport safe */ _array__WEBPACK_IMPORTED_MODULE_1__.unique),
4375
+ /* harmony export */ writeLocalStorageJson: () => (/* reexport safe */ _env__WEBPACK_IMPORTED_MODULE_4__.writeLocalStorageJson)
4002
4376
  /* harmony export */ });
4003
4377
  /* harmony import */ var _a11y__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a11y */ "./src/a11y/index.ts");
4004
4378
  /* harmony import */ var _array__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./array */ "./src/array/index.ts");