@uniflowed/web 0.0.0-alpha.18 → 0.14.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/cookie.js +3 -3
- package/head.js +2 -2
- package/internal/image-endpoint.js +41 -0
- package/internal/remote-image.js +42 -0
- package/media.js +45 -17
- package/package.json +6 -5
- package/regions.js +4 -4
- package/time.js +4 -1
package/cookie.js
CHANGED
|
@@ -49,7 +49,7 @@ const SERVER_COOKIES: string = "__ufServerCookies";
|
|
|
49
49
|
* sharing it would mean importing that package here.
|
|
50
50
|
*/
|
|
51
51
|
export function parse(header: string | null): { [string]: string } {
|
|
52
|
-
const out: { [string]: string } = Object.
|
|
52
|
+
const out: { [string]: string } = Object.setPrototypeOf({}, null);
|
|
53
53
|
if (header == null || header === "") {
|
|
54
54
|
return out;
|
|
55
55
|
}
|
|
@@ -93,7 +93,7 @@ export function serialize(name: string, value: string, options?: CookieOptions):
|
|
|
93
93
|
|
|
94
94
|
/** Whatever cookies are readable here, server or browser. */
|
|
95
95
|
function readAll(): { [string]: string } {
|
|
96
|
-
const host =
|
|
96
|
+
const host = globalThis as $FlowFixMe;
|
|
97
97
|
if (host.document != null) {
|
|
98
98
|
return parse(host.document.cookie);
|
|
99
99
|
}
|
|
@@ -123,7 +123,7 @@ export function useCookie(
|
|
|
123
123
|
|
|
124
124
|
const write = React.useCallback(
|
|
125
125
|
(next: string, options?: CookieOptions) => {
|
|
126
|
-
const host =
|
|
126
|
+
const host = globalThis as $FlowFixMe;
|
|
127
127
|
if (host.document == null) {
|
|
128
128
|
throw new Error(
|
|
129
129
|
`useCookie: ${name} cannot be written during a server render. ` +
|
package/head.js
CHANGED
|
@@ -80,7 +80,7 @@ export function useHead(head: Head): void {
|
|
|
80
80
|
const key = JSON.stringify(head);
|
|
81
81
|
|
|
82
82
|
React.useEffect(() => {
|
|
83
|
-
const document = (globalThis
|
|
83
|
+
const document = (globalThis as $FlowFixMe).document;
|
|
84
84
|
if (document == null) {
|
|
85
85
|
return undefined;
|
|
86
86
|
}
|
|
@@ -124,7 +124,7 @@ export function useHead(head: Head): void {
|
|
|
124
124
|
document.title = previousTitle;
|
|
125
125
|
}
|
|
126
126
|
for (const element of added) {
|
|
127
|
-
(element
|
|
127
|
+
(element as $FlowFixMe).remove();
|
|
128
128
|
}
|
|
129
129
|
};
|
|
130
130
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/web`: where `Image` sends a remote `src`.
|
|
4
|
+
//
|
|
5
|
+
// This file is what `Image` reads *outside* a uf build — under `uf test`, in a
|
|
6
|
+
// library that renders one without uf's Vite plugins — and there it says there
|
|
7
|
+
// is no endpoint, so a remote `src` is written into `<img>` as it was given.
|
|
8
|
+
//
|
|
9
|
+
// Inside one, `uf:asset` (`@uniflowed/vite`'s `internal/assets.js`) answers
|
|
10
|
+
// for this module with the project's own values: the endpoint's path under the
|
|
11
|
+
// base path when `app.builtins.images.remotePatterns` lists anything, and the
|
|
12
|
+
// widths and quality the endpoint will accept. Generated rather than read at
|
|
13
|
+
// runtime, because the browser bundle and the server one both render `Image`
|
|
14
|
+
// and have to write the same `srcset`, or hydration disagrees about it.
|
|
15
|
+
//
|
|
16
|
+
// Which is also why nothing but the value lives here: the whole module is
|
|
17
|
+
// replaced, so a function beside it would vanish from the build. What `Image`
|
|
18
|
+
// does with the value is `./remote-image.js`.
|
|
19
|
+
|
|
20
|
+
/** What `Image` needs to know about `/__uf/image`. */
|
|
21
|
+
export type ImageEndpoint = {|
|
|
22
|
+
/** The endpoint's path, or `null` when the project has none. */
|
|
23
|
+
readonly path: string | null,
|
|
24
|
+
/** The widths it accepts, which are the only ones worth a `srcset` rung. */
|
|
25
|
+
readonly widths: $ReadOnlyArray<number>,
|
|
26
|
+
/** The quality it encodes at when a request names none. */
|
|
27
|
+
readonly quality: number,
|
|
28
|
+
|};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* No endpoint, and uf's defaults for the rest.
|
|
32
|
+
*
|
|
33
|
+
* The widths and quality are `uf_assets`' defaults, and
|
|
34
|
+
* `tests/library/image-endpoint.test.js` holds them to the Rust ones and to
|
|
35
|
+
* `@uniflowed/server/image`'s.
|
|
36
|
+
*/
|
|
37
|
+
export const IMAGE_ENDPOINT: ImageEndpoint = {
|
|
38
|
+
path: null,
|
|
39
|
+
widths: [640, 750, 828, 1080, 1200, 1440, 1920],
|
|
40
|
+
quality: 75,
|
|
41
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/web`: the `src` and `srcset` `Image` writes for a
|
|
4
|
+
// remote image, from what `./image-endpoint.js` says about the endpoint.
|
|
5
|
+
|
|
6
|
+
import type { ImageEndpoint } from "./image-endpoint.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The `src` and `srcset` for a remote image, through the endpoint.
|
|
10
|
+
*
|
|
11
|
+
* One rung per accepted width narrower than the image, and one more — the
|
|
12
|
+
* narrowest accepted width that covers it, or the widest there is — described
|
|
13
|
+
* at the width it will actually come back at, because the endpoint never
|
|
14
|
+
* upscales and a descriptor wider than the file is a browser choosing a rung
|
|
15
|
+
* for pixels that do not exist.
|
|
16
|
+
*
|
|
17
|
+
* `null` when the project has no endpoint, or the URL is not one it fetches.
|
|
18
|
+
*/
|
|
19
|
+
export function remoteSources(
|
|
20
|
+
endpoint: ImageEndpoint,
|
|
21
|
+
src: string,
|
|
22
|
+
intrinsic: number,
|
|
23
|
+
quality: number,
|
|
24
|
+
): {| readonly src: string, readonly srcSet: string |} | null {
|
|
25
|
+
const path = endpoint.path;
|
|
26
|
+
if (path == null) return null;
|
|
27
|
+
const scheme = src.slice(0, 8).toLowerCase();
|
|
28
|
+
if (!scheme.startsWith("https://") && !scheme.startsWith("http://")) return null;
|
|
29
|
+
const widths = [...endpoint.widths].sort((a, b) => a - b);
|
|
30
|
+
if (widths.length === 0) return null;
|
|
31
|
+
const at = (width: number): string =>
|
|
32
|
+
`${path}?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`;
|
|
33
|
+
const cover = widths.find((width) => width >= intrinsic) ?? widths[widths.length - 1];
|
|
34
|
+
// Everything narrower than the rung that covers the image is narrower than
|
|
35
|
+
// the image too, so each of those comes back at its own width.
|
|
36
|
+
const rungs = widths.filter((width) => width < cover).map((width) => [width, width]);
|
|
37
|
+
rungs.push([cover, Math.min(cover, intrinsic)]);
|
|
38
|
+
return {
|
|
39
|
+
src: at(cover),
|
|
40
|
+
srcSet: rungs.map(([asked, described]) => `${at(asked)} ${described}w`).join(", "),
|
|
41
|
+
};
|
|
42
|
+
}
|
package/media.js
CHANGED
|
@@ -33,6 +33,13 @@
|
|
|
33
33
|
// `public/` — and for it the author supplies `width`, `height` and `srcSet`
|
|
34
34
|
// themselves, as they always did.
|
|
35
35
|
//
|
|
36
|
+
// With one exception, which a project opts into: when
|
|
37
|
+
// `app.builtins.images.remotePatterns` lists hosts, an absolute `http(s)` URL
|
|
38
|
+
// is routed through `/__uf/image` — `@uniflowed/server/image` — which fetches,
|
|
39
|
+
// resizes and re-encodes it on request. `Image` then writes the `srcset` for
|
|
40
|
+
// it, one rung per width the endpoint accepts, because now something does
|
|
41
|
+
// know which sizes exist. `unoptimized` keeps one image out of it.
|
|
42
|
+
//
|
|
36
43
|
// # What belongs in this module
|
|
37
44
|
//
|
|
38
45
|
// Anything a page renders in order to make the browser fetch bytes while the
|
|
@@ -52,6 +59,10 @@
|
|
|
52
59
|
// an instant (`time.js`) or about a stored value (`cookie.js`).
|
|
53
60
|
|
|
54
61
|
import * as React from "@uniflowed/react";
|
|
62
|
+
import type * as ReactTypes from "react";
|
|
63
|
+
|
|
64
|
+
import { IMAGE_ENDPOINT } from "./internal/image-endpoint.js";
|
|
65
|
+
import { remoteSources } from "./internal/remote-image.js";
|
|
55
66
|
|
|
56
67
|
/** How an image should be fetched relative to the rest of the page. */
|
|
57
68
|
export type Loading = "eager" | "lazy";
|
|
@@ -66,7 +77,7 @@ export type Source = {
|
|
|
66
77
|
/**
|
|
67
78
|
* What importing an image evaluates to.
|
|
68
79
|
*
|
|
69
|
-
* Produced by `uf:asset`; see `
|
|
80
|
+
* Produced by `uf:asset`; see `npm/vite/internal/assets.js`. Every field
|
|
70
81
|
* is a fact about files that exist on disk, which is the difference between
|
|
71
82
|
* this and a set of props.
|
|
72
83
|
*/
|
|
@@ -215,6 +226,14 @@ function defaultSizes(width: number): string {
|
|
|
215
226
|
* gives it a high fetch priority, and preloads it — a `srcSet` behind a lazy
|
|
216
227
|
* `<img>` is discovered late, and for the largest image on the page that is
|
|
217
228
|
* usually the whole of the Largest Contentful Paint.
|
|
229
|
+
*
|
|
230
|
+
* A remote `src` — an absolute `http(s)` URL — is sent through `/__uf/image`
|
|
231
|
+
* when the project lists remote hosts in `app.builtins.images.remotePatterns`:
|
|
232
|
+
* `src` and `srcSet` then name the endpoint, one rung per width it accepts,
|
|
233
|
+
* at `quality` or the project's own. `width` and `height` are still required,
|
|
234
|
+
* because the endpoint is asked for the image only after the page has laid it
|
|
235
|
+
* out. A `srcSet` of the author's own, or `unoptimized`, leaves the URL as it
|
|
236
|
+
* was written.
|
|
218
237
|
*/
|
|
219
238
|
export component Image(
|
|
220
239
|
src: string | ImageAsset,
|
|
@@ -226,10 +245,12 @@ export component Image(
|
|
|
226
245
|
sizes?: string,
|
|
227
246
|
srcSet?: string,
|
|
228
247
|
placeholder?: boolean = true,
|
|
248
|
+
quality?: number,
|
|
249
|
+
unoptimized?: boolean = false,
|
|
229
250
|
className?: string,
|
|
230
251
|
style?: { readonly [string]: string | number },
|
|
231
|
-
...rest:
|
|
232
|
-
)
|
|
252
|
+
...rest: ReactTypes.ElementConfig<"img">
|
|
253
|
+
) {
|
|
233
254
|
// Refined rather than cast: `src` is a union and `typeof` narrows it, so
|
|
234
255
|
// neither branch needs `any`. A cast here would be the one place in this
|
|
235
256
|
// module where the manifest's shape stopped being checked.
|
|
@@ -255,7 +276,14 @@ export component Image(
|
|
|
255
276
|
throw new Error("<Image> was given an imported image with no emitted file.");
|
|
256
277
|
}
|
|
257
278
|
|
|
258
|
-
|
|
279
|
+
// A remote URL goes through `/__uf/image` when the project has one, the
|
|
280
|
+
// author wrote no `srcSet` of their own, and did not opt this image out.
|
|
281
|
+
const remote =
|
|
282
|
+
asset == null && srcSet == null && !unoptimized
|
|
283
|
+
? remoteSources(IMAGE_ENDPOINT, url, finalWidth, quality ?? IMAGE_ENDPOINT.quality)
|
|
284
|
+
: null;
|
|
285
|
+
const finalUrl = remote?.src ?? url;
|
|
286
|
+
const finalSrcSet = srcSet ?? remote?.srcSet ?? asset?.srcSet;
|
|
259
287
|
const finalSizes =
|
|
260
288
|
sizes ?? (finalSrcSet != null && finalSrcSet !== "" ? defaultSizes(finalWidth) : undefined);
|
|
261
289
|
const finalLoading: Loading = loading ?? (priority ? "eager" : "lazy");
|
|
@@ -263,7 +291,8 @@ export component Image(
|
|
|
263
291
|
|
|
264
292
|
const image = (
|
|
265
293
|
<img
|
|
266
|
-
|
|
294
|
+
{...rest}
|
|
295
|
+
src={finalUrl}
|
|
267
296
|
alt={alt}
|
|
268
297
|
width={finalWidth}
|
|
269
298
|
height={finalHeight}
|
|
@@ -286,13 +315,12 @@ export component Image(
|
|
|
286
315
|
// it needs no element, it cannot be announced twice to a screen
|
|
287
316
|
// reader, and the browser paints over it as the real image
|
|
288
317
|
// decodes instead of swapping one node for another.
|
|
289
|
-
backgroundImage: `url("${blur}")`,
|
|
290
|
-
backgroundSize: "cover",
|
|
291
|
-
backgroundPosition: "center",
|
|
292
318
|
...style,
|
|
319
|
+
backgroundImage: style?.backgroundImage ?? `url("${blur}")`,
|
|
320
|
+
backgroundSize: style?.backgroundSize ?? "cover",
|
|
321
|
+
backgroundPosition: style?.backgroundPosition ?? "center",
|
|
293
322
|
}
|
|
294
323
|
}
|
|
295
|
-
{...rest}
|
|
296
324
|
/>
|
|
297
325
|
);
|
|
298
326
|
|
|
@@ -307,7 +335,7 @@ export component Image(
|
|
|
307
335
|
<link
|
|
308
336
|
rel="preload"
|
|
309
337
|
as="image"
|
|
310
|
-
href={
|
|
338
|
+
href={finalUrl}
|
|
311
339
|
imageSrcSet={finalSrcSet}
|
|
312
340
|
imageSizes={finalSizes}
|
|
313
341
|
fetchPriority="high"
|
|
@@ -365,7 +393,7 @@ export component Picture(
|
|
|
365
393
|
sources?: $ReadOnlyArray<Source> = [],
|
|
366
394
|
loading?: Loading = "lazy",
|
|
367
395
|
className?: string,
|
|
368
|
-
)
|
|
396
|
+
) {
|
|
369
397
|
return (
|
|
370
398
|
<picture>
|
|
371
399
|
{sources.map((source) => (
|
|
@@ -449,7 +477,7 @@ export component Font(
|
|
|
449
477
|
type?: string,
|
|
450
478
|
crossOrigin?: "anonymous" | "use-credentials" = "anonymous",
|
|
451
479
|
preload?: boolean,
|
|
452
|
-
)
|
|
480
|
+
) {
|
|
453
481
|
const asset: FontAsset | null = typeof src === "string" ? null : src;
|
|
454
482
|
const url: string = typeof src === "string" ? src : src.src;
|
|
455
483
|
const finalType = type ?? asset?.type ?? "font/woff2";
|
|
@@ -527,7 +555,7 @@ export component Font(
|
|
|
527
555
|
* here is that the string was produced by uf from files in the repository, not
|
|
528
556
|
* by anything at runtime.
|
|
529
557
|
*/
|
|
530
|
-
export component IconSprite(sprite: SpriteAsset)
|
|
558
|
+
export component IconSprite(sprite: SpriteAsset) {
|
|
531
559
|
return (
|
|
532
560
|
<div
|
|
533
561
|
// Out of the flow entirely rather than `display: none`: a `<use>` may
|
|
@@ -577,10 +605,11 @@ export component Icon(
|
|
|
577
605
|
width?: number,
|
|
578
606
|
height?: number,
|
|
579
607
|
className?: string,
|
|
580
|
-
...rest:
|
|
581
|
-
)
|
|
608
|
+
...rest: ReactTypes.ElementConfig<"svg">
|
|
609
|
+
) {
|
|
582
610
|
return (
|
|
583
611
|
<svg
|
|
612
|
+
{...rest}
|
|
584
613
|
viewBox={icon.viewBox}
|
|
585
614
|
width={width ?? size}
|
|
586
615
|
height={height ?? size}
|
|
@@ -592,7 +621,6 @@ export component Icon(
|
|
|
592
621
|
aria-label={label}
|
|
593
622
|
aria-hidden={label == null ? true : undefined}
|
|
594
623
|
focusable="false"
|
|
595
|
-
{...rest}
|
|
596
624
|
>
|
|
597
625
|
<use href={icon.href} />
|
|
598
626
|
</svg>
|
|
@@ -619,7 +647,7 @@ export component Icon(
|
|
|
619
647
|
* template. It is a template and not a renderer: uf will not turn JSX into an
|
|
620
648
|
* image, and it refuses text it cannot lay out rather than drawing it wrong.
|
|
621
649
|
*/
|
|
622
|
-
export component OgImage(card: OgAsset, origin: string, alt?: string)
|
|
650
|
+
export component OgImage(card: OgAsset, origin: string, alt?: string) {
|
|
623
651
|
const absolute = card.url.startsWith("http")
|
|
624
652
|
? card.url
|
|
625
653
|
: `${origin.replace(/\/+$/, "")}/${card.url.replace(/^\/+/, "")}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "The primitives a web page is built from, for the Unified Toolchain for Flow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/ubugeeei-prod/uf.git",
|
|
11
|
-
"directory": "
|
|
11
|
+
"directory": "npm/web"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
14
|
".": "./index.js",
|
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"*.js",
|
|
24
|
+
"internal/*.js",
|
|
24
25
|
"!*.test.js"
|
|
25
26
|
],
|
|
26
27
|
"dependencies": {
|
|
27
|
-
"@uniflowed/core": "0.
|
|
28
|
-
"@uniflowed/hooks": "0.
|
|
29
|
-
"@uniflowed/react": "0.
|
|
28
|
+
"@uniflowed/core": "0.14.1",
|
|
29
|
+
"@uniflowed/hooks": "0.14.1",
|
|
30
|
+
"@uniflowed/react": "0.14.1"
|
|
30
31
|
}
|
|
31
32
|
}
|
package/regions.js
CHANGED
|
@@ -75,7 +75,7 @@ export component Announcer(
|
|
|
75
75
|
children?: React.Node,
|
|
76
76
|
urgency?: "polite" | "assertive" = "polite",
|
|
77
77
|
atomic?: boolean = true,
|
|
78
|
-
)
|
|
78
|
+
) {
|
|
79
79
|
return (
|
|
80
80
|
<div aria-live={urgency} aria-atomic={atomic} style={VISUALLY_HIDDEN}>
|
|
81
81
|
{children}
|
|
@@ -90,7 +90,7 @@ export component Announcer(
|
|
|
90
90
|
* landmark list offers, and there must be exactly one per document — which is
|
|
91
91
|
* why this is not something a layout should also render.
|
|
92
92
|
*/
|
|
93
|
-
export component Page(children?: React.Node, className?: string)
|
|
93
|
+
export component Page(children?: React.Node, className?: string) {
|
|
94
94
|
return (
|
|
95
95
|
<main id={MAIN_ID} className={className}>
|
|
96
96
|
{children}
|
|
@@ -110,7 +110,7 @@ export component Page(children?: React.Node, className?: string) renders React.N
|
|
|
110
110
|
* it takes focus and then shows itself, which is why it does not cost a
|
|
111
111
|
* design anything: the only person who ever sees it is the person using it.
|
|
112
112
|
*/
|
|
113
|
-
export component SkipLink(children?: React.Node = "Skip to content")
|
|
113
|
+
export component SkipLink(children?: React.Node = "Skip to content") {
|
|
114
114
|
const [focused, setFocused] = React.useState(false);
|
|
115
115
|
|
|
116
116
|
return (
|
|
@@ -150,6 +150,6 @@ export component SkipLink(children?: React.Node = "Skip to content") renders Rea
|
|
|
150
150
|
* inside it — `<nav>`, `<main>`, `<footer>` — are the ones a reader navigates
|
|
151
151
|
* by, and wrapping them in another one adds a level to walk through.
|
|
152
152
|
*/
|
|
153
|
-
export component Layout(children?: React.Node, className?: string)
|
|
153
|
+
export component Layout(children?: React.Node, className?: string) {
|
|
154
154
|
return <div className={className}>{children}</div>;
|
|
155
155
|
}
|
package/time.js
CHANGED
|
@@ -159,7 +159,7 @@ export component Time(
|
|
|
159
159
|
zone?: string,
|
|
160
160
|
locale?: string,
|
|
161
161
|
className?: string,
|
|
162
|
-
)
|
|
162
|
+
) {
|
|
163
163
|
const at = asInstant(value);
|
|
164
164
|
const machine = at.toString();
|
|
165
165
|
const rendered = useRenderEnvelope();
|
|
@@ -171,6 +171,9 @@ export component Time(
|
|
|
171
171
|
|
|
172
172
|
React.useEffect(() => {
|
|
173
173
|
if (format === "local") {
|
|
174
|
+
// The local zone is a browser value; the server text stays deterministic
|
|
175
|
+
// until the client can replace it after hydration.
|
|
176
|
+
// uf-lint-disable-next-line react-compiler/set-state-in-effect
|
|
174
177
|
setText(at.toZonedDateTimeISO(Temporal.Now.timeZoneId()).toLocaleString(locale));
|
|
175
178
|
} else if (format === "relative") {
|
|
176
179
|
setText(relative(at));
|