@sarimarcus/content-sites-ui 0.23.0 → 0.25.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/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All packages in the platform release in lockstep; entries are per release version.
4
4
 
5
+ ## 0.25.0 (2026-10-05)
6
+
7
+ - `./editorial` `ArticleCategoryNav` builds its own links: it takes the articles and the site's category map (`categories`, in display order), links `/articles` and `/articles/topic/<slug>`, and defaults its "All" label and aria label. New type `ArticleCategoryMeta` (SLA-2961).
8
+
9
+ ### Breaking
10
+
11
+ - `ArticleCategoryNav` no longer takes resolved `categories`/`all` links: pass `articles` and `categories={ARTICLE_CATEGORIES}`. The tourism sites delete their adapter with `rewrite-imports.mjs --delete-local`, then run `scripts/platform/migrations/promoted-adapters.mjs`, which adds the prop at every call site (SLA-2961).
12
+
13
+ ## 0.24.0 (2026-10-05)
14
+
15
+ - `./primitives`: `ResponsiveImage` and `ResponsivePicture`, the responsive image call site. They take `sizes` and never `widths`, `formats` or `fallbackFormat`: the variant service builds the ladder and serves WebP only (SLA-2960).
16
+ - Every package component with a ladder image uses them. Output is unchanged except that each `<picture>` drops its `<source>`: with WebP the only format it repeated the `<img>` srcset, and for an image without variants (a gallery) it was an empty `srcset`. Proven on all three sites: every changed page equals its 0.23.0 build with those `<source>` elements removed (SLA-2960).
17
+
18
+ ### Deprecated
19
+
20
+ - `EntityCard` and `InlineImage` `formats`: ignored, since images are WebP only. Removed in 0.25.0; `scripts/platform/migrations/responsive-image.mjs` drops it from a site's call sites (SLA-2960).
21
+
22
+ ### Breaking
23
+
24
+ - The image-variants push gate refuses any `widths=` call site once a site pins core 0.24.0. Run `scripts/platform/migrations/responsive-image.mjs --site-root=<site>` in the pin-bump commit; it moves every `<Picture>`/`<Image widths={IMAGE_LADDER}>` onto ui `ResponsivePicture`/`ResponsiveImage` (SLA-2960).
25
+
5
26
  ## 0.23.0 (2026-10-04)
6
27
 
7
28
  - No changes; lockstep release with core (SLA-2958).
package/README.md CHANGED
@@ -12,7 +12,7 @@ text arrives through props and slots.
12
12
  ## Primitives
13
13
 
14
14
  ```ts
15
- import { Icon, Badge, Chip, ArrowLabel, SectionDivider, LinkArrow, Button, Callout, FeatureList, Blockquote, SectionHeader, PILL_BASE, isBadgeLength, PRICE_LABEL_MAX } from '@sarimarcus/content-sites-ui/primitives';
15
+ import { Icon, Badge, Chip, ArrowLabel, SectionDivider, LinkArrow, Button, Callout, FeatureList, Blockquote, SectionHeader, ResponsiveImage, ResponsivePicture, PILL_BASE, isBadgeLength, PRICE_LABEL_MAX } from '@sarimarcus/content-sites-ui/primitives';
16
16
  ```
17
17
 
18
18
  - `Icon`: any Lucide icon by kebab-case name.
@@ -42,6 +42,11 @@ import { Icon, Badge, Chip, ArrowLabel, SectionDivider, LinkArrow, Button, Callo
42
42
  - `Blockquote`: a centred pull-quote, `quote` plus an optional `source`.
43
43
  - `SectionHeader`: an icon tile (slot `icon`), an `h2` `title` and an optional `action` slot. `iconBg` and
44
44
  `iconColor` are raw classes the site supplies.
45
+ - `ResponsiveImage`: Astro's `<Image>` at every `IMAGE_LADDER` width the source has variants for. Takes `<Image>`'s
46
+ props less `widths`, `densities` and `format`, and requires `sizes`. A fixed-size image (avatar, thumbnail) stays a
47
+ plain `<Image width={N}>`.
48
+ - `ResponsivePicture`: `ResponsiveImage` inside a `<picture>` that takes `pictureAttributes`, for a styled wrapper.
49
+ Same HTML as Astro's `<Picture>` with WebP, less its redundant `<source>`.
45
50
  - `PILL_BASE`: the shared pill geometry. `isBadgeLength`, `BADGE_MAX_CHARS`, `PRICE_LABEL_MAX`: the label-length
46
51
  limits a call site branches on (pill or prose); a price-range schema `.max()` must equal `PRICE_LABEL_MAX`.
47
52
 
@@ -120,7 +125,7 @@ let a site keep its own roles.
120
125
  - `AuthorCard`: whole-card link, polaroid, or plain card with a profile link. `AuthorStrip`: portraits row.
121
126
  - `ArticleCard` / `ArticleGrid`: listing cards; `pagefindIgnore` for a listing that is itself indexed.
122
127
  `ArticleFeatureCard` is the wide lead card.
123
- - `ArticleCategoryNav`: "All" plus one chip per topic; the site supplies the links and counts.
128
+ - `ArticleCategoryNav`: "All" plus one chip per populated topic, with counts. Pass the articles and the site's category map (`label`, `icon`, in display order); it links `/articles` and `/articles/topic/<slug>`.
124
129
  - `RelatedContent` / `RelatedContentCard` (in `./composed`, beside the CardCarousel they render): grid or carousel of `RelatedItem` cards, `tone` primary/accent/secondary.
125
130
  Resolving which items to show (collections, scoring, backfill) stays a site adapter.
126
131
  - `InsiderTips`: tip kinds are site keys mapped to a label and a tone.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarimarcus/content-sites-ui",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "Universal Astro UI components, layout shell and design tokens shared by the content sites.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,6 +49,6 @@
49
49
  "@lucide/astro": "^1.23.0",
50
50
  "astro": "^7.0.6",
51
51
  "tailwindcss": "^4.1.18",
52
- "@sarimarcus/content-sites-core": "0.23.0"
52
+ "@sarimarcus/content-sites-core": "0.25.0"
53
53
  }
54
54
  }
@@ -1,5 +1,4 @@
1
1
  ---
2
- import { IMAGE_LADDER } from '@sarimarcus/content-sites-core/images';
3
2
  /**
4
3
  * AffiliateCard — a bookable activity as one sponsored link: image with the title over it, description, duration,
5
4
  * price, rating and the CTA. `href` arrives finished (tagged by the site) and `productId` is the site's.
@@ -17,12 +16,12 @@ import { IMAGE_LADDER } from '@sarimarcus/content-sites-core/images';
17
16
  * the default derives a page-unique id).
18
17
  */
19
18
  import type { ImageMetadata } from 'astro';
20
- import { Picture } from 'astro:assets';
21
19
  import { deterministicId } from '@sarimarcus/content-sites-core/dom';
22
20
  import Badge from '../primitives/Badge.astro';
23
21
  import Icon from '../primitives/Icon.astro';
24
22
  import type { AffiliateCardLabels } from './types';
25
23
  import { templateLine } from './templateText';
24
+ import ResponsivePicture from '../primitives/ResponsivePicture.astro';
26
25
 
27
26
  interface Props {
28
27
  href: string;
@@ -168,13 +167,10 @@ const priceFrame = skipQueueFrame && title.toLowerCase().includes('skip')
168
167
  class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
169
168
  />
170
169
  ) : (
171
- <Picture
170
+ <ResponsivePicture
172
171
  src={image}
173
172
  alt={title}
174
- widths={IMAGE_LADDER}
175
173
  sizes={imageSizes}
176
- formats={['webp']}
177
- fallbackFormat="webp"
178
174
  loading="lazy"
179
175
  decoding="async"
180
176
  fetchpriority={imageFetchpriority}
@@ -1,5 +1,4 @@
1
1
  ---
2
- import { IMAGE_LADDER } from '@sarimarcus/content-sites-core/images';
3
2
  /**
4
3
  * EntityCard — the shared frame of an entity teaser card: a whole-card link with a focus ring, an image block
5
4
  * (optimized Picture, or a fallback img), a content area and a footer row. Everything that varies between card types
@@ -15,10 +14,8 @@ import { IMAGE_LADDER } from '@sarimarcus/content-sites-core/images';
15
14
  * footer-meta — start of the footer row (location, closing time…).
16
15
  * footer-cta — end of the footer row.
17
16
  */
18
- import { Picture } from 'astro:assets';
19
17
  import type { ImageMetadata } from 'astro';
20
-
21
- type ImageFormat = 'avif' | 'webp' | 'png' | 'jpeg' | 'jpg' | 'svg';
18
+ import ResponsivePicture from '../primitives/ResponsivePicture.astro';
22
19
 
23
20
  interface Props {
24
21
  href: string;
@@ -27,7 +24,8 @@ interface Props {
27
24
  /** Shown when `image` is absent. */
28
25
  fallbackSrc: string;
29
26
  imageAlt: string;
30
- formats?: ImageFormat[];
27
+ /** @deprecated Ignored: images are WebP only (SLA-2960). Removed in the next minor. */
28
+ formats?: Array<'avif' | 'webp' | 'png' | 'jpeg' | 'jpg' | 'svg'>;
31
29
  /** Position in the list: the first three load eagerly, the first one at high priority. */
32
30
  index?: number;
33
31
  class?: string;
@@ -39,7 +37,6 @@ const {
39
37
  image,
40
38
  fallbackSrc,
41
39
  imageAlt,
42
- formats = ['webp'],
43
40
  index = 999,
44
41
  class: className,
45
42
  } = Astro.props;
@@ -63,13 +60,10 @@ const footerClass = poster
63
60
  <a href={href} class={linkClass}>
64
61
  <div class={mediaClass}>
65
62
  {image ? (
66
- <Picture
63
+ <ResponsivePicture
67
64
  src={image}
68
65
  alt={imageAlt}
69
- widths={IMAGE_LADDER}
70
66
  sizes="(max-width: 639px) 100vw, (max-width: 1023px) 50vw, 33vw"
71
- formats={formats}
72
- fallbackFormat="webp"
73
67
  pictureAttributes={{ class: 'block w-full h-full' }}
74
68
  class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
75
69
  loading={index < 3 ? "eager" : "lazy"}
@@ -1,8 +1,7 @@
1
1
  ---
2
- import { IMAGE_LADDER } from '@sarimarcus/content-sites-core/images';
3
- import { Picture } from 'astro:assets';
4
2
  import type { ImageMetadata } from 'astro';
5
3
  import type { TrustedHtml } from '../types/index.ts';
4
+ import ResponsivePicture from '../primitives/ResponsivePicture.astro';
6
5
 
7
6
  interface Props {
8
7
  /** Imported image (ImageMetadata from Astro asset pipeline) */
@@ -43,7 +42,7 @@ interface Props {
43
42
  aspectRatio?: string;
44
43
  /** Optional URL to wrap the image in a link */
45
44
  href?: string;
46
- /** Image formats for the <picture> sources (WebP only: the variant service serves nothing else). */
45
+ /** @deprecated Ignored: images are WebP only (SLA-2960). Removed in the next minor. */
47
46
  formats?: 'webp'[];
48
47
  /** Additional CSS classes on the outer figure element */
49
48
  class?: string;
@@ -61,7 +60,6 @@ const {
61
60
  size = 'medium',
62
61
  aspectRatio = '16/9',
63
62
  href,
64
- formats = ['webp'],
65
63
  class: className,
66
64
  } = Astro.props;
67
65
 
@@ -104,13 +102,10 @@ const sizes = isFloat
104
102
  >
105
103
  {href ? (
106
104
  <a href={href} class="block group" aria-label={captionText || alt}>
107
- <Picture
105
+ <ResponsivePicture
108
106
  src={src}
109
107
  alt={alt}
110
- widths={IMAGE_LADDER}
111
108
  sizes={sizes}
112
- formats={formats}
113
- fallbackFormat="webp"
114
109
  pictureAttributes={{ class: 'block w-full h-full' }}
115
110
  class:list={[
116
111
  'w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]',
@@ -122,13 +117,10 @@ const sizes = isFloat
122
117
  />
123
118
  </a>
124
119
  ) : (
125
- <Picture
120
+ <ResponsivePicture
126
121
  src={src}
127
122
  alt={alt}
128
- widths={IMAGE_LADDER}
129
123
  sizes={sizes}
130
- formats={formats}
131
- fallbackFormat="webp"
132
124
  pictureAttributes={{ class: 'block w-full h-full' }}
133
125
  class:list={[
134
126
  'w-full object-cover',
@@ -1,12 +1,11 @@
1
1
  ---
2
- import { IMAGE_LADDER } from '@sarimarcus/content-sites-core/images';
3
2
  /**
4
3
  * RelatedContentCard — one card inside RelatedContent. The grid and the carousel render this same card; only the
5
4
  * shell classes and the responsive image hints differ. Three shapes, by precedence: image-led, icon-led, plain text.
6
5
  */
7
- import { Picture } from 'astro:assets';
8
6
  import Icon from '../primitives/Icon.astro';
9
7
  import type { RelatedItem, RelatedTone } from './related';
8
+ import ResponsivePicture from '../primitives/ResponsivePicture.astro';
10
9
 
11
10
  interface Props {
12
11
  item: RelatedItem;
@@ -60,12 +59,9 @@ const hasImage = !!item.image;
60
59
  >
61
60
  {hasImage && (
62
61
  <div class="relative aspect-video overflow-hidden">
63
- <Picture
62
+ <ResponsivePicture
64
63
  src={item.image!}
65
64
  alt={item.imageAlt || item.title}
66
- widths={IMAGE_LADDER}
67
- formats={['webp']}
68
- fallbackFormat="webp"
69
65
  sizes={pictureSizes}
70
66
  class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
71
67
  />
@@ -1,13 +1,12 @@
1
1
  ---
2
- import { IMAGE_LADDER } from '@sarimarcus/content-sites-core/images';
3
2
  /**
4
3
  * ArticleCard — one article in a grid: image, title, byline with the published or "Updated" date, description and
5
4
  * a read link. Dates arrive formatted; the image is the route's import.
6
5
  */
7
- import { Picture } from 'astro:assets';
8
6
  import ArticleByline from './ArticleByline.astro';
9
7
  import Icon from '../primitives/Icon.astro';
10
8
  import type { ArticleSummary } from './types';
9
+ import ResponsivePicture from '../primitives/ResponsivePicture.astro';
11
10
 
12
11
  interface Props {
13
12
  article: ArticleSummary;
@@ -29,13 +28,10 @@ const { article: a, readLabel, updatedLabel, loading = 'lazy', pagefindIgnore =
29
28
  >
30
29
  {a.image ? (
31
30
  <div class="aspect-[4/3] overflow-hidden">
32
- <Picture
31
+ <ResponsivePicture
33
32
  src={a.image}
34
33
  alt={a.imageAlt ?? ''}
35
- widths={IMAGE_LADDER}
36
34
  sizes="(max-width: 640px) 100vw, 33vw"
37
- formats={['webp']}
38
- fallbackFormat="webp"
39
35
  class="w-full h-full object-cover motion-safe:transition-transform motion-safe:duration-500 motion-safe:group-hover:scale-105"
40
36
  loading={loading}
41
37
  />
@@ -1,17 +1,45 @@
1
1
  ---
2
- /** ArticleCategoryNav — "All" plus one chip per populated topic, each with a count badge. The site resolves the links. */
2
+ /**
3
+ * ArticleCategoryNav — "All" plus one chip per populated topic, each with a count badge. `categories` is the site's
4
+ * category map in display order; a category with no article is left out.
5
+ */
6
+ import { url } from '@sarimarcus/content-sites-core/url';
3
7
  import Chip from '../primitives/Chip.astro';
4
8
  import Badge from '../primitives/Badge.astro';
5
- import type { ArticleCategoryLink } from './types';
9
+ import type { ArticleCategoryLink, ArticleCategoryMeta } from './types';
6
10
 
7
11
  interface Props {
8
- categories: ArticleCategoryLink[];
9
- all: Omit<ArticleCategoryLink, 'icon'>;
10
- ariaLabel: string;
12
+ articles: readonly { data: { category?: string | null } }[];
13
+ categories: Readonly<Record<string, ArticleCategoryMeta>>;
14
+ current?: string;
15
+ ariaLabel?: string;
16
+ allLabel?: string;
11
17
  class?: string;
12
18
  }
13
19
 
14
- const { categories, all, ariaLabel, class: className } = Astro.props;
20
+ const {
21
+ articles,
22
+ categories: categoryMeta,
23
+ current,
24
+ ariaLabel = 'Browse articles by topic',
25
+ allLabel = 'All',
26
+ class: className,
27
+ } = Astro.props;
28
+
29
+ const counts = new Map<string, number>();
30
+ for (const a of articles) {
31
+ if (a.data.category) counts.set(a.data.category, (counts.get(a.data.category) ?? 0) + 1);
32
+ }
33
+ const categories: ArticleCategoryLink[] = Object.keys(categoryMeta)
34
+ .filter((slug) => counts.has(slug))
35
+ .map((slug) => ({
36
+ href: url(`/articles/topic/${slug}`),
37
+ label: categoryMeta[slug].label,
38
+ icon: categoryMeta[slug].icon,
39
+ count: counts.get(slug)!,
40
+ active: current === slug,
41
+ }));
42
+ const all = { href: url('/articles'), label: allLabel, count: articles.length, active: !current };
15
43
  const countClass = (active: boolean) => (active ? 'bg-overlay-foreground/20' : 'bg-muted');
16
44
  ---
17
45
 
@@ -1,13 +1,12 @@
1
1
  ---
2
- import { IMAGE_LADDER } from '@sarimarcus/content-sites-core/images';
3
2
  /**
4
3
  * ArticleFeatureCard — the wide lead card above an article grid: image left, label, title, byline with the
5
4
  * published or "Updated" date, description and a read link. Dates arrive formatted; the image is the route's import.
6
5
  */
7
- import { Picture } from 'astro:assets';
8
6
  import type { ImageMetadata } from 'astro';
9
7
  import ArticleByline from './ArticleByline.astro';
10
8
  import Icon from '../primitives/Icon.astro';
9
+ import ResponsivePicture from '../primitives/ResponsivePicture.astro';
11
10
 
12
11
  interface Props {
13
12
  href: string;
@@ -41,13 +40,10 @@ const Heading = `h${headingLevel}` as 'h2' | 'h3';
41
40
  <div class="grid sm:grid-cols-[1.2fr_1fr] gap-0">
42
41
  {image ? (
43
42
  <div class="aspect-[4/3] sm:aspect-auto overflow-hidden">
44
- <Picture
43
+ <ResponsivePicture
45
44
  src={image}
46
45
  alt={imageAlt}
47
- widths={IMAGE_LADDER}
48
46
  sizes="(max-width: 640px) 100vw, 50vw"
49
- formats={['webp']}
50
- fallbackFormat="webp"
51
47
  class="w-full h-full object-cover motion-safe:transition-transform motion-safe:duration-500 motion-safe:group-hover:scale-105"
52
48
  loading={loading}
53
49
  fetchpriority={loading === 'eager' ? 'high' : undefined}
@@ -13,7 +13,7 @@ export { default as AuthorCard } from './AuthorCard.astro';
13
13
  export { default as ArticleCard } from './ArticleCard.astro';
14
14
  export { default as ArticleGrid } from './ArticleGrid.astro';
15
15
  export { default as ArticleCategoryNav } from './ArticleCategoryNav.astro';
16
- export type { AuthorSummary, ArticleSummary, KeyFact, SourceItem, ArticleCategoryLink } from './types';
16
+ export type { AuthorSummary, ArticleSummary, KeyFact, SourceItem, ArticleCategoryLink, ArticleCategoryMeta } from './types';
17
17
  export { default as InsiderTips } from './InsiderTips.astro';
18
18
  export type { InsiderTip, InsiderTipAuthor, InsiderTipKinds, InsiderTipTone } from './types';
19
19
  export { default as AuthorStrip } from './AuthorStrip.astro';
@@ -1,12 +1,11 @@
1
1
  ---
2
- import { IMAGE_LADDER } from '@sarimarcus/content-sites-core/images';
3
2
  /**
4
3
  * RelatedCard — an in-article link to another page. `inline`: a ruled "read next" row. `featured`: the mid-article
5
4
  * feature card, image left. The site resolves href, eyebrow and image; an unresolved target should render nothing.
6
5
  */
7
6
  import type { ImageMetadata } from 'astro';
8
- import { Picture } from 'astro:assets';
9
7
  import Icon from '../../primitives/Icon.astro';
8
+ import ResponsivePicture from '../../primitives/ResponsivePicture.astro';
10
9
 
11
10
  interface Props {
12
11
  href: string;
@@ -51,13 +50,10 @@ const { href, eyebrow, title, description, variant = 'inline', image, imageAlt,
51
50
  >
52
51
  <div class="sm:w-2/5 shrink-0 overflow-hidden bg-muted aspect-video sm:aspect-auto">
53
52
  {image ? (
54
- <Picture
53
+ <ResponsivePicture
55
54
  src={image}
56
55
  alt={imageAlt ?? title}
57
- widths={IMAGE_LADDER}
58
56
  sizes="(min-width: 640px) 40vw, 100vw"
59
- formats={['webp']}
60
- fallbackFormat="webp"
61
57
  class="w-full h-full object-cover"
62
58
  loading="lazy"
63
59
  />
@@ -36,6 +36,12 @@ export interface KeyFact {
36
36
 
37
37
  export type SourceItem = SourceInput;
38
38
 
39
+ /** The fields ArticleCategoryNav reads from a site's category map entry. */
40
+ export interface ArticleCategoryMeta {
41
+ label: string;
42
+ icon?: string;
43
+ }
44
+
39
45
  /** One category chip in an article topic nav. */
40
46
  export interface ArticleCategoryLink {
41
47
  href: string;
@@ -0,0 +1,14 @@
1
+ ---
2
+ /**
3
+ * ResponsiveImage — a content image served at every IMAGE_LADDER width the source has variants for. The variant
4
+ * service builds the ladder and serves WebP only, so a call site passes `sizes` and never widths or formats.
5
+ * A fixed-size image (an avatar, a thumbnail) stays a plain `<Image width={N}>`: without `sizes` a srcset
6
+ * reads as 100vw.
7
+ */
8
+ import { Image } from 'astro:assets';
9
+ import type { ComponentProps } from 'astro/types';
10
+ import { IMAGE_LADDER } from '@sarimarcus/content-sites-core/images';
11
+
12
+ type Props = Omit<ComponentProps<typeof Image>, 'widths' | 'densities' | 'format' | 'sizes'> & { sizes: string };
13
+ ---
14
+ <Image {...Astro.props} widths={IMAGE_LADDER} />
@@ -0,0 +1,22 @@
1
+ ---
2
+ /**
3
+ * ResponsivePicture — ResponsiveImage inside a `<picture>`, for call sites that style or select the wrapper. It
4
+ * renders what Astro's `<Picture>` does minus its `<source>`: with WebP the only format, the source repeated the
5
+ * img's srcset.
6
+ */
7
+ import type { ComponentProps, HTMLAttributes } from 'astro/types';
8
+ import ResponsiveImage from './ResponsiveImage.astro';
9
+
10
+ type Props = ComponentProps<typeof ResponsiveImage> & { pictureAttributes?: HTMLAttributes<'picture'> };
11
+
12
+ const { pictureAttributes = {}, ...props } = Astro.props;
13
+ // Forwarded to <picture> as Astro's <Picture> does, so a parent's scoped styles still reach the wrapper.
14
+ const attrs: Record<string, unknown> = { ...pictureAttributes };
15
+ const scoped = typeof props.class === 'string' ? props.class.match(/\bastro-\w{8}\b/)?.[0] : undefined;
16
+ if (scoped) attrs.class = attrs.class ? `${attrs.class} ${scoped}` : scoped;
17
+ for (const key in props) if (key.startsWith('data-astro-cid')) attrs[key] = (props as Record<string, unknown>)[key];
18
+ // No whitespace before <ResponsiveImage>: Astro's <Image> emits the space before its <img>, so the output matches
19
+ // <Picture>'s byte for byte, less the <source>.
20
+ ---
21
+ <picture {...attrs}><ResponsiveImage {...props} />
22
+ </picture>
@@ -10,5 +10,7 @@ export { default as Callout } from './Callout.astro';
10
10
  export { default as FeatureList } from './FeatureList.astro';
11
11
  export { default as Blockquote } from './Blockquote.astro';
12
12
  export { default as SectionHeader } from './SectionHeader.astro';
13
+ export { default as ResponsiveImage } from './ResponsiveImage.astro';
14
+ export { default as ResponsivePicture } from './ResponsivePicture.astro';
13
15
  export { PILL_BASE } from './pillRecipe';
14
16
  export { BADGE_MAX_CHARS, PRICE_LABEL_MAX, isBadgeLength } from './badgeText';