@reykjavik/webtools 0.1.19 → 0.1.21

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/CHANGELOG.md CHANGED
@@ -4,22 +4,26 @@
4
4
 
5
5
  - ... <!-- Add new lines here. -->
6
6
 
7
- ## 0.1.19
7
+ ## 0.1.21
8
8
 
9
- _2024-03-11_
9
+ _2024-03-14_
10
10
 
11
- - fix: Incorrect alphabetization of accented characters as part of a word …
12
- (not just a single character) This fix corrects the sorting of initial
13
- letters, but characters inside the string stay mixed in with their
14
- unaccented base character.
15
- - fix: Incorrect `Intl.ListFormat` format in narrow+unit mode
11
+ - feat: Add `@reykjavik/webtools/remix/Wait` component
12
+ - feat: Add `@reykjavik/webtools/remix/http` module with `isClientFetch`
13
+ helper
14
+ - feat: Add `@reykjavik/webtools/remix/async` module with promise helpers
16
15
 
17
- ## 0.1.18
16
+ ## 0.1.18 – 0.1.20
18
17
 
19
18
  _2024-03-11_
20
19
 
21
20
  - `@reykjavik/webtools/fixIcelandicLocale`:
22
21
  - feat: Patch `Intl.PluralRules` and `Intl.ListFormat`
22
+ - fix: Incorrect alphabetization of accented characters as part of a word …
23
+ (not just a single character) This fix corrects the sorting of initial
24
+ letters, but characters inside the string stay mixed in with their
25
+ unaccented base character.
26
+ - fix: Make all pached `Intl.*` methods bound to their instances
23
27
 
24
28
  ## 0.1.16 – 0.1.17
25
29
 
@@ -49,8 +53,8 @@ _2024-03-06_
49
53
 
50
54
  _2024-02-29_
51
55
 
52
- - feat: Add `@reykjavik/webtools/next/vanillaExtract` component — with
53
- plain-CSS injection helpers
56
+ - feat: Add `@reykjavik/webtools/next/vanillaExtract` module — with plain-CSS
57
+ injection helpers
54
58
  - fix: Mark `peerDependencies` as optional
55
59
 
56
60
  ## 0.1.11
package/README.md CHANGED
@@ -25,6 +25,9 @@ bun add @reykjavik/webtools
25
25
  - [`@reykjavik/webtools/CookieHubConsent`](#reykjavikwebtoolscookiehubconsent)
26
26
  - [`CookieHubProvider` component](#cookiehubprovider-component)
27
27
  - [`useCookieHubConsent`](#usecookiehubconsent)
28
+ - [`@reykjavik/webtools/async`](#reykjavikwebtoolsasync)
29
+ - [`promiseAllObject`](#promiseallobject)
30
+ - [`maxWait`](#maxwait)
28
31
  - [`@reykjavik/webtools/vanillaExtract`](#reykjavikwebtoolsvanillaextract)
29
32
  - [`vanillaGlobal`](#vanillaglobal)
30
33
  - [`vanillaProps`](#vanillaprops)
@@ -35,6 +38,7 @@ bun add @reykjavik/webtools
35
38
  - [Limitations](#limitations)
36
39
  - [Framework Specific Tools](#framework-specific-tools)
37
40
  - [Next.js Tools](#nextjs-tools)
41
+ - [Remix.run Tools](#remixrun-tools)
38
42
  - [Contributing](#contributing)
39
43
  - [Changelog](#changelog)
40
44
 
@@ -165,7 +169,7 @@ behavior.
165
169
  ### `toSec` TTL helper
166
170
 
167
171
  **Syntax:**
168
- `` toSec; (ttl: number | `${number}${'s'|'m'|'h'|'d'|'w'}`) => number ``
172
+ `` toSec(ttl: number | `${number}${'s'|'m'|'h'|'d'|'w'}`): number ``
169
173
 
170
174
  Converts a `TTL` (max-age) value into seconds, and returns `0` for bad and/or
171
175
  negative input values.
@@ -253,6 +257,62 @@ this hook will return an empty object.
253
257
 
254
258
  ---
255
259
 
260
+ ## `@reykjavik/webtools/async`
261
+
262
+ Contains a few small helpers for working with async functions and promises.
263
+
264
+ ---
265
+
266
+ ### `promiseAllObject`
267
+
268
+ **Syntax:**
269
+ `promiseAllObject<T extends PlainObj>(promisesMap: T>): Promise<{ [K in keyof T]: Awaited<T[K]>; }>`
270
+
271
+ A variation of `Promise.all()` that accepts an object with named promises and
272
+ returns a same-shaped object with the resolved values.
273
+
274
+ ```ts
275
+ import { promiseAllObject } from '@reykjavik/webtools/async';
276
+
277
+ const { user, posts } = await promiseAllObject({
278
+ user: fetchUser(),
279
+ posts: fetchPosts(),
280
+ });
281
+ ```
282
+
283
+ ---
284
+
285
+ ### `maxWait`
286
+
287
+ **Syntax:** `maxWait(timeout: number, promises: Array<any>): Promise<void>`
288
+ **Syntax:**
289
+ `maxWait<T extends PlainObj>(timeout: number, promises: T): Promise<{ [K in keyof T]: { value: Awaited<T[K]> } | undefined }>`
290
+
291
+ This somewhat esoteric helper resolves soon as all of the passed `promises`
292
+ have resolved, or after `timeout` milliseconds — whichever comes first.
293
+
294
+ If an object is passed, the resolved value will be an object with the same
295
+ keys, with undefined values for any promises that didn't resolve in time, and
296
+ the resolved values in a `value` container object.
297
+
298
+ ```ts
299
+ import { maxWait } from '@reykjavik/webtools/async';
300
+
301
+ const user = fetchUser();
302
+ const posts = fetchPosts();
303
+
304
+ // Array of promises resolves to void
305
+ await maxWait(500, [user, posts]);
306
+
307
+ // Object of promises resolves to an object with any resolved values at that time
308
+ const { user, posts } = await maxWait(500, { user, posts });
309
+
310
+ console.log(user?.value); // undefined | User
311
+ console.log(posts?.value); // undefined | Array<Post>
312
+ ```
313
+
314
+ ---
315
+
256
316
  ## `@reykjavik/webtools/vanillaExtract`
257
317
 
258
318
  Contains helpers for writing [vanilla-extract](https://vanilla-extract.style)
@@ -265,7 +325,7 @@ CSS.
265
325
 
266
326
  ### `vanillaGlobal`
267
327
 
268
- **Syntax:** `vanillaGlobal: (css: string) => void`
328
+ **Syntax:** `vanillaGlobal(css: string): void`
269
329
 
270
330
  Inserts free-form CSS as a vanilla-extract `globalStyle`.
271
331
 
@@ -282,7 +342,7 @@ vanillaGlobal(`
282
342
 
283
343
  ### `vanillaProps`
284
344
 
285
- **Syntax:** `vanillaProps: (css: string) => GlobalStyleRule`
345
+ **Syntax:** `vanillaProps(css: string): GlobalStyleRule`
286
346
 
287
347
  Spreads the return value into a style object, to inject free-form CSS
288
348
  properties (or nested blocks)
@@ -307,8 +367,8 @@ const myStyle = style({
307
367
 
308
368
  ### `vanillaClass`
309
369
 
310
- **Syntax:** `vanillaClass: (css: string) => string`
311
- **Syntax:** `vanillaClass: (debugId: string, css: string) => string`
370
+ **Syntax:** `vanillaClass(css: string): string`
371
+ **Syntax:** `vanillaClass(debugId: string, css: string): string`
312
372
 
313
373
  Returns a scoped cssClassName styled with free-form CSS. This function is a
314
374
  thin wrapper around vanilla-extract's `style` function.
@@ -333,8 +393,8 @@ export const humanReadableClass = vanillaClass(
333
393
 
334
394
  ### `vanillaClassNested`
335
395
 
336
- **Syntax:** `vanillaClassNested: (css: string) => string`
337
- **Syntax:** `vanillaClassNested: (debugId: string, css: string) => string`
396
+ **Syntax:** `vanillaClassNested(css: string): string`
397
+ **Syntax:** `vanillaClassNested(debugId: string, css: string): string`
338
398
 
339
399
  Returns a scoped cssClassName styled with free-form CSS.
340
400
 
@@ -373,7 +433,7 @@ comments, etc. If you need something more sophisticated, use a custom
373
433
 
374
434
  ### `vanillaNest`
375
435
 
376
- **Syntax:** `vanillaNest: (ampSelector: string, css: string) => string`
436
+ **Syntax:** `vanillaNest(ampSelector: string, css: string): string`
377
437
 
378
438
  Replaces all `&` tokens with the given selector string, in a direct (read.
379
439
  "dumb") way. It's mainly useful when used with style-mixins, etc.
@@ -504,7 +564,16 @@ detection test.)
504
564
  This package contains some helpers and components that are specifically
505
565
  designed for use in [Next.js](https://nextjs.org/) projects.
506
566
 
507
- See [README-nextjs.md](README-nextjs.md) for more info.
567
+ See [README-nextjs.md](./README-nextjs.md) for more info.
568
+
569
+ ---
570
+
571
+ ### Remix.run Tools
572
+
573
+ This package contains some helpers and components that are specifically
574
+ designed for use in [Remix.run](https://remix.run) projects.
575
+
576
+ See [README-remix.md](./README-remix.md) for more info.
508
577
 
509
578
  ---
510
579
 
package/async.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ type PlainObj = Record<string, unknown>;
2
+ /**
3
+ * Simple sleep function. Returns a promise that resolves after `length`
4
+ * milliseconds.
5
+ */
6
+ export declare const sleep: (length: number) => Promise<void>;
7
+ /**
8
+ * Resolves soon as all of the passed `promises` have resolved, or after
9
+ * `timeout` milliseconds — whichever comes first.
10
+ *
11
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#maxwait
12
+ */
13
+ export declare function maxWait(timeout: number, promises: Array<unknown>): Promise<void>;
14
+ export declare function maxWait<PromiseMap extends PlainObj>(timeout: number, promises: PromiseMap): Promise<{
15
+ -readonly [K in keyof PromiseMap]: {
16
+ value: Awaited<PromiseMap[K]>;
17
+ } | undefined;
18
+ }>;
19
+ /**
20
+ * A variation of `Promise.all()` that accepts an object with named promises
21
+ * and returns a same-shaped object with the resolved values.
22
+ *
23
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#promiseallobject
24
+ */
25
+ export declare const promiseAllObject: <T extends PlainObj>(promisesMap: T) => Promise<{ -readonly [K in keyof T]: Awaited<T[K]>; }>;
26
+ export {};
package/async.js ADDED
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.promiseAllObject = exports.maxWait = exports.sleep = void 0;
4
+ /**
5
+ * Simple sleep function. Returns a promise that resolves after `length`
6
+ * milliseconds.
7
+ */
8
+ const sleep = (length) => new Promise((resolve) => setTimeout(resolve, length));
9
+ exports.sleep = sleep;
10
+ function maxWait(timeout, promises) {
11
+ if (Array.isArray(promises)) {
12
+ return Promise.race([(0, exports.sleep)(timeout), Promise.all(promises).then(() => undefined)]);
13
+ }
14
+ return Promise.race([(0, exports.sleep)(timeout), Promise.all(Object.values(promises))]).then(() => {
15
+ Object.entries(promises).forEach(([key, value]) => {
16
+ if (value instanceof Promise) {
17
+ promises[key] = undefined;
18
+ value.then((value) => {
19
+ promises[key] = { value };
20
+ });
21
+ }
22
+ else {
23
+ promises[key] = { value };
24
+ }
25
+ });
26
+ return (0, exports.sleep)(0).then(() => promises);
27
+ });
28
+ }
29
+ exports.maxWait = maxWait;
30
+ // ---------------------------------------------------------------------------
31
+ // Adapted from https://github.com/marcelowa/promise-all-properties
32
+ /**
33
+ * A variation of `Promise.all()` that accepts an object with named promises
34
+ * and returns a same-shaped object with the resolved values.
35
+ *
36
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#promiseallobject
37
+ */
38
+ const promiseAllObject = (promisesMap) => Promise.all(Object.values(promisesMap)).then((results) => {
39
+ const keys = Object.keys(promisesMap);
40
+ const resolvedMap = {};
41
+ for (let i = 0; i < results.length; i++) {
42
+ resolvedMap[keys[i]] = results[i];
43
+ }
44
+ return resolvedMap;
45
+ });
46
+ exports.promiseAllObject = promiseAllObject;
package/esm/async.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ type PlainObj = Record<string, unknown>;
2
+ /**
3
+ * Simple sleep function. Returns a promise that resolves after `length`
4
+ * milliseconds.
5
+ */
6
+ export declare const sleep: (length: number) => Promise<void>;
7
+ /**
8
+ * Resolves soon as all of the passed `promises` have resolved, or after
9
+ * `timeout` milliseconds — whichever comes first.
10
+ *
11
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#maxwait
12
+ */
13
+ export declare function maxWait(timeout: number, promises: Array<unknown>): Promise<void>;
14
+ export declare function maxWait<PromiseMap extends PlainObj>(timeout: number, promises: PromiseMap): Promise<{
15
+ -readonly [K in keyof PromiseMap]: {
16
+ value: Awaited<PromiseMap[K]>;
17
+ } | undefined;
18
+ }>;
19
+ /**
20
+ * A variation of `Promise.all()` that accepts an object with named promises
21
+ * and returns a same-shaped object with the resolved values.
22
+ *
23
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#promiseallobject
24
+ */
25
+ export declare const promiseAllObject: <T extends PlainObj>(promisesMap: T) => Promise<{ -readonly [K in keyof T]: Awaited<T[K]>; }>;
26
+ export {};
package/esm/async.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Simple sleep function. Returns a promise that resolves after `length`
3
+ * milliseconds.
4
+ */
5
+ export const sleep = (length) => new Promise((resolve) => setTimeout(resolve, length));
6
+ export function maxWait(timeout, promises) {
7
+ if (Array.isArray(promises)) {
8
+ return Promise.race([sleep(timeout), Promise.all(promises).then(() => undefined)]);
9
+ }
10
+ return Promise.race([sleep(timeout), Promise.all(Object.values(promises))]).then(() => {
11
+ Object.entries(promises).forEach(([key, value]) => {
12
+ if (value instanceof Promise) {
13
+ promises[key] = undefined;
14
+ value.then((value) => {
15
+ promises[key] = { value };
16
+ });
17
+ }
18
+ else {
19
+ promises[key] = { value };
20
+ }
21
+ });
22
+ return sleep(0).then(() => promises);
23
+ });
24
+ }
25
+ // ---------------------------------------------------------------------------
26
+ // Adapted from https://github.com/marcelowa/promise-all-properties
27
+ /**
28
+ * A variation of `Promise.all()` that accepts an object with named promises
29
+ * and returns a same-shaped object with the resolved values.
30
+ *
31
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#promiseallobject
32
+ */
33
+ export const promiseAllObject = (promisesMap) => Promise.all(Object.values(promisesMap)).then((results) => {
34
+ const keys = Object.keys(promisesMap);
35
+ const resolvedMap = {};
36
+ for (let i = 0; i < results.length; i++) {
37
+ resolvedMap[keys[i]] = results[i];
38
+ }
39
+ return resolvedMap;
40
+ });
@@ -27,29 +27,24 @@ const PatchedCollator = function Collator(locales, options) {
27
27
  return new PatchedCollator(locales, options);
28
28
  }
29
29
  const mappedLocales = mapLocales(locales);
30
- this.super = _Collator(mappedLocales || locales, options);
31
- this.mapped = !!mappedLocales;
32
- };
33
- // This is all very hacky, but extending the class *AND* preseving the
34
- // ability to instantiate without `new` is a bit of a pain.
35
- // Eagerly interested in finding a better way to do this.
36
- const CollatorProto = {
37
- constructor: PatchedCollator,
38
- compare(a, b) {
39
- const res1 = this.super.compare(a, b);
30
+ const parent = _Collator(mappedLocales || locales, options);
31
+ const mapped = !!mappedLocales;
32
+ this.compare = (a, b) => {
33
+ const res1 = parent.compare(a, b);
34
+ if (!mapped) {
35
+ return res1;
36
+ }
40
37
  const a0 = a.charAt(0);
41
38
  const b0 = b.charAt(0);
42
39
  if (/\d/.test(a0 + b0)) {
43
40
  return res1;
44
41
  }
45
- const res2 = this.super.compare(a0, b0);
42
+ const res2 = parent.compare(a0, b0);
46
43
  return res2 !== 0 ? res2 : res1;
47
- },
48
- resolvedOptions() {
49
- return this.super.resolvedOptions();
50
- },
44
+ };
45
+ this.resolvedOptions = () => parent.resolvedOptions();
51
46
  };
52
- PatchedCollator.prototype = CollatorProto;
47
+ PatchedCollator.prototype = { constructor: PatchedCollator };
53
48
  // Static methods (not patched since "is" is not ACTUALLY supported.)
54
49
  PatchedCollator.supportedLocalesOf = _Collator.supportedLocalesOf;
55
50
  PatchedCollator.$original = _Collator;
@@ -64,11 +59,8 @@ _patchedLocaleCompare.$original = _localeCompare;
64
59
  // NumberFormat
65
60
  // ===========================================================================
66
61
  const _NumberFormat = Intl.NumberFormat;
67
- const reformatNumberParts = function (parts) {
68
- if (!this.mapped) {
69
- return parts;
70
- }
71
- const options = this.super.resolvedOptions();
62
+ const reformatNumberParts = function (parent, parts) {
63
+ const options = parent.resolvedOptions();
72
64
  if (options.style === 'currency' && options.currencyDisplay === 'symbol') {
73
65
  const currency = options.currency;
74
66
  if (currency === 'DKK' || currency === 'ISK') {
@@ -87,31 +79,21 @@ const PatchedNumberFormat = function NumberFormat(locales, options) {
87
79
  return new PatchedNumberFormat(locales, options);
88
80
  }
89
81
  const mappedLocales = mapLocales(locales);
90
- this.super = _NumberFormat(mappedLocales || locales, options);
91
- this.mapped = !!mappedLocales;
82
+ const parent = _NumberFormat(mappedLocales || locales, options);
83
+ const mapped = !!mappedLocales;
84
+ this.format = (value) => combineParts(this.formatToParts(value));
85
+ this.formatRange = (value1, value2) => combineParts(this.formatRangeToParts(value1, value2));
86
+ this.formatToParts = (value) => {
87
+ const parts = parent.formatToParts(value);
88
+ return mapped ? reformatNumberParts(parent, parts) : parts;
89
+ };
90
+ this.formatRangeToParts = (value1, value2) => {
91
+ const parts = parent.formatRangeToParts(value1, value2);
92
+ return mapped ? reformatNumberParts(parent, parts) : parts;
93
+ };
94
+ this.resolvedOptions = () => parent.resolvedOptions();
92
95
  };
93
- // This is all very hacky, but extending the class *AND* preseving the
94
- // ability to instantiate without `new` is a bit of a pain.
95
- // Eagerly interested in finding a better way to do this.
96
- const numberFormatProto = {
97
- constructor: PatchedNumberFormat,
98
- format(value) {
99
- return combineParts(this.formatToParts(value));
100
- },
101
- formatRange(value1, value2) {
102
- return combineParts(this.formatRangeToParts(value1, value2));
103
- },
104
- formatToParts(value) {
105
- return reformatNumberParts.call(this, this.super.formatToParts(value));
106
- },
107
- formatRangeToParts(value1, value2) {
108
- return reformatNumberParts.call(this, this.super.formatRangeToParts(value1, value2));
109
- },
110
- resolvedOptions() {
111
- return this.super.resolvedOptions();
112
- },
113
- };
114
- PatchedNumberFormat.prototype = numberFormatProto;
96
+ PatchedNumberFormat.prototype = { constructor: PatchedNumberFormat };
115
97
  // Static methods (not patched since "is" is not ACTUALLY supported.)
116
98
  PatchedNumberFormat.supportedLocalesOf = _NumberFormat.supportedLocalesOf;
117
99
  PatchedNumberFormat.$original = _NumberFormat;
@@ -197,11 +179,8 @@ const partMappers = {
197
179
  }
198
180
  },
199
181
  };
200
- const reformatDateTimeParts = function (parts) {
201
- if (!this.mapped) {
202
- return parts;
203
- }
204
- const options = this.super.resolvedOptions();
182
+ const reformatDateTimeParts = function (parent, parts) {
183
+ const options = parent.resolvedOptions();
205
184
  parts.forEach((part, idx) => {
206
185
  var _a;
207
186
  const mapper = partMappers[part.type];
@@ -226,31 +205,21 @@ const PatchedDateTimeFormat = function DateTimeFormat(locales, options) {
226
205
  hourCycle: 'h11',
227
206
  };
228
207
  }
229
- this.super = _DateTimeFormat(mappedLocales || locales, options);
230
- this.mapped = !!mappedLocales;
231
- };
232
- // This is all very hacky, but extending the class *AND* preseving the
233
- // ability to instantiate without `new` is a bit of a pain.
234
- // Eagerly interested in finding a better way to do this.
235
- const dateTimeFormatProto = {
236
- constructor: PatchedDateTimeFormat,
237
- format(value) {
238
- return combineParts(this.formatToParts(value));
239
- },
240
- formatRange(value1, value2) {
241
- return combineParts(this.formatRangeToParts(value1, value2));
242
- },
243
- formatToParts(value) {
244
- return reformatDateTimeParts.call(this, this.super.formatToParts(value));
245
- },
246
- formatRangeToParts(value1, value2) {
247
- return reformatDateTimeParts.call(this, this.super.formatRangeToParts(value1, value2));
248
- },
249
- resolvedOptions() {
250
- return this.super.resolvedOptions();
251
- },
208
+ const parent = _DateTimeFormat(mappedLocales || locales, options);
209
+ const mapped = !!mappedLocales;
210
+ this.format = (value) => combineParts(this.formatToParts(value));
211
+ this.formatRange = (value1, value2) => combineParts(this.formatRangeToParts(value1, value2));
212
+ this.formatToParts = (value) => {
213
+ const parts = parent.formatToParts(value);
214
+ return mapped ? reformatDateTimeParts(parent, parts) : parts;
215
+ };
216
+ this.formatRangeToParts = (value1, value2) => {
217
+ const parts = parent.formatRangeToParts(value1, value2);
218
+ return mapped ? reformatDateTimeParts(parent, parts) : parts;
219
+ };
220
+ this.resolvedOptions = () => parent.resolvedOptions();
252
221
  };
253
- PatchedDateTimeFormat.prototype = dateTimeFormatProto;
222
+ PatchedDateTimeFormat.prototype = { constructor: PatchedDateTimeFormat };
254
223
  // Static methods (not patched since "is" is not ACTUALLY supported.)
255
224
  PatchedDateTimeFormat.supportedLocalesOf = _DateTimeFormat.supportedLocalesOf;
256
225
  PatchedDateTimeFormat.$original = _DateTimeFormat;
@@ -277,6 +246,8 @@ if (_PluralRules) {
277
246
  super(mappedLocales || locales, options);
278
247
  this.mapped = !!mappedLocales;
279
248
  this.ord = (options === null || options === void 0 ? void 0 : options.type) === 'ordinal';
249
+ this.select = this.select.bind(this);
250
+ this.selectRange = this.selectRange.bind(this);
280
251
  }
281
252
  select(n) {
282
253
  if (this.mapped) {
@@ -310,6 +281,8 @@ if (_ListFormat) {
310
281
  const mappedLocales = mapLocales(locales);
311
282
  super(mappedLocales || locales, options);
312
283
  this.mapped = !!mappedLocales;
284
+ this.format = this.format.bind(this);
285
+ this.formatToParts = this.formatToParts.bind(this);
313
286
  }
314
287
  format(list) {
315
288
  return this.mapped ? combineParts(this.formatToParts(list)) : super.format(list);
package/esm/index.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  /// <reference path="./vanillaExtract.d.ts" />
2
2
  /// <reference path="./http.d.ts" />
3
+ /// <reference path="./async.d.ts" />
3
4
  /// <reference path="./fixIcelandicLocale.d.ts" />
5
+ /// <reference path="./remix/http.d.ts" />
4
6
  /// <reference path="./CookieHubConsent.d.tsx" />
7
+ /// <reference path="./remix/Wait.d.tsx" />
5
8
  /// <reference path="./next/SiteImprove.d.tsx" />
6
9
  /// <reference path="./next/http.d.tsx" />
7
10
 
@@ -0,0 +1,49 @@
1
+ import { ReactElement, ReactNode } from 'react';
2
+ type WaitPropsBase<T> = {
3
+ /**
4
+ * The value you want to wait for before rendering
5
+ */
6
+ for: Promise<T> | T;
7
+ /**
8
+ * A function to render the children when the value is resolved.
9
+ *
10
+ * (If the promise resolved to an object with a truthy `$error` property,
11
+ * then the `$error` is thrown and this function skipped.)
12
+ */
13
+ children: (data: Exclude<T, {
14
+ $error: string | number | true | object;
15
+ }>) => ReactNode;
16
+ };
17
+ type WaitFallbacks = {
18
+ /**
19
+ * Custom loading/spinner component.
20
+ */
21
+ meanwhile?: Exclude<ReactNode, number>;
22
+ /**
23
+ * Custom error component if the promise is rejected or if it resolves to an
24
+ * object with an `error` property.
25
+ */
26
+ error?: Exclude<ReactNode, number>;
27
+ };
28
+ export type WaitProps<T> = WaitPropsBase<T> & WaitFallbacks;
29
+ /**
30
+ * A function component that wraps `@reykjavik/webtools/remix/Wait` to provide
31
+ * custom properties for `meanwhile` and `error` fallbacks, and/or other
32
+ * behaviors.
33
+ *
34
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README-remix.md#type-waitcomponent
35
+ */
36
+ export type WaitComponent<CustomProps extends Record<string, unknown> = Record<never, never>> = (<T>(props: WaitPropsBase<T> & CustomProps) => ReactElement) & {
37
+ displayName?: string;
38
+ };
39
+ /**
40
+ * Wrapper around [Remix's `Await`](https://remix.run/docs/en/2/components/await)
41
+ * component, to provide a more ergonomic API.
42
+ *
43
+ * If the awaited promise (`props.for`) resolves to an object with a truthy
44
+ * `$error` property, the `$error` will be thrown.
45
+ *
46
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README-remix.md#wait-component
47
+ */
48
+ export declare const Wait: WaitComponent<WaitFallbacks>;
49
+ export {};
@@ -0,0 +1,21 @@
1
+ import React, { Suspense } from 'react';
2
+ import { Await } from '@remix-run/react';
3
+ /**
4
+ * Wrapper around [Remix's `Await`](https://remix.run/docs/en/2/components/await)
5
+ * component, to provide a more ergonomic API.
6
+ *
7
+ * If the awaited promise (`props.for`) resolves to an object with a truthy
8
+ * `$error` property, the `$error` will be thrown.
9
+ *
10
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README-remix.md#wait-component
11
+ */
12
+ export const Wait = (props) => (React.createElement(Suspense, { fallback: props.meanwhile || 'Loading...' },
13
+ React.createElement(Await, { resolve: props.for, errorElement: props.error || 'An error occurred.' }, (value) => {
14
+ if (value && // eslint-disable-line @typescript-eslint/no-unnecessary-condition
15
+ typeof value === 'object' &&
16
+ '$error' in value &&
17
+ value.$error) {
18
+ throw value.$error;
19
+ }
20
+ return props.children(value);
21
+ })));
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Detects if the request is a client fetch, or an initial/full-page load.
3
+ * Useful for deciding whether to defer data fetching or not.
4
+ *
5
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README-remix.md#isclientfetch
6
+ */
7
+ export declare const isClientFetch: (request: Request) => boolean;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Detects if the request is a client fetch, or an initial/full-page load.
3
+ * Useful for deciding whether to defer data fetching or not.
4
+ *
5
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README-remix.md#isclientfetch
6
+ */
7
+ export const isClientFetch = (request) =>
8
+ // For info about this detection method:
9
+ // - https://github.com/remix-run/remix/discussions/5583
10
+ // - https://github.com/sergiodxa/remix-utils/discussions/311#discussioncomment-8572497
11
+ (request.headers.get('Sec-Fetch-Dest') || '') === 'empty';
@@ -30,29 +30,24 @@ const PatchedCollator = function Collator(locales, options) {
30
30
  return new PatchedCollator(locales, options);
31
31
  }
32
32
  const mappedLocales = mapLocales(locales);
33
- this.super = _Collator(mappedLocales || locales, options);
34
- this.mapped = !!mappedLocales;
35
- };
36
- // This is all very hacky, but extending the class *AND* preseving the
37
- // ability to instantiate without `new` is a bit of a pain.
38
- // Eagerly interested in finding a better way to do this.
39
- const CollatorProto = {
40
- constructor: PatchedCollator,
41
- compare(a, b) {
42
- const res1 = this.super.compare(a, b);
33
+ const parent = _Collator(mappedLocales || locales, options);
34
+ const mapped = !!mappedLocales;
35
+ this.compare = (a, b) => {
36
+ const res1 = parent.compare(a, b);
37
+ if (!mapped) {
38
+ return res1;
39
+ }
43
40
  const a0 = a.charAt(0);
44
41
  const b0 = b.charAt(0);
45
42
  if (/\d/.test(a0 + b0)) {
46
43
  return res1;
47
44
  }
48
- const res2 = this.super.compare(a0, b0);
45
+ const res2 = parent.compare(a0, b0);
49
46
  return res2 !== 0 ? res2 : res1;
50
- },
51
- resolvedOptions() {
52
- return this.super.resolvedOptions();
53
- },
47
+ };
48
+ this.resolvedOptions = () => parent.resolvedOptions();
54
49
  };
55
- PatchedCollator.prototype = CollatorProto;
50
+ PatchedCollator.prototype = { constructor: PatchedCollator };
56
51
  // Static methods (not patched since "is" is not ACTUALLY supported.)
57
52
  PatchedCollator.supportedLocalesOf = _Collator.supportedLocalesOf;
58
53
  PatchedCollator.$original = _Collator;
@@ -68,11 +63,8 @@ exports._patchedLocaleCompare.$original = _localeCompare;
68
63
  // NumberFormat
69
64
  // ===========================================================================
70
65
  const _NumberFormat = Intl.NumberFormat;
71
- const reformatNumberParts = function (parts) {
72
- if (!this.mapped) {
73
- return parts;
74
- }
75
- const options = this.super.resolvedOptions();
66
+ const reformatNumberParts = function (parent, parts) {
67
+ const options = parent.resolvedOptions();
76
68
  if (options.style === 'currency' && options.currencyDisplay === 'symbol') {
77
69
  const currency = options.currency;
78
70
  if (currency === 'DKK' || currency === 'ISK') {
@@ -91,31 +83,21 @@ const PatchedNumberFormat = function NumberFormat(locales, options) {
91
83
  return new PatchedNumberFormat(locales, options);
92
84
  }
93
85
  const mappedLocales = mapLocales(locales);
94
- this.super = _NumberFormat(mappedLocales || locales, options);
95
- this.mapped = !!mappedLocales;
86
+ const parent = _NumberFormat(mappedLocales || locales, options);
87
+ const mapped = !!mappedLocales;
88
+ this.format = (value) => combineParts(this.formatToParts(value));
89
+ this.formatRange = (value1, value2) => combineParts(this.formatRangeToParts(value1, value2));
90
+ this.formatToParts = (value) => {
91
+ const parts = parent.formatToParts(value);
92
+ return mapped ? reformatNumberParts(parent, parts) : parts;
93
+ };
94
+ this.formatRangeToParts = (value1, value2) => {
95
+ const parts = parent.formatRangeToParts(value1, value2);
96
+ return mapped ? reformatNumberParts(parent, parts) : parts;
97
+ };
98
+ this.resolvedOptions = () => parent.resolvedOptions();
96
99
  };
97
- // This is all very hacky, but extending the class *AND* preseving the
98
- // ability to instantiate without `new` is a bit of a pain.
99
- // Eagerly interested in finding a better way to do this.
100
- const numberFormatProto = {
101
- constructor: PatchedNumberFormat,
102
- format(value) {
103
- return combineParts(this.formatToParts(value));
104
- },
105
- formatRange(value1, value2) {
106
- return combineParts(this.formatRangeToParts(value1, value2));
107
- },
108
- formatToParts(value) {
109
- return reformatNumberParts.call(this, this.super.formatToParts(value));
110
- },
111
- formatRangeToParts(value1, value2) {
112
- return reformatNumberParts.call(this, this.super.formatRangeToParts(value1, value2));
113
- },
114
- resolvedOptions() {
115
- return this.super.resolvedOptions();
116
- },
117
- };
118
- PatchedNumberFormat.prototype = numberFormatProto;
100
+ PatchedNumberFormat.prototype = { constructor: PatchedNumberFormat };
119
101
  // Static methods (not patched since "is" is not ACTUALLY supported.)
120
102
  PatchedNumberFormat.supportedLocalesOf = _NumberFormat.supportedLocalesOf;
121
103
  PatchedNumberFormat.$original = _NumberFormat;
@@ -202,11 +184,8 @@ const partMappers = {
202
184
  }
203
185
  },
204
186
  };
205
- const reformatDateTimeParts = function (parts) {
206
- if (!this.mapped) {
207
- return parts;
208
- }
209
- const options = this.super.resolvedOptions();
187
+ const reformatDateTimeParts = function (parent, parts) {
188
+ const options = parent.resolvedOptions();
210
189
  parts.forEach((part, idx) => {
211
190
  var _a;
212
191
  const mapper = partMappers[part.type];
@@ -231,31 +210,21 @@ const PatchedDateTimeFormat = function DateTimeFormat(locales, options) {
231
210
  hourCycle: 'h11',
232
211
  };
233
212
  }
234
- this.super = _DateTimeFormat(mappedLocales || locales, options);
235
- this.mapped = !!mappedLocales;
236
- };
237
- // This is all very hacky, but extending the class *AND* preseving the
238
- // ability to instantiate without `new` is a bit of a pain.
239
- // Eagerly interested in finding a better way to do this.
240
- const dateTimeFormatProto = {
241
- constructor: PatchedDateTimeFormat,
242
- format(value) {
243
- return combineParts(this.formatToParts(value));
244
- },
245
- formatRange(value1, value2) {
246
- return combineParts(this.formatRangeToParts(value1, value2));
247
- },
248
- formatToParts(value) {
249
- return reformatDateTimeParts.call(this, this.super.formatToParts(value));
250
- },
251
- formatRangeToParts(value1, value2) {
252
- return reformatDateTimeParts.call(this, this.super.formatRangeToParts(value1, value2));
253
- },
254
- resolvedOptions() {
255
- return this.super.resolvedOptions();
256
- },
213
+ const parent = _DateTimeFormat(mappedLocales || locales, options);
214
+ const mapped = !!mappedLocales;
215
+ this.format = (value) => combineParts(this.formatToParts(value));
216
+ this.formatRange = (value1, value2) => combineParts(this.formatRangeToParts(value1, value2));
217
+ this.formatToParts = (value) => {
218
+ const parts = parent.formatToParts(value);
219
+ return mapped ? reformatDateTimeParts(parent, parts) : parts;
220
+ };
221
+ this.formatRangeToParts = (value1, value2) => {
222
+ const parts = parent.formatRangeToParts(value1, value2);
223
+ return mapped ? reformatDateTimeParts(parent, parts) : parts;
224
+ };
225
+ this.resolvedOptions = () => parent.resolvedOptions();
257
226
  };
258
- PatchedDateTimeFormat.prototype = dateTimeFormatProto;
227
+ PatchedDateTimeFormat.prototype = { constructor: PatchedDateTimeFormat };
259
228
  // Static methods (not patched since "is" is not ACTUALLY supported.)
260
229
  PatchedDateTimeFormat.supportedLocalesOf = _DateTimeFormat.supportedLocalesOf;
261
230
  PatchedDateTimeFormat.$original = _DateTimeFormat;
@@ -283,6 +252,8 @@ if (_PluralRules) {
283
252
  super(mappedLocales || locales, options);
284
253
  this.mapped = !!mappedLocales;
285
254
  this.ord = (options === null || options === void 0 ? void 0 : options.type) === 'ordinal';
255
+ this.select = this.select.bind(this);
256
+ this.selectRange = this.selectRange.bind(this);
286
257
  }
287
258
  select(n) {
288
259
  if (this.mapped) {
@@ -316,6 +287,8 @@ if (_ListFormat) {
316
287
  const mappedLocales = mapLocales(locales);
317
288
  super(mappedLocales || locales, options);
318
289
  this.mapped = !!mappedLocales;
290
+ this.format = this.format.bind(this);
291
+ this.formatToParts = this.formatToParts.bind(this);
319
292
  }
320
293
  format(list) {
321
294
  return this.mapped ? combineParts(this.formatToParts(list)) : super.format(list);
package/index.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  /// <reference path="./vanillaExtract.d.ts" />
2
2
  /// <reference path="./http.d.ts" />
3
+ /// <reference path="./async.d.ts" />
3
4
  /// <reference path="./fixIcelandicLocale.d.ts" />
5
+ /// <reference path="./remix/http.d.ts" />
4
6
  /// <reference path="./CookieHubConsent.d.tsx" />
7
+ /// <reference path="./remix/Wait.d.tsx" />
5
8
  /// <reference path="./next/SiteImprove.d.tsx" />
6
9
  /// <reference path="./next/http.d.tsx" />
7
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reykjavik/webtools",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Misc. JS/TS helpers used by Reykjavík City's web dev teams.",
5
5
  "main": "index.js",
6
6
  "repository": "ssh://git@github.com:reykjavikcity/webtools.git",
@@ -14,12 +14,16 @@
14
14
  "@reykjavik/hanna-utils": "^0.2.3"
15
15
  },
16
16
  "peerDependencies": {
17
+ "@remix-run/react": "^2.6.0",
17
18
  "@vanilla-extract/css": "^1.14.1",
18
19
  "next": ">=11",
19
20
  "react": ">=16.8.0",
20
21
  "react-dom": ">=16.8.0"
21
22
  },
22
23
  "peerDependenciesMeta": {
24
+ "@remix-run/react": {
25
+ "optional": true
26
+ },
23
27
  "@vanilla-extract/css": {
24
28
  "optional": true
25
29
  },
@@ -52,14 +56,26 @@
52
56
  "import": "./esm/http.js",
53
57
  "require": "./http.js"
54
58
  },
59
+ "./async": {
60
+ "import": "./esm/async.js",
61
+ "require": "./async.js"
62
+ },
55
63
  "./fixIcelandicLocale": {
56
64
  "import": "./esm/fixIcelandicLocale.js",
57
65
  "require": "./fixIcelandicLocale.js"
58
66
  },
67
+ "./remix/http": {
68
+ "import": "./esm/remix/http.js",
69
+ "require": "./remix/http.js"
70
+ },
59
71
  "./CookieHubConsent": {
60
72
  "import": "./esm/CookieHubConsent.js",
61
73
  "require": "./CookieHubConsent.js"
62
74
  },
75
+ "./remix/Wait": {
76
+ "import": "./esm/remix/Wait.js",
77
+ "require": "./remix/Wait.js"
78
+ },
63
79
  "./next/SiteImprove": {
64
80
  "import": "./esm/next/SiteImprove.js",
65
81
  "require": "./next/SiteImprove.js"
@@ -0,0 +1,49 @@
1
+ import { ReactElement, ReactNode } from 'react';
2
+ type WaitPropsBase<T> = {
3
+ /**
4
+ * The value you want to wait for before rendering
5
+ */
6
+ for: Promise<T> | T;
7
+ /**
8
+ * A function to render the children when the value is resolved.
9
+ *
10
+ * (If the promise resolved to an object with a truthy `$error` property,
11
+ * then the `$error` is thrown and this function skipped.)
12
+ */
13
+ children: (data: Exclude<T, {
14
+ $error: string | number | true | object;
15
+ }>) => ReactNode;
16
+ };
17
+ type WaitFallbacks = {
18
+ /**
19
+ * Custom loading/spinner component.
20
+ */
21
+ meanwhile?: Exclude<ReactNode, number>;
22
+ /**
23
+ * Custom error component if the promise is rejected or if it resolves to an
24
+ * object with an `error` property.
25
+ */
26
+ error?: Exclude<ReactNode, number>;
27
+ };
28
+ export type WaitProps<T> = WaitPropsBase<T> & WaitFallbacks;
29
+ /**
30
+ * A function component that wraps `@reykjavik/webtools/remix/Wait` to provide
31
+ * custom properties for `meanwhile` and `error` fallbacks, and/or other
32
+ * behaviors.
33
+ *
34
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README-remix.md#type-waitcomponent
35
+ */
36
+ export type WaitComponent<CustomProps extends Record<string, unknown> = Record<never, never>> = (<T>(props: WaitPropsBase<T> & CustomProps) => ReactElement) & {
37
+ displayName?: string;
38
+ };
39
+ /**
40
+ * Wrapper around [Remix's `Await`](https://remix.run/docs/en/2/components/await)
41
+ * component, to provide a more ergonomic API.
42
+ *
43
+ * If the awaited promise (`props.for`) resolves to an object with a truthy
44
+ * `$error` property, the `$error` will be thrown.
45
+ *
46
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README-remix.md#wait-component
47
+ */
48
+ export declare const Wait: WaitComponent<WaitFallbacks>;
49
+ export {};
package/remix/Wait.js ADDED
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.Wait = void 0;
27
+ const react_1 = __importStar(require("react"));
28
+ const react_2 = require("@remix-run/react");
29
+ /**
30
+ * Wrapper around [Remix's `Await`](https://remix.run/docs/en/2/components/await)
31
+ * component, to provide a more ergonomic API.
32
+ *
33
+ * If the awaited promise (`props.for`) resolves to an object with a truthy
34
+ * `$error` property, the `$error` will be thrown.
35
+ *
36
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README-remix.md#wait-component
37
+ */
38
+ const Wait = (props) => (react_1.default.createElement(react_1.Suspense, { fallback: props.meanwhile || 'Loading...' },
39
+ react_1.default.createElement(react_2.Await, { resolve: props.for, errorElement: props.error || 'An error occurred.' }, (value) => {
40
+ if (value && // eslint-disable-line @typescript-eslint/no-unnecessary-condition
41
+ typeof value === 'object' &&
42
+ '$error' in value &&
43
+ value.$error) {
44
+ throw value.$error;
45
+ }
46
+ return props.children(value);
47
+ })));
48
+ exports.Wait = Wait;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Detects if the request is a client fetch, or an initial/full-page load.
3
+ * Useful for deciding whether to defer data fetching or not.
4
+ *
5
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README-remix.md#isclientfetch
6
+ */
7
+ export declare const isClientFetch: (request: Request) => boolean;
package/remix/http.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isClientFetch = void 0;
4
+ /**
5
+ * Detects if the request is a client fetch, or an initial/full-page load.
6
+ * Useful for deciding whether to defer data fetching or not.
7
+ *
8
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README-remix.md#isclientfetch
9
+ */
10
+ const isClientFetch = (request) =>
11
+ // For info about this detection method:
12
+ // - https://github.com/remix-run/remix/discussions/5583
13
+ // - https://github.com/sergiodxa/remix-utils/discussions/311#discussioncomment-8572497
14
+ (request.headers.get('Sec-Fetch-Dest') || '') === 'empty';
15
+ exports.isClientFetch = isClientFetch;