@sarimarcus/content-sites-ui 0.18.0 → 0.20.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.
@@ -0,0 +1,87 @@
1
+ ---
2
+ /**
3
+ * RelatedCard — an in-article link to another page. `inline`: a ruled "read next" row. `featured`: the mid-article
4
+ * feature card, image left. The site resolves href, eyebrow and image; an unresolved target should render nothing.
5
+ */
6
+ import type { ImageMetadata } from 'astro';
7
+ import { Picture } from 'astro:assets';
8
+ import Icon from '../../primitives/Icon.astro';
9
+
10
+ interface Props {
11
+ href: string;
12
+ /** Full eyebrow text, e.g. "Read next · Monument". */
13
+ eyebrow: string;
14
+ title: string;
15
+ description?: string;
16
+ variant?: 'inline' | 'featured';
17
+ image?: ImageMetadata;
18
+ imageAlt?: string;
19
+ eyebrowClass?: string;
20
+ }
21
+ const { href, eyebrow, title, description, variant = 'inline', image, imageAlt, eyebrowClass } = Astro.props;
22
+ ---
23
+
24
+ {variant === 'inline' ? (
25
+ <a
26
+ href={href}
27
+ class="group not-prose my-8 flex items-center justify-between gap-6 py-4 border-y border-border hover:border-primary/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background transition-colors"
28
+ >
29
+ <div class="min-w-0 flex-1">
30
+ <p class={eyebrowClass ?? 'text-xs font-semibold uppercase tracking-[0.16em] text-primary mb-1'}>
31
+ {eyebrow}
32
+ </p>
33
+ <h4 class="text-base sm:text-lg font-semibold font-display text-foreground leading-snug group-hover:text-primary transition-colors">
34
+ {title}
35
+ </h4>
36
+ {description && (
37
+ <p class="mt-1 text-sm text-muted-foreground leading-relaxed line-clamp-2">{description}</p>
38
+ )}
39
+ </div>
40
+ <Icon
41
+ name="arrow-right"
42
+ class="shrink-0 w-5 h-5 text-primary transition-transform motion-safe:group-hover:translate-x-1"
43
+ aria-hidden="true"
44
+ />
45
+ </a>
46
+ ) : (
47
+ <a
48
+ href={href}
49
+ class="group not-prose my-8 flex flex-col sm:flex-row overflow-hidden rounded-xl border border-border bg-card hover:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background transition-colors"
50
+ >
51
+ <div class="sm:w-2/5 shrink-0 overflow-hidden bg-muted aspect-video sm:aspect-auto">
52
+ {image ? (
53
+ <Picture
54
+ src={image}
55
+ alt={imageAlt ?? title}
56
+ widths={[320, 480]}
57
+ sizes="(min-width: 640px) 40vw, 100vw"
58
+ formats={['avif', 'webp']}
59
+ fallbackFormat="webp"
60
+ class="w-full h-full object-cover"
61
+ loading="lazy"
62
+ />
63
+ ) : (
64
+ <div class="w-full h-full min-h-[160px] sm:min-h-full bg-muted" />
65
+ )}
66
+ </div>
67
+
68
+ <div class="flex flex-col justify-center gap-2 p-5 sm:p-6 min-w-0 flex-1">
69
+ <p class={eyebrowClass ?? 'text-xs font-medium uppercase tracking-[0.16em] text-primary'}>
70
+ {eyebrow}
71
+ </p>
72
+ <h4 class="text-lg font-semibold font-display text-foreground leading-snug group-hover:text-primary transition-colors">
73
+ {title}
74
+ </h4>
75
+ {description && (
76
+ <p class="text-sm text-muted-foreground leading-relaxed line-clamp-2">{description}</p>
77
+ )}
78
+ <div class="flex justify-end mt-1">
79
+ <Icon
80
+ name="arrow-right"
81
+ class="w-4 h-4 text-primary transition-transform motion-safe:group-hover:translate-x-1"
82
+ aria-hidden="true"
83
+ />
84
+ </div>
85
+ </div>
86
+ </a>
87
+ )}
@@ -0,0 +1,18 @@
1
+ ---
2
+ import type { TrustedHtml } from '../../types';
3
+
4
+ interface Props {
5
+ figure: TrustedHtml;
6
+ caption: TrustedHtml;
7
+ }
8
+ const { figure, caption } = Astro.props;
9
+ ---
10
+
11
+ <figure class="my-16 sm:my-20 text-center">
12
+ <span class="block mx-auto h-px w-12 bg-primary/40 mb-6" aria-hidden="true"></span>
13
+ <p
14
+ class="font-display text-5xl sm:text-6xl text-primary leading-none tracking-[-0.03em] tabular-nums"
15
+ set:html={figure}
16
+ />
17
+ <figcaption class="mt-5 text-sm leading-relaxed text-muted-foreground max-w-md mx-auto" set:html={caption} />
18
+ </figure>
@@ -0,0 +1,34 @@
1
+ ---
2
+ import type { TrustedHtml } from '../../types';
3
+
4
+ interface TimelineEvent {
5
+ date: string;
6
+ label: TrustedHtml;
7
+ description?: TrustedHtml;
8
+ }
9
+ interface Props {
10
+ events: TimelineEvent[];
11
+ /** Figcaption, e.g. "Timeline". */
12
+ label: string;
13
+ }
14
+ const { events, label } = Astro.props;
15
+ ---
16
+
17
+ <figure class="my-8 p-6 sm:p-7 rounded-lg bg-card border border-border">
18
+ <figcaption class="text-xs font-semibold uppercase tracking-[0.18em] text-secondary mb-5">{label}</figcaption>
19
+ <ol class="space-y-4">
20
+ {events.map((event) => (
21
+ <li class="grid grid-cols-[5rem_1fr] sm:grid-cols-[7rem_1fr] gap-x-4 gap-y-1">
22
+ <span class="text-sm font-semibold font-display text-secondary tabular-nums leading-snug pt-0.5">
23
+ {event.date}
24
+ </span>
25
+ <div>
26
+ <p class="text-sm sm:text-base font-semibold text-foreground leading-snug" set:html={event.label} />
27
+ {event.description && (
28
+ <p class="mt-1 text-sm text-muted-foreground leading-relaxed" set:html={event.description} />
29
+ )}
30
+ </div>
31
+ </li>
32
+ ))}
33
+ </ol>
34
+ </figure>
@@ -0,0 +1,40 @@
1
+ ---
2
+ /**
3
+ * Tldr — end-of-section summary: a prose paragraph (`content`) or a "key takeaways" list (`bullets`).
4
+ * An authored `title` wins over the shape's label.
5
+ */
6
+ import type { TrustedHtml } from '../../types';
7
+
8
+ interface Props {
9
+ content?: TrustedHtml;
10
+ bullets?: TrustedHtml[];
11
+ title?: string;
12
+ /** Heading for the bullets shape, e.g. "Key takeaways". */
13
+ bulletsLabel: string;
14
+ /** Heading for the prose shape, e.g. "In short". */
15
+ proseLabel: string;
16
+ }
17
+ const { content, bullets = [], title, bulletsLabel, proseLabel } = Astro.props;
18
+ const hasBullets = bullets.length > 0;
19
+ const heading = title ?? (hasBullets ? bulletsLabel : proseLabel);
20
+ ---
21
+
22
+ <aside class="my-8 p-5 sm:p-6 rounded-lg bg-muted border border-border">
23
+ <p class="inline-block text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground mb-3 pb-1 border-b border-accent">
24
+ {heading}
25
+ </p>
26
+ {hasBullets ? (
27
+ /* list-none + an absolute dot: the aside sits inside .article-body, whose `ul` rule would add its own
28
+ padding and marker. */
29
+ <ul class="m-0 list-none space-y-2.5 p-0">
30
+ {bullets.map((b) => (
31
+ <li class="relative pl-6 text-sm sm:text-base leading-relaxed text-foreground">
32
+ <span class="absolute left-0 top-[0.62em] block h-1.5 w-1.5 rounded-full bg-primary" aria-hidden="true"></span>
33
+ <span set:html={b} />
34
+ </li>
35
+ ))}
36
+ </ul>
37
+ ) : (
38
+ <p class="text-sm sm:text-base leading-relaxed text-foreground" set:html={content} />
39
+ )}
40
+ </aside>
@@ -0,0 +1,36 @@
1
+ import type { InterludeType } from '@sarimarcus/content-sites-core/interludes';
2
+ import type { TrustedHtml } from '../../types';
3
+
4
+ /** One authored interlude as the content layer returns it. String fields are markdown until `renderInline` runs. */
5
+ export type InterludeData =
6
+ | { type: 'quote'; variant: 'pull' | 'external'; content: string; attribution?: { name: string; role?: string } }
7
+ | { type: 'stat-fact'; figure: string; caption: string }
8
+ | { type: 'evidence-context'; finding: string; basis: string; boundary: string }
9
+ | { type: 'tldr'; content?: string; bullets?: string[]; title?: string }
10
+ | { type: 'answer-box'; question: string; answer: string }
11
+ | { type: 'field-note'; content: string }
12
+ | { type: 'caution'; variant: 'tip' | 'warning' | 'info'; title?: string; content: string }
13
+ | { type: 'checklist'; title: string; items: string[] }
14
+ | { type: 'did-you-know'; content: string }
15
+ | { type: 'timeline'; events: Array<{ date: string; label: string; description?: string }> }
16
+ | { type: 'comparison-table'; headers: string[]; rows: string[][] }
17
+ /** Entity interludes: the site resolves the target and renders RelatedCard itself. */
18
+ | { type: 'related-card' | 'monument-feature' | 'article-feature' };
19
+
20
+ // Fails to compile when core's union gains or loses a tag this type does not mirror.
21
+ type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
22
+ const mirrorsCore: Same<InterludeData['type'], InterludeType> = true;
23
+ void mirrorsCore;
24
+
25
+ /** Every string the interludes print. */
26
+ export interface InterludeLabels {
27
+ answerBox: string;
28
+ evidence: { title: string; finding: string; basis: string; boundary: string };
29
+ fieldNote: string;
30
+ timeline: string;
31
+ tldr: { bullets: string; prose: string };
32
+ didYouKnow: string;
33
+ }
34
+
35
+ /** The site's inline markdown renderer: links resolved against its base path, footnote markers bounded. */
36
+ export type RenderInline = (markdown: string) => TrustedHtml;
@@ -0,0 +1,68 @@
1
+ import type { ImageMetadata } from 'astro';
2
+ import type { TrustedHtml } from '../types';
3
+ import type { SourceInput } from '../seo/types';
4
+
5
+ /** A person as listed on author strips and cards. The byline shape is ui/seo `ArticleAuthor`. */
6
+ export interface AuthorSummary {
7
+ name: string;
8
+ /** Profile URL, base path already applied. */
9
+ href: string;
10
+ /** Optimised portrait, or a public URL with the base path applied. */
11
+ image?: ImageMetadata | string;
12
+ }
13
+
14
+ /** One article as a card or grid entry. Dates arrive formatted, so the package never picks a time zone. */
15
+ export interface ArticleSummary {
16
+ href: string;
17
+ title: string;
18
+ description: string;
19
+ author?: string;
20
+ image?: ImageMetadata;
21
+ imageAlt?: string;
22
+ /** ISO value for `<time datetime>`: dateModified when `updated`, else datePublished. */
23
+ dateTime?: string;
24
+ dateLabel?: string;
25
+ updated?: boolean;
26
+ /** Pre-composed reading-time text, e.g. "6 min read". */
27
+ readingLabel?: string;
28
+ }
29
+
30
+ /** A key-fact tile. `detailHtml` is parsed by the site, so footnote markers resolve. */
31
+ export interface KeyFact {
32
+ figure: string;
33
+ label: string;
34
+ detailHtml?: TrustedHtml;
35
+ }
36
+
37
+ export type SourceItem = SourceInput;
38
+
39
+ /** One category chip in an article topic nav. */
40
+ export interface ArticleCategoryLink {
41
+ href: string;
42
+ label: string;
43
+ icon?: string;
44
+ count: number;
45
+ active: boolean;
46
+ }
47
+
48
+ export type InsiderTipTone = 'primary' | 'accent' | 'secondary' | 'muted';
49
+
50
+ /** Site-defined tip kinds: the key stored in content, its badge label and its tone. */
51
+ export type InsiderTipKinds = Record<string, { label: string; tone: InsiderTipTone }>;
52
+
53
+ export interface InsiderTip {
54
+ kind: string;
55
+ title: string;
56
+ bodyHtml: TrustedHtml;
57
+ }
58
+
59
+ export interface InsiderTipAuthor {
60
+ name: string;
61
+ href?: string;
62
+ /** Portrait URL, base path already applied. */
63
+ photoSrc?: string;
64
+ portraitAlt: string;
65
+ /** Pre-joined expertise line. */
66
+ expertise?: string;
67
+ credentials?: string;
68
+ }