@ultimat3/ui 22.2.2 → 22.3.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/CATALOG.md CHANGED
@@ -478,17 +478,19 @@ A button whose only content is an icon, so `label` is mandatory: it becomes the
478
478
 
479
479
  ### Image
480
480
 
481
- Zero-CLS image primitive: a plain <img>, no JS, no fetch, no client state. The build-time half of the pipeline in `docs/idea/07-rendering-seo.md` — reading real dimensions, encoding AVIF/WebP renditions, the data-URI blur placeholder — is NOT here. This component emits exactly what it is handed and fabricates nothing: no variants it was not given, no dimensions it did not measure.
481
+ Zero-CLS image primitive: a plain <img>, or a <picture> around one when AVIF/WebP renditions are handed in. No JS, no fetch, no client state. The encoding half of the pipeline — AVIF/WebP renditions, measured dimensions — is the app's build step; the URLs come from `asset('assets/…')` in the page. This component emits exactly what it is handed and fabricates nothing, except the one thing it insists on: a reserved box.
482
482
 
483
483
  | Prop | Type | Required | Notes |
484
484
  |---|---|---|---|
485
- | `src` | `string` | yes | |
485
+ | `src` | `string` | yes | The fallback every browser can decode — usually the JPEG/PNG rendition. |
486
486
  | `alt` | `string` | yes | Required, with no default: an <Image> whose meaning is undescribed is a type error at the call site. Pass `alt=""` for a decorative image, deliberately. |
487
- | `variants` | `readonly ImageVariant[]` | — | Renditions to offer; the descriptors and their order are derived, not written. |
488
- | `sizes` | `string` | — | Layout width of the box, e.g. `(max-width: 700px) 100vw, 620px`. |
489
- | `priority` | `boolean` | — | The LCP image, at most one per route: eager, high fetch priority. |
490
- | `width` | `number` | — | Intrinsic dimensions, from the build step that measured them. Both or neither. |
487
+ | `variants` | `readonly ImageVariant[]` | — | Renditions of `src`'s own encoding; the descriptors and their order are derived. |
488
+ | `sources` | `ImageSources` | — | AVIF and WebP renditions, each a width list: `{ avif: [{ src: asset('…'), width: 640 }] }`. |
489
+ | `sizes` | `string` | — | Layout width of the box, e.g. `(max-width: 700px) 100vw, 620px`. Applies to every set. |
490
+ | `priority` | `boolean` | — | The LCP image, at most one per route: eager, `fetchpriority="high"`, never lazy. |
491
+ | `width` | `number` | — | Intrinsic size in CSS pixels, from the step that encoded it. Both, or `aspectRatio`. |
491
492
  | `height` | `number` | — | |
493
+ | `aspectRatio` | `ImageAspectRatio` | — | `'16 / 9'` — the reservation when the intrinsic size is not known. Never beside width/height. |
492
494
  | `class` | `string` | — | |
493
495
 
494
496
  ### InfiniteScroll
@@ -565,10 +567,12 @@ Locale picker. Option labels come from `Intl.DisplayNames` in each locale's own
565
567
 
566
568
  | Prop | Type | Required | Notes |
567
569
  |---|---|---|---|
568
- | `locales` | `readonly Locale[]` | yes | |
570
+ | `locales` | `readonly Locale[]` | yes | Every locale to offer, the DEFAULT first — `routedLocales()` from `@ultimat3/i18n`. |
569
571
  | `value` | `Locale` | — | Defaults to the context locale. |
570
572
  | `onLocaleChange` | `((locale: Locale) => void)` | — | |
571
- | `hrefFor` | `((locale: Locale) => string)` | — | Render as links instead of a select, for a 0kb-JS `site/` route. |
573
+ | `hrefFor` | `((locale: Locale) => string)` | — | Render as links instead of a select, for a 0kb-JS `site/` route. Defaults, when `path` is given, to that page in each locale — `localizedPath`: unprefixed for the default, `/en/…` otherwise — so a site's switcher needs no href of its own. |
574
+ | `path` | `string` | — | The page's own path, prefixed or not — `new URL(props.url).pathname`. Turns on links mode. |
575
+ | `defaultLocale` | `Locale` | — | The unprefixed locale. Defaults to `locales[0]`, which is where `routedLocales()` puts it. |
572
576
  | `class` | `string` | — | |
573
577
 
574
578
  ### Menu
@@ -879,7 +883,7 @@ Theme control. `toggle` flips light/dark; `select` also offers "system", which c
879
883
  | Prop | Type | Required | Notes |
880
884
  |---|---|---|---|
881
885
  | `mode` | `'toggle' \| 'select'` | — | |
882
- | `initial` | `Theme` | — | Server-render value; the effect corrects it on the client before paint. |
886
+ | `initial` | `Theme` | — | The theme in force at server render — the toggle's glyph. Never the select's choice: nothing on the server knows whether the visitor chose it, so the select shows "system" until the effect reads storage. |
883
887
  | `env` | `ThemeEnv` | — | Injectable for tests and for non-DOM hosts. |
884
888
  | `class` | `string` | — | |
885
889
 
package/README.md CHANGED
@@ -401,21 +401,23 @@ transparent inside a heading or a caption.
401
401
  | `weight` | keys of `fontWeightTokens`: `normal` `medium` `semibold` `bold` |
402
402
  | `as` | `span` (default) `p` `div` `strong` `em` |
403
403
 
404
- `<Image>` is one `<img>` — no JS, no fetch, no client state. `alt` is a required
405
- prop, so a missing description is a type error rather than a review comment.
404
+ `<Image>` is one `<img>`, or a `<picture>` around one when `sources` is given — no
405
+ JS, no fetch, no client state. `alt` is a required prop, so a missing description
406
+ is a type error rather than a review comment.
406
407
 
407
408
  | Prop | Emitted |
408
409
  |---|---|
409
410
  | `variants` | `srcset`, descriptors derived and ordered ascending (`srcsetFor`) |
410
- | `sizes` | `sizes`, verbatim |
411
+ | `sources` | `{ avif?, webp? }` width lists → `<source type="image/avif">`, then `image/webp`, before the `<img>` (`sourceSetsFor`) |
412
+ | `sizes` | `sizes`, verbatim, on the `<img>` and every `<source>` |
411
413
  | `priority` | `loading="eager"` + `fetchpriority="high"`; otherwise `lazy` + `auto`, always `decoding="async"` |
412
- | `width` + `height` | inlined attributes — both or neither, so the ratio is always reservable |
414
+ | `width` + `height`, or `aspectRatio` | required — one of the two. Inlined attributes plus `--image-ratio` (`aspect-ratio`), so the box is always reserved (`reservedRatio`) |
413
415
 
414
- Shipped here: the element. Measuring intrinsic dimensions, encoding AVIF/WebP
415
- renditions and the data-URI blur placeholder are build-pipeline steps
416
- ([`docs/idea/07-rendering-seo.md`](../../docs/idea/07-rendering-seo.md)), not part
417
- of this package. The component emits what it is handed and fabricates nothing —
418
- no variants it was not given, no dimensions it did not measure.
416
+ Shipped here: the element. Measuring intrinsic dimensions and encoding AVIF/WebP
417
+ renditions are build-pipeline steps; the URLs come from `asset('assets/…')` in
418
+ `@ultimat3/render` ([Static Assets](../../wiki/Static-Assets.md)). The component
419
+ emits what it is handed and fabricates nothing — no variants it was not given, no
420
+ dimensions it did not measure.
419
421
 
420
422
  ## Keyboard groups
421
423
 
@@ -543,7 +545,7 @@ of truth for the one thing the server decides.
543
545
  | `X_UI_FORM_PATH_INVALID` | a form field or control name the path grammar cannot read (`items.0.price`, `items[]`, `__proto__`), or two control names describing different shapes for one path (`user` beside `user.name`) |
544
546
  | `X_UI_CONTRAST_INSUFFICIENT` | a `defineTheme()` palette whose resolved channels put a pairing in `CONTRAST_PAIRS` below WCAG 2.2 AA — 4.5:1 for text, 3:1 for the focus ring. Only pairings the brand changed are measured; the cause names the measured ratio and the required one |
545
547
  | `X_UI_QR_CAPACITY` | a `<QrCode>` value over version 3's 42-byte ceiling (byte mode, error-correction level M). The encoder draws versions 1–3 only; the cause names the byte count and the ceiling |
546
- | `X_UI_INVALID_VALUE` | `<Money>` given a float, `<DateTime>` given an unparseable instant, `<Image>` given mixed `w`/`x` descriptors or one dimension without the other, a heading level off 1–6, a `defineTheme()` value that is not a token value, an `<Icon>` glyph with a tag/attribute/colour outside `ICON_TAGS`, two `Accordion` items sharing an id, `InfiniteScroll` with `hasMore` and no `nextHref`, a negative `debounce` window, or (`As of 2026-08`) upstream icon data `bun run icons` refuses (not an object, no renderable nodes, an attribute value that is not glyph geometry) |
548
+ | `X_UI_INVALID_VALUE` | `<Money>` given a float, `<DateTime>` given an unparseable instant, `<Image>` given mixed `w`/`x` descriptors, one dimension without the other, or no reserved box at all, a heading level off 1–6, a `defineTheme()` value that is not a token value, an `<Icon>` glyph with a tag/attribute/colour outside `ICON_TAGS`, two `Accordion` items sharing an id, `InfiniteScroll` with `hasMore` and no `nextHref`, a negative `debounce` window, or (`As of 2026-08`) upstream icon data `bun run icons` refuses (not an object, no renderable nodes, an attribute value that is not glyph geometry) |
547
549
 
548
550
  ### Error classes
549
551
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/ui",
3
- "version": "22.2.2",
3
+ "version": "22.3.1",
4
4
  "description": "SolidJS design system: semantic design tokens, dark/RTL-ready SCSS modules, a11y primitives",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,10 +48,10 @@
48
48
  "icons": "bun run src/icons/build-icons.ts"
49
49
  },
50
50
  "dependencies": {
51
- "@ultimat3/core": "22.2.2",
52
- "@ultimat3/i18n": "22.2.2",
53
- "@ultimat3/money": "22.2.2",
54
- "@ultimat3/time": "22.2.2"
51
+ "@ultimat3/core": "22.3.1",
52
+ "@ultimat3/i18n": "22.3.1",
53
+ "@ultimat3/money": "22.3.1",
54
+ "@ultimat3/time": "22.3.1"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "solid-js": "^1.9.0"
@@ -17,3 +17,9 @@
17
17
  color: t.role('fg-muted');
18
18
  font-size: t.text(sm);
19
19
  }
20
+
21
+ // A <picture> only chooses a source; it must not become a box of its own, or the <img>'s
22
+ // reservation above would sit inside an inline wrapper with a line box of its own.
23
+ .picture {
24
+ display: contents;
25
+ }
@@ -1,9 +1,9 @@
1
- // Zero-CLS image primitive: a plain <img>, no JS, no fetch, no client state.
1
+ // Zero-CLS image primitive: a plain <img>, or a <picture> around one when AVIF/WebP renditions
2
+ // are handed in. No JS, no fetch, no client state.
2
3
  //
3
- // The build-time half of the pipeline in `docs/idea/07-rendering-seo.md` — reading
4
- // real dimensions, encoding AVIF/WebP renditions, the data-URI blur placeholder —
5
- // is NOT here. This component emits exactly what it is handed and fabricates
6
- // nothing: no variants it was not given, no dimensions it did not measure.
4
+ // The encoding half of the pipeline — AVIF/WebP renditions, measured dimensions — is the app's
5
+ // build step; the URLs come from `asset('assets/…')` in the page. This component emits exactly
6
+ // what it is handed and fabricates nothing, except the one thing it insists on: a reserved box.
7
7
 
8
8
  import type { JSX } from 'solid-js';
9
9
  import { cx } from '../cx';
@@ -11,44 +11,70 @@ import styles from './Image.module.scss';
11
11
  import {
12
12
  assertNonEmptySrc,
13
13
  boxFor,
14
+ type ImageAspectRatio,
14
15
  type ImageBox,
15
16
  type ImageLoadingHints,
17
+ type ImageSourceSet,
18
+ type ImageSources,
16
19
  type ImageVariant,
17
20
  loadingHints,
18
- ratioFor,
21
+ reservedRatio,
22
+ sourceSetsFor,
19
23
  srcsetFor,
20
24
  } from './image-source';
21
25
 
22
- export interface ImageProps {
26
+ /**
27
+ * The box is ALWAYS reserved: the intrinsic size, or an explicit ratio when the size is not known
28
+ * (a CSS-sized hero). Neither is a type error here and `X_UI_INVALID_VALUE` at render.
29
+ */
30
+ export type ImageDimensions =
31
+ | { width: number; height: number; aspectRatio?: undefined }
32
+ | { aspectRatio: ImageAspectRatio; width?: undefined; height?: undefined };
33
+
34
+ export interface ImageBaseProps {
35
+ /** The fallback every browser can decode — usually the JPEG/PNG rendition. */
23
36
  src: string;
24
37
  /**
25
38
  * Required, with no default: an <Image> whose meaning is undescribed is a type
26
39
  * error at the call site. Pass `alt=""` for a decorative image, deliberately.
27
40
  */
28
41
  alt: string;
29
- /** Renditions to offer; the descriptors and their order are derived, not written. */
42
+ /** Renditions of `src`'s own encoding; the descriptors and their order are derived. */
30
43
  variants?: readonly ImageVariant[] | undefined;
31
- /** Layout width of the box, e.g. `(max-width: 700px) 100vw, 620px`. */
44
+ /** AVIF and WebP renditions, each a width list: `{ avif: [{ src: asset('…'), width: 640 }] }`. */
45
+ sources?: ImageSources | undefined;
46
+ /** Layout width of the box, e.g. `(max-width: 700px) 100vw, 620px`. Applies to every set. */
32
47
  sizes?: string | undefined;
33
- /** The LCP image, at most one per route: eager, high fetch priority. */
48
+ /** The LCP image, at most one per route: eager, `fetchpriority="high"`, never lazy. */
34
49
  priority?: boolean | undefined;
35
- /** Intrinsic dimensions, from the build step that measured them. Both or neither. */
50
+ /** Intrinsic size in CSS pixels, from the step that encoded it. Both, or `aspectRatio`. */
36
51
  width?: number | undefined;
37
52
  height?: number | undefined;
53
+ /** `'16 / 9'` — the reservation when the intrinsic size is not known. Never beside width/height. */
54
+ aspectRatio?: ImageAspectRatio | undefined;
38
55
  class?: string | undefined;
39
56
  }
40
57
 
41
- export function Image(props: ImageProps): JSX.Element {
58
+ /** What a call site passes: the base props, with exactly one of the two box forms required. */
59
+ export type ImageProps = ImageBaseProps & ImageDimensions;
60
+
61
+ export function Image(props: ImageBaseProps & ImageDimensions): JSX.Element {
42
62
  const src = (): string => assertNonEmptySrc('Image', props.src, props.src);
43
63
  const hints = (): ImageLoadingHints => loadingHints(props.priority);
64
+ // Validated first: `reservedRatio` refuses a missing box before anything is emitted.
65
+ const ratio = (): string => reservedRatio(props.width, props.height, props.aspectRatio);
44
66
  const box = (): ImageBox | undefined => boxFor(props.width, props.height);
67
+ const sets = (): readonly ImageSourceSet[] => sourceSetsFor(props.sources);
68
+ // Once, eagerly: a compiled `style` binding may be read inside an effect, and a refusal thrown
69
+ // there reaches no caller. Here it fails the render that wrote the bad props.
70
+ ratio();
45
71
 
46
- return (
72
+ const img = (): JSX.Element => (
47
73
  <img
48
74
  class={cx(styles['image'], props.class)}
49
- // A custom property, because that is the one thing an inline `style` may carry here. Absent
50
- // when the dimensions are unknown, and the stylesheet's fallback is then `auto`.
51
- style={box() === undefined ? undefined : { '--image-ratio': ratioFor(box()) }}
75
+ // A custom property, because that is the one thing an inline `style` may carry here. The
76
+ // stylesheet turns it into `aspect-ratio`, the half of the reservation that survives styling.
77
+ style={{ '--image-ratio': ratio() }}
52
78
  src={src()}
53
79
  alt={props.alt}
54
80
  srcset={srcsetFor(props.variants)}
@@ -60,4 +86,14 @@ export function Image(props: ImageProps): JSX.Element {
60
86
  decoding={hints().decoding}
61
87
  />
62
88
  );
89
+
90
+ if (sets().length === 0) return img();
91
+ return (
92
+ <picture class={styles['picture']}>
93
+ {sets().map((set) => (
94
+ <source type={set.type} srcset={set.srcset} sizes={props.sizes} />
95
+ ))}
96
+ {img()}
97
+ </picture>
98
+ );
63
99
  }
@@ -1,6 +1,9 @@
1
1
  // Locale picker. Option labels come from `Intl.DisplayNames` in each locale's own
2
2
  // language (endonyms), so the list needs no translation catalog of its own.
3
3
 
4
+ // Core's, never the i18n barrel's: that barrel installs the framework catalog at import, and this
5
+ // component ships in browser chunks (issue #490). `localizePath` is what i18n's `localizedPath` is.
6
+ import { localizePath } from '@ultimat3/core';
4
7
  import type { Locale } from '@ultimat3/i18n';
5
8
  import type { JSX } from 'solid-js';
6
9
  import { cx } from '../cx';
@@ -10,12 +13,21 @@ import styles from './LocaleSwitcher.module.scss';
10
13
  import { Select, type SelectOption } from './Select';
11
14
 
12
15
  export interface LocaleSwitcherProps {
16
+ /** Every locale to offer, the DEFAULT first — `routedLocales()` from `@ultimat3/i18n`. */
13
17
  locales: readonly Locale[];
14
18
  /** Defaults to the context locale. */
15
19
  value?: Locale | undefined;
16
20
  onLocaleChange?: ((locale: Locale) => void) | undefined;
17
- /** Render as links instead of a select, for a 0kb-JS `site/` route. */
21
+ /**
22
+ * Render as links instead of a select, for a 0kb-JS `site/` route. Defaults, when `path` is
23
+ * given, to that page in each locale — `localizedPath`: unprefixed for the default, `/en/…`
24
+ * otherwise — so a site's switcher needs no href of its own.
25
+ */
18
26
  hrefFor?: ((locale: Locale) => string) | undefined;
27
+ /** The page's own path, prefixed or not — `new URL(props.url).pathname`. Turns on links mode. */
28
+ path?: string | undefined;
29
+ /** The unprefixed locale. Defaults to `locales[0]`, which is where `routedLocales()` puts it. */
30
+ defaultLocale?: Locale | undefined;
19
31
  class?: string | undefined;
20
32
  }
21
33
 
@@ -31,13 +43,22 @@ export function LocaleSwitcher(props: LocaleSwitcherProps): JSX.Element {
31
43
  const options = (): SelectOption[] =>
32
44
  props.locales.map((tag) => ({ value: tag, label: localeLabel(tag) }));
33
45
 
34
- if (props.hrefFor !== undefined) {
46
+ const hrefFor = (): ((locale: Locale) => string) | undefined => {
47
+ if (props.hrefFor !== undefined) return props.hrefFor;
48
+ const path = props.path;
49
+ if (path === undefined) return undefined;
50
+ const fallback = props.defaultLocale ?? props.locales[0] ?? current();
51
+ return (locale) => localizePath(path, locale, props.locales, fallback);
52
+ };
53
+
54
+ const linkFor = hrefFor();
55
+ if (linkFor !== undefined) {
35
56
  return (
36
57
  <nav class={cx(styles['links'], props.class)} aria-label={ui.t(UI_KEYS.language)}>
37
58
  {props.locales.map((tag) => (
38
59
  <a
39
60
  class={styles['link']}
40
- href={props.hrefFor?.(tag)}
61
+ href={linkFor(tag)}
41
62
  hreflang={tag}
42
63
  lang={tag}
43
64
  aria-current={tag === current() ? 'true' : undefined}
@@ -26,7 +26,11 @@ export type ThemeChoice = Theme | 'system';
26
26
 
27
27
  export interface ThemeToggleProps {
28
28
  mode?: 'toggle' | 'select' | undefined;
29
- /** Server-render value; the effect corrects it on the client before paint. */
29
+ /**
30
+ * The theme in force at server render — the toggle's glyph. Never the select's choice: nothing
31
+ * on the server knows whether the visitor chose it, so the select shows "system" until the
32
+ * effect reads storage.
33
+ */
30
34
  initial?: Theme | undefined;
31
35
  /** Injectable for tests and for non-DOM hosts. */
32
36
  env?: ThemeEnv | undefined;
@@ -36,7 +40,10 @@ export interface ThemeToggleProps {
36
40
  export function ThemeToggle(props: ThemeToggleProps): JSX.Element {
37
41
  const ui = useUi();
38
42
  const rt = solid();
39
- const [choice, setChoice] = rt.createSignal<ThemeChoice>(props.initial ?? 'system');
43
+ // "system" until the effect reads storage, whatever `initial` says: `initial` is the RESOLVED
44
+ // theme the boot stamped on <html>, and seeding the choice with it made the select claim the
45
+ // visitor had picked it — so picking "system" looked like a no-op on every first paint.
46
+ const [choice, setChoice] = rt.createSignal<ThemeChoice>('system');
40
47
  const [resolved, setResolved] = rt.createSignal<Theme>(props.initial ?? 'light');
41
48
 
42
49
  const env = (): ThemeEnv => props.env ?? browserThemeEnv();
@@ -180,3 +180,68 @@ function assertPixels(value: number): void {
180
180
  throw invalidValueError('Image', value, 'a positive whole number of CSS pixels');
181
181
  }
182
182
  }
183
+
184
+ /** `16 / 9`, as CSS writes it — the reservation when the intrinsic size is not known. */
185
+ export type ImageAspectRatio = `${number} / ${number}`;
186
+
187
+ const RATIO = /^\s*(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)\s*$/;
188
+
189
+ /**
190
+ * The box an image reserves, which it ALWAYS reserves: both intrinsic dimensions, or an explicit
191
+ * `aspectRatio` — never neither, which is a layout shift waiting for the decode, and never both,
192
+ * which is two answers to one question. The returned ratio is what `--image-ratio` carries.
193
+ */
194
+ export function reservedRatio(
195
+ width: number | undefined,
196
+ height: number | undefined,
197
+ aspectRatio: string | undefined,
198
+ ): string {
199
+ if (aspectRatio !== undefined) {
200
+ if (width !== undefined || height !== undefined) {
201
+ throw invalidValueError(
202
+ 'Image',
203
+ { width, height, aspectRatio },
204
+ 'width and height, or aspectRatio — not both, which would be two ratios for one box',
205
+ );
206
+ }
207
+ const match = RATIO.exec(aspectRatio);
208
+ if (match === null || Number(match[1]) <= 0 || Number(match[2]) <= 0) {
209
+ throw invalidValueError('Image', aspectRatio, 'an aspectRatio such as "16 / 9"');
210
+ }
211
+ return `${match[1]} / ${match[2]}`;
212
+ }
213
+ const box = boxFor(width, height);
214
+ if (box === undefined) {
215
+ throw invalidValueError(
216
+ 'Image',
217
+ { width, height },
218
+ 'width and height (the intrinsic size) or an aspectRatio — an image with neither reserves no box and shifts the layout when it decodes',
219
+ );
220
+ }
221
+ return ratioFor(box) as string;
222
+ }
223
+
224
+ /** Modern encodings, offered in this order ahead of the `<img>`'s own `src`. */
225
+ export interface ImageSources {
226
+ readonly avif?: readonly ImageVariant[] | undefined;
227
+ readonly webp?: readonly ImageVariant[] | undefined;
228
+ }
229
+
230
+ export interface ImageSourceSet {
231
+ readonly type: 'image/avif' | 'image/webp';
232
+ readonly srcset: string;
233
+ }
234
+
235
+ /**
236
+ * One `<source>` per encoding handed in, AVIF first: the browser takes the FIRST type it can
237
+ * decode, so the smaller encoding has to be asked about before the larger one.
238
+ */
239
+ export function sourceSetsFor(sources: ImageSources | undefined): readonly ImageSourceSet[] {
240
+ if (sources === undefined) return [];
241
+ const sets: ImageSourceSet[] = [];
242
+ const avif = srcsetFor(sources.avif);
243
+ if (avif !== undefined) sets.push({ type: 'image/avif', srcset: avif });
244
+ const webp = srcsetFor(sources.webp);
245
+ if (webp !== undefined) sets.push({ type: 'image/webp', srcset: webp });
246
+ return sets;
247
+ }
package/src/index.ts CHANGED
@@ -140,7 +140,7 @@ export type { IconProps } from './components/Icon';
140
140
  export { Icon } from './components/Icon';
141
141
  export type { IconButtonProps } from './components/IconButton';
142
142
  export { IconButton } from './components/IconButton';
143
- export type { ImageProps } from './components/Image';
143
+ export type { ImageBaseProps, ImageDimensions, ImageProps } from './components/Image';
144
144
  export { Image } from './components/Image';
145
145
  export type { InfiniteScrollProps } from './components/InfiniteScroll';
146
146
  export { InfiniteScroll } from './components/InfiniteScroll';
@@ -148,8 +148,15 @@ export type { InputProps, InputType } from './components/Input';
148
148
  export { Input } from './components/Input';
149
149
  export type { IconElement, IconGlyph, IconTag } from './components/icon-glyph';
150
150
  export { ICON_TAGS, iconElements } from './components/icon-glyph';
151
- export type { ImageBox, ImageLoadingHints, ImageVariant } from './components/image-source';
152
- export { boxFor, srcsetFor } from './components/image-source';
151
+ export type {
152
+ ImageAspectRatio,
153
+ ImageBox,
154
+ ImageLoadingHints,
155
+ ImageSourceSet,
156
+ ImageSources,
157
+ ImageVariant,
158
+ } from './components/image-source';
159
+ export { boxFor, reservedRatio, sourceSetsFor, srcsetFor } from './components/image-source';
153
160
  export type { LoadMoreInput, LoadMoreState } from './components/infinite-scroll-view';
154
161
  export type { KbdProps } from './components/Kbd';
155
162
  export { Kbd } from './components/Kbd';