@ssa-ui-kit/utils 3.21.2 → 3.22.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.
- 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
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
|
-
|
|
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);
|