ferst-core 0.7.0 → 0.7.4
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/components/BlockRenderer.astro +6 -0
- package/components/Pagination.astro +51 -0
- package/components/blocks/Heading.astro +47 -46
- package/components/blocks/Prose.astro +10 -2
- package/components/blocks/Quote.astro +40 -38
- package/components/recipes/Accordion.astro +91 -84
- package/components/recipes/ContactCard.astro +172 -170
- package/components/recipes/Document.astro +152 -0
- package/components/recipes/FeatureCards.astro +112 -111
- package/components/recipes/Hero.astro +120 -8
- package/components/recipes/LinkRow.astro +91 -0
- package/components/recipes/MediaCards.astro +213 -212
- package/components/recipes/Notice.astro +90 -89
- package/components/recipes/Stats.astro +152 -145
- package/components/recipes/Table.astro +98 -97
- package/components/recipes/Tabs.astro +134 -133
- package/components/recipes/Tiles.astro +287 -271
- package/components/recipes/Timetable.astro +189 -0
- package/components/recipes/Video.astro +177 -157
- package/content/blocks.ts +68 -2
- package/content/cookies.ts +10 -0
- package/examples/primitives.ts +2 -0
- package/examples/recipes.ts +42 -36
- package/integration.mjs +83 -64
- package/layouts/Base.astro +481 -475
- package/lib/documents.ts +68 -62
- package/lib/embed-consent.ts +78 -73
- package/lib/inline-markdown.ts +12 -0
- package/lib/links.ts +12 -0
- package/lib/paginate.ts +23 -0
- package/lib/posts.ts +20 -2
- package/lib/timetable.ts +27 -0
- package/lib/video.ts +101 -56
- package/package.json +1 -1
- package/routes/cookie-policy.astro +27 -0
- package/routes/posts/index.astro +36 -31
- package/routes/posts/page/[page].astro +36 -0
- package/routes/posts/tag/[...tag].astro +41 -37
|
@@ -46,6 +46,9 @@ import ContactCard from './recipes/ContactCard.astro';
|
|
|
46
46
|
import Locations from './recipes/Locations.astro';
|
|
47
47
|
import Video from './recipes/Video.astro';
|
|
48
48
|
import Documents from './recipes/Documents.astro';
|
|
49
|
+
import LinkRow from './recipes/LinkRow.astro';
|
|
50
|
+
import DocumentRecipe from './recipes/Document.astro';
|
|
51
|
+
import Timetable from './recipes/Timetable.astro';
|
|
49
52
|
|
|
50
53
|
interface Props {
|
|
51
54
|
blocks: Block[];
|
|
@@ -94,6 +97,9 @@ const registry: Record<BlockType, any> = {
|
|
|
94
97
|
locations: Locations,
|
|
95
98
|
video: Video,
|
|
96
99
|
documents: Documents,
|
|
100
|
+
linkRow: LinkRow,
|
|
101
|
+
document: DocumentRecipe,
|
|
102
|
+
timetable: Timetable,
|
|
97
103
|
};
|
|
98
104
|
---
|
|
99
105
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Pagination for a post listing: "Newer" / "Older" links (rel prev/next) around
|
|
4
|
+
* "Page 2 of 8". Renders nothing when there's only one page.
|
|
5
|
+
*/
|
|
6
|
+
import { pagerLinks } from '../lib/paginate';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
base: string;
|
|
10
|
+
page: number;
|
|
11
|
+
pages: number;
|
|
12
|
+
}
|
|
13
|
+
const { base, page, pages } = Astro.props;
|
|
14
|
+
const { prev, next } = pagerLinks(base, page, pages);
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
{pages > 1 && (
|
|
18
|
+
<nav class="pager" aria-label="Pages">
|
|
19
|
+
{prev ? <a class="pager__link" href={prev} rel="prev">← Newer</a> : <span class="pager__link is-off" aria-hidden="true">← Newer</span>}
|
|
20
|
+
<span class="pager__status">Page {page} of {pages}</span>
|
|
21
|
+
{next ? <a class="pager__link" href={next} rel="next">Older →</a> : <span class="pager__link is-off" aria-hidden="true">Older →</span>}
|
|
22
|
+
</nav>
|
|
23
|
+
)}
|
|
24
|
+
|
|
25
|
+
<style>
|
|
26
|
+
.pager {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
gap: 1rem;
|
|
31
|
+
padding-top: 1rem;
|
|
32
|
+
border-top: var(--card-border);
|
|
33
|
+
}
|
|
34
|
+
.pager__link {
|
|
35
|
+
font-weight: 600;
|
|
36
|
+
color: var(--gold);
|
|
37
|
+
text-decoration: none;
|
|
38
|
+
}
|
|
39
|
+
.pager__link:hover {
|
|
40
|
+
text-decoration: underline;
|
|
41
|
+
}
|
|
42
|
+
.pager__link.is-off {
|
|
43
|
+
color: var(--muted);
|
|
44
|
+
opacity: 0.5;
|
|
45
|
+
}
|
|
46
|
+
.pager__status {
|
|
47
|
+
color: var(--muted);
|
|
48
|
+
font-size: 0.9375rem;
|
|
49
|
+
font-variant-numeric: tabular-nums;
|
|
50
|
+
}
|
|
51
|
+
</style>
|
|
@@ -1,46 +1,47 @@
|
|
|
1
|
-
---
|
|
2
|
-
interface Props {
|
|
3
|
-
text: string;
|
|
4
|
-
level?: number;
|
|
5
|
-
align?: 'start' | 'center' | 'end';
|
|
6
|
-
gradient?: boolean;
|
|
7
|
-
}
|
|
8
|
-
const { text, level = 2, align, gradient = false } = Astro.props;
|
|
9
|
-
const Tag = `h${level}` as unknown as keyof HTMLElementTagNameMap;
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
<Tag class="b-heading" data-align={align} data-gradient={gradient ? 'true' : 'false'}>{text}</Tag>
|
|
13
|
-
|
|
14
|
-
<style>
|
|
15
|
-
/* By default a heading INHERITS the page/section content alignment (the
|
|
16
|
-
`--content-align` cascade set on `.site-main` / a `section`), so a whole
|
|
17
|
-
template stays coherent. An explicit `align` prop overrides it for this one
|
|
18
|
-
heading. (`text-align` inherits, and inheritance carries the resolved
|
|
19
|
-
alignment; the `[data-align]` rules win over it when set.) */
|
|
20
|
-
.b-heading {
|
|
21
|
-
color: var(--fg);
|
|
22
|
-
margin: 0;
|
|
23
|
-
text-wrap: balance;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
background-clip: text;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
text: string;
|
|
4
|
+
level?: number;
|
|
5
|
+
align?: 'start' | 'center' | 'end';
|
|
6
|
+
gradient?: boolean;
|
|
7
|
+
}
|
|
8
|
+
const { text, level = 2, align, gradient = false } = Astro.props;
|
|
9
|
+
const Tag = `h${level}` as unknown as keyof HTMLElementTagNameMap;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<Tag class="b-heading" data-align={align} data-gradient={gradient ? 'true' : 'false'}>{text}</Tag>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
/* By default a heading INHERITS the page/section content alignment (the
|
|
16
|
+
`--content-align` cascade set on `.site-main` / a `section`), so a whole
|
|
17
|
+
template stays coherent. An explicit `align` prop overrides it for this one
|
|
18
|
+
heading. (`text-align` inherits, and inheritance carries the resolved
|
|
19
|
+
alignment; the `[data-align]` rules win over it when set.) */
|
|
20
|
+
.b-heading {
|
|
21
|
+
color: var(--fg);
|
|
22
|
+
margin: 0;
|
|
23
|
+
text-wrap: balance;
|
|
24
|
+
white-space: pre-line; /* a title typed over several lines keeps its breaks */
|
|
25
|
+
line-height: 1.2;
|
|
26
|
+
text-align: inherit;
|
|
27
|
+
}
|
|
28
|
+
.b-heading[data-align='start'] {
|
|
29
|
+
text-align: start;
|
|
30
|
+
}
|
|
31
|
+
.b-heading[data-align='center'] {
|
|
32
|
+
text-align: center;
|
|
33
|
+
}
|
|
34
|
+
.b-heading[data-align='end'] {
|
|
35
|
+
text-align: end;
|
|
36
|
+
}
|
|
37
|
+
/* Gradient text — only clip to text where supported, so it never renders as
|
|
38
|
+
invisible/transparent on older browsers (it stays solid ink there). */
|
|
39
|
+
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
|
|
40
|
+
.b-heading[data-gradient='true'] {
|
|
41
|
+
background-image: linear-gradient(120deg, var(--gold), var(--accent-2, var(--gold)));
|
|
42
|
+
-webkit-background-clip: text;
|
|
43
|
+
background-clip: text;
|
|
44
|
+
color: transparent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
import { proseParts } from '../../lib/prose';
|
|
3
3
|
interface Props {
|
|
4
4
|
text: string;
|
|
5
|
+
variant?: 'body' | 'lead';
|
|
5
6
|
}
|
|
6
|
-
const { text } = Astro.props;
|
|
7
|
+
const { text, variant = 'body' } = Astro.props;
|
|
7
8
|
// Blank lines separate paragraphs; `- ` / `1. ` lines make lists; links, bold and
|
|
8
9
|
// italic are rendered safely (lib/prose.ts → inlineMarkdown).
|
|
9
10
|
const parts = proseParts(text);
|
|
10
11
|
---
|
|
11
12
|
|
|
12
|
-
<div class="b-prose">
|
|
13
|
+
<div class="b-prose" data-variant={variant === 'lead' ? 'lead' : undefined}>
|
|
13
14
|
{parts.map((part) =>
|
|
14
15
|
part.kind === 'p' ? <p set:html={part.html} />
|
|
15
16
|
: part.kind === 'ul' ? <ul>{part.items.map((i) => <li set:html={i} />)}</ul>
|
|
@@ -38,6 +39,13 @@ const parts = proseParts(text);
|
|
|
38
39
|
.b-prose > :last-child {
|
|
39
40
|
margin-bottom: 0;
|
|
40
41
|
}
|
|
42
|
+
/* The page's introduction: larger, quieter, a narrower measure. */
|
|
43
|
+
.b-prose[data-variant='lead'] p {
|
|
44
|
+
font-size: clamp(1.125rem, 1.6vw, 1.3125rem);
|
|
45
|
+
line-height: 1.6;
|
|
46
|
+
color: var(--muted);
|
|
47
|
+
max-width: 40em;
|
|
48
|
+
}
|
|
41
49
|
.b-prose :global(a) {
|
|
42
50
|
color: var(--accent);
|
|
43
51
|
text-decoration: underline;
|
|
@@ -1,38 +1,40 @@
|
|
|
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
|
-
|
|
1
|
+
---
|
|
2
|
+
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
3
|
+
interface Props {
|
|
4
|
+
text: string;
|
|
5
|
+
cite?: string;
|
|
6
|
+
}
|
|
7
|
+
const { text, cite } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<figure class="b-quote">
|
|
11
|
+
<blockquote set:html={inlineMarkdown(text)} />
|
|
12
|
+
{cite && <figcaption>— {cite}</figcaption>}
|
|
13
|
+
</figure>
|
|
14
|
+
|
|
15
|
+
<style>
|
|
16
|
+
/* inline markdown already turns single newlines into <br>: a prayer or verse keeps its lines. */
|
|
17
|
+
.b-quote {
|
|
18
|
+
margin: 0;
|
|
19
|
+
border-inline-start: 3px solid var(--gold);
|
|
20
|
+
padding-inline-start: 1rem;
|
|
21
|
+
}
|
|
22
|
+
/* On a centred page/section the left rule reads as off-axis — drop it so the
|
|
23
|
+
quote centres cleanly. (Inherits text-align from the content-align cascade.) */
|
|
24
|
+
:global([data-content-align='center']) .b-quote {
|
|
25
|
+
border-inline-start: none;
|
|
26
|
+
padding-inline-start: 0;
|
|
27
|
+
}
|
|
28
|
+
.b-quote blockquote {
|
|
29
|
+
margin: 0;
|
|
30
|
+
color: var(--fg);
|
|
31
|
+
font-style: italic;
|
|
32
|
+
font-size: 1.125rem;
|
|
33
|
+
line-height: 1.6;
|
|
34
|
+
}
|
|
35
|
+
.b-quote figcaption {
|
|
36
|
+
color: var(--muted);
|
|
37
|
+
margin-top: 0.5rem;
|
|
38
|
+
font-size: 0.9375rem;
|
|
39
|
+
}
|
|
40
|
+
</style>
|
|
@@ -1,84 +1,91 @@
|
|
|
1
|
-
---
|
|
2
|
-
/** Accordion — a Level-2 recipe: an optional heading over expand/collapse rows
|
|
3
|
-
* (title + body). Generic sibling of FAQ (which is question/answer-flavoured).
|
|
4
|
-
* Native <details>/<summary> — accessible, interactive, zero JavaScript. */
|
|
5
|
-
import Heading from '../blocks/Heading.astro';
|
|
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
|
-
padding: 0.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
content:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
.b-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
1
|
+
---
|
|
2
|
+
/** Accordion — a Level-2 recipe: an optional heading over expand/collapse rows
|
|
3
|
+
* (title + body). Generic sibling of FAQ (which is question/answer-flavoured).
|
|
4
|
+
* Native <details>/<summary> — accessible, interactive, zero JavaScript. */
|
|
5
|
+
import Heading from '../blocks/Heading.astro';
|
|
6
|
+
|
|
7
|
+
import { proseParts } from '../../lib/prose';
|
|
8
|
+
interface Props {
|
|
9
|
+
title?: string;
|
|
10
|
+
items?: { heading: string; body: string }[];
|
|
11
|
+
}
|
|
12
|
+
const { title, items = [] } = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<section class="b-accordion">
|
|
16
|
+
{title && <Heading text={title} level={2} align="center" />}
|
|
17
|
+
<div class="b-accordion__list">
|
|
18
|
+
{items.map((item) => (
|
|
19
|
+
<details class="b-accordion__item">
|
|
20
|
+
<summary class="b-accordion__head">{item.heading}</summary>
|
|
21
|
+
<div class="b-accordion__body">
|
|
22
|
+
{proseParts(item.body).map((part) =>
|
|
23
|
+
part.kind === 'p' ? <p set:html={part.html} />
|
|
24
|
+
: part.kind === 'ul' ? <ul>{part.items.map((x) => <li set:html={x} />)}</ul>
|
|
25
|
+
: <ol>{part.items.map((x) => <li set:html={x} />)}</ol>
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
</details>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
</section>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.b-accordion {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
gap: 1.5rem;
|
|
38
|
+
padding-inline: 1rem;
|
|
39
|
+
}
|
|
40
|
+
.b-accordion__list {
|
|
41
|
+
max-width: var(--max);
|
|
42
|
+
margin-inline: auto;
|
|
43
|
+
width: 100%;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
gap: 0.5rem;
|
|
47
|
+
}
|
|
48
|
+
.b-accordion__item {
|
|
49
|
+
border: var(--card-border);
|
|
50
|
+
border-radius: var(--radius-sm);
|
|
51
|
+
padding: 0.25rem 1rem;
|
|
52
|
+
background: var(--bg-surface);
|
|
53
|
+
}
|
|
54
|
+
.b-accordion__head {
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
color: var(--fg);
|
|
58
|
+
padding: 0.85rem 0;
|
|
59
|
+
list-style: none;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: space-between;
|
|
63
|
+
gap: 0.75rem;
|
|
64
|
+
}
|
|
65
|
+
.b-accordion__head::-webkit-details-marker {
|
|
66
|
+
display: none;
|
|
67
|
+
}
|
|
68
|
+
.b-accordion__head::after {
|
|
69
|
+
content: '+';
|
|
70
|
+
color: var(--gold);
|
|
71
|
+
font-weight: 700;
|
|
72
|
+
font-size: 1.25rem;
|
|
73
|
+
line-height: 1;
|
|
74
|
+
transition: transform 0.2s ease;
|
|
75
|
+
}
|
|
76
|
+
.b-accordion__item[open] .b-accordion__head::after {
|
|
77
|
+
content: '\2013'; /* en dash */
|
|
78
|
+
}
|
|
79
|
+
.b-accordion__head:focus-visible {
|
|
80
|
+
outline: 2px solid var(--gold);
|
|
81
|
+
outline-offset: 2px;
|
|
82
|
+
}
|
|
83
|
+
.b-accordion__body {
|
|
84
|
+
padding: 0 0 0.9rem;
|
|
85
|
+
color: var(--muted);
|
|
86
|
+
line-height: 1.6;
|
|
87
|
+
}
|
|
88
|
+
.b-accordion__body p {
|
|
89
|
+
margin: 0;
|
|
90
|
+
}
|
|
91
|
+
</style>
|