@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 +21 -0
- package/README.md +7 -2
- package/package.json +2 -2
- package/src/affiliate/AffiliateCard.astro +2 -6
- package/src/composed/EntityCard.astro +4 -10
- package/src/composed/InlineImage.astro +4 -12
- package/src/composed/RelatedContentCard.astro +2 -6
- package/src/editorial/ArticleCard.astro +2 -6
- package/src/editorial/ArticleCategoryNav.astro +34 -6
- package/src/editorial/ArticleFeatureCard.astro +2 -6
- package/src/editorial/index.ts +1 -1
- package/src/editorial/interludes/RelatedCard.astro +2 -6
- package/src/editorial/types.ts +6 -0
- package/src/primitives/ResponsiveImage.astro +14 -0
- package/src/primitives/ResponsivePicture.astro +22 -0
- package/src/primitives/index.ts +2 -0
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
|
|
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.
|
|
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.
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
/**
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
/**
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 {
|
|
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
|
-
<
|
|
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}
|
package/src/editorial/index.ts
CHANGED
|
@@ -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
|
-
<
|
|
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
|
/>
|
package/src/editorial/types.ts
CHANGED
|
@@ -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>
|
package/src/primitives/index.ts
CHANGED
|
@@ -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';
|