@ssa-ui-kit/utils 3.21.3 → 3.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -46,6 +46,21 @@ __webpack_require__.d(dateFormatters_namespaceObject, {
46
46
 
47
47
  ;// ./src/utils/CallAll.ts
48
48
  /* eslint-disable @typescript-eslint/no-explicit-any */
49
+ /**
50
+ * Combines several handlers into one that calls each with the same arguments.
51
+ *
52
+ * Nullish entries are skipped, so an optional prop can be passed straight
53
+ * through without a guard — the usual case is merging a component's own handler
54
+ * with one a consumer supplied.
55
+ *
56
+ * Handlers run in order and their return values are discarded. Because they all
57
+ * receive the same event object, one calling `stopPropagation` affects the
58
+ * others' view of it, but it does not stop later handlers from running.
59
+ *
60
+ * @example
61
+ * // `props.onClick` may be undefined — callAll skips it
62
+ * <button onClick={callAll(closeMenu, props.onClick)}>Close</button>
63
+ */
49
64
  const callAll = (...fns) => (...args) => fns.forEach(fn => fn?.(...args));
50
65
  /* eslint-enable @typescript-eslint/no-explicit-any */
51
66
  ;// ./src/utils/isNill.ts
@@ -63,6 +78,33 @@ const callAll = (...fns) => (...args) => fns.forEach(fn => fn?.(...args));
63
78
  */
64
79
  const isNill = value => value === null || value === undefined;
65
80
  ;// ./src/utils/throttle/throttle.ts
81
+ /**
82
+ * Rate-limits a function to at most one call per interval.
83
+ *
84
+ * ### Returns a `[throttled, cancel]` tuple
85
+ * Not a callable function with a `.cancel()` method as in lodash — destructure
86
+ * it: `const [onScroll, cancelScroll] = throttle(fn, 100)`.
87
+ *
88
+ * Leading **and** trailing: the first call runs immediately, calls during the
89
+ * cooling-off window are dropped except the last, and that one runs when the
90
+ * window closes. So a burst produces a call at the start and a call at the end,
91
+ * never a silent gap — unlike `debounce`, which only fires at the end.
92
+ *
93
+ * Call `cancel` on unmount to clear a pending trailing invocation.
94
+ *
95
+ * @param delayMs - Minimum gap between invocations, in ms.
96
+ *
97
+ * @example
98
+ * const [onScroll, cancel] = throttle(() => setOffset(window.scrollY), 100);
99
+ *
100
+ * useEffect(() => {
101
+ * window.addEventListener('scroll', onScroll);
102
+ * return () => {
103
+ * window.removeEventListener('scroll', onScroll);
104
+ * cancel();
105
+ * };
106
+ * }, [onScroll, cancel]);
107
+ */
66
108
  const throttle = (fn, delayMs) => {
67
109
  let isThrottled = false;
68
110
  let savedArgs = null;
@@ -91,6 +133,29 @@ const throttle = (fn, delayMs) => {
91
133
  }];
92
134
  };
93
135
  ;// ./src/utils/debounce/debounce.ts
136
+ /**
137
+ * Delays a call until the input stops arriving.
138
+ *
139
+ * ### Returns a `[debounced, cancel]` tuple
140
+ * Not a callable function with a `.cancel()` method as in lodash — destructure
141
+ * it: `const [search, cancelSearch] = debounce(fn, 300)`. Calling the returned
142
+ * value directly is a type error, because it is an array.
143
+ *
144
+ * Trailing edge only: nothing runs until `wait` ms have passed with no further
145
+ * calls, and only the most recent arguments are used. For a call that fires
146
+ * immediately and then rate-limits, use `throttle`.
147
+ *
148
+ * Call `cancel` on unmount so a pending invocation cannot fire against a
149
+ * component that is gone.
150
+ *
151
+ * @param wait - Quiet period in ms before the call goes through. @default 200
152
+ *
153
+ * @example
154
+ * const [search, cancel] = debounce((term: string) => void fetchResults(term), 300);
155
+ *
156
+ * useEffect(() => cancel, [cancel]);
157
+ * <Input onChange={(e) => search(e.target.value)} />
158
+ */
94
159
  const debounce = (func, wait = 200) => {
95
160
  let timeoutId = null;
96
161
  const executedFunction = (...args) => {
@@ -124,15 +189,64 @@ const periodValues = {
124
189
  hour: 3600,
125
190
  min: 60
126
191
  };
192
+
193
+ /**
194
+ * Formats a timestamp as a zero-padded 12-hour clock time, e.g. `"09:05 AM"`.
195
+ *
196
+ * Locale is hard-coded to `en-US`, so output does not follow the user's locale.
197
+ * The **time zone is not** — it uses the runtime's, so a server and a browser
198
+ * can render the same timestamp differently.
199
+ *
200
+ * @param timestampMs - Milliseconds since the epoch, as `Date.now()` returns.
201
+ *
202
+ * @example
203
+ * dateFormatters.formatTime(1735732800000); // '09:20 AM'
204
+ */
127
205
  const formatTime = timestampMs => new Date(timestampMs).toLocaleTimeString('en-US', {
128
206
  hour: '2-digit',
129
207
  minute: '2-digit'
130
208
  });
209
+
210
+ /**
211
+ * Returns the three-letter English weekday for a timestamp, e.g. `"Mon"`.
212
+ *
213
+ * Uses the runtime's time zone, so a timestamp near midnight can resolve to a
214
+ * different day than the user expects.
215
+ *
216
+ * @param timestampMs - Milliseconds since the epoch.
217
+ *
218
+ * @example
219
+ * dateFormatters.formatDayOfWeek(Date.now()); // 'Thu'
220
+ */
131
221
  const formatDayOfWeek = timestampMs => dayOfWeekNames[new Date(timestampMs).getDay()];
222
+
223
+ /**
224
+ * Formats a timestamp as day and abbreviated month, e.g. `"05 Jan"` — no year.
225
+ *
226
+ * Locale is hard-coded to `en-US`. Since the year is omitted, do not use it
227
+ * where dates span more than one year without adding the year yourself.
228
+ *
229
+ * @param timestampMs - Milliseconds since the epoch.
230
+ *
231
+ * @example
232
+ * dateFormatters.formatDate(1735732800000); // '01 Jan'
233
+ */
132
234
  const formatDate = timestampMs => new Date(timestampMs).toLocaleDateString('en-US', {
133
235
  day: '2-digit',
134
236
  month: 'short'
135
237
  });
238
+
239
+ /**
240
+ * Maps a weekday index to its three-letter English name.
241
+ *
242
+ * Takes a **day index** (0 = Sunday … 6 = Saturday), not a timestamp — for a
243
+ * timestamp use `formatDayOfWeek`. Anything outside 0–6 returns `undefined`,
244
+ * so the return type is `string | undefined`.
245
+ *
246
+ * @example
247
+ * dateFormatters.printDayOfTheWeek(0); // 'Sun'
248
+ * dateFormatters.printDayOfTheWeek(9); // undefined
249
+ */
136
250
  const printDayOfTheWeek = day => {
137
251
  switch (day) {
138
252
  case 0:
@@ -151,6 +265,25 @@ const printDayOfTheWeek = day => {
151
265
  return 'Sat';
152
266
  }
153
267
  };
268
+
269
+ /**
270
+ * Renders how long ago a date was, e.g. `"3 days ago"`, `"1 hour ago"`.
271
+ *
272
+ * Falls back to `"Just Now"` for anything under a minute. Granularity stops at
273
+ * years, and months are approximated as 30 days, so long gaps drift from the
274
+ * calendar.
275
+ *
276
+ * Throws `Error('Invalid date')` when the value cannot be parsed — including
277
+ * date strings the runtime does not recognise, so prefer passing a timestamp.
278
+ * The result is computed against `Date.now()` at call time and does not update
279
+ * on its own; re-render on a timer if it needs to stay current.
280
+ *
281
+ * @param timeValue - Timestamp in ms, or any string `new Date()` accepts.
282
+ *
283
+ * @example
284
+ * dateFormatters.getTimeAgo(Date.now() - 3 * 86400 * 1000); // '3 days ago'
285
+ * dateFormatters.getTimeAgo(Date.now() - 5000); // 'Just Now'
286
+ */
154
287
  const getTimeAgo = timeValue => {
155
288
  const date = new Date(timeValue).getTime();
156
289
  if (Number.isNaN(date)) {
@@ -168,6 +301,25 @@ const getTimeAgo = timeValue => {
168
301
  return 'Just Now';
169
302
  };
170
303
  ;// ./src/utils/objects/mapObjIndexed.ts
304
+ /**
305
+ * Maps over an object's values, keeping its keys.
306
+ *
307
+ * ### Not curried, unlike the accessors in this package
308
+ * `prop`, `path`, `propOr` and `assocPath` all take their arguments one group
309
+ * at a time; this one takes both at once — `mapObjIndexed(fn, obj)`. Ramda's
310
+ * version is curried, so a habit carried over from there will not compile here.
311
+ *
312
+ * The callback receives `(value, key, obj)`, mirroring `Array.prototype.map`.
313
+ * Only own enumerable keys are visited, and a new object is returned.
314
+ *
315
+ * @example
316
+ * mapObjIndexed((value) => value * 2, { a: 1, b: 2 }); // { a: 2, b: 4 }
317
+ *
318
+ * @example
319
+ * // The key is available as the second argument
320
+ * mapObjIndexed((value, key) => `${key}=${value}`, { page: 2, size: 20 });
321
+ * // { page: 'page=2', size: 'size=20' }
322
+ */
171
323
  const mapObjIndexed = (fn, obj) => {
172
324
  return Object.keys(obj).reduce((result, key) => {
173
325
  result[key] = fn(obj[key], key, obj);
@@ -176,16 +328,6 @@ const mapObjIndexed = (fn, obj) => {
176
328
  };
177
329
  ;// ./src/utils/pagination/generateRange.ts
178
330
 
179
- /**
180
- * The function that returns an array of page numbers to show in the pagination
181
- * component.
182
- *
183
- * Rules:
184
- * - To always show the 1st and the last page.
185
- * - To show one item before and one item after the selected page.
186
- * - To return "-1" for the skipped items. This is to be able to display "..." in
187
- * the pagination component.
188
- * */
189
331
  const SKIPPED_ITEMS_DELTA = 2;
190
332
  const getSelectedRange = (pagesCount, selectedPage) => {
191
333
  const range = [];
@@ -205,6 +347,34 @@ const fill = (range, minValue, maxValue) => {
205
347
  range.push(i);
206
348
  }
207
349
  };
350
+
351
+ /**
352
+ * Builds the list of page numbers a paginator should display.
353
+ *
354
+ * Always includes the first and last page, plus one page either side of the
355
+ * selection, and collapses the rest.
356
+ *
357
+ * ### `-1` marks a gap, it is not a page
358
+ * Skipped stretches are represented by `-1`, which the caller renders as an
359
+ * ellipsis. Render the array directly and a `-1` button appears — check for it:
360
+ * `range.map((page) => (page === -1 ? <Ellipsis /> : <PageButton page={page} />))`.
361
+ *
362
+ * ### It throws on bad input rather than clamping
363
+ * A non-integer `pagesCount` or `selectedPage`, or a selection outside
364
+ * `1…pagesCount`, raises an `Error`. Only `pagesCount <= 0` is tolerated,
365
+ * returning `[]`. Note pages are **1-based**, so `selectedPage: 0` throws.
366
+ *
367
+ * @param pagesCount - Total pages. `<= 0` yields an empty array.
368
+ * @param selectedPage - Current page, 1-based. Omit for the unselected layout.
369
+ *
370
+ * @example
371
+ * generateRange(5); // [1, 2, 3, 4, 5] — short enough to show in full
372
+ * generateRange(10); // [1, 2, 3, -1, 10]
373
+ *
374
+ * @example
375
+ * generateRange(10, 5); // [1, -1, 4, 5, 6, -1, 10] — gaps on both sides
376
+ * generateRange(10, 10); // [1, -1, 9, 10]
377
+ */
208
378
  const generateRange = (pagesCount, selectedPage) => {
209
379
  if (isNill(pagesCount) || !Number.isInteger(pagesCount)) {
210
380
  throw new Error('Pages count should be an integer');
@@ -246,11 +416,65 @@ const generateRange = (pagesCount, selectedPage) => {
246
416
  };
247
417
  /* harmony default export */ const pagination_generateRange = (generateRange);
248
418
  ;// ./src/utils/objects/assocPath.ts
419
+ /**
420
+ * Returns a copy of an object with a nested value replaced.
421
+ *
422
+ * **Curried, and only curried**: `assocPath(keys, value)(obj)`. The path is an
423
+ * array of keys; intermediate objects are created as needed. The input is never
424
+ * mutated.
425
+ *
426
+ * ### It deep-clones through JSON, which is lossy
427
+ * The copy is produced with `JSON.parse(JSON.stringify(...))`, so any value the
428
+ * JSON round-trip cannot represent is altered — silently, and with no type
429
+ * error, because the signature still claims `T`:
430
+ *
431
+ * - `Date` becomes an ISO **string**
432
+ * - `Map`, `Set` and class instances become `{}` (prototypes are lost)
433
+ * - keys whose value is `undefined` are **dropped**
434
+ * - `NaN` and `Infinity` become `null`
435
+ * - functions are dropped
436
+ *
437
+ * Safe for plain JSON-shaped state; for anything richer, spread the update by
438
+ * hand or use a structural-sharing helper.
439
+ *
440
+ * @example
441
+ * assocPath(['user', 'name'], 'Ada')({ user: { name: 'Grace', id: 1 } });
442
+ * // { user: { name: 'Ada', id: 1 } }
443
+ *
444
+ * @example
445
+ * // Lossy: `when` comes back as a string, not a Date
446
+ * assocPath(['id'], 2)({ id: 1, when: new Date() });
447
+ * // { id: 2, when: '2020-01-01T00:00:00.000Z' }
448
+ */
249
449
  const assocPath = ([first, ...rest], value) => sourceObject => JSON.parse(JSON.stringify({
250
450
  ...sourceObject,
251
451
  [first]: rest.length ? assocPath(rest, value)(sourceObject[first]) : value
252
452
  }));
253
453
  ;// ./src/utils/objects/dissocPath.ts
454
+ /**
455
+ * Returns a copy of an object with the key at a nested path removed.
456
+ *
457
+ * **Curried, and only curried**: `dissocPath(keys)(obj)`. The path is an array
458
+ * of keys. The input is never mutated.
459
+ *
460
+ * ### Missing intermediate keys throw
461
+ * Only the final key is optional. If a key *along the way* is absent the walk
462
+ * dereferences `undefined` and raises a `TypeError`, so guard paths that may
463
+ * not exist. Removing an already-absent leaf is a harmless no-op.
464
+ *
465
+ * ### It deep-clones through JSON, which is lossy
466
+ * Same caveat as `assocPath`: `Date` becomes a string, `Map`/`Set`/class
467
+ * instances become `{}`, `undefined` values are dropped, and `NaN`/`Infinity`
468
+ * become `null`. Use it on plain JSON-shaped state.
469
+ *
470
+ * @example
471
+ * dissocPath(['user', 'token'])({ user: { id: 1, token: 'x' } });
472
+ * // { user: { id: 1 } }
473
+ *
474
+ * @example
475
+ * dissocPath(['missing'])({ x: 1 }); // { x: 1 } — no-op
476
+ * dissocPath(['a', 'b'])({ x: 1 }); // TypeError: `a` does not exist
477
+ */
254
478
  const dissocPath = path => sourceObject => {
255
479
  const resultObject = JSON.parse(JSON.stringify(sourceObject));
256
480
  path.reduce((acc, key, index) => {
@@ -262,12 +486,48 @@ const dissocPath = path => sourceObject => {
262
486
  return resultObject;
263
487
  };
264
488
  ;// ./src/utils/objects/path.ts
489
+ /**
490
+ * Reads a nested value by key path.
491
+ *
492
+ * **Curried, and only curried**: `path(keys)(obj)`. The path is an **array** of
493
+ * keys, not a dotted string — `path(['user', 'name'])`, never
494
+ * `path('user.name')`. Numeric keys index into arrays.
495
+ *
496
+ * Every step is optional-chained, so a missing key anywhere along the way
497
+ * yields `undefined` instead of throwing. The return type defaults to `unknown`,
498
+ * so annotate it when you need a narrower type.
499
+ *
500
+ * @example
501
+ * path(['user', 'name'])({ user: { name: 'Ada' } }); // 'Ada'
502
+ * path(['user', 'name'])({}); // undefined
503
+ *
504
+ * @example
505
+ * // Numeric keys walk into arrays
506
+ * path<Response, string>(['items', 0, 'label'])(response);
507
+ */
265
508
  const path_path = path => obj => path.reduce((prev, curr) => prev?.[curr], obj);
266
509
  ;// ./src/utils/objects/pathOr.ts
267
510
 
268
511
 
269
512
 
270
513
  /* eslint-disable @typescript-eslint/no-explicit-any */
514
+ /**
515
+ * Reads a nested value by key path, falling back to a default when missing.
516
+ *
517
+ * **Curried, and only curried**: `pathOr(fallback, keys)(obj)`. Default first,
518
+ * then an **array** of keys — not a dotted string, and not lodash's
519
+ * `get(obj, path, fallback)` order.
520
+ *
521
+ * ### Only `null` and `undefined` trigger the default
522
+ * As with `propOr`, the fallback applies to nullish values only. A nested `0`
523
+ * or `''` is returned unchanged. Because every step is optional-chained, a
524
+ * break anywhere along the path resolves to the default rather than throwing.
525
+ *
526
+ * @example
527
+ * pathOr('—', ['user', 'name'])({ user: { name: 'Ada' } }); // 'Ada'
528
+ * pathOr('—', ['user', 'name'])({}); // '—'
529
+ * pathOr('—', ['stats', 'total'])({ stats: { total: 0 } }); // 0, not '—'
530
+ */
271
531
  const pathOr = (defaultValue, path) => obj => {
272
532
  const result = path_path(path)(obj);
273
533
  return isNill(result) ? defaultValue : result;
@@ -275,12 +535,52 @@ const pathOr = (defaultValue, path) => obj => {
275
535
  /* eslint-enable @typescript-eslint/no-explicit-any */
276
536
  ;// ./src/utils/objects/prop.ts
277
537
  /* eslint-disable @typescript-eslint/no-explicit-any */
538
+ /**
539
+ * Reads one own property from an object.
540
+ *
541
+ * **Curried, and only curried** — unlike Ramda's `prop`, this takes the key
542
+ * first and returns a reader. `prop('name', user)` is a type error; write
543
+ * `prop('name')(user)`. That shape is what makes it usable point-free, e.g. as
544
+ * `items.map(prop('id'))`.
545
+ *
546
+ * Reads a single key, not a dotted path: `prop('a.b')` looks for a literal
547
+ * `"a.b"` key. Use `path` for nested access. A nullish object yields
548
+ * `undefined` rather than throwing.
549
+ *
550
+ * @example
551
+ * prop('name')({ name: 'Ada' }); // 'Ada'
552
+ * prop('name')(undefined); // undefined
553
+ *
554
+ * @example
555
+ * // Point-free over a collection
556
+ * users.map(prop<User, string>('id'));
557
+ */
278
558
  const prop = propName => obj => obj?.[propName];
279
559
  ;// ./src/utils/objects/propOr.ts
280
560
 
281
561
 
282
562
 
283
563
  /* eslint-disable @typescript-eslint/no-explicit-any */
564
+ /**
565
+ * Reads one own property, falling back to a default when it is missing.
566
+ *
567
+ * **Curried, and only curried**: `propOr(fallback, key)(obj)`. The default
568
+ * comes first, matching Ramda's argument order — not lodash's
569
+ * `get(obj, key, fallback)`.
570
+ *
571
+ * ### Only `null` and `undefined` trigger the default
572
+ * The fallback applies when the value is nullish, **not** when it is merely
573
+ * falsy. `0`, `''` and `false` are returned as-is, so `propOr('—', 'count')`
574
+ * on `{ count: 0 }` gives `0`, not `'—'`. That is usually what you want for
575
+ * numbers; if you need empty strings to fall back too, check explicitly.
576
+ *
577
+ * Reads a single key, not a dotted path — use `pathOr` for nested access.
578
+ *
579
+ * @example
580
+ * propOr('—', 'name')({ name: 'Ada' }); // 'Ada'
581
+ * propOr('—', 'name')({}); // '—'
582
+ * propOr('—', 'count')({ count: 0 }); // 0, not '—'
583
+ */
284
584
  const propOr = (defaultValue, propName) => obj => {
285
585
  const result = prop(propName)(obj);
286
586
  return isNill(result) ? defaultValue : result;
@@ -295,7 +595,22 @@ const propOr = (defaultValue, propName) => obj => {
295
595
 
296
596
 
297
597
  ;// ./src/utils/story.ts
298
- // Useful for generating test datasets for storybook stories
598
+ /**
599
+ * A deterministic stand-in for `Math.random`, for generating fixture data.
600
+ *
601
+ * Returns a generator function; calling it yields the next value in `[0, 1)`.
602
+ * The same seed always produces the same sequence, which is the point — chart
603
+ * and table stories keep identical data between renders, so visual diffs show
604
+ * real changes rather than new noise.
605
+ *
606
+ * Linear congruential, so it is fine for demo data and unfit for anything
607
+ * needing statistical quality or unpredictability.
608
+ *
609
+ * @example
610
+ * const random = seededRandom(42);
611
+ * const series = Array.from({ length: 30 }, () => Math.round(random() * 100));
612
+ * // identical on every run
613
+ */
299
614
  const seededRandom = seed => {
300
615
  return () => {
301
616
  // Linear congruential generator
@@ -304,6 +619,32 @@ const seededRandom = seed => {
304
619
  };
305
620
  };
306
621
  ;// ./src/utils/ClassnameArray/ClassnameArray.ts
622
+ /**
623
+ * A `string[]` with chainable helpers for building a class list.
624
+ *
625
+ * ### It mutates, and it is a real Array
626
+ * `toggle` and `clear` change the instance in place and return `this`, so calls
627
+ * chain but the original is modified — do not share one instance across
628
+ * renders. Because it extends `Array`, `join(' ')` produces the final
629
+ * `className`, and every array method still works.
630
+ *
631
+ * Start from `new ClassnameArray()` and toggle onto it, or seed one with
632
+ * `ClassnameArray.from([...])`. Avoid `new ClassnameArray(n)` with a single
633
+ * number — the inherited `Array` constructor reads that as a length, giving you
634
+ * `n` empty slots.
635
+ *
636
+ * @example
637
+ * const classNames = new ClassnameArray();
638
+ * classNames
639
+ * .toggle('day--selected', isSelected)
640
+ * .toggle('day--disabled', isDisabled);
641
+ *
642
+ * <td className={classNames.join(' ')} />
643
+ *
644
+ * @example
645
+ * // Seeded with a base class
646
+ * ClassnameArray.from(['button']).toggle('button--active', isActive);
647
+ */
307
648
  class ClassnameArray extends Array {
308
649
  toggle(className, condition) {
309
650
  const index = this.indexOf(className);