@sarimarcus/content-sites-ui 0.41.2 → 0.41.3
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 +6 -0
- package/package.json +2 -2
- package/src/composed/RelatedContent.astro +11 -21
- package/src/composed/RelatedContentGrid.astro +32 -0
- package/src/composed/index.ts +3 -1
- package/src/editorial/AuthorCard.astro +7 -7
- package/src/editorial/PersonCard.astro +74 -0
- package/src/editorial/PersonChip.astro +44 -0
- package/src/editorial/PersonPage.astro +167 -0
- package/src/editorial/RoleBadge.astro +6 -0
- package/src/editorial/index.ts +3 -0
- package/src/internal/labels.ts +8 -0
- package/src/primitives/Badge.astro +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All packages in the platform release in lockstep; entries are per release version.
|
|
4
4
|
|
|
5
|
+
## 0.41.3 (2026-10-21)
|
|
6
|
+
|
|
7
|
+
- `editorial`: new `PersonChip` (a Chip link to a historical figure's page, name and life dates), `PersonCard` (an `EntityCard` poster teaser with portrait, dates, role badges and a clamped description) and `PersonPage` (the person page shell: portrait, roles, name, dates with the `EraChip`, short bio, "Read the full story" link to the primary article, places to see and related reading as `RelatedContentGrid`s, a `fallbackSrc` prop for a string `image`, and the era on a `ChronologyTimeline`; slots `breadcrumb`, `before-body`, `after-body`, `tail`). Copy defaults live in `LABELS.personChip`, `personCard` and `personPage`. No change to existing components' markup (SLA-3260).
|
|
8
|
+
- `composed`: new `RelatedContentGrid` (the cards-only responsive grid of `RelatedContent`, no heading, surface or carousel CSS); `RelatedContent` renders it for `layout="grid"` with identical HTML.
|
|
9
|
+
- `primitives`: `Badge` gains `tone="outline"` (bordered, muted fill); the role and expertise pills of `PersonCard`, `PersonPage` and `AuthorCard` share it through an internal `RoleBadge`, with `AuthorCard`'s markup unchanged. `PersonPage` now requires `imageAlt` whenever `image` or `fallbackSrc` is set.
|
|
10
|
+
|
|
5
11
|
## 0.41.2 (2026-10-21)
|
|
6
12
|
|
|
7
13
|
- `editorial`: `ChronologyTimeline`'s era index is now `TableOfContents` (card variant) instead of a row of chips: a collapsed jump list on mobile, a sticky bar once the index scrolls away, and the era in view marked with `data-active` / `aria-current`. The era anchor ids move from the era heading to its `<li>`, so the scrollspy follows the whole era and the sticky bar hides after the last one. Its markup changes on every page that renders the timeline (SLA-3274).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sarimarcus/content-sites-ui",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.3",
|
|
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",
|
|
@@ -50,6 +50,6 @@
|
|
|
50
50
|
"@lucide/astro": "^1.23.0",
|
|
51
51
|
"astro": "^7.0.6",
|
|
52
52
|
"tailwindcss": "^4.1.18",
|
|
53
|
-
"@sarimarcus/content-sites-core": "0.41.
|
|
53
|
+
"@sarimarcus/content-sites-core": "0.41.3"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -7,6 +7,7 @@ import ArrowLabel from '../primitives/ArrowLabel.astro';
|
|
|
7
7
|
import SectionDivider from '../primitives/SectionDivider.astro';
|
|
8
8
|
import CardCarousel from './CardCarousel.astro';
|
|
9
9
|
import RelatedContentCard from './RelatedContentCard.astro';
|
|
10
|
+
import RelatedContentGrid from './RelatedContentGrid.astro';
|
|
10
11
|
import type { RelatedItem } from './related';
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
@@ -61,10 +62,6 @@ const wrapperClass = variant === 'bordered'
|
|
|
61
62
|
? ''
|
|
62
63
|
: 'p-6 bg-muted/50 rounded-xl border border-border';
|
|
63
64
|
|
|
64
|
-
const gridClass = columns === 3
|
|
65
|
-
? 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'
|
|
66
|
-
: 'grid gap-4 sm:grid-cols-2';
|
|
67
|
-
|
|
68
65
|
// At `lg` a card fits `columns` per view, so the track stops overflowing and the controls hide. Percentages, not
|
|
69
66
|
// fixed widths: the block usually sits inside a ~700px prose column. Below `lg` cards are wider than a column share
|
|
70
67
|
// so the next one peeks in.
|
|
@@ -72,13 +69,9 @@ const cardWidthClass = columns === 3
|
|
|
72
69
|
? 'w-[80vw] sm:w-64 lg:w-[calc((100%-2rem)/3)]'
|
|
73
70
|
: 'w-[85vw] sm:w-80 lg:w-[calc((100%-1rem)/2)]';
|
|
74
71
|
|
|
75
|
-
const
|
|
76
|
-
? (
|
|
77
|
-
|
|
78
|
-
: '(min-width: 640px) 320px, 85vw')
|
|
79
|
-
: (columns === 3
|
|
80
|
-
? '(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw'
|
|
81
|
-
: '(min-width: 640px) 50vw, 100vw'));
|
|
72
|
+
const carouselPictureSizes = pictureSizesProp ?? (columns === 3
|
|
73
|
+
? '(min-width: 1024px) 240px, (min-width: 640px) 256px, 80vw'
|
|
74
|
+
: '(min-width: 640px) 320px, 85vw');
|
|
82
75
|
|
|
83
76
|
// CardCarousel's contract: every direct child of the track is one card with `snap-start shrink-0` and a width.
|
|
84
77
|
const carouselShell = `shrink-0 snap-start ${cardWidthClass}`;
|
|
@@ -101,7 +94,7 @@ const carouselShell = `shrink-0 snap-start ${cardWidthClass}`;
|
|
|
101
94
|
<RelatedContentCard
|
|
102
95
|
item={item}
|
|
103
96
|
shellClass={carouselShell}
|
|
104
|
-
pictureSizes={
|
|
97
|
+
pictureSizes={carouselPictureSizes}
|
|
105
98
|
trackingBlock={trackingBlock}
|
|
106
99
|
/>
|
|
107
100
|
))}
|
|
@@ -121,15 +114,12 @@ const carouselShell = `shrink-0 snap-start ${cardWidthClass}`;
|
|
|
121
114
|
)}
|
|
122
115
|
|
|
123
116
|
{layout !== 'carousel' && (
|
|
124
|
-
<
|
|
125
|
-
{items
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
/>
|
|
131
|
-
))}
|
|
132
|
-
</div>
|
|
117
|
+
<RelatedContentGrid
|
|
118
|
+
items={items}
|
|
119
|
+
columns={columns}
|
|
120
|
+
pictureSizes={pictureSizesProp}
|
|
121
|
+
trackingBlock={trackingBlock}
|
|
122
|
+
/>
|
|
133
123
|
)}
|
|
134
124
|
</Wrapper>
|
|
135
125
|
)}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* RelatedContentGrid — related link cards as a responsive grid, with no heading, surface or carousel. `RelatedContent`
|
|
4
|
+
* renders it for `layout="grid"`; a page that owns its own heading uses it directly.
|
|
5
|
+
*/
|
|
6
|
+
import RelatedContentCard from './RelatedContentCard.astro';
|
|
7
|
+
import type { RelatedItem } from './related';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
items: readonly RelatedItem[];
|
|
11
|
+
columns?: 2 | 3;
|
|
12
|
+
trackingBlock?: string;
|
|
13
|
+
/** Image `sizes`; the defaults match the column count in a ~700px prose column. */
|
|
14
|
+
pictureSizes?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { items, columns = 2, trackingBlock, pictureSizes: pictureSizesProp } = Astro.props;
|
|
18
|
+
|
|
19
|
+
const gridClass = columns === 3
|
|
20
|
+
? 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'
|
|
21
|
+
: 'grid gap-4 sm:grid-cols-2';
|
|
22
|
+
|
|
23
|
+
const pictureSizes = pictureSizesProp ?? (columns === 3
|
|
24
|
+
? '(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw'
|
|
25
|
+
: '(min-width: 640px) 50vw, 100vw');
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<div class={gridClass}>
|
|
29
|
+
{items.map((item) => (
|
|
30
|
+
<RelatedContentCard item={item} pictureSizes={pictureSizes} trackingBlock={trackingBlock} />
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
package/src/composed/index.ts
CHANGED
|
@@ -10,4 +10,6 @@ export { default as EntityCard } from './EntityCard.astro';
|
|
|
10
10
|
export { default as ArrowLinkGrid } from './ArrowLinkGrid.astro';
|
|
11
11
|
// RelatedContent sits here, not in ./editorial: it renders CardCarousel, whose <style> ships to every importer of the barrel.
|
|
12
12
|
export { default as RelatedContent } from './RelatedContent.astro';
|
|
13
|
-
export { default as RelatedContentCard } from './RelatedContentCard.astro';
|
|
13
|
+
export { default as RelatedContentCard } from './RelatedContentCard.astro';
|
|
14
|
+
export { default as RelatedContentGrid } from './RelatedContentGrid.astro';
|
|
15
|
+
export type { RelatedItem, RelatedTone } from './related';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (`href`), a centred polaroid portrait (`polaroid`), or a plain card with a profile link. `roleLabel` is free text
|
|
5
5
|
* whose meaning is the caller's: a job title on one page, "Curated by" on another.
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
7
|
+
import RoleBadge from './RoleBadge.astro';
|
|
8
8
|
import LinkArrow from '../primitives/LinkArrow.astro';
|
|
9
9
|
import ArrowLabel from '../primitives/ArrowLabel.astro';
|
|
10
10
|
|
|
@@ -79,9 +79,9 @@ const Heading = `h${headingLevel}` as 'h2' | 'h3';
|
|
|
79
79
|
|
|
80
80
|
<div class="mt-5 flex flex-wrap gap-2">
|
|
81
81
|
{expertise.map((topic) => (
|
|
82
|
-
<
|
|
82
|
+
<RoleBadge>
|
|
83
83
|
{topic}
|
|
84
|
-
</
|
|
84
|
+
</RoleBadge>
|
|
85
85
|
))}
|
|
86
86
|
</div>
|
|
87
87
|
|
|
@@ -123,9 +123,9 @@ const Heading = `h${headingLevel}` as 'h2' | 'h3';
|
|
|
123
123
|
|
|
124
124
|
<div class="mt-5 flex flex-wrap justify-center gap-2 lg:justify-start">
|
|
125
125
|
{expertise.map((topic) => (
|
|
126
|
-
<
|
|
126
|
+
<RoleBadge>
|
|
127
127
|
{topic}
|
|
128
|
-
</
|
|
128
|
+
</RoleBadge>
|
|
129
129
|
))}
|
|
130
130
|
</div>
|
|
131
131
|
|
|
@@ -159,9 +159,9 @@ const Heading = `h${headingLevel}` as 'h2' | 'h3';
|
|
|
159
159
|
|
|
160
160
|
<div class="mt-5 flex flex-wrap gap-2">
|
|
161
161
|
{expertise.map((topic) => (
|
|
162
|
-
<
|
|
162
|
+
<RoleBadge>
|
|
163
163
|
{topic}
|
|
164
|
-
</
|
|
164
|
+
</RoleBadge>
|
|
165
165
|
))}
|
|
166
166
|
</div>
|
|
167
167
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* PersonCard — a historical figure's teaser card on EntityCard's poster frame: portrait, name, life dates, role
|
|
4
|
+
* labels and a clamped description. Every string arrives as a prop (roles are the site's display labels); `ctaLabel`
|
|
5
|
+
* falls back to LABELS. Without `image` or `fallbackSrc` the image block stays an empty placeholder.
|
|
6
|
+
*/
|
|
7
|
+
import type { ImageMetadata } from 'astro';
|
|
8
|
+
import EntityCard from '../composed/EntityCard.astro';
|
|
9
|
+
import RoleBadge from './RoleBadge.astro';
|
|
10
|
+
import ArrowLabel from '../primitives/ArrowLabel.astro';
|
|
11
|
+
import { LABELS } from '../internal/labels';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
href: string;
|
|
15
|
+
name: string;
|
|
16
|
+
image?: ImageMetadata;
|
|
17
|
+
fallbackSrc?: string;
|
|
18
|
+
imageAlt: string;
|
|
19
|
+
/** Life dates as displayed, e.g. core `formatLifeDates`. */
|
|
20
|
+
dates?: string;
|
|
21
|
+
roles?: readonly string[];
|
|
22
|
+
description?: string;
|
|
23
|
+
ctaLabel?: string;
|
|
24
|
+
headingLevel?: 2 | 3;
|
|
25
|
+
index?: number;
|
|
26
|
+
eager?: number;
|
|
27
|
+
sizes?: string;
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
href,
|
|
33
|
+
name,
|
|
34
|
+
image,
|
|
35
|
+
fallbackSrc,
|
|
36
|
+
imageAlt,
|
|
37
|
+
dates,
|
|
38
|
+
roles = [],
|
|
39
|
+
description,
|
|
40
|
+
ctaLabel = LABELS.personCard.cta,
|
|
41
|
+
headingLevel = 3,
|
|
42
|
+
index,
|
|
43
|
+
eager,
|
|
44
|
+
sizes,
|
|
45
|
+
class: className,
|
|
46
|
+
} = Astro.props;
|
|
47
|
+
|
|
48
|
+
const Heading = `h${headingLevel}` as 'h2' | 'h3';
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
<EntityCard
|
|
52
|
+
href={href}
|
|
53
|
+
variant="poster"
|
|
54
|
+
image={image}
|
|
55
|
+
fallbackSrc={fallbackSrc}
|
|
56
|
+
imageAlt={imageAlt}
|
|
57
|
+
index={index}
|
|
58
|
+
eager={eager}
|
|
59
|
+
sizes={sizes}
|
|
60
|
+
footerJustify="end"
|
|
61
|
+
class={className}
|
|
62
|
+
>
|
|
63
|
+
<Heading class="text-xl font-display font-semibold text-foreground">{name}</Heading>
|
|
64
|
+
{dates && <p class="mt-1 text-sm tabular-nums text-muted-foreground">{dates}</p>}
|
|
65
|
+
{roles.length > 0 && (
|
|
66
|
+
<div class="mt-3 flex flex-wrap gap-2">
|
|
67
|
+
{roles.map((role) => (
|
|
68
|
+
<RoleBadge>{role}</RoleBadge>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
{description && <p class="mt-3 text-sm leading-relaxed text-muted-foreground line-clamp-3">{description}</p>}
|
|
73
|
+
<ArrowLabel slot="footer-cta" class="text-sm font-medium text-primary">{ctaLabel}</ArrowLabel>
|
|
74
|
+
</EntityCard>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* PersonChip — links an item to a historical figure's page. Visible text is `name` and, when given, the life `dates`
|
|
4
|
+
* (core `formatLifeDates`), within the Chip 80-char ceiling (validate-badge-overflow.mjs). Renders nothing without
|
|
5
|
+
* a name or an href.
|
|
6
|
+
*/
|
|
7
|
+
import Chip from '../primitives/Chip.astro';
|
|
8
|
+
import { LABELS } from '../internal/labels';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
name?: string;
|
|
12
|
+
href?: string;
|
|
13
|
+
dates?: string;
|
|
14
|
+
/** Accessible name; keep the visible name and dates in it. */
|
|
15
|
+
ariaLabel?: (name: string, dates?: string) => string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
name,
|
|
22
|
+
href,
|
|
23
|
+
dates,
|
|
24
|
+
ariaLabel = LABELS.personChip.ariaLabel,
|
|
25
|
+
icon = 'user',
|
|
26
|
+
class: className,
|
|
27
|
+
}: Props = Astro.props;
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
{name && href && (
|
|
31
|
+
<Chip
|
|
32
|
+
as="a"
|
|
33
|
+
href={href}
|
|
34
|
+
size="md"
|
|
35
|
+
tone="tint"
|
|
36
|
+
accent="primary"
|
|
37
|
+
icon={icon}
|
|
38
|
+
data-person-chip
|
|
39
|
+
aria-label={ariaLabel(name, dates)}
|
|
40
|
+
class={className}
|
|
41
|
+
>
|
|
42
|
+
{name}{dates && (<><span aria-hidden="true">·</span><span class="whitespace-nowrap tabular-nums">{dates}</span></>)}
|
|
43
|
+
</Chip>
|
|
44
|
+
)}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* PersonPage — the shell and shared body of a historical figure's page. The route keeps paths, data, SEO and schema
|
|
4
|
+
* (core `historicalPersonSchema`), renders markdown to `bioHtml`, and wraps this template in the site Layout. Order:
|
|
5
|
+
* portrait and roles, name, life dates with the era chip, the short bio, "Read the full story" to the person's
|
|
6
|
+
* primary article, places to see and related reading (`RelatedContentGrid`, not `RelatedContent`: the person page
|
|
7
|
+
* owns its headings and `RelatedContent` carries the carousel CSS), then the era on a ChronologyTimeline. Every string
|
|
8
|
+
* is a prop or a LABELS default; a block with no data renders nothing. Slots:
|
|
9
|
+
* breadcrumb — the site Breadcrumb, in the band under the header.
|
|
10
|
+
* before-body — site blocks between the header and the bio.
|
|
11
|
+
* after-body — site blocks after the bio and the full-story link.
|
|
12
|
+
* tail — full-width blocks after the column.
|
|
13
|
+
*/
|
|
14
|
+
import type { ImageMetadata } from 'astro';
|
|
15
|
+
import type { EraDefinition } from '@sarimarcus/content-sites-core/eras';
|
|
16
|
+
import Container from '../layout/Container.astro';
|
|
17
|
+
import BreadcrumbBand from '../layout/BreadcrumbBand.astro';
|
|
18
|
+
import RoleBadge from './RoleBadge.astro';
|
|
19
|
+
import LinkArrow from '../primitives/LinkArrow.astro';
|
|
20
|
+
import ResponsivePicture from '../primitives/ResponsivePicture.astro';
|
|
21
|
+
import RelatedContentGrid from '../composed/RelatedContentGrid.astro';
|
|
22
|
+
import type { RelatedItem } from '../composed/related';
|
|
23
|
+
import type { TrustedHtml } from '../types';
|
|
24
|
+
import EraChip from './EraChip.astro';
|
|
25
|
+
import ChronologyTimeline from './ChronologyTimeline.astro';
|
|
26
|
+
import type { TimelineItem } from './chronology';
|
|
27
|
+
import { LABELS } from '../internal/labels';
|
|
28
|
+
|
|
29
|
+
type PersonImage =
|
|
30
|
+
| { image?: undefined; fallbackSrc?: undefined; imageAlt?: string }
|
|
31
|
+
| {
|
|
32
|
+
image: ImageMetadata;
|
|
33
|
+
/** Plain path when the collection stores `image` as a string (no asset import). */
|
|
34
|
+
fallbackSrc?: string;
|
|
35
|
+
imageAlt: string;
|
|
36
|
+
}
|
|
37
|
+
| { image?: ImageMetadata; fallbackSrc: string; imageAlt: string };
|
|
38
|
+
|
|
39
|
+
type Props = PersonImage & {
|
|
40
|
+
name: string;
|
|
41
|
+
/** Life dates as displayed, e.g. core `formatLifeDates`. */
|
|
42
|
+
dates?: string;
|
|
43
|
+
/** Role labels as displayed. */
|
|
44
|
+
roles?: readonly string[];
|
|
45
|
+
era?: EraDefinition;
|
|
46
|
+
eraBasePath?: string;
|
|
47
|
+
bioHtml: TrustedHtml;
|
|
48
|
+
/** Prose modifiers after `prose`. */
|
|
49
|
+
proseClass?: string;
|
|
50
|
+
/** The person's primary article; `label` falls back to LABELS. */
|
|
51
|
+
fullStory?: { href: string; label?: string };
|
|
52
|
+
places?: readonly RelatedItem[];
|
|
53
|
+
placesHeading?: string;
|
|
54
|
+
readMore?: readonly RelatedItem[];
|
|
55
|
+
readMoreHeading?: string;
|
|
56
|
+
/** The era in context: pass the person's era only, or the whole chronology. */
|
|
57
|
+
timeline?: { eras: readonly EraDefinition[]; items: Record<string, readonly TimelineItem[]>; heading?: string };
|
|
58
|
+
pagefindType?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const {
|
|
62
|
+
name,
|
|
63
|
+
dates,
|
|
64
|
+
roles = [],
|
|
65
|
+
image,
|
|
66
|
+
fallbackSrc,
|
|
67
|
+
imageAlt,
|
|
68
|
+
era,
|
|
69
|
+
eraBasePath,
|
|
70
|
+
bioHtml,
|
|
71
|
+
proseClass = 'prose-lg',
|
|
72
|
+
fullStory,
|
|
73
|
+
places = [],
|
|
74
|
+
placesHeading = LABELS.personPage.placesHeading,
|
|
75
|
+
readMore = [],
|
|
76
|
+
readMoreHeading = LABELS.personPage.readMoreHeading,
|
|
77
|
+
timeline,
|
|
78
|
+
pagefindType = 'person',
|
|
79
|
+
} = Astro.props;
|
|
80
|
+
|
|
81
|
+
const timelineHeading = timeline?.heading ?? LABELS.personPage.timelineHeading;
|
|
82
|
+
const hasTimeline = timeline && Object.values(timeline.items).some((list) => list.length > 0);
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
<BreadcrumbBand>
|
|
86
|
+
<slot name="breadcrumb" />
|
|
87
|
+
</BreadcrumbBand>
|
|
88
|
+
|
|
89
|
+
<main class="pt-6 pb-12 lg:pt-8 lg:pb-20" data-person-page>
|
|
90
|
+
<div data-pagefind-body data-pagefind-meta={`type:${pagefindType}`}>
|
|
91
|
+
<Container size="measure">
|
|
92
|
+
<header class="flex flex-col gap-6 sm:flex-row sm:items-start">
|
|
93
|
+
{(image || fallbackSrc) && (
|
|
94
|
+
<div class="w-40 shrink-0 overflow-hidden rounded-xl bg-muted aspect-[4/5] sm:w-48">
|
|
95
|
+
{image ? (
|
|
96
|
+
<ResponsivePicture
|
|
97
|
+
src={image}
|
|
98
|
+
alt={imageAlt}
|
|
99
|
+
sizes="(min-width: 640px) 192px, 160px"
|
|
100
|
+
loading="eager"
|
|
101
|
+
fetchpriority="high"
|
|
102
|
+
decoding="async"
|
|
103
|
+
class="size-full object-cover"
|
|
104
|
+
/>
|
|
105
|
+
) : (
|
|
106
|
+
<img
|
|
107
|
+
src={fallbackSrc}
|
|
108
|
+
alt={imageAlt}
|
|
109
|
+
loading="eager"
|
|
110
|
+
fetchpriority="high"
|
|
111
|
+
decoding="async"
|
|
112
|
+
class="size-full object-cover"
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
<div class="min-w-0">
|
|
118
|
+
{roles.length > 0 && (
|
|
119
|
+
<div class="mb-3 flex flex-wrap gap-2">
|
|
120
|
+
{roles.map((role) => (
|
|
121
|
+
<RoleBadge>{role}</RoleBadge>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
<h1 class="font-display text-4xl leading-tight text-balance text-foreground">{name}</h1>
|
|
126
|
+
{(dates || era) && (
|
|
127
|
+
<div class="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2">
|
|
128
|
+
{dates && <p class="font-display text-xl tabular-nums text-secondary" data-person-dates>{dates}</p>}
|
|
129
|
+
<EraChip era={era} basePath={eraBasePath} />
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</header>
|
|
134
|
+
|
|
135
|
+
<slot name="before-body" />
|
|
136
|
+
|
|
137
|
+
<div class={`prose ${proseClass} mt-10 max-w-none`} data-person-bio set:html={bioHtml} />
|
|
138
|
+
|
|
139
|
+
{fullStory && (
|
|
140
|
+
<p class="mt-6">
|
|
141
|
+
<LinkArrow href={fullStory.href} size="base">{fullStory.label ?? LABELS.personPage.readFullStory}</LinkArrow>
|
|
142
|
+
</p>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<slot name="after-body" />
|
|
146
|
+
|
|
147
|
+
{[
|
|
148
|
+
{ id: 'places', heading: placesHeading, items: places },
|
|
149
|
+
{ id: 'read-more', heading: readMoreHeading, items: readMore },
|
|
150
|
+
].map((group) => group.items.length > 0 && (
|
|
151
|
+
<section class="mt-12" aria-labelledby={`person-${group.id}`} data-person-section={group.id}>
|
|
152
|
+
<h2 id={`person-${group.id}`} class="mb-6 text-2xl font-display font-semibold text-foreground">{group.heading}</h2>
|
|
153
|
+
<RelatedContentGrid items={group.items} />
|
|
154
|
+
</section>
|
|
155
|
+
))}
|
|
156
|
+
</Container>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{timeline && hasTimeline && (
|
|
160
|
+
<Container size="content" class="mt-16">
|
|
161
|
+
<h2 class="mb-8 font-display text-2xl font-semibold text-foreground">{timelineHeading}</h2>
|
|
162
|
+
<ChronologyTimeline eras={timeline.eras} items={timeline.items} index={false} />
|
|
163
|
+
</Container>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
<slot name="tail" />
|
|
167
|
+
</main>
|
package/src/editorial/index.ts
CHANGED
|
@@ -22,6 +22,9 @@ export { default as InsiderTips } from './InsiderTips.astro';
|
|
|
22
22
|
export type { InsiderTip, InsiderTipAuthor, InsiderTipKinds, InsiderTipTone } from './types';
|
|
23
23
|
export { default as AuthorStrip } from './AuthorStrip.astro';
|
|
24
24
|
export { default as EraChip } from './EraChip.astro';
|
|
25
|
+
export { default as PersonChip } from './PersonChip.astro';
|
|
26
|
+
export { default as PersonCard } from './PersonCard.astro';
|
|
27
|
+
export { default as PersonPage } from './PersonPage.astro';
|
|
25
28
|
export { default as AlsoExplore } from './AlsoExplore.astro';
|
|
26
29
|
export { default as VisitTiming } from './VisitTiming.astro';
|
|
27
30
|
export { default as ChronologyTimeline } from './ChronologyTimeline.astro';
|
package/src/internal/labels.ts
CHANGED
|
@@ -73,6 +73,14 @@ export const LABELS = {
|
|
|
73
73
|
articleCategoryNav: { ariaLabel: 'Browse articles by topic', allLabel: 'All' },
|
|
74
74
|
articleFeatureCard: { readLabel: 'Read article', updatedLabel: 'Updated' },
|
|
75
75
|
eraChip: { ariaLabel: (label: string, range: string) => `${label}, ${range}: see this era on the chronology` },
|
|
76
|
+
personChip: { ariaLabel: (name: string, dates?: string) => `${name}${dates ? `, ${dates}` : ''}: read their profile` },
|
|
77
|
+
personCard: { cta: 'Read the profile' },
|
|
78
|
+
personPage: {
|
|
79
|
+
readFullStory: 'Read the full story',
|
|
80
|
+
placesHeading: 'Places to see',
|
|
81
|
+
readMoreHeading: 'Read more',
|
|
82
|
+
timelineHeading: 'In their time',
|
|
83
|
+
},
|
|
76
84
|
articleTopicPager: {
|
|
77
85
|
heading: (topic: string) => `Next in ${topic}`,
|
|
78
86
|
newerLabel: 'Newer',
|
|
@@ -31,7 +31,7 @@ import { PILL_BASE } from './pillRecipe';
|
|
|
31
31
|
// Extends the span attributes so call sites can pass id / aria-* / data-* / slot through.
|
|
32
32
|
interface Props extends Omit<HTMLAttributes<'span'>, 'class'> {
|
|
33
33
|
/** Preset color. Use `custom` + `toneClass` for runtime category/type color maps. */
|
|
34
|
-
tone?: BrandTone | 'neutral' | 'onImage' | 'custom';
|
|
34
|
+
tone?: BrandTone | 'neutral' | 'outline' | 'onImage' | 'custom';
|
|
35
35
|
/** Color classes for tone="custom" (e.g. typeColors[type]). */
|
|
36
36
|
toneClass?: string;
|
|
37
37
|
size?: 'sm' | 'md' | 'lg';
|
|
@@ -72,6 +72,7 @@ const tones = {
|
|
|
72
72
|
secondary: 'bg-secondary text-secondary-foreground',
|
|
73
73
|
accent: 'bg-accent-tint text-accent-emphasis',
|
|
74
74
|
neutral: 'bg-muted text-muted-foreground',
|
|
75
|
+
outline: 'border bg-muted/50 text-foreground border-border',
|
|
75
76
|
onImage: 'bg-badge-on-image text-badge-on-image-foreground backdrop-blur-sm',
|
|
76
77
|
custom: '',
|
|
77
78
|
} as const;
|