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.
Files changed (38) hide show
  1. package/components/BlockRenderer.astro +6 -0
  2. package/components/Pagination.astro +51 -0
  3. package/components/blocks/Heading.astro +47 -46
  4. package/components/blocks/Prose.astro +10 -2
  5. package/components/blocks/Quote.astro +40 -38
  6. package/components/recipes/Accordion.astro +91 -84
  7. package/components/recipes/ContactCard.astro +172 -170
  8. package/components/recipes/Document.astro +152 -0
  9. package/components/recipes/FeatureCards.astro +112 -111
  10. package/components/recipes/Hero.astro +120 -8
  11. package/components/recipes/LinkRow.astro +91 -0
  12. package/components/recipes/MediaCards.astro +213 -212
  13. package/components/recipes/Notice.astro +90 -89
  14. package/components/recipes/Stats.astro +152 -145
  15. package/components/recipes/Table.astro +98 -97
  16. package/components/recipes/Tabs.astro +134 -133
  17. package/components/recipes/Tiles.astro +287 -271
  18. package/components/recipes/Timetable.astro +189 -0
  19. package/components/recipes/Video.astro +177 -157
  20. package/content/blocks.ts +68 -2
  21. package/content/cookies.ts +10 -0
  22. package/examples/primitives.ts +2 -0
  23. package/examples/recipes.ts +42 -36
  24. package/integration.mjs +83 -64
  25. package/layouts/Base.astro +481 -475
  26. package/lib/documents.ts +68 -62
  27. package/lib/embed-consent.ts +78 -73
  28. package/lib/inline-markdown.ts +12 -0
  29. package/lib/links.ts +12 -0
  30. package/lib/paginate.ts +23 -0
  31. package/lib/posts.ts +20 -2
  32. package/lib/timetable.ts +27 -0
  33. package/lib/video.ts +101 -56
  34. package/package.json +1 -1
  35. package/routes/cookie-policy.astro +27 -0
  36. package/routes/posts/index.astro +36 -31
  37. package/routes/posts/page/[page].astro +36 -0
  38. 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">&larr; Newer</a> : <span class="pager__link is-off" aria-hidden="true">&larr; Newer</span>}
20
+ <span class="pager__status">Page {page} of {pages}</span>
21
+ {next ? <a class="pager__link" href={next} rel="next">Older &rarr;</a> : <span class="pager__link is-off" aria-hidden="true">Older &rarr;</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
- line-height: 1.2;
25
- text-align: inherit;
26
- }
27
- .b-heading[data-align='start'] {
28
- text-align: start;
29
- }
30
- .b-heading[data-align='center'] {
31
- text-align: center;
32
- }
33
- .b-heading[data-align='end'] {
34
- text-align: end;
35
- }
36
- /* Gradient text — only clip to text where supported, so it never renders as
37
- invisible/transparent on older browsers (it stays solid ink there). */
38
- @supports ((-webkit-background-clip: text) or (background-clip: text)) {
39
- .b-heading[data-gradient='true'] {
40
- background-image: linear-gradient(120deg, var(--gold), var(--accent-2, var(--gold)));
41
- -webkit-background-clip: text;
42
- background-clip: text;
43
- color: transparent;
44
- }
45
- }
46
- </style>
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
- interface Props {
3
- text: string;
4
- cite?: string;
5
- }
6
- const { text, cite } = Astro.props;
7
- ---
8
-
9
- <figure class="b-quote">
10
- <blockquote>{text}</blockquote>
11
- {cite && <figcaption>— {cite}</figcaption>}
12
- </figure>
13
-
14
- <style>
15
- .b-quote {
16
- margin: 0;
17
- border-inline-start: 3px solid var(--gold);
18
- padding-inline-start: 1rem;
19
- }
20
- /* On a centred page/section the left rule reads as off-axis — drop it so the
21
- quote centres cleanly. (Inherits text-align from the content-align cascade.) */
22
- :global([data-content-align='center']) .b-quote {
23
- border-inline-start: none;
24
- padding-inline-start: 0;
25
- }
26
- .b-quote blockquote {
27
- margin: 0;
28
- color: var(--fg);
29
- font-style: italic;
30
- font-size: 1.125rem;
31
- line-height: 1.6;
32
- }
33
- .b-quote figcaption {
34
- color: var(--muted);
35
- margin-top: 0.5rem;
36
- font-size: 0.9375rem;
37
- }
38
- </style>
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
- interface Props {
8
- title?: string;
9
- items?: { heading: string; body: string }[];
10
- }
11
- const { title, items = [] } = Astro.props;
12
- ---
13
-
14
- <section class="b-accordion">
15
- {title && <Heading text={title} level={2} align="center" />}
16
- <div class="b-accordion__list">
17
- {items.map((item) => (
18
- <details class="b-accordion__item">
19
- <summary class="b-accordion__head">{item.heading}</summary>
20
- <div class="b-accordion__body"><p>{item.body}</p></div>
21
- </details>
22
- ))}
23
- </div>
24
- </section>
25
-
26
- <style>
27
- .b-accordion {
28
- display: flex;
29
- flex-direction: column;
30
- gap: 1.5rem;
31
- padding-inline: 1rem;
32
- }
33
- .b-accordion__list {
34
- max-width: var(--max);
35
- margin-inline: auto;
36
- width: 100%;
37
- display: flex;
38
- flex-direction: column;
39
- gap: 0.5rem;
40
- }
41
- .b-accordion__item {
42
- border: var(--card-border);
43
- border-radius: var(--radius-sm);
44
- padding: 0.25rem 1rem;
45
- background: var(--bg-surface);
46
- }
47
- .b-accordion__head {
48
- cursor: pointer;
49
- font-weight: 600;
50
- color: var(--fg);
51
- padding: 0.85rem 0;
52
- list-style: none;
53
- display: flex;
54
- align-items: center;
55
- justify-content: space-between;
56
- gap: 0.75rem;
57
- }
58
- .b-accordion__head::-webkit-details-marker {
59
- display: none;
60
- }
61
- .b-accordion__head::after {
62
- content: '+';
63
- color: var(--gold);
64
- font-weight: 700;
65
- font-size: 1.25rem;
66
- line-height: 1;
67
- transition: transform 0.2s ease;
68
- }
69
- .b-accordion__item[open] .b-accordion__head::after {
70
- content: '\2013'; /* en dash */
71
- }
72
- .b-accordion__head:focus-visible {
73
- outline: 2px solid var(--gold);
74
- outline-offset: 2px;
75
- }
76
- .b-accordion__body {
77
- padding: 0 0 0.9rem;
78
- color: var(--muted);
79
- line-height: 1.6;
80
- }
81
- .b-accordion__body p {
82
- margin: 0;
83
- }
84
- </style>
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>