ferst-core 0.7.6 → 0.7.8
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/bin/ferst-modules.mjs +107 -0
- package/components/BlockRenderer.astro +125 -117
- package/components/Button.astro +1 -1
- package/components/Card.astro +2 -0
- package/components/ProseParts.astro +26 -0
- package/components/SiteHeader.astro +24 -0
- package/components/blocks/Prose.astro +12 -17
- package/components/recipes/Accordion.astro +91 -91
- package/components/recipes/Bento.astro +7 -3
- package/components/recipes/ContactForm.astro +6 -3
- package/components/recipes/Cta.astro +5 -1
- package/components/recipes/Document.astro +7 -11
- package/components/recipes/Faq.astro +5 -1
- package/components/recipes/FeatureCards.astro +115 -112
- package/components/recipes/Form.astro +30 -0
- package/components/recipes/Hero.astro +502 -495
- package/components/recipes/LatestPosts.astro +1 -1
- package/components/recipes/MediaCards.astro +214 -213
- package/components/recipes/MediaText.astro +88 -92
- package/components/recipes/Notice.astro +90 -90
- package/components/recipes/Pricing.astro +7 -4
- package/components/recipes/Stats.astro +155 -152
- package/components/recipes/Steps.astro +124 -121
- package/components/recipes/Table.astro +3 -3
- package/components/recipes/Tabs.astro +138 -134
- package/components/recipes/Tiles.astro +291 -287
- package/content/blocks.ts +943 -895
- package/layouts/Base.astro +495 -484
- package/lib/forms.ts +57 -0
- package/lib/slug.ts +1 -1
- package/package.json +70 -69
|
@@ -1,91 +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
|
-
import
|
|
8
|
-
interface Props {
|
|
9
|
-
title?: string;
|
|
10
|
-
items?: { heading: string; body: string }[];
|
|
11
|
-
}
|
|
12
|
-
const { title, items = [] } = Astro.props;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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>
|
|
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 '../ProseParts.astro';
|
|
8
|
+
interface Props {
|
|
9
|
+
title?: string;
|
|
10
|
+
items?: { heading: string; body: string }[];
|
|
11
|
+
}
|
|
12
|
+
const { title, items = [] } = Astro.props;
|
|
13
|
+
// Alignment overrides (BlockLayout): the title + intro default to centred; the block's
|
|
14
|
+
// own `align`, or `headingAlign` for the title alone, overrides it from the page data.
|
|
15
|
+
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
16
|
+
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<section class="b-accordion">
|
|
20
|
+
{title && <Heading text={title} level={2} align={headAlign} />}
|
|
21
|
+
<div class="b-accordion__list">
|
|
22
|
+
{items.map((item) => (
|
|
23
|
+
<details class="b-accordion__item">
|
|
24
|
+
<summary class="b-accordion__head">{item.heading}</summary>
|
|
25
|
+
<div class="b-accordion__body">
|
|
26
|
+
<ProseParts text={item.body} />
|
|
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 :global(p) {
|
|
89
|
+
margin: 0;
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
@@ -23,12 +23,16 @@ interface Props {
|
|
|
23
23
|
items?: Item[];
|
|
24
24
|
}
|
|
25
25
|
const { title, intro, items = [] } = Astro.props;
|
|
26
|
+
// Alignment overrides (BlockLayout): the title + intro default to centred; the block's
|
|
27
|
+
// own `align`, or `headingAlign` for the title alone, overrides it from the page data.
|
|
28
|
+
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
29
|
+
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
26
30
|
---
|
|
27
31
|
|
|
28
32
|
<section class="b-bento">
|
|
29
33
|
{(title || intro) && (
|
|
30
|
-
<div class="b-bento__head">
|
|
31
|
-
{title && <Heading text={title} level={2}
|
|
34
|
+
<div class="b-bento__head" data-recipe-head data-content-align={headAlign}>
|
|
35
|
+
{title && <Heading text={title} level={2} />}
|
|
32
36
|
{intro && <p class="b-bento__intro">{intro}</p>}
|
|
33
37
|
</div>
|
|
34
38
|
)}
|
|
@@ -62,7 +66,6 @@ const { title, intro, items = [] } = Astro.props;
|
|
|
62
66
|
.b-bento__head {
|
|
63
67
|
max-width: var(--max);
|
|
64
68
|
margin-inline: auto;
|
|
65
|
-
text-align: center;
|
|
66
69
|
display: flex;
|
|
67
70
|
flex-direction: column;
|
|
68
71
|
gap: 0.75rem;
|
|
@@ -91,6 +94,7 @@ const { title, intro, items = [] } = Astro.props;
|
|
|
91
94
|
position: relative;
|
|
92
95
|
display: flex;
|
|
93
96
|
flex-direction: column;
|
|
97
|
+
align-items: var(--content-items, stretch); /* follows the page / section alignment */
|
|
94
98
|
gap: 0.4rem;
|
|
95
99
|
padding: 1.5rem;
|
|
96
100
|
border-radius: var(--radius);
|
|
@@ -37,12 +37,16 @@ const {
|
|
|
37
37
|
note,
|
|
38
38
|
fields = [],
|
|
39
39
|
} = Astro.props;
|
|
40
|
+
// Alignment overrides (BlockLayout): the title + intro default to centred; the block's
|
|
41
|
+
// own `align`, or `headingAlign` for the title alone, overrides it from the page data.
|
|
42
|
+
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
43
|
+
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
40
44
|
---
|
|
41
45
|
|
|
42
46
|
<section class="b-contact-form">
|
|
43
47
|
{(title || intro) && (
|
|
44
|
-
<div class="b-contact-form__head">
|
|
45
|
-
{title && <Heading text={title} level={2}
|
|
48
|
+
<div class="b-contact-form__head" data-recipe-head data-content-align={headAlign}>
|
|
49
|
+
{title && <Heading text={title} level={2} />}
|
|
46
50
|
{intro && <p class="b-contact-form__intro">{intro}</p>}
|
|
47
51
|
</div>
|
|
48
52
|
)}
|
|
@@ -81,7 +85,6 @@ const {
|
|
|
81
85
|
padding-inline: 1rem;
|
|
82
86
|
}
|
|
83
87
|
.b-contact-form__head {
|
|
84
|
-
text-align: center;
|
|
85
88
|
display: flex;
|
|
86
89
|
flex-direction: column;
|
|
87
90
|
gap: 0.75rem;
|
|
@@ -11,11 +11,15 @@ interface Props {
|
|
|
11
11
|
background?: 'muted' | 'accent';
|
|
12
12
|
}
|
|
13
13
|
const { title, text, cta, background = 'accent' } = Astro.props;
|
|
14
|
+
// Alignment overrides (BlockLayout): the title + intro default to centred; the block's
|
|
15
|
+
// own `align`, or `headingAlign` for the title alone, overrides it from the page data.
|
|
16
|
+
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
17
|
+
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
14
18
|
---
|
|
15
19
|
|
|
16
20
|
<section class="b-cta" data-bg={background}>
|
|
17
21
|
<div class="b-cta__inner">
|
|
18
|
-
<Heading text={title} level={2} align=
|
|
22
|
+
<Heading text={title} level={2} align={headAlign} />
|
|
19
23
|
{text && <p class="b-cta__text">{text}</p>}
|
|
20
24
|
<div class="b-cta__action">
|
|
21
25
|
<Button href={cta.href} variant="primary">{cta.label}</Button>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (links, bold, lists); `items` add a list. Print-friendly: the contents list is
|
|
7
7
|
* dropped on paper.
|
|
8
8
|
*/
|
|
9
|
-
import
|
|
9
|
+
import ProseParts from '../ProseParts.astro';
|
|
10
10
|
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
11
11
|
import { longDate } from '../../lib/documents';
|
|
12
12
|
import { pageSlugs, uniqueSlug } from '../../lib/slug';
|
|
@@ -44,11 +44,7 @@ const ids = sections.map((s) => uniqueSlug(s.title, used));
|
|
|
44
44
|
{sections.map((s, i) => (
|
|
45
45
|
<section class="b-doc__clause" id={ids[i]}>
|
|
46
46
|
<h2 id={`clause-${i + 1}`}>{numbered && <span class="b-doc__num">{i + 1}.</span>} {s.title}</h2>
|
|
47
|
-
{
|
|
48
|
-
part.kind === 'p' ? <p set:html={part.html} />
|
|
49
|
-
: part.kind === 'ul' ? <ul>{part.items.map((x) => <li set:html={x} />)}</ul>
|
|
50
|
-
: <ol>{part.items.map((x) => <li set:html={x} />)}</ol>
|
|
51
|
-
)}
|
|
47
|
+
<ProseParts text={s.body} />
|
|
52
48
|
{s.items && s.items.length > 0 && <ul>{s.items.map((x) => <li set:html={inlineMarkdown(x)} />)}</ul>}
|
|
53
49
|
</section>
|
|
54
50
|
))}
|
|
@@ -135,14 +131,14 @@ const ids = sections.map((s) => uniqueSlug(s.title, used));
|
|
|
135
131
|
color: var(--gold);
|
|
136
132
|
font-variant-numeric: tabular-nums;
|
|
137
133
|
}
|
|
138
|
-
.b-doc__clause p,
|
|
139
|
-
.b-doc__clause ul,
|
|
140
|
-
.b-doc__clause ol {
|
|
134
|
+
.b-doc__clause :global(p),
|
|
135
|
+
.b-doc__clause :global(ul),
|
|
136
|
+
.b-doc__clause :global(ol) {
|
|
141
137
|
margin: 0 0 0.75rem;
|
|
142
138
|
line-height: 1.7;
|
|
143
139
|
}
|
|
144
|
-
.b-doc__clause ul,
|
|
145
|
-
.b-doc__clause ol {
|
|
140
|
+
.b-doc__clause :global(ul),
|
|
141
|
+
.b-doc__clause :global(ol) {
|
|
146
142
|
padding-inline-start: 1.4em;
|
|
147
143
|
}
|
|
148
144
|
.b-doc__clause :global(a) {
|
|
@@ -10,13 +10,17 @@ interface Props {
|
|
|
10
10
|
items?: { question: string; answer: string }[];
|
|
11
11
|
}
|
|
12
12
|
const { title, items = [] } = Astro.props;
|
|
13
|
+
// Alignment overrides (BlockLayout): the title + intro default to centred; the block's
|
|
14
|
+
// own `align`, or `headingAlign` for the title alone, overrides it from the page data.
|
|
15
|
+
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
16
|
+
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
13
17
|
// Answers may carry a link or bold via a tiny markdown subset (inlineMarkdown),
|
|
14
18
|
// split into paragraphs on blank lines. Trusted editor/CMS text, sanitised.
|
|
15
19
|
const paragraphs = (answer: string) => answer.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
|
|
16
20
|
---
|
|
17
21
|
|
|
18
22
|
<section class="b-faq">
|
|
19
|
-
{title && <Heading text={title} level={2} align=
|
|
23
|
+
{title && <Heading text={title} level={2} align={headAlign} />}
|
|
20
24
|
<div class="b-faq__list">
|
|
21
25
|
{items.map((item) => (
|
|
22
26
|
<details class="b-faq__item">
|
|
@@ -1,112 +1,115 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* FeatureCards — a Level-2 recipe: an optional section header over a responsive
|
|
4
|
-
* grid of cards, each with an optional icon, a title, text, and an optional
|
|
5
|
-
* link. Composes the Card, Icon, Heading and Button primitives.
|
|
6
|
-
*/
|
|
7
|
-
import Heading from '../blocks/Heading.astro';
|
|
8
|
-
import Card from '../Card.astro';
|
|
9
|
-
import Icon from '../Icon.astro';
|
|
10
|
-
import Button from '../Button.astro';
|
|
11
|
-
import { isIconKey } from '../../lib/icons';
|
|
12
|
-
|
|
13
|
-
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
14
|
-
interface Props {
|
|
15
|
-
title?: string;
|
|
16
|
-
intro?: string;
|
|
17
|
-
columns?: 2 | 3 | 4;
|
|
18
|
-
cards?: { icon?: string; title: string; text?: string; href?: string }[];
|
|
19
|
-
}
|
|
20
|
-
const { title, intro, columns = 3, cards = [] } = Astro.props;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{card.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* FeatureCards — a Level-2 recipe: an optional section header over a responsive
|
|
4
|
+
* grid of cards, each with an optional icon, a title, text, and an optional
|
|
5
|
+
* link. Composes the Card, Icon, Heading and Button primitives.
|
|
6
|
+
*/
|
|
7
|
+
import Heading from '../blocks/Heading.astro';
|
|
8
|
+
import Card from '../Card.astro';
|
|
9
|
+
import Icon from '../Icon.astro';
|
|
10
|
+
import Button from '../Button.astro';
|
|
11
|
+
import { isIconKey } from '../../lib/icons';
|
|
12
|
+
|
|
13
|
+
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
14
|
+
interface Props {
|
|
15
|
+
title?: string;
|
|
16
|
+
intro?: string;
|
|
17
|
+
columns?: 2 | 3 | 4;
|
|
18
|
+
cards?: { icon?: string; title: string; text?: string; href?: string }[];
|
|
19
|
+
}
|
|
20
|
+
const { title, intro, columns = 3, cards = [] } = Astro.props;
|
|
21
|
+
// Alignment overrides (BlockLayout): the title + intro default to centred; the block's
|
|
22
|
+
// own `align`, or `headingAlign` for the title alone, overrides it from the page data.
|
|
23
|
+
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
24
|
+
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
25
|
+
// Never reserve more columns than there are cards (a lone card in a 3-col grid
|
|
26
|
+
// would strand two empty cells); `data-single` centres a solitary card.
|
|
27
|
+
const cols = Math.min(columns, Math.max(cards.length, 1)) as 1 | 2 | 3 | 4;
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<section class="b-feature-cards">
|
|
31
|
+
{(title || intro) && (
|
|
32
|
+
<div class="b-feature-cards__head" data-recipe-head data-content-align={headAlign}>
|
|
33
|
+
{title && <Heading text={title} level={2} />}
|
|
34
|
+
{intro && <p class="b-feature-cards__intro" set:html={inlineMarkdown(intro)} />}
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
37
|
+
<div class="b-feature-cards__grid" data-cols={cols} data-single={cards.length === 1 ? 'true' : undefined}>
|
|
38
|
+
{cards.map((card) => (
|
|
39
|
+
<Card>
|
|
40
|
+
{card.icon && isIconKey(card.icon) && (
|
|
41
|
+
<span class="b-feature-cards__icon"><Icon name={card.icon} size={28} /></span>
|
|
42
|
+
)}
|
|
43
|
+
<h3 class="b-feature-cards__title">{card.title}</h3>
|
|
44
|
+
{card.text && <p class="b-feature-cards__text" set:html={inlineMarkdown(card.text)} />}
|
|
45
|
+
{card.href && (
|
|
46
|
+
<span class="b-feature-cards__cta">
|
|
47
|
+
<Button href={card.href} variant="secondary" size="sm">Learn more</Button>
|
|
48
|
+
</span>
|
|
49
|
+
)}
|
|
50
|
+
</Card>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
|
|
55
|
+
<style>
|
|
56
|
+
.b-feature-cards {
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
gap: 2rem;
|
|
60
|
+
max-width: 72rem;
|
|
61
|
+
margin-inline: auto;
|
|
62
|
+
padding-inline: 1rem;
|
|
63
|
+
}
|
|
64
|
+
.b-feature-cards__head {
|
|
65
|
+
max-width: var(--max);
|
|
66
|
+
margin-inline: auto;
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
gap: 0.75rem;
|
|
70
|
+
}
|
|
71
|
+
.b-feature-cards__intro {
|
|
72
|
+
margin: 0;
|
|
73
|
+
color: var(--muted);
|
|
74
|
+
font-size: 1.0625rem;
|
|
75
|
+
line-height: 1.6;
|
|
76
|
+
}
|
|
77
|
+
.b-feature-cards__grid {
|
|
78
|
+
display: grid;
|
|
79
|
+
grid-template-columns: 1fr; /* mobile-first */
|
|
80
|
+
gap: 1.5rem;
|
|
81
|
+
}
|
|
82
|
+
@media (min-width: 48em) {
|
|
83
|
+
.b-feature-cards__grid[data-cols='2'] { grid-template-columns: repeat(2, 1fr); }
|
|
84
|
+
.b-feature-cards__grid[data-cols='3'] { grid-template-columns: repeat(3, 1fr); }
|
|
85
|
+
.b-feature-cards__grid[data-cols='4'] { grid-template-columns: repeat(2, 1fr); }
|
|
86
|
+
}
|
|
87
|
+
/* A solitary card is a centred feature, not a full-width slab. */
|
|
88
|
+
.b-feature-cards__grid[data-single] { max-width: 26rem; margin-inline: auto;
|
|
89
|
+
}
|
|
90
|
+
@media (min-width: 64em) {
|
|
91
|
+
.b-feature-cards__grid[data-cols='4'] { grid-template-columns: repeat(4, 1fr); }
|
|
92
|
+
}
|
|
93
|
+
.b-feature-cards__icon {
|
|
94
|
+
color: var(--gold);
|
|
95
|
+
margin-bottom: 0.5rem;
|
|
96
|
+
display: inline-flex;
|
|
97
|
+
}
|
|
98
|
+
.b-feature-cards__title {
|
|
99
|
+
margin: 0 0 0.5rem;
|
|
100
|
+
font-size: 1.125rem;
|
|
101
|
+
color: var(--fg);
|
|
102
|
+
}
|
|
103
|
+
.b-feature-cards__text {
|
|
104
|
+
margin: 0;
|
|
105
|
+
color: var(--muted);
|
|
106
|
+
line-height: 1.6;
|
|
107
|
+
}
|
|
108
|
+
/* Push the CTA to the bottom so a row of cards aligns its buttons regardless
|
|
109
|
+
of how much text each card has. Cards are equal height already (grid items
|
|
110
|
+
stretch), and Card is a flex column — margin-top:auto does the rest. */
|
|
111
|
+
.b-feature-cards__cta {
|
|
112
|
+
margin-top: auto;
|
|
113
|
+
padding-top: 1rem;
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Form — one of the organisation's forms from the Ferst portal, placed by id. Reads the
|
|
4
|
+
* form's fields, button and submit address from the platform at build time (lib/forms)
|
|
5
|
+
* and renders it with the ContactForm recipe, so it looks like every other form and a
|
|
6
|
+
* change made in the portal reaches the site on its next build.
|
|
7
|
+
*/
|
|
8
|
+
import ContactForm from './ContactForm.astro';
|
|
9
|
+
import { loadFormDefinition } from '../../lib/forms';
|
|
10
|
+
import { resolveGitBase } from '../../bin/cms-config.mjs';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
form: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
intro?: string;
|
|
16
|
+
note?: string;
|
|
17
|
+
}
|
|
18
|
+
const { form, title, intro, note } = Astro.props;
|
|
19
|
+
const def = await loadFormDefinition(form, { base: resolveGitBase() });
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<ContactForm
|
|
23
|
+
title={title ?? def.name}
|
|
24
|
+
intro={intro}
|
|
25
|
+
action={def.action}
|
|
26
|
+
method="post"
|
|
27
|
+
submitLabel={def.submitLabel}
|
|
28
|
+
note={note}
|
|
29
|
+
fields={def.fields}
|
|
30
|
+
/>
|