@meginhufr/brua-web 0.2.0 → 0.3.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.
@@ -4,10 +4,10 @@
4
4
  setBrua(); the last release it read shows at once, else the children until it has loaded, and when nothing is released under the id. -->
5
5
  <script>
6
6
  import { getContext } from "svelte";
7
- import { pictureModel } from "./picture.ts";
7
+ import { fillStyle, pictureModel } from "./picture.ts";
8
8
 
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();
9
+ /** @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 }} */
10
+ let { asset: given, id, brua = getContext("brua"), sizes, loading, class: imgClass, colorScheme, objectFit, objectPosition, children } = $props();
11
11
  // The last release the Brua read renders at once, so a remounted picture doesn't flash its children.
12
12
  let fetched = $state(id === undefined ? undefined : brua?.cachedAsset(id));
13
13
 
@@ -21,7 +21,7 @@
21
21
  });
22
22
 
23
23
  let asset = $derived(given ?? fetched);
24
- let model = $derived(asset ? pictureModel(asset, { sizes, loading, className: imgClass, colorScheme }) : undefined);
24
+ let model = $derived(asset ? pictureModel(asset, { sizes, loading, className: imgClass, colorScheme, objectFit, objectPosition }) : undefined);
25
25
  </script>
26
26
 
27
27
  {#if model}
@@ -40,6 +40,7 @@
40
40
  loading={model.img.loading}
41
41
  decoding={model.img.decoding}
42
42
  class={model.img.className}
43
+ style={fillStyle(model.img)}
43
44
  />
44
45
  </picture>
45
46
  {:else if children}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type { AssetResponse, AssetVariant, 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
6
  export { 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
5
  export { 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
@@ -29,7 +29,7 @@ export interface BruaPictureProps extends PictureOptions {
29
29
  * The asset as a picture element (SPEC.md 7.2): the browser picks the format, density and color scheme,
30
30
  * and follows dark mode live. Animations show their still.
31
31
  */
32
- export declare function BruaPicture({ asset, id, brua, fallback, sizes, loading, className, colorScheme }: BruaPictureProps): ReactElement | null;
32
+ export declare function BruaPicture({ asset, id, brua, fallback, sizes, loading, className, colorScheme, objectFit, objectPosition }: BruaPictureProps): ReactElement | null;
33
33
  /**
34
34
  * One variant URL for the asset, chosen for this browser and kept current as its appearance, motion setting
35
35
  * or pixel density changes: for a CSS background, a canvas or an animation player. Renders light at 1x on
package/dist/react.js CHANGED
@@ -38,12 +38,12 @@ export function useBruaAsset(id, brua) {
38
38
  * The asset as a picture element (SPEC.md 7.2): the browser picks the format, density and color scheme,
39
39
  * and follows dark mode live. Animations show their still.
40
40
  */
41
- export function BruaPicture({ asset, id, brua, fallback = null, sizes, loading, className, colorScheme }) {
41
+ export function BruaPicture({ asset, id, brua, fallback = null, sizes, loading, className, colorScheme, objectFit, objectPosition }) {
42
42
  const fetched = useBruaAsset(asset ? undefined : id, brua);
43
43
  const shown = asset ?? fetched;
44
44
  if (!shown)
45
45
  return fallback;
46
- const { sources, img } = pictureModel(shown, { sizes, loading, className, colorScheme });
46
+ const { sources, img } = pictureModel(shown, { sizes, loading, className, colorScheme, objectFit, objectPosition });
47
47
  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
48
  src: img.src,
49
49
  srcSet: img.srcset,
@@ -55,6 +55,7 @@ export function BruaPicture({ asset, id, brua, fallback = null, sizes, loading,
55
55
  loading: img.loading,
56
56
  decoding: img.decoding,
57
57
  className: img.className,
58
+ style: img.objectFit === undefined ? undefined : { objectFit: img.objectFit, objectPosition: img.objectPosition },
58
59
  }));
59
60
  }
60
61
  /**
@@ -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,6 +8,10 @@ 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
  }
12
16
  /** Value fields are plain strings, not unions, so new values from the API never break decoding (SPEC.md 14). */
13
17
  export interface AssetVariant {
package/dist/vue.d.ts CHANGED
@@ -45,6 +45,16 @@ export declare const BruaPicture: import("vue").DefineComponent<import("vue").Ex
45
45
  type: PropType<"light" | "dark">;
46
46
  required: false;
47
47
  };
48
+ /** "contain" or "cover" to override the asset's fit, or "none" to leave the fit to the page's CSS. */
49
+ objectFit: {
50
+ type: PropType<"contain" | "cover" | "none">;
51
+ required: false;
52
+ };
53
+ /** Where the img sits in its box, as "x% y%", instead of the asset's position. */
54
+ objectPosition: {
55
+ type: StringConstructor;
56
+ required: false;
57
+ };
48
58
  }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
49
59
  [key: string]: any;
50
60
  }> | import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
@@ -79,6 +89,16 @@ export declare const BruaPicture: import("vue").DefineComponent<import("vue").Ex
79
89
  type: PropType<"light" | "dark">;
80
90
  required: false;
81
91
  };
92
+ /** "contain" or "cover" to override the asset's fit, or "none" to leave the fit to the page's CSS. */
93
+ objectFit: {
94
+ type: PropType<"contain" | "cover" | "none">;
95
+ required: false;
96
+ };
97
+ /** Where the img sits in its box, as "x% y%", instead of the asset's position. */
98
+ objectPosition: {
99
+ type: StringConstructor;
100
+ required: false;
101
+ };
82
102
  }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
83
103
  /**
84
104
  * One variant URL for the asset, chosen for this browser and kept current as its appearance, motion setting
package/dist/vue.js CHANGED
@@ -1,6 +1,6 @@
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
6
  import { createBrua } from "./brua.js";
@@ -54,6 +54,10 @@ export const BruaPicture = defineComponent({
54
54
  loading: { type: String, required: false },
55
55
  /** "light" or "dark" to show that scheme's files only, for apps that switch themes themselves. */
56
56
  colorScheme: { type: String, required: false },
57
+ /** "contain" or "cover" to override the asset's fit, or "none" to leave the fit to the page's CSS. */
58
+ objectFit: { type: String, required: false },
59
+ /** Where the img sits in its box, as "x% y%", instead of the asset's position. */
60
+ objectPosition: { type: String, required: false },
57
61
  },
58
62
  setup(props, { attrs, slots }) {
59
63
  const fetched = useBruaAsset(() => (props.asset ? undefined : props.id), props.brua);
@@ -61,7 +65,9 @@ export const BruaPicture = defineComponent({
61
65
  const asset = props.asset ?? fetched.value;
62
66
  if (!asset)
63
67
  return slots.default?.() ?? null;
64
- const { sources, img } = pictureModel(asset, { sizes: props.sizes, loading: props.loading, colorScheme: props.colorScheme });
68
+ const { sources, img } = pictureModel(asset, {
69
+ sizes: props.sizes, loading: props.loading, colorScheme: props.colorScheme, objectFit: props.objectFit, objectPosition: props.objectPosition,
70
+ });
65
71
  return h("picture", [
66
72
  ...sources.map((s) => h("source", { key: `${s.type}${s.media ?? ""}`, type: s.type, media: s.media, srcset: s.srcset, sizes: s.sizes })),
67
73
  h("img", {
@@ -75,6 +81,8 @@ export const BruaPicture = defineComponent({
75
81
  lang: img.lang,
76
82
  loading: img.loading,
77
83
  decoding: img.decoding,
84
+ // The fill first, so a style the page gives the component still wins.
85
+ style: [fillStyle(img), attrs.style],
78
86
  }),
79
87
  ]);
80
88
  };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@meginhufr/brua-web",
3
- "version": "0.2.0",
3
+ "version": "0.3.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,