eratic / packages / core / src / components / Figure.astro
468 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
---
/**
 * A photograph with a caption and, optionally, the camera settings.
 *
 * Astro re-encodes the source through sharp into AVIF and WebP at several
 * widths and emits a srcset, so a phone downloads a phone-sized file. That
 * re-encode also strips every scrap of metadata from the served image — which
 * is the privacy behaviour we want, and the reason the EXIF you *do* want
 * shown has to be rendered here as text. See lib/exif.ts.
 *
 *   <Figure src={photo} alt="Fog over the lake" width="wide" exif />
 */
import { Picture } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import { readExif, exifLine } from '../lib/exif.js';

interface Props {
	src: ImageMetadata;
	alt: string;
	caption?: string;
	/** Which grid track to occupy. */
	width?: 'content' | 'wide' | 'full';
	/** Render the camera settings under the caption. */
	exif?: boolean;
	/** Above-the-fold images should not lazy-load. */
	priority?: boolean;
}

const { src, alt, caption, width = 'content', exif = false, priority = false } = Astro.props;

const settings = exif ? exifLine(await readExif(src)) : null;
---

<figure class:list={[width === 'wide' && 'u-wide', width === 'full' && 'u-full']}>
	{/*
		Picture, not Image, for two reasons. AVIF is meaningfully smaller than
		WebP on photographs, and Picture is what emits a <source> per format.
		Just as importantly, fallbackFormat="webp" keeps the fallback in a
		processed format — an Image falls back to the original file, and the
		original is the one copy that still carries the photographer's metadata.
	*/}
	<Picture
		src={src}
		alt={alt}
		layout="constrained"
		widths={[480, 800, 1200, 1600]}
		sizes={width === 'full'
			? '100vw'
			: width === 'wide'
				? '(width >= 52rem) 52rem, 100vw'
				: '(width >= 34rem) 34rem, 100vw'}
		formats={['avif', 'webp']}
		fallbackFormat="webp"
		loading={priority ? 'eager' : 'lazy'}
		decoding={priority ? 'sync' : 'async'}
		fetchpriority={priority ? 'high' : 'auto'}
	/>

	{
		(caption || settings) && (
			<figcaption>
				{caption}
				{settings && <span class="exif">{settings}</span>}
			</figcaption>
		)
	}
</figure>

<style>
	figure :global(img) {
		inline-size: 100%;
		background: var(--surface-sunk);
		border-radius: var(--radius);
	}

	/*
	 * The caption is styled here rather than left to prose.css, because a
	 * Figure is not always inside a .prose. A photo set renders its grid
	 * directly into the entry, above the article body — there the prose rules
	 * never apply, and captions inherited full-strength ink at body size,
	 * which read as the loudest thing on a page of photographs.
	 *
	 * prose.css keeps its own figcaption rule for <figure> elements written by
	 * hand in Markdown. The two agree; this one just does not depend on
	 * ancestry.
	 */
	figcaption {
		margin-block-start: var(--space-2xs);
		font-family: var(--font-ui);
		font-size: var(--step--1);
		line-height: var(--leading-prose);
		color: var(--ink-muted);
		text-wrap: pretty;
	}

	.exif {
		display: block;
		margin-block-start: var(--space-3xs);
		font-family: var(--font-mono);
		font-size: var(--step--2);
		font-variant-numeric: tabular-nums;
		color: var(--ink-faint);
		/* Long lens names should wrap rather than overflow on a phone. */
		overflow-wrap: break-word;
	}
</style>

static mirror of HEAD · about · clone: git clone https://git.makefuture.ro/eratic.git