// Zero-CLS image primitive: a plain , or a 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.
import type { JSX } from 'solid-js';
import { cx } from '../cx';
import styles from './Image.module.scss';
import {
assertNonEmptySrc,
boxFor,
type ImageAspectRatio,
type ImageBox,
type ImageLoadingHints,
type ImageSourceSet,
type ImageSources,
type ImageVariant,
loadingHints,
reservedRatio,
sourceSetsFor,
variantSrcset,
} from './image-source';
/**
* The box is ALWAYS reserved: the intrinsic size, or an explicit ratio when the size is not known
* (a CSS-sized hero). Neither is a type error here and `X_UI_INVALID_VALUE` at render.
*/
export type ImageDimensions =
| { width: number; height: number; aspectRatio?: undefined }
| { aspectRatio: ImageAspectRatio; width?: undefined; height?: undefined };
export interface ImageBaseProps {
/** The fallback every browser can decode — usually the JPEG/PNG rendition. */
src: string;
/**
* Required, with no default: an whose meaning is undescribed is a type
* error at the call site. Pass `alt=""` for a decorative image, deliberately.
*/
alt: string;
/** Renditions of `src`'s own encoding; the descriptors and their order are derived. */
variants?: readonly ImageVariant[] | undefined;
/** AVIF and WebP renditions, each a width list: `{ avif: [{ src: asset('…'), width: 640 }] }`. */
sources?: ImageSources | undefined;
/** Layout width of the box, e.g. `(max-width: 700px) 100vw, 620px`. Applies to every set. */
sizes?: string | undefined;
/** The LCP image, at most one per route: eager, `fetchpriority="high"`, never lazy. */
priority?: boolean | undefined;
/** Intrinsic size in CSS pixels, from the step that encoded it. Both, or `aspectRatio`. */
width?: number | undefined;
height?: number | undefined;
/** `'16 / 9'` — the reservation when the intrinsic size is not known. Never beside width/height. */
aspectRatio?: ImageAspectRatio | undefined;
class?: string | undefined;
}
/** What a call site passes: the base props, with exactly one of the two box forms required. */
export type ImageProps = ImageBaseProps & ImageDimensions;
export function Image(props: ImageBaseProps & ImageDimensions): JSX.Element {
const src = (): string => assertNonEmptySrc('Image', props.src, props.src);
const hints = (): ImageLoadingHints => loadingHints(props.priority);
// Validated first: `reservedRatio` refuses a missing box before anything is emitted.
const ratio = (): string => reservedRatio(props.width, props.height, props.aspectRatio);
const box = (): ImageBox | undefined => boxFor(props.width, props.height);
const sets = (): readonly ImageSourceSet[] => sourceSetsFor(props.sources);
// Once, eagerly: a compiled `style` binding may be read inside an effect, and a refusal thrown
// there reaches no caller. Here it fails the render that wrote the bad props.
ratio();
const img = (): JSX.Element => (
);
if (sets().length === 0) return img();
return (
{sets().map((set) => (
))}
{img()}
);
}