@meginhufr/brua-web 0.2.0 → 0.4.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/BruaPicture.svelte +21 -7
- package/dist/brua.d.ts +9 -1
- package/dist/brua.js +13 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/picture.d.ts +12 -0
- package/dist/picture.js +18 -0
- package/dist/react.d.ts +13 -3
- package/dist/react.js +25 -11
- package/dist/sizing.d.ts +12 -0
- package/dist/sizing.js +21 -0
- package/dist/types.d.ts +17 -0
- package/dist/vue.d.ts +31 -2
- package/dist/vue.js +36 -13
- package/package.json +2 -2
package/dist/BruaPicture.svelte
CHANGED
|
@@ -1,30 +1,42 @@
|
|
|
1
1
|
<!-- Brua for Svelte 5: the asset as a picture element (SPEC.md 7.2). The browser picks the format, density
|
|
2
2
|
and color scheme, and follows dark mode live. Animations show their still. Import from "@meginhufr/brua-web/svelte".
|
|
3
3
|
Given an id instead of an asset, the current release is read from the edge through the Brua set with
|
|
4
|
-
setBrua(); the last release it read shows at once, else the children until it has loaded
|
|
4
|
+
setBrua(); the last release it read shows at once, else the children until it has loaded. The error snippet, given the
|
|
5
|
+
reason, shows when there is no picture: nothing released, the edge unreachable, or the file failed; without it, the
|
|
6
|
+
children show then. -->
|
|
5
7
|
<script>
|
|
6
8
|
import { getContext } from "svelte";
|
|
7
|
-
import { pictureModel } from "./picture.ts";
|
|
9
|
+
import { fillStyle, pictureModel } from "./picture.ts";
|
|
10
|
+
import { assetOrError } from "./brua.ts";
|
|
8
11
|
|
|
9
|
-
/** @type {{ asset?: import("./types.ts").AssetResponse, id?: string, brua?: import("./brua.ts").Brua, sizes?: string, loading?: "lazy" | "eager", class?: string, colorScheme?: "light" | "dark", children?: import("svelte").Snippet }} */
|
|
10
|
-
let { asset: given, id, brua = getContext("brua"), sizes, loading, class: imgClass, colorScheme, children } = $props();
|
|
12
|
+
/** @type {{ asset?: import("./types.ts").AssetResponse, id?: string, brua?: import("./brua.ts").Brua, sizes?: string, loading?: "lazy" | "eager", class?: string, colorScheme?: "light" | "dark", objectFit?: "contain" | "cover" | "none", objectPosition?: string, children?: import("svelte").Snippet, error?: import("svelte").Snippet<[import("./types.ts").BruaImageError]> }} */
|
|
13
|
+
let { asset: given, id, brua = getContext("brua"), sizes, loading, class: imgClass, colorScheme, objectFit, objectPosition, children, error } = $props();
|
|
11
14
|
// The last release the Brua read renders at once, so a remounted picture doesn't flash its children.
|
|
12
15
|
let fetched = $state(id === undefined ? undefined : brua?.cachedAsset(id));
|
|
16
|
+
/** @type {import("./types.ts").BruaImageError | undefined} */
|
|
17
|
+
let reason = $state();
|
|
18
|
+
/** @type {string | undefined} */
|
|
19
|
+
let failed = $state();
|
|
13
20
|
|
|
14
21
|
$effect(() => {
|
|
15
22
|
if (given || id === undefined) return;
|
|
16
23
|
if (!brua) throw new Error("BruaPicture with an id needs setBrua() in a parent, or a brua prop.");
|
|
17
24
|
let live = true;
|
|
18
25
|
fetched = brua.cachedAsset(id);
|
|
19
|
-
|
|
26
|
+
reason = undefined;
|
|
27
|
+
assetOrError(brua, id).then((read) => { if (live) { fetched = read.asset; reason = read.error; } });
|
|
20
28
|
return () => { live = false; };
|
|
21
29
|
});
|
|
22
30
|
|
|
23
31
|
let asset = $derived(given ?? fetched);
|
|
24
|
-
let model = $derived(asset ? pictureModel(asset, { sizes, loading, className: imgClass, colorScheme }) : undefined);
|
|
32
|
+
let model = $derived(asset ? pictureModel(asset, { sizes, loading, className: imgClass, colorScheme, objectFit, objectPosition }) : undefined);
|
|
33
|
+
// The img's own error event: the file failed. Another file (a merge, another size) tries again.
|
|
34
|
+
let failure = $derived(model && failed === model.img.src ? { reason: "file", url: model.img.src } : !asset ? reason : undefined);
|
|
25
35
|
</script>
|
|
26
36
|
|
|
27
|
-
{#if
|
|
37
|
+
{#if failure}
|
|
38
|
+
{#if error}{@render error(failure)}{:else if children}{@render children()}{/if}
|
|
39
|
+
{:else if model}
|
|
28
40
|
<picture>
|
|
29
41
|
{#each model.sources as source (source.type + (source.media ?? ""))}
|
|
30
42
|
<source type={source.type} media={source.media} srcset={source.srcset} sizes={source.sizes} />
|
|
@@ -40,6 +52,8 @@
|
|
|
40
52
|
loading={model.img.loading}
|
|
41
53
|
decoding={model.img.decoding}
|
|
42
54
|
class={model.img.className}
|
|
55
|
+
style={fillStyle(model.img)}
|
|
56
|
+
onerror={() => (failed = model.img.src)}
|
|
43
57
|
/>
|
|
44
58
|
</picture>
|
|
45
59
|
{:else if children}
|
package/dist/brua.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssetResponse } from "./types.ts";
|
|
1
|
+
import type { AssetResponse, BruaImageError } from "./types.ts";
|
|
2
2
|
export interface BruaOptions {
|
|
3
3
|
/** The organization's asset prefix on the edge, such as https://cdn.brua.dev/assets/{org}. */
|
|
4
4
|
base: string;
|
|
@@ -28,3 +28,11 @@ export interface Brua {
|
|
|
28
28
|
prefetch(ids: string[]): Promise<void>;
|
|
29
29
|
}
|
|
30
30
|
export declare function createBrua(options: BruaOptions): Brua;
|
|
31
|
+
/**
|
|
32
|
+
* The asset with this id, or why there is none: not released, or the edge can't be reached with no release known. The
|
|
33
|
+
* framework components show their error view from it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function assetOrError(brua: Brua, id: string): Promise<{
|
|
36
|
+
asset?: AssetResponse;
|
|
37
|
+
error?: BruaImageError;
|
|
38
|
+
}>;
|
package/dist/brua.js
CHANGED
|
@@ -58,3 +58,16 @@ export function createBrua(options) {
|
|
|
58
58
|
};
|
|
59
59
|
return brua;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The asset with this id, or why there is none: not released, or the edge can't be reached with no release known. The
|
|
63
|
+
* framework components show their error view from it.
|
|
64
|
+
*/
|
|
65
|
+
export async function assetOrError(brua, id) {
|
|
66
|
+
try {
|
|
67
|
+
const asset = await brua.asset(id);
|
|
68
|
+
return asset ? { asset } : { error: { reason: "notReleased" } };
|
|
69
|
+
}
|
|
70
|
+
catch (cause) {
|
|
71
|
+
return { error: { reason: "unreachable", cause } };
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export type { AssetResponse, AssetVariant, ClientState } from "./types.ts";
|
|
1
|
+
export type { AssetResponse, AssetVariant, BruaImageError, ClientState } from "./types.ts";
|
|
2
2
|
export { selectUrl, selectVariant } from "./select.ts";
|
|
3
|
-
export { pictureHtml, pictureModel, type PictureOptions, type PictureModel, type PictureSource, type PictureImage } from "./picture.ts";
|
|
3
|
+
export { fillStyle, pictureHtml, pictureModel, type PictureOptions, type PictureModel, type PictureSource, type PictureImage } from "./picture.ts";
|
|
4
|
+
export { objectFitOf, objectPositionOf, targetWidth, type ObjectFit } from "./sizing.ts";
|
|
4
5
|
export { browserClientState, defaultClientState, watchClientState, type ClientOptions } from "./client.ts";
|
|
5
|
-
export { createBrua, type Brua, type BruaOptions } from "./brua.ts";
|
|
6
|
+
export { assetOrError, createBrua, type Brua, type BruaOptions } from "./brua.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { selectUrl, selectVariant } from "./select.js";
|
|
2
|
-
export { pictureHtml, pictureModel } from "./picture.js";
|
|
2
|
+
export { fillStyle, pictureHtml, pictureModel } from "./picture.js";
|
|
3
|
+
export { objectFitOf, objectPositionOf, targetWidth } from "./sizing.js";
|
|
3
4
|
export { browserClientState, defaultClientState, watchClientState } from "./client.js";
|
|
4
|
-
export { createBrua } from "./brua.js";
|
|
5
|
+
export { assetOrError, createBrua } from "./brua.js";
|
package/dist/picture.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ export interface PictureOptions {
|
|
|
9
9
|
* the dark files go behind a media query and the browser follows the system setting.
|
|
10
10
|
*/
|
|
11
11
|
colorScheme?: "light" | "dark";
|
|
12
|
+
/**
|
|
13
|
+
* How the img fills its box: "contain" or "cover" overrides the asset's own fit, and "none" leaves the fit and
|
|
14
|
+
* the position to the page's CSS. Left out, the asset's fit is used.
|
|
15
|
+
*/
|
|
16
|
+
objectFit?: "contain" | "cover" | "none";
|
|
17
|
+
/** Where the img sits in its box, as "x% y%"; left out, the asset's position. */
|
|
18
|
+
objectPosition?: string;
|
|
12
19
|
}
|
|
13
20
|
/** A `<source>` of the picture: one format, optionally one color scheme. */
|
|
14
21
|
export interface PictureSource {
|
|
@@ -29,6 +36,9 @@ export interface PictureImage {
|
|
|
29
36
|
loading: "lazy" | "eager";
|
|
30
37
|
decoding: "async";
|
|
31
38
|
className?: string;
|
|
39
|
+
/** The object-fit and object-position the img is drawn with, when the asset or the call site gives a fill. */
|
|
40
|
+
objectFit?: "contain" | "cover";
|
|
41
|
+
objectPosition?: string;
|
|
32
42
|
}
|
|
33
43
|
/** What a picture element for the asset consists of; the HTML renderer and the framework components build from it. */
|
|
34
44
|
export interface PictureModel {
|
|
@@ -41,5 +51,7 @@ export interface PictureModel {
|
|
|
41
51
|
* without client hints and switches live with the color scheme. Animations aren't rendered; their stills are.
|
|
42
52
|
*/
|
|
43
53
|
export declare function pictureModel(asset: AssetResponse, options?: PictureOptions): PictureModel;
|
|
54
|
+
/** The img's fill as a style attribute's value. */
|
|
55
|
+
export declare function fillStyle(img: PictureImage): string | undefined;
|
|
44
56
|
/** The picture as an HTML string, for innerHTML or server-rendered pages. */
|
|
45
57
|
export declare function pictureHtml(asset: AssetResponse, options?: PictureOptions): string;
|
package/dist/picture.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { objectFitOf, objectPositionOf } from "./sizing.js";
|
|
1
2
|
const SVG = "image/svg+xml";
|
|
2
3
|
/** Formats offered as <source> elements, best first; anything else becomes the <img> fallback. */
|
|
3
4
|
const SOURCE_TYPES = ["image/avif", "image/webp"];
|
|
@@ -51,9 +52,25 @@ export function pictureModel(asset, options = {}) {
|
|
|
51
52
|
loading: options.loading ?? "lazy",
|
|
52
53
|
decoding: "async",
|
|
53
54
|
className: options.className,
|
|
55
|
+
...fill(asset, options),
|
|
54
56
|
},
|
|
55
57
|
};
|
|
56
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The fill to put on the img: the call site's or the asset's, cleaned to what CSS takes. A response from before the
|
|
61
|
+
* fill, with no override either, gets none, so the page draws it as it always has.
|
|
62
|
+
*/
|
|
63
|
+
function fill(asset, options) {
|
|
64
|
+
if (options.objectFit === "none")
|
|
65
|
+
return {};
|
|
66
|
+
if (asset.objectFit === undefined && asset.objectPosition === undefined && options.objectFit === undefined && options.objectPosition === undefined)
|
|
67
|
+
return {};
|
|
68
|
+
return { objectFit: objectFitOf(asset, options.objectFit), objectPosition: objectPositionOf(asset, options.objectPosition) };
|
|
69
|
+
}
|
|
70
|
+
/** The img's fill as a style attribute's value. */
|
|
71
|
+
export function fillStyle(img) {
|
|
72
|
+
return img.objectFit === undefined ? undefined : `object-fit: ${img.objectFit}; object-position: ${img.objectPosition}`;
|
|
73
|
+
}
|
|
57
74
|
/** The picture as an HTML string, for innerHTML or server-rendered pages. */
|
|
58
75
|
export function pictureHtml(asset, options = {}) {
|
|
59
76
|
const { sources, img } = pictureModel(asset, options);
|
|
@@ -69,6 +86,7 @@ export function pictureHtml(asset, options = {}) {
|
|
|
69
86
|
loading: img.loading,
|
|
70
87
|
decoding: img.decoding,
|
|
71
88
|
class: img.className,
|
|
89
|
+
style: fillStyle(img),
|
|
72
90
|
}));
|
|
73
91
|
return `<picture>${rendered.join("")}</picture>`;
|
|
74
92
|
}
|
package/dist/react.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type ReactElement, type ReactNode } from "react";
|
|
|
2
2
|
import { type PictureOptions } from "./picture.ts";
|
|
3
3
|
import { type ClientOptions } from "./client.ts";
|
|
4
4
|
import { type Brua, type BruaOptions } from "./brua.ts";
|
|
5
|
-
import type { AssetResponse } from "./types.ts";
|
|
5
|
+
import type { AssetResponse, BruaImageError } from "./types.ts";
|
|
6
6
|
export type BruaProviderProps = {
|
|
7
7
|
children?: ReactNode;
|
|
8
8
|
} & ({
|
|
@@ -15,6 +15,14 @@ export declare function BruaProvider(props: BruaProviderProps): ReactElement;
|
|
|
15
15
|
* undefined until it has loaded, and when nothing is released under the id. Loads only in the browser; a server render shows the fallback.
|
|
16
16
|
*/
|
|
17
17
|
export declare function useBruaAsset(id: string | undefined, brua?: Brua): AssetResponse | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* The asset's current release for an id, as useBruaAsset gives it, or the error when there is none: not released, or
|
|
20
|
+
* the edge can't be reached with no release known. Both undefined while it loads.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useBruaAssetState(id: string | undefined, brua?: Brua): {
|
|
23
|
+
asset?: AssetResponse;
|
|
24
|
+
error?: BruaImageError;
|
|
25
|
+
};
|
|
18
26
|
export interface BruaPictureProps extends PictureOptions {
|
|
19
27
|
/** The asset as your backend embedded it: a snapshot of one release. */
|
|
20
28
|
asset?: AssetResponse;
|
|
@@ -22,14 +30,16 @@ export interface BruaPictureProps extends PictureOptions {
|
|
|
22
30
|
id?: string;
|
|
23
31
|
/** The Brua to read the id through, instead of the provider's. */
|
|
24
32
|
brua?: Brua;
|
|
25
|
-
/** What shows until the id has loaded, and when
|
|
33
|
+
/** What shows until the id has loaded, and on failure when no error view is given. */
|
|
26
34
|
fallback?: ReactNode;
|
|
35
|
+
/** What shows when there is no picture, with the reason: nothing released, the edge unreachable, or the file failed. */
|
|
36
|
+
error?: ReactNode | ((error: BruaImageError) => ReactNode);
|
|
27
37
|
}
|
|
28
38
|
/**
|
|
29
39
|
* The asset as a picture element (SPEC.md 7.2): the browser picks the format, density and color scheme,
|
|
30
40
|
* and follows dark mode live. Animations show their still.
|
|
31
41
|
*/
|
|
32
|
-
export declare function BruaPicture({ asset, id, brua, fallback, sizes, loading, className, colorScheme }: BruaPictureProps): ReactElement | null;
|
|
42
|
+
export declare function BruaPicture({ asset, id, brua, fallback, error, sizes, loading, className, colorScheme, objectFit, objectPosition }: BruaPictureProps): ReactElement | null;
|
|
33
43
|
/**
|
|
34
44
|
* One variant URL for the asset, chosen for this browser and kept current as its appearance, motion setting
|
|
35
45
|
* or pixel density changes: for a CSS background, a canvas or an animation player. Renders light at 1x on
|
package/dist/react.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createContext, createElement, useContext, useEffect, useMemo, useState
|
|
|
3
3
|
import { pictureModel } from "./picture.js";
|
|
4
4
|
import { selectUrl } from "./select.js";
|
|
5
5
|
import { defaultClientState, watchClientState } from "./client.js";
|
|
6
|
-
import { createBrua } from "./brua.js";
|
|
6
|
+
import { assetOrError, createBrua } from "./brua.js";
|
|
7
7
|
const BruaContext = createContext(undefined);
|
|
8
8
|
/** Where the app's assets are read from by id: wrap the app once, then <BruaPicture id="…"> anywhere below. */
|
|
9
9
|
export function BruaProvider(props) {
|
|
@@ -17,33 +17,45 @@ export function BruaProvider(props) {
|
|
|
17
17
|
* undefined until it has loaded, and when nothing is released under the id. Loads only in the browser; a server render shows the fallback.
|
|
18
18
|
*/
|
|
19
19
|
export function useBruaAsset(id, brua) {
|
|
20
|
+
return useBruaAssetState(id, brua).asset;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The asset's current release for an id, as useBruaAsset gives it, or the error when there is none: not released, or
|
|
24
|
+
* the edge can't be reached with no release known. Both undefined while it loads.
|
|
25
|
+
*/
|
|
26
|
+
export function useBruaAssetState(id, brua) {
|
|
20
27
|
const fromContext = useContext(BruaContext);
|
|
21
28
|
const client = brua ?? fromContext;
|
|
22
29
|
// The last release the Brua read renders at once, so a remounted picture doesn't flash its fallback.
|
|
23
|
-
const [
|
|
30
|
+
const [state, setState] = useState(() => ({ asset: id === undefined ? undefined : client?.cachedAsset(id) }));
|
|
24
31
|
if (id !== undefined && !client)
|
|
25
32
|
throw new Error("useBruaAsset needs a <BruaProvider> above it, or a Brua passed in.");
|
|
26
33
|
useEffect(() => {
|
|
27
34
|
if (id === undefined || !client)
|
|
28
35
|
return;
|
|
29
36
|
let live = true;
|
|
30
|
-
|
|
31
|
-
client
|
|
32
|
-
|
|
37
|
+
setState({ asset: client.cachedAsset(id) });
|
|
38
|
+
assetOrError(client, id).then((read) => { if (live)
|
|
39
|
+
setState(read); });
|
|
33
40
|
return () => { live = false; };
|
|
34
41
|
}, [client, id]);
|
|
35
|
-
return id === undefined ?
|
|
42
|
+
return id === undefined ? {} : state;
|
|
36
43
|
}
|
|
37
44
|
/**
|
|
38
45
|
* The asset as a picture element (SPEC.md 7.2): the browser picks the format, density and color scheme,
|
|
39
46
|
* and follows dark mode live. Animations show their still.
|
|
40
47
|
*/
|
|
41
|
-
export function BruaPicture({ asset, id, brua, fallback = null, sizes, loading, className, colorScheme }) {
|
|
42
|
-
const fetched =
|
|
43
|
-
const
|
|
48
|
+
export function BruaPicture({ asset, id, brua, fallback = null, error, sizes, loading, className, colorScheme, objectFit, objectPosition }) {
|
|
49
|
+
const fetched = useBruaAssetState(asset ? undefined : id, brua);
|
|
50
|
+
const [failed, setFailed] = useState();
|
|
51
|
+
const shown = asset ?? fetched.asset;
|
|
52
|
+
const failure = (reason) => (error === undefined ? fallback : typeof error === "function" ? error(reason) : error);
|
|
44
53
|
if (!shown)
|
|
45
|
-
return fallback;
|
|
46
|
-
const { sources, img } = pictureModel(shown, { sizes, loading, className, colorScheme });
|
|
54
|
+
return fetched.error ? failure(fetched.error) : fallback;
|
|
55
|
+
const { sources, img } = pictureModel(shown, { sizes, loading, className, colorScheme, objectFit, objectPosition });
|
|
56
|
+
// The img's own error event: the file failed. Another file (a merge, another size) tries again.
|
|
57
|
+
if (failed === img.src)
|
|
58
|
+
return failure({ reason: "file", url: img.src });
|
|
47
59
|
return createElement("picture", null, ...sources.map((s) => createElement("source", { key: `${s.type}${s.media ?? ""}`, type: s.type, media: s.media, srcSet: s.srcset, sizes: s.sizes })), createElement("img", {
|
|
48
60
|
src: img.src,
|
|
49
61
|
srcSet: img.srcset,
|
|
@@ -55,6 +67,8 @@ export function BruaPicture({ asset, id, brua, fallback = null, sizes, loading,
|
|
|
55
67
|
loading: img.loading,
|
|
56
68
|
decoding: img.decoding,
|
|
57
69
|
className: img.className,
|
|
70
|
+
style: img.objectFit === undefined ? undefined : { objectFit: img.objectFit, objectPosition: img.objectPosition },
|
|
71
|
+
onError: () => setFailed(img.src),
|
|
58
72
|
}));
|
|
59
73
|
}
|
|
60
74
|
/**
|
package/dist/sizing.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AssetResponse } from "./types.ts";
|
|
2
|
+
export type ObjectFit = "contain" | "cover";
|
|
3
|
+
/** The fit to draw with: the call site's override, else the asset's, else contain. Unknown values are contain. */
|
|
4
|
+
export declare function objectFitOf(asset: Pick<AssetResponse, "objectFit">, override?: string): ObjectFit;
|
|
5
|
+
/** Where the picture sits in its box, as "x% y%": the call site's, else the asset's, else the centre. */
|
|
6
|
+
export declare function objectPositionOf(asset: Pick<AssetResponse, "objectPosition">, override?: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* How wide the picture is drawn in a box of this size, in CSS pixels or points, for the selection rule's
|
|
9
|
+
* `targetWidth`. Contain, or a box without a height yet, draws it at the box width; cover at the larger of the box
|
|
10
|
+
* width and the box height times the asset's proportions.
|
|
11
|
+
*/
|
|
12
|
+
export declare function targetWidth(asset: Pick<AssetResponse, "width" | "height" | "objectFit">, boxWidth: number, boxHeight?: number, objectFit?: string): number;
|
package/dist/sizing.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const POSITION = /^(100|[1-9]?[0-9])% (100|[1-9]?[0-9])%$/;
|
|
2
|
+
/** The fit to draw with: the call site's override, else the asset's, else contain. Unknown values are contain. */
|
|
3
|
+
export function objectFitOf(asset, override) {
|
|
4
|
+
const fit = override ?? asset.objectFit;
|
|
5
|
+
return fit === "cover" ? "cover" : "contain";
|
|
6
|
+
}
|
|
7
|
+
/** Where the picture sits in its box, as "x% y%": the call site's, else the asset's, else the centre. */
|
|
8
|
+
export function objectPositionOf(asset, override) {
|
|
9
|
+
const position = override ?? asset.objectPosition;
|
|
10
|
+
return position !== undefined && POSITION.test(position) ? position : "50% 50%";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* How wide the picture is drawn in a box of this size, in CSS pixels or points, for the selection rule's
|
|
14
|
+
* `targetWidth`. Contain, or a box without a height yet, draws it at the box width; cover at the larger of the box
|
|
15
|
+
* width and the box height times the asset's proportions.
|
|
16
|
+
*/
|
|
17
|
+
export function targetWidth(asset, boxWidth, boxHeight, objectFit) {
|
|
18
|
+
if (objectFitOf(asset, objectFit) !== "cover" || boxHeight === undefined || boxHeight <= 0 || asset.height <= 0)
|
|
19
|
+
return boxWidth;
|
|
20
|
+
return Math.max(boxWidth, boxHeight * (asset.width / asset.height));
|
|
21
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -8,7 +8,24 @@ export interface AssetResponse {
|
|
|
8
8
|
alt: string;
|
|
9
9
|
lang?: string;
|
|
10
10
|
variants: AssetVariant[];
|
|
11
|
+
/** How a layout's box of another shape holds the asset, as CSS object-fit: "contain" or "cover". */
|
|
12
|
+
objectFit?: string;
|
|
13
|
+
/** Where it sits in the box, as CSS object-position: "50% 35%", the point a cover crop keeps in view. */
|
|
14
|
+
objectPosition?: string;
|
|
11
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Why a picture has none to show, with the same reasons on every platform's SDK: nothing is released under the id (the
|
|
18
|
+
* edge answered 404, usually draw nothing), the edge can't be reached and no release is known, or the file failed.
|
|
19
|
+
*/
|
|
20
|
+
export type BruaImageError = {
|
|
21
|
+
reason: "notReleased";
|
|
22
|
+
} | {
|
|
23
|
+
reason: "unreachable";
|
|
24
|
+
cause: unknown;
|
|
25
|
+
} | {
|
|
26
|
+
reason: "file";
|
|
27
|
+
url: string;
|
|
28
|
+
};
|
|
12
29
|
/** Value fields are plain strings, not unions, so new values from the API never break decoding (SPEC.md 14). */
|
|
13
30
|
export interface AssetVariant {
|
|
14
31
|
type: string;
|
package/dist/vue.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ComputedRef, type InjectionKey, type MaybeRef, type MaybeRefOrGetter, type PropType, type Ref } from "vue";
|
|
2
2
|
import { type ClientOptions } from "./client.ts";
|
|
3
3
|
import { type Brua, type BruaOptions } from "./brua.ts";
|
|
4
|
-
import type { AssetResponse } from "./types.ts";
|
|
4
|
+
import type { AssetResponse, BruaImageError } from "./types.ts";
|
|
5
5
|
export declare const bruaKey: InjectionKey<Brua>;
|
|
6
6
|
/** Where the app's assets are read from by id: call once in the root component, then <BruaPicture id="…"> anywhere below. */
|
|
7
7
|
export declare function provideBrua(options: Brua | BruaOptions): Brua;
|
|
@@ -10,10 +10,19 @@ export declare function provideBrua(options: Brua | BruaOptions): Brua;
|
|
|
10
10
|
* undefined until it has loaded, and when nothing is released under the id. A server render shows the fallback.
|
|
11
11
|
*/
|
|
12
12
|
export declare function useBruaAsset(id: MaybeRefOrGetter<string | undefined>, brua?: Brua): Ref<AssetResponse | undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* The asset's current release for an id, as useBruaAsset gives it, or the error when there is none: not released, or
|
|
15
|
+
* the edge can't be reached with no release known. Both undefined while it loads.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useBruaAssetState(id: MaybeRefOrGetter<string | undefined>, brua?: Brua): Ref<{
|
|
18
|
+
asset?: AssetResponse;
|
|
19
|
+
error?: BruaImageError;
|
|
20
|
+
}>;
|
|
13
21
|
/**
|
|
14
22
|
* The asset as a picture element (SPEC.md 7.2): the browser picks the format, density and color scheme,
|
|
15
23
|
* and follows dark mode live. Animations show their still. Attributes such as class go on the img.
|
|
16
|
-
* The default slot shows until an id has loaded,
|
|
24
|
+
* The default slot shows until an id has loaded; the error slot, given the reason as `error`, when there is no picture:
|
|
25
|
+
* nothing released, the edge unreachable, or the file failed. Without an error slot the default one shows then.
|
|
17
26
|
*/
|
|
18
27
|
export declare const BruaPicture: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
19
28
|
/** The asset as your backend embedded it: a snapshot of one release. */
|
|
@@ -45,6 +54,16 @@ export declare const BruaPicture: import("vue").DefineComponent<import("vue").Ex
|
|
|
45
54
|
type: PropType<"light" | "dark">;
|
|
46
55
|
required: false;
|
|
47
56
|
};
|
|
57
|
+
/** "contain" or "cover" to override the asset's fit, or "none" to leave the fit to the page's CSS. */
|
|
58
|
+
objectFit: {
|
|
59
|
+
type: PropType<"contain" | "cover" | "none">;
|
|
60
|
+
required: false;
|
|
61
|
+
};
|
|
62
|
+
/** Where the img sits in its box, as "x% y%", instead of the asset's position. */
|
|
63
|
+
objectPosition: {
|
|
64
|
+
type: StringConstructor;
|
|
65
|
+
required: false;
|
|
66
|
+
};
|
|
48
67
|
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
49
68
|
[key: string]: any;
|
|
50
69
|
}> | import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
@@ -79,6 +98,16 @@ export declare const BruaPicture: import("vue").DefineComponent<import("vue").Ex
|
|
|
79
98
|
type: PropType<"light" | "dark">;
|
|
80
99
|
required: false;
|
|
81
100
|
};
|
|
101
|
+
/** "contain" or "cover" to override the asset's fit, or "none" to leave the fit to the page's CSS. */
|
|
102
|
+
objectFit: {
|
|
103
|
+
type: PropType<"contain" | "cover" | "none">;
|
|
104
|
+
required: false;
|
|
105
|
+
};
|
|
106
|
+
/** Where the img sits in its box, as "x% y%", instead of the asset's position. */
|
|
107
|
+
objectPosition: {
|
|
108
|
+
type: StringConstructor;
|
|
109
|
+
required: false;
|
|
110
|
+
};
|
|
82
111
|
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
83
112
|
/**
|
|
84
113
|
* One variant URL for the asset, chosen for this browser and kept current as its appearance, motion setting
|
package/dist/vue.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Brua for Vue 3: provideBrua, <BruaPicture>, useBruaAsset and useBruaUrl. Import from "@meginhufr/brua-web/vue".
|
|
2
2
|
import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, toValue, unref, watchEffect, } from "vue";
|
|
3
|
-
import { pictureModel } from "./picture.js";
|
|
3
|
+
import { fillStyle, pictureModel } from "./picture.js";
|
|
4
4
|
import { selectUrl } from "./select.js";
|
|
5
5
|
import { defaultClientState, watchClientState } from "./client.js";
|
|
6
|
-
import { createBrua } from "./brua.js";
|
|
6
|
+
import { assetOrError, createBrua } from "./brua.js";
|
|
7
7
|
export const bruaKey = Symbol("brua");
|
|
8
8
|
/** Where the app's assets are read from by id: call once in the root component, then <BruaPicture id="…"> anywhere below. */
|
|
9
9
|
export function provideBrua(options) {
|
|
@@ -16,28 +16,37 @@ export function provideBrua(options) {
|
|
|
16
16
|
* undefined until it has loaded, and when nothing is released under the id. A server render shows the fallback.
|
|
17
17
|
*/
|
|
18
18
|
export function useBruaAsset(id, brua) {
|
|
19
|
+
const state = useBruaAssetState(id, brua);
|
|
20
|
+
return computed(() => state.value.asset);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The asset's current release for an id, as useBruaAsset gives it, or the error when there is none: not released, or
|
|
24
|
+
* the edge can't be reached with no release known. Both undefined while it loads.
|
|
25
|
+
*/
|
|
26
|
+
export function useBruaAssetState(id, brua) {
|
|
19
27
|
const client = brua ?? inject(bruaKey, undefined);
|
|
20
|
-
const
|
|
28
|
+
const state = ref({});
|
|
21
29
|
watchEffect((onCleanup) => {
|
|
22
30
|
const wanted = toValue(id);
|
|
23
31
|
if (wanted === undefined) {
|
|
24
|
-
|
|
32
|
+
state.value = {};
|
|
25
33
|
return;
|
|
26
34
|
}
|
|
27
35
|
if (!client)
|
|
28
36
|
throw new Error("useBruaAsset needs provideBrua() in a parent, or a Brua passed in.");
|
|
29
37
|
let live = true;
|
|
30
38
|
onCleanup(() => { live = false; });
|
|
31
|
-
|
|
32
|
-
client
|
|
33
|
-
|
|
39
|
+
state.value = { asset: client.cachedAsset(wanted) };
|
|
40
|
+
assetOrError(client, wanted).then((read) => { if (live)
|
|
41
|
+
state.value = read; });
|
|
34
42
|
});
|
|
35
|
-
return
|
|
43
|
+
return state;
|
|
36
44
|
}
|
|
37
45
|
/**
|
|
38
46
|
* The asset as a picture element (SPEC.md 7.2): the browser picks the format, density and color scheme,
|
|
39
47
|
* and follows dark mode live. Animations show their still. Attributes such as class go on the img.
|
|
40
|
-
* The default slot shows until an id has loaded,
|
|
48
|
+
* The default slot shows until an id has loaded; the error slot, given the reason as `error`, when there is no picture:
|
|
49
|
+
* nothing released, the edge unreachable, or the file failed. Without an error slot the default one shows then.
|
|
41
50
|
*/
|
|
42
51
|
export const BruaPicture = defineComponent({
|
|
43
52
|
name: "BruaPicture",
|
|
@@ -54,14 +63,25 @@ export const BruaPicture = defineComponent({
|
|
|
54
63
|
loading: { type: String, required: false },
|
|
55
64
|
/** "light" or "dark" to show that scheme's files only, for apps that switch themes themselves. */
|
|
56
65
|
colorScheme: { type: String, required: false },
|
|
66
|
+
/** "contain" or "cover" to override the asset's fit, or "none" to leave the fit to the page's CSS. */
|
|
67
|
+
objectFit: { type: String, required: false },
|
|
68
|
+
/** Where the img sits in its box, as "x% y%", instead of the asset's position. */
|
|
69
|
+
objectPosition: { type: String, required: false },
|
|
57
70
|
},
|
|
58
71
|
setup(props, { attrs, slots }) {
|
|
59
|
-
const fetched =
|
|
72
|
+
const fetched = useBruaAssetState(() => (props.asset ? undefined : props.id), props.brua);
|
|
73
|
+
const failed = ref();
|
|
74
|
+
const failure = (error) => (slots.error ?? slots.default)?.({ error }) ?? null;
|
|
60
75
|
return () => {
|
|
61
|
-
const asset = props.asset ?? fetched.value;
|
|
76
|
+
const asset = props.asset ?? fetched.value.asset;
|
|
62
77
|
if (!asset)
|
|
63
|
-
return slots.default?.() ?? null;
|
|
64
|
-
const { sources, img } = pictureModel(asset, {
|
|
78
|
+
return fetched.value.error ? failure(fetched.value.error) : slots.default?.() ?? null;
|
|
79
|
+
const { sources, img } = pictureModel(asset, {
|
|
80
|
+
sizes: props.sizes, loading: props.loading, colorScheme: props.colorScheme, objectFit: props.objectFit, objectPosition: props.objectPosition,
|
|
81
|
+
});
|
|
82
|
+
// The img's own error event: the file failed. Another file (a merge, another size) tries again.
|
|
83
|
+
if (failed.value === img.src)
|
|
84
|
+
return failure({ reason: "file", url: img.src });
|
|
65
85
|
return h("picture", [
|
|
66
86
|
...sources.map((s) => h("source", { key: `${s.type}${s.media ?? ""}`, type: s.type, media: s.media, srcset: s.srcset, sizes: s.sizes })),
|
|
67
87
|
h("img", {
|
|
@@ -75,6 +95,9 @@ export const BruaPicture = defineComponent({
|
|
|
75
95
|
lang: img.lang,
|
|
76
96
|
loading: img.loading,
|
|
77
97
|
decoding: img.decoding,
|
|
98
|
+
// The fill first, so a style the page gives the component still wins.
|
|
99
|
+
style: [fillStyle(img), attrs.style],
|
|
100
|
+
onError: () => { failed.value = img.src; },
|
|
78
101
|
}),
|
|
79
102
|
]);
|
|
80
103
|
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meginhufr/brua-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Brua for the web: AssetResponse types, the variant selection rule, <picture> rendering, and components for React, Vue and Svelte.",
|
|
5
5
|
"author": "Meginhufr AS <hello@brua.dev> (https://brua.dev)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://brua.dev",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/Meginhufr/brua-web.git"
|
|
10
|
+
"url": "git+https://github.com/Meginhufr/brua-web.git"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
13
13
|
"sideEffects": false,
|