@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/ai/source-map.md +41 -0
- package/dist/index.js +352 -11
- package/dist/index.mjs +352 -11
- package/dist/tsbuildcache +1 -1
- package/dist/utils/CallAll.d.ts +15 -0
- package/dist/utils/ClassnameArray/ClassnameArray.d.ts +26 -0
- package/dist/utils/dates/dateFormatters.d.ts +63 -0
- package/dist/utils/debounce/debounce.d.ts +23 -0
- package/dist/utils/objects/assocPath.d.ts +30 -0
- package/dist/utils/objects/dissocPath.d.ts +24 -0
- package/dist/utils/objects/mapObjIndexed.d.ts +19 -0
- package/dist/utils/objects/path.d.ts +19 -0
- package/dist/utils/objects/pathOr.d.ts +17 -0
- package/dist/utils/objects/prop.d.ts +20 -0
- package/dist/utils/objects/propOr.d.ts +20 -0
- package/dist/utils/pagination/generateRange.d.ts +27 -0
- package/dist/utils/pagination/types.d.ts +6 -0
- package/dist/utils/story.d.ts +16 -0
- package/dist/utils/throttle/throttle.d.ts +27 -0
- package/dist/utils/types.d.ts +40 -0
- package/package.json +2 -2
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @ssa-ui-kit/utils — AI source map
|
|
2
|
+
|
|
3
|
+
Generated from the TypeScript program at build time — do not edit by hand.
|
|
4
|
+
|
|
5
|
+
**Everything below is imported from the package root:** `import { X } from '@ssa-ui-kit/utils'`
|
|
6
|
+
|
|
7
|
+
13 functions · 1 class · 4 exported types
|
|
8
|
+
|
|
9
|
+
## Reading this file
|
|
10
|
+
|
|
11
|
+
- This is an index, not a reference. For full documentation and usage examples, read the matching `.d.ts` in `dist/`.
|
|
12
|
+
|
|
13
|
+
## Utility namespaces
|
|
14
|
+
|
|
15
|
+
- `dateFormatters` — formatTime, formatDayOfWeek, formatDate, printDayOfTheWeek, getTimeAgo
|
|
16
|
+
|
|
17
|
+
## Functions
|
|
18
|
+
|
|
19
|
+
- `assocPath` — Returns a copy of an object with a nested value replaced.
|
|
20
|
+
- `callAll` — Combines several handlers into one that calls each with the same arguments.
|
|
21
|
+
- `debounce` — Delays a call until the input stops arriving.
|
|
22
|
+
- `dissocPath` — Returns a copy of an object with the key at a nested path removed.
|
|
23
|
+
- `generateRange` — Builds the list of page numbers a paginator should display.
|
|
24
|
+
- `isNill` — Checks whether a value is `null` or `undefined`.
|
|
25
|
+
- `mapObjIndexed` — Maps over an object's values, keeping its keys.
|
|
26
|
+
- `path` — Reads a nested value by key path.
|
|
27
|
+
- `pathOr` — Reads a nested value by key path, falling back to a default when missing.
|
|
28
|
+
- `prop` — Reads one own property from an object.
|
|
29
|
+
- `propOr` — Reads one own property, falling back to a default when it is missing.
|
|
30
|
+
- `seededRandom` — A deterministic stand-in for `Math.random`, for generating fixture data.
|
|
31
|
+
- `throttle` — Rate-limits a function to at most one call per interval.
|
|
32
|
+
|
|
33
|
+
## Classes
|
|
34
|
+
|
|
35
|
+
- `ClassnameArray` — A `string[]` with chainable helpers for building a class list.
|
|
36
|
+
|
|
37
|
+
## Exported types
|
|
38
|
+
|
|
39
|
+
_Names only — read the `.d.ts` for their shape._
|
|
40
|
+
|
|
41
|
+
`Has` · `MustInclude` · `PathObject` · `PathValue`
|
package/dist/index.js
CHANGED
|
@@ -79,6 +79,21 @@ __webpack_require__.d(dateFormatters_namespaceObject, {
|
|
|
79
79
|
|
|
80
80
|
;// ./src/utils/CallAll.ts
|
|
81
81
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
82
|
+
/**
|
|
83
|
+
* Combines several handlers into one that calls each with the same arguments.
|
|
84
|
+
*
|
|
85
|
+
* Nullish entries are skipped, so an optional prop can be passed straight
|
|
86
|
+
* through without a guard — the usual case is merging a component's own handler
|
|
87
|
+
* with one a consumer supplied.
|
|
88
|
+
*
|
|
89
|
+
* Handlers run in order and their return values are discarded. Because they all
|
|
90
|
+
* receive the same event object, one calling `stopPropagation` affects the
|
|
91
|
+
* others' view of it, but it does not stop later handlers from running.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // `props.onClick` may be undefined — callAll skips it
|
|
95
|
+
* <button onClick={callAll(closeMenu, props.onClick)}>Close</button>
|
|
96
|
+
*/
|
|
82
97
|
const callAll = (...fns) => (...args) => fns.forEach(fn => fn?.(...args));
|
|
83
98
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
84
99
|
;// ./src/utils/isNill.ts
|
|
@@ -96,6 +111,33 @@ const callAll = (...fns) => (...args) => fns.forEach(fn => fn?.(...args));
|
|
|
96
111
|
*/
|
|
97
112
|
const isNill = value => value === null || value === undefined;
|
|
98
113
|
;// ./src/utils/throttle/throttle.ts
|
|
114
|
+
/**
|
|
115
|
+
* Rate-limits a function to at most one call per interval.
|
|
116
|
+
*
|
|
117
|
+
* ### Returns a `[throttled, cancel]` tuple
|
|
118
|
+
* Not a callable function with a `.cancel()` method as in lodash — destructure
|
|
119
|
+
* it: `const [onScroll, cancelScroll] = throttle(fn, 100)`.
|
|
120
|
+
*
|
|
121
|
+
* Leading **and** trailing: the first call runs immediately, calls during the
|
|
122
|
+
* cooling-off window are dropped except the last, and that one runs when the
|
|
123
|
+
* window closes. So a burst produces a call at the start and a call at the end,
|
|
124
|
+
* never a silent gap — unlike `debounce`, which only fires at the end.
|
|
125
|
+
*
|
|
126
|
+
* Call `cancel` on unmount to clear a pending trailing invocation.
|
|
127
|
+
*
|
|
128
|
+
* @param delayMs - Minimum gap between invocations, in ms.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* const [onScroll, cancel] = throttle(() => setOffset(window.scrollY), 100);
|
|
132
|
+
*
|
|
133
|
+
* useEffect(() => {
|
|
134
|
+
* window.addEventListener('scroll', onScroll);
|
|
135
|
+
* return () => {
|
|
136
|
+
* window.removeEventListener('scroll', onScroll);
|
|
137
|
+
* cancel();
|
|
138
|
+
* };
|
|
139
|
+
* }, [onScroll, cancel]);
|
|
140
|
+
*/
|
|
99
141
|
const throttle = (fn, delayMs) => {
|
|
100
142
|
let isThrottled = false;
|
|
101
143
|
let savedArgs = null;
|
|
@@ -124,6 +166,29 @@ const throttle = (fn, delayMs) => {
|
|
|
124
166
|
}];
|
|
125
167
|
};
|
|
126
168
|
;// ./src/utils/debounce/debounce.ts
|
|
169
|
+
/**
|
|
170
|
+
* Delays a call until the input stops arriving.
|
|
171
|
+
*
|
|
172
|
+
* ### Returns a `[debounced, cancel]` tuple
|
|
173
|
+
* Not a callable function with a `.cancel()` method as in lodash — destructure
|
|
174
|
+
* it: `const [search, cancelSearch] = debounce(fn, 300)`. Calling the returned
|
|
175
|
+
* value directly is a type error, because it is an array.
|
|
176
|
+
*
|
|
177
|
+
* Trailing edge only: nothing runs until `wait` ms have passed with no further
|
|
178
|
+
* calls, and only the most recent arguments are used. For a call that fires
|
|
179
|
+
* immediately and then rate-limits, use `throttle`.
|
|
180
|
+
*
|
|
181
|
+
* Call `cancel` on unmount so a pending invocation cannot fire against a
|
|
182
|
+
* component that is gone.
|
|
183
|
+
*
|
|
184
|
+
* @param wait - Quiet period in ms before the call goes through. @default 200
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* const [search, cancel] = debounce((term: string) => void fetchResults(term), 300);
|
|
188
|
+
*
|
|
189
|
+
* useEffect(() => cancel, [cancel]);
|
|
190
|
+
* <Input onChange={(e) => search(e.target.value)} />
|
|
191
|
+
*/
|
|
127
192
|
const debounce = (func, wait = 200) => {
|
|
128
193
|
let timeoutId = null;
|
|
129
194
|
const executedFunction = (...args) => {
|
|
@@ -157,15 +222,64 @@ const periodValues = {
|
|
|
157
222
|
hour: 3600,
|
|
158
223
|
min: 60
|
|
159
224
|
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Formats a timestamp as a zero-padded 12-hour clock time, e.g. `"09:05 AM"`.
|
|
228
|
+
*
|
|
229
|
+
* Locale is hard-coded to `en-US`, so output does not follow the user's locale.
|
|
230
|
+
* The **time zone is not** — it uses the runtime's, so a server and a browser
|
|
231
|
+
* can render the same timestamp differently.
|
|
232
|
+
*
|
|
233
|
+
* @param timestampMs - Milliseconds since the epoch, as `Date.now()` returns.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* dateFormatters.formatTime(1735732800000); // '09:20 AM'
|
|
237
|
+
*/
|
|
160
238
|
const formatTime = timestampMs => new Date(timestampMs).toLocaleTimeString('en-US', {
|
|
161
239
|
hour: '2-digit',
|
|
162
240
|
minute: '2-digit'
|
|
163
241
|
});
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Returns the three-letter English weekday for a timestamp, e.g. `"Mon"`.
|
|
245
|
+
*
|
|
246
|
+
* Uses the runtime's time zone, so a timestamp near midnight can resolve to a
|
|
247
|
+
* different day than the user expects.
|
|
248
|
+
*
|
|
249
|
+
* @param timestampMs - Milliseconds since the epoch.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* dateFormatters.formatDayOfWeek(Date.now()); // 'Thu'
|
|
253
|
+
*/
|
|
164
254
|
const formatDayOfWeek = timestampMs => dayOfWeekNames[new Date(timestampMs).getDay()];
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Formats a timestamp as day and abbreviated month, e.g. `"05 Jan"` — no year.
|
|
258
|
+
*
|
|
259
|
+
* Locale is hard-coded to `en-US`. Since the year is omitted, do not use it
|
|
260
|
+
* where dates span more than one year without adding the year yourself.
|
|
261
|
+
*
|
|
262
|
+
* @param timestampMs - Milliseconds since the epoch.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* dateFormatters.formatDate(1735732800000); // '01 Jan'
|
|
266
|
+
*/
|
|
165
267
|
const formatDate = timestampMs => new Date(timestampMs).toLocaleDateString('en-US', {
|
|
166
268
|
day: '2-digit',
|
|
167
269
|
month: 'short'
|
|
168
270
|
});
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Maps a weekday index to its three-letter English name.
|
|
274
|
+
*
|
|
275
|
+
* Takes a **day index** (0 = Sunday … 6 = Saturday), not a timestamp — for a
|
|
276
|
+
* timestamp use `formatDayOfWeek`. Anything outside 0–6 returns `undefined`,
|
|
277
|
+
* so the return type is `string | undefined`.
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* dateFormatters.printDayOfTheWeek(0); // 'Sun'
|
|
281
|
+
* dateFormatters.printDayOfTheWeek(9); // undefined
|
|
282
|
+
*/
|
|
169
283
|
const printDayOfTheWeek = day => {
|
|
170
284
|
switch (day) {
|
|
171
285
|
case 0:
|
|
@@ -184,6 +298,25 @@ const printDayOfTheWeek = day => {
|
|
|
184
298
|
return 'Sat';
|
|
185
299
|
}
|
|
186
300
|
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Renders how long ago a date was, e.g. `"3 days ago"`, `"1 hour ago"`.
|
|
304
|
+
*
|
|
305
|
+
* Falls back to `"Just Now"` for anything under a minute. Granularity stops at
|
|
306
|
+
* years, and months are approximated as 30 days, so long gaps drift from the
|
|
307
|
+
* calendar.
|
|
308
|
+
*
|
|
309
|
+
* Throws `Error('Invalid date')` when the value cannot be parsed — including
|
|
310
|
+
* date strings the runtime does not recognise, so prefer passing a timestamp.
|
|
311
|
+
* The result is computed against `Date.now()` at call time and does not update
|
|
312
|
+
* on its own; re-render on a timer if it needs to stay current.
|
|
313
|
+
*
|
|
314
|
+
* @param timeValue - Timestamp in ms, or any string `new Date()` accepts.
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* dateFormatters.getTimeAgo(Date.now() - 3 * 86400 * 1000); // '3 days ago'
|
|
318
|
+
* dateFormatters.getTimeAgo(Date.now() - 5000); // 'Just Now'
|
|
319
|
+
*/
|
|
187
320
|
const getTimeAgo = timeValue => {
|
|
188
321
|
const date = new Date(timeValue).getTime();
|
|
189
322
|
if (Number.isNaN(date)) {
|
|
@@ -201,6 +334,25 @@ const getTimeAgo = timeValue => {
|
|
|
201
334
|
return 'Just Now';
|
|
202
335
|
};
|
|
203
336
|
;// ./src/utils/objects/mapObjIndexed.ts
|
|
337
|
+
/**
|
|
338
|
+
* Maps over an object's values, keeping its keys.
|
|
339
|
+
*
|
|
340
|
+
* ### Not curried, unlike the accessors in this package
|
|
341
|
+
* `prop`, `path`, `propOr` and `assocPath` all take their arguments one group
|
|
342
|
+
* at a time; this one takes both at once — `mapObjIndexed(fn, obj)`. Ramda's
|
|
343
|
+
* version is curried, so a habit carried over from there will not compile here.
|
|
344
|
+
*
|
|
345
|
+
* The callback receives `(value, key, obj)`, mirroring `Array.prototype.map`.
|
|
346
|
+
* Only own enumerable keys are visited, and a new object is returned.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* mapObjIndexed((value) => value * 2, { a: 1, b: 2 }); // { a: 2, b: 4 }
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* // The key is available as the second argument
|
|
353
|
+
* mapObjIndexed((value, key) => `${key}=${value}`, { page: 2, size: 20 });
|
|
354
|
+
* // { page: 'page=2', size: 'size=20' }
|
|
355
|
+
*/
|
|
204
356
|
const mapObjIndexed = (fn, obj) => {
|
|
205
357
|
return Object.keys(obj).reduce((result, key) => {
|
|
206
358
|
result[key] = fn(obj[key], key, obj);
|
|
@@ -209,16 +361,6 @@ const mapObjIndexed = (fn, obj) => {
|
|
|
209
361
|
};
|
|
210
362
|
;// ./src/utils/pagination/generateRange.ts
|
|
211
363
|
|
|
212
|
-
/**
|
|
213
|
-
* The function that returns an array of page numbers to show in the pagination
|
|
214
|
-
* component.
|
|
215
|
-
*
|
|
216
|
-
* Rules:
|
|
217
|
-
* - To always show the 1st and the last page.
|
|
218
|
-
* - To show one item before and one item after the selected page.
|
|
219
|
-
* - To return "-1" for the skipped items. This is to be able to display "..." in
|
|
220
|
-
* the pagination component.
|
|
221
|
-
* */
|
|
222
364
|
const SKIPPED_ITEMS_DELTA = 2;
|
|
223
365
|
const getSelectedRange = (pagesCount, selectedPage) => {
|
|
224
366
|
const range = [];
|
|
@@ -238,6 +380,34 @@ const fill = (range, minValue, maxValue) => {
|
|
|
238
380
|
range.push(i);
|
|
239
381
|
}
|
|
240
382
|
};
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Builds the list of page numbers a paginator should display.
|
|
386
|
+
*
|
|
387
|
+
* Always includes the first and last page, plus one page either side of the
|
|
388
|
+
* selection, and collapses the rest.
|
|
389
|
+
*
|
|
390
|
+
* ### `-1` marks a gap, it is not a page
|
|
391
|
+
* Skipped stretches are represented by `-1`, which the caller renders as an
|
|
392
|
+
* ellipsis. Render the array directly and a `-1` button appears — check for it:
|
|
393
|
+
* `range.map((page) => (page === -1 ? <Ellipsis /> : <PageButton page={page} />))`.
|
|
394
|
+
*
|
|
395
|
+
* ### It throws on bad input rather than clamping
|
|
396
|
+
* A non-integer `pagesCount` or `selectedPage`, or a selection outside
|
|
397
|
+
* `1…pagesCount`, raises an `Error`. Only `pagesCount <= 0` is tolerated,
|
|
398
|
+
* returning `[]`. Note pages are **1-based**, so `selectedPage: 0` throws.
|
|
399
|
+
*
|
|
400
|
+
* @param pagesCount - Total pages. `<= 0` yields an empty array.
|
|
401
|
+
* @param selectedPage - Current page, 1-based. Omit for the unselected layout.
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* generateRange(5); // [1, 2, 3, 4, 5] — short enough to show in full
|
|
405
|
+
* generateRange(10); // [1, 2, 3, -1, 10]
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* generateRange(10, 5); // [1, -1, 4, 5, 6, -1, 10] — gaps on both sides
|
|
409
|
+
* generateRange(10, 10); // [1, -1, 9, 10]
|
|
410
|
+
*/
|
|
241
411
|
const generateRange = (pagesCount, selectedPage) => {
|
|
242
412
|
if (isNill(pagesCount) || !Number.isInteger(pagesCount)) {
|
|
243
413
|
throw new Error('Pages count should be an integer');
|
|
@@ -279,11 +449,65 @@ const generateRange = (pagesCount, selectedPage) => {
|
|
|
279
449
|
};
|
|
280
450
|
/* harmony default export */ const pagination_generateRange = (generateRange);
|
|
281
451
|
;// ./src/utils/objects/assocPath.ts
|
|
452
|
+
/**
|
|
453
|
+
* Returns a copy of an object with a nested value replaced.
|
|
454
|
+
*
|
|
455
|
+
* **Curried, and only curried**: `assocPath(keys, value)(obj)`. The path is an
|
|
456
|
+
* array of keys; intermediate objects are created as needed. The input is never
|
|
457
|
+
* mutated.
|
|
458
|
+
*
|
|
459
|
+
* ### It deep-clones through JSON, which is lossy
|
|
460
|
+
* The copy is produced with `JSON.parse(JSON.stringify(...))`, so any value the
|
|
461
|
+
* JSON round-trip cannot represent is altered — silently, and with no type
|
|
462
|
+
* error, because the signature still claims `T`:
|
|
463
|
+
*
|
|
464
|
+
* - `Date` becomes an ISO **string**
|
|
465
|
+
* - `Map`, `Set` and class instances become `{}` (prototypes are lost)
|
|
466
|
+
* - keys whose value is `undefined` are **dropped**
|
|
467
|
+
* - `NaN` and `Infinity` become `null`
|
|
468
|
+
* - functions are dropped
|
|
469
|
+
*
|
|
470
|
+
* Safe for plain JSON-shaped state; for anything richer, spread the update by
|
|
471
|
+
* hand or use a structural-sharing helper.
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* assocPath(['user', 'name'], 'Ada')({ user: { name: 'Grace', id: 1 } });
|
|
475
|
+
* // { user: { name: 'Ada', id: 1 } }
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* // Lossy: `when` comes back as a string, not a Date
|
|
479
|
+
* assocPath(['id'], 2)({ id: 1, when: new Date() });
|
|
480
|
+
* // { id: 2, when: '2020-01-01T00:00:00.000Z' }
|
|
481
|
+
*/
|
|
282
482
|
const assocPath = ([first, ...rest], value) => sourceObject => JSON.parse(JSON.stringify({
|
|
283
483
|
...sourceObject,
|
|
284
484
|
[first]: rest.length ? assocPath(rest, value)(sourceObject[first]) : value
|
|
285
485
|
}));
|
|
286
486
|
;// ./src/utils/objects/dissocPath.ts
|
|
487
|
+
/**
|
|
488
|
+
* Returns a copy of an object with the key at a nested path removed.
|
|
489
|
+
*
|
|
490
|
+
* **Curried, and only curried**: `dissocPath(keys)(obj)`. The path is an array
|
|
491
|
+
* of keys. The input is never mutated.
|
|
492
|
+
*
|
|
493
|
+
* ### Missing intermediate keys throw
|
|
494
|
+
* Only the final key is optional. If a key *along the way* is absent the walk
|
|
495
|
+
* dereferences `undefined` and raises a `TypeError`, so guard paths that may
|
|
496
|
+
* not exist. Removing an already-absent leaf is a harmless no-op.
|
|
497
|
+
*
|
|
498
|
+
* ### It deep-clones through JSON, which is lossy
|
|
499
|
+
* Same caveat as `assocPath`: `Date` becomes a string, `Map`/`Set`/class
|
|
500
|
+
* instances become `{}`, `undefined` values are dropped, and `NaN`/`Infinity`
|
|
501
|
+
* become `null`. Use it on plain JSON-shaped state.
|
|
502
|
+
*
|
|
503
|
+
* @example
|
|
504
|
+
* dissocPath(['user', 'token'])({ user: { id: 1, token: 'x' } });
|
|
505
|
+
* // { user: { id: 1 } }
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* dissocPath(['missing'])({ x: 1 }); // { x: 1 } — no-op
|
|
509
|
+
* dissocPath(['a', 'b'])({ x: 1 }); // TypeError: `a` does not exist
|
|
510
|
+
*/
|
|
287
511
|
const dissocPath = path => sourceObject => {
|
|
288
512
|
const resultObject = JSON.parse(JSON.stringify(sourceObject));
|
|
289
513
|
path.reduce((acc, key, index) => {
|
|
@@ -295,12 +519,48 @@ const dissocPath = path => sourceObject => {
|
|
|
295
519
|
return resultObject;
|
|
296
520
|
};
|
|
297
521
|
;// ./src/utils/objects/path.ts
|
|
522
|
+
/**
|
|
523
|
+
* Reads a nested value by key path.
|
|
524
|
+
*
|
|
525
|
+
* **Curried, and only curried**: `path(keys)(obj)`. The path is an **array** of
|
|
526
|
+
* keys, not a dotted string — `path(['user', 'name'])`, never
|
|
527
|
+
* `path('user.name')`. Numeric keys index into arrays.
|
|
528
|
+
*
|
|
529
|
+
* Every step is optional-chained, so a missing key anywhere along the way
|
|
530
|
+
* yields `undefined` instead of throwing. The return type defaults to `unknown`,
|
|
531
|
+
* so annotate it when you need a narrower type.
|
|
532
|
+
*
|
|
533
|
+
* @example
|
|
534
|
+
* path(['user', 'name'])({ user: { name: 'Ada' } }); // 'Ada'
|
|
535
|
+
* path(['user', 'name'])({}); // undefined
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* // Numeric keys walk into arrays
|
|
539
|
+
* path<Response, string>(['items', 0, 'label'])(response);
|
|
540
|
+
*/
|
|
298
541
|
const path_path = path => obj => path.reduce((prev, curr) => prev?.[curr], obj);
|
|
299
542
|
;// ./src/utils/objects/pathOr.ts
|
|
300
543
|
|
|
301
544
|
|
|
302
545
|
|
|
303
546
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
547
|
+
/**
|
|
548
|
+
* Reads a nested value by key path, falling back to a default when missing.
|
|
549
|
+
*
|
|
550
|
+
* **Curried, and only curried**: `pathOr(fallback, keys)(obj)`. Default first,
|
|
551
|
+
* then an **array** of keys — not a dotted string, and not lodash's
|
|
552
|
+
* `get(obj, path, fallback)` order.
|
|
553
|
+
*
|
|
554
|
+
* ### Only `null` and `undefined` trigger the default
|
|
555
|
+
* As with `propOr`, the fallback applies to nullish values only. A nested `0`
|
|
556
|
+
* or `''` is returned unchanged. Because every step is optional-chained, a
|
|
557
|
+
* break anywhere along the path resolves to the default rather than throwing.
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* pathOr('—', ['user', 'name'])({ user: { name: 'Ada' } }); // 'Ada'
|
|
561
|
+
* pathOr('—', ['user', 'name'])({}); // '—'
|
|
562
|
+
* pathOr('—', ['stats', 'total'])({ stats: { total: 0 } }); // 0, not '—'
|
|
563
|
+
*/
|
|
304
564
|
const pathOr = (defaultValue, path) => obj => {
|
|
305
565
|
const result = path_path(path)(obj);
|
|
306
566
|
return isNill(result) ? defaultValue : result;
|
|
@@ -308,12 +568,52 @@ const pathOr = (defaultValue, path) => obj => {
|
|
|
308
568
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
309
569
|
;// ./src/utils/objects/prop.ts
|
|
310
570
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
571
|
+
/**
|
|
572
|
+
* Reads one own property from an object.
|
|
573
|
+
*
|
|
574
|
+
* **Curried, and only curried** — unlike Ramda's `prop`, this takes the key
|
|
575
|
+
* first and returns a reader. `prop('name', user)` is a type error; write
|
|
576
|
+
* `prop('name')(user)`. That shape is what makes it usable point-free, e.g. as
|
|
577
|
+
* `items.map(prop('id'))`.
|
|
578
|
+
*
|
|
579
|
+
* Reads a single key, not a dotted path: `prop('a.b')` looks for a literal
|
|
580
|
+
* `"a.b"` key. Use `path` for nested access. A nullish object yields
|
|
581
|
+
* `undefined` rather than throwing.
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* prop('name')({ name: 'Ada' }); // 'Ada'
|
|
585
|
+
* prop('name')(undefined); // undefined
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* // Point-free over a collection
|
|
589
|
+
* users.map(prop<User, string>('id'));
|
|
590
|
+
*/
|
|
311
591
|
const prop = propName => obj => obj?.[propName];
|
|
312
592
|
;// ./src/utils/objects/propOr.ts
|
|
313
593
|
|
|
314
594
|
|
|
315
595
|
|
|
316
596
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
597
|
+
/**
|
|
598
|
+
* Reads one own property, falling back to a default when it is missing.
|
|
599
|
+
*
|
|
600
|
+
* **Curried, and only curried**: `propOr(fallback, key)(obj)`. The default
|
|
601
|
+
* comes first, matching Ramda's argument order — not lodash's
|
|
602
|
+
* `get(obj, key, fallback)`.
|
|
603
|
+
*
|
|
604
|
+
* ### Only `null` and `undefined` trigger the default
|
|
605
|
+
* The fallback applies when the value is nullish, **not** when it is merely
|
|
606
|
+
* falsy. `0`, `''` and `false` are returned as-is, so `propOr('—', 'count')`
|
|
607
|
+
* on `{ count: 0 }` gives `0`, not `'—'`. That is usually what you want for
|
|
608
|
+
* numbers; if you need empty strings to fall back too, check explicitly.
|
|
609
|
+
*
|
|
610
|
+
* Reads a single key, not a dotted path — use `pathOr` for nested access.
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* propOr('—', 'name')({ name: 'Ada' }); // 'Ada'
|
|
614
|
+
* propOr('—', 'name')({}); // '—'
|
|
615
|
+
* propOr('—', 'count')({ count: 0 }); // 0, not '—'
|
|
616
|
+
*/
|
|
317
617
|
const propOr = (defaultValue, propName) => obj => {
|
|
318
618
|
const result = prop(propName)(obj);
|
|
319
619
|
return isNill(result) ? defaultValue : result;
|
|
@@ -328,7 +628,22 @@ const propOr = (defaultValue, propName) => obj => {
|
|
|
328
628
|
|
|
329
629
|
|
|
330
630
|
;// ./src/utils/story.ts
|
|
331
|
-
|
|
631
|
+
/**
|
|
632
|
+
* A deterministic stand-in for `Math.random`, for generating fixture data.
|
|
633
|
+
*
|
|
634
|
+
* Returns a generator function; calling it yields the next value in `[0, 1)`.
|
|
635
|
+
* The same seed always produces the same sequence, which is the point — chart
|
|
636
|
+
* and table stories keep identical data between renders, so visual diffs show
|
|
637
|
+
* real changes rather than new noise.
|
|
638
|
+
*
|
|
639
|
+
* Linear congruential, so it is fine for demo data and unfit for anything
|
|
640
|
+
* needing statistical quality or unpredictability.
|
|
641
|
+
*
|
|
642
|
+
* @example
|
|
643
|
+
* const random = seededRandom(42);
|
|
644
|
+
* const series = Array.from({ length: 30 }, () => Math.round(random() * 100));
|
|
645
|
+
* // identical on every run
|
|
646
|
+
*/
|
|
332
647
|
const seededRandom = seed => {
|
|
333
648
|
return () => {
|
|
334
649
|
// Linear congruential generator
|
|
@@ -337,6 +652,32 @@ const seededRandom = seed => {
|
|
|
337
652
|
};
|
|
338
653
|
};
|
|
339
654
|
;// ./src/utils/ClassnameArray/ClassnameArray.ts
|
|
655
|
+
/**
|
|
656
|
+
* A `string[]` with chainable helpers for building a class list.
|
|
657
|
+
*
|
|
658
|
+
* ### It mutates, and it is a real Array
|
|
659
|
+
* `toggle` and `clear` change the instance in place and return `this`, so calls
|
|
660
|
+
* chain but the original is modified — do not share one instance across
|
|
661
|
+
* renders. Because it extends `Array`, `join(' ')` produces the final
|
|
662
|
+
* `className`, and every array method still works.
|
|
663
|
+
*
|
|
664
|
+
* Start from `new ClassnameArray()` and toggle onto it, or seed one with
|
|
665
|
+
* `ClassnameArray.from([...])`. Avoid `new ClassnameArray(n)` with a single
|
|
666
|
+
* number — the inherited `Array` constructor reads that as a length, giving you
|
|
667
|
+
* `n` empty slots.
|
|
668
|
+
*
|
|
669
|
+
* @example
|
|
670
|
+
* const classNames = new ClassnameArray();
|
|
671
|
+
* classNames
|
|
672
|
+
* .toggle('day--selected', isSelected)
|
|
673
|
+
* .toggle('day--disabled', isDisabled);
|
|
674
|
+
*
|
|
675
|
+
* <td className={classNames.join(' ')} />
|
|
676
|
+
*
|
|
677
|
+
* @example
|
|
678
|
+
* // Seeded with a base class
|
|
679
|
+
* ClassnameArray.from(['button']).toggle('button--active', isActive);
|
|
680
|
+
*/
|
|
340
681
|
class ClassnameArray extends Array {
|
|
341
682
|
toggle(className, condition) {
|
|
342
683
|
const index = this.indexOf(className);
|