ferst-core 0.7.7 → 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 +18 -2
- package/components/BlockRenderer.astro +125 -119
- package/components/Button.astro +1 -1
- package/components/Card.astro +2 -0
- package/components/SiteHeader.astro +24 -0
- package/components/recipes/Accordion.astro +91 -87
- package/components/recipes/Bento.astro +7 -3
- package/components/recipes/ContactForm.astro +6 -3
- package/components/recipes/Cta.astro +5 -1
- package/components/recipes/Faq.astro +5 -1
- package/components/recipes/FeatureCards.astro +115 -112
- package/components/recipes/Hero.astro +502 -495
- package/components/recipes/LatestPosts.astro +1 -1
- package/components/recipes/MediaCards.astro +214 -213
- 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/Tabs.astro +138 -134
- package/components/recipes/Tiles.astro +291 -287
- package/content/blocks.ts +75 -49
- package/layouts/Base.astro +495 -484
- package/lib/slug.ts +1 -1
- package/package.json +1 -1
|
@@ -198,7 +198,7 @@ const isNew = (date: Date | string) => isRecent(date, newWithinDays, nowMs);
|
|
|
198
198
|
}
|
|
199
199
|
/* Category chip — the post's tags, joined, as a small brand-tinted pill. */
|
|
200
200
|
.b-post-row__cat {
|
|
201
|
-
align-self: flex-start;
|
|
201
|
+
align-self: var(--content-self, flex-start);
|
|
202
202
|
max-width: 100%;
|
|
203
203
|
padding: 0.1rem 0.5rem;
|
|
204
204
|
border-radius: var(--radius-pill);
|
|
@@ -1,213 +1,214 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* MediaCards — a Level-2 recipe: a grid of rich content cards with the IMAGE as a
|
|
4
|
-
* header on top (cover-cropped to a uniform ratio, like PostCard), then an optional
|
|
5
|
-
* icon badge, a title, a meta line (e.g. "Sundays 10:45 · Main hall"), a description, and
|
|
6
|
-
* an optional CTA link. The image-HEADER pattern (distinct from `tiles`, which puts
|
|
7
|
-
* the image BEHIND the text): use it for groups, team members, services or events —
|
|
8
|
-
* anywhere the photo and the copy both need to be fully legible. Whole card links
|
|
9
|
-
* when `href` is set. Token-driven, equal-height.
|
|
10
|
-
*/
|
|
11
|
-
import Icon from '../Icon.astro';
|
|
12
|
-
import { isIconKey } from '../../lib/icons';
|
|
13
|
-
import Heading from '../blocks/Heading.astro';
|
|
14
|
-
|
|
15
|
-
import { inlineMarkdown, inlineMarkdownNoLinks } from '../../lib/inline-markdown';
|
|
16
|
-
interface Card {
|
|
17
|
-
image?: { src: string; alt?: string };
|
|
18
|
-
icon?: string;
|
|
19
|
-
title: string;
|
|
20
|
-
meta?: string;
|
|
21
|
-
text?: string;
|
|
22
|
-
href?: string;
|
|
23
|
-
linkLabel?: string;
|
|
24
|
-
}
|
|
25
|
-
interface Props {
|
|
26
|
-
title?: string;
|
|
27
|
-
intro?: string;
|
|
28
|
-
columns?: 2 | 3;
|
|
29
|
-
cards?: Card[];
|
|
30
|
-
}
|
|
31
|
-
const { title, intro, columns = 3, cards = [] } = Astro.props;
|
|
32
|
-
// Never reserve more columns than there are cards; `data-single` centres a lone one.
|
|
33
|
-
const cols = Math.min(columns, Math.max(cards.length, 1)) as 1 | 2 | 3;
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
<section class="b-media-cards">
|
|
37
|
-
{(title || intro) && (
|
|
38
|
-
<div class="b-media-cards__head">
|
|
39
|
-
{title && <Heading text={title} level={2} />}
|
|
40
|
-
{intro && <p class="b-media-cards__intro" set:html={inlineMarkdown(intro)} />}
|
|
41
|
-
</div>
|
|
42
|
-
)}
|
|
43
|
-
<div class="b-media-cards__grid" data-cols={cols} data-single={cards.length === 1 ? 'true' : undefined}>
|
|
44
|
-
{cards.map((card) => {
|
|
45
|
-
// Surface a mailto:/tel: as visible, copyable text — a plain mailto does
|
|
46
|
-
// nothing on a device with no mail app, so the address must be readable too.
|
|
47
|
-
const email = card.href?.startsWith('mailto:') ? card.href.slice(7).split('?')[0] : null;
|
|
48
|
-
const phone = card.href?.startsWith('tel:') ? card.href.slice(4) : null;
|
|
49
|
-
return (
|
|
50
|
-
<article class:list={['b-media-cards__card', 'ferst-tile', 'ferst-tile--sm', card.href && 'is-link']}>
|
|
51
|
-
{card.image && (
|
|
52
|
-
<div class="b-media-cards__media">
|
|
53
|
-
<img src={card.image.src} alt={card.image.alt ?? ''} loading="lazy" />
|
|
54
|
-
</div>
|
|
55
|
-
)}
|
|
56
|
-
<div class="b-media-cards__body">
|
|
57
|
-
{card.icon && isIconKey(card.icon) && (
|
|
58
|
-
<span class="b-media-cards__badge" aria-hidden="true"><Icon name={card.icon} size={20} /></span>
|
|
59
|
-
)}
|
|
60
|
-
<h3 class="b-media-cards__title">
|
|
61
|
-
{card.href ? <a href={card.href}>{card.title}</a> : card.title}
|
|
62
|
-
</h3>
|
|
63
|
-
{card.meta && <p class="b-media-cards__meta">{card.meta}</p>}
|
|
64
|
-
{card.text && <p class="b-media-cards__text" set:html={card.href ? inlineMarkdownNoLinks(card.text) : inlineMarkdown(card.text)} />}
|
|
65
|
-
{(email || phone) && (
|
|
66
|
-
<span class="b-media-cards__contact">{email ?? phone}</span>
|
|
67
|
-
)}
|
|
68
|
-
{card.href && !email && !phone && (
|
|
69
|
-
<span class="b-media-cards__cta" aria-hidden="true">{card.linkLabel ?? 'Learn more'} →</span>
|
|
70
|
-
)}
|
|
71
|
-
</div>
|
|
72
|
-
</article>
|
|
73
|
-
);
|
|
74
|
-
})}
|
|
75
|
-
</div>
|
|
76
|
-
</section>
|
|
77
|
-
|
|
78
|
-
<style>
|
|
79
|
-
.b-media-cards {
|
|
80
|
-
display: flex;
|
|
81
|
-
flex-direction: column;
|
|
82
|
-
gap: 2rem;
|
|
83
|
-
max-width: 72rem;
|
|
84
|
-
margin-inline: auto;
|
|
85
|
-
padding-inline: 1rem;
|
|
86
|
-
}
|
|
87
|
-
.b-media-cards__head {
|
|
88
|
-
display: flex;
|
|
89
|
-
flex-direction: column;
|
|
90
|
-
gap: 0.75rem;
|
|
91
|
-
}
|
|
92
|
-
.b-media-cards__intro {
|
|
93
|
-
margin: 0;
|
|
94
|
-
color: var(--muted);
|
|
95
|
-
font-size: 1.0625rem;
|
|
96
|
-
line-height: 1.6;
|
|
97
|
-
max-width: var(--max);
|
|
98
|
-
}
|
|
99
|
-
.b-media-cards__grid {
|
|
100
|
-
display: grid;
|
|
101
|
-
grid-template-columns: 1fr;
|
|
102
|
-
gap: 1.5rem;
|
|
103
|
-
}
|
|
104
|
-
/* A solitary card is a centred feature, not a full-width slab. */
|
|
105
|
-
.b-media-cards__grid[data-single] { max-width: 28rem; margin-inline: auto; }
|
|
106
|
-
@media (min-width: 40em) {
|
|
107
|
-
.b-media-cards__grid[data-cols='2'] { grid-template-columns: repeat(2, 1fr); }
|
|
108
|
-
.b-media-cards__grid[data-cols='3'] { grid-template-columns: repeat(2, 1fr); }
|
|
109
|
-
}
|
|
110
|
-
@media (min-width: 60em) {
|
|
111
|
-
.b-media-cards__grid[data-cols='3'] { grid-template-columns: repeat(3, 1fr); }
|
|
112
|
-
}
|
|
113
|
-
.b-media-cards__card {
|
|
114
|
-
position: relative;
|
|
115
|
-
display: flex;
|
|
116
|
-
flex-direction: column;
|
|
117
|
-
/* surface comes from .ferst-tile .ferst-tile--sm (bg/border/radius/shadow-sm) */
|
|
118
|
-
overflow: hidden;
|
|
119
|
-
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
|
120
|
-
}
|
|
121
|
-
.b-media-cards__card.is-link:hover {
|
|
122
|
-
transform: translateY(-2px);
|
|
123
|
-
box-shadow: var(--shadow-md);
|
|
124
|
-
border-color: var(--gold);
|
|
125
|
-
}
|
|
126
|
-
/* Image header — cover-cropped to a uniform 16:9 so any source ratio fits (the
|
|
127
|
-
PostCard technique; also out-specifies the global img{height:auto}). */
|
|
128
|
-
.b-media-cards__media {
|
|
129
|
-
position: relative;
|
|
130
|
-
aspect-ratio: 16 / 9;
|
|
131
|
-
overflow: hidden;
|
|
132
|
-
background: var(--bg-section);
|
|
133
|
-
}
|
|
134
|
-
.b-media-cards .b-media-cards__media img {
|
|
135
|
-
position: absolute;
|
|
136
|
-
inset: 0;
|
|
137
|
-
width: 100%;
|
|
138
|
-
height: 100%;
|
|
139
|
-
object-fit: cover;
|
|
140
|
-
display: block;
|
|
141
|
-
}
|
|
142
|
-
.b-media-cards__body {
|
|
143
|
-
display: flex;
|
|
144
|
-
flex-direction: column;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
margin-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
.b-media-cards__title a { color: var(--
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.b-media-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
font-
|
|
187
|
-
font-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
font-
|
|
200
|
-
font-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
font-
|
|
209
|
-
font-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* MediaCards — a Level-2 recipe: a grid of rich content cards with the IMAGE as a
|
|
4
|
+
* header on top (cover-cropped to a uniform ratio, like PostCard), then an optional
|
|
5
|
+
* icon badge, a title, a meta line (e.g. "Sundays 10:45 · Main hall"), a description, and
|
|
6
|
+
* an optional CTA link. The image-HEADER pattern (distinct from `tiles`, which puts
|
|
7
|
+
* the image BEHIND the text): use it for groups, team members, services or events —
|
|
8
|
+
* anywhere the photo and the copy both need to be fully legible. Whole card links
|
|
9
|
+
* when `href` is set. Token-driven, equal-height.
|
|
10
|
+
*/
|
|
11
|
+
import Icon from '../Icon.astro';
|
|
12
|
+
import { isIconKey } from '../../lib/icons';
|
|
13
|
+
import Heading from '../blocks/Heading.astro';
|
|
14
|
+
|
|
15
|
+
import { inlineMarkdown, inlineMarkdownNoLinks } from '../../lib/inline-markdown';
|
|
16
|
+
interface Card {
|
|
17
|
+
image?: { src: string; alt?: string };
|
|
18
|
+
icon?: string;
|
|
19
|
+
title: string;
|
|
20
|
+
meta?: string;
|
|
21
|
+
text?: string;
|
|
22
|
+
href?: string;
|
|
23
|
+
linkLabel?: string;
|
|
24
|
+
}
|
|
25
|
+
interface Props {
|
|
26
|
+
title?: string;
|
|
27
|
+
intro?: string;
|
|
28
|
+
columns?: 2 | 3;
|
|
29
|
+
cards?: Card[];
|
|
30
|
+
}
|
|
31
|
+
const { title, intro, columns = 3, cards = [] } = Astro.props;
|
|
32
|
+
// Never reserve more columns than there are cards; `data-single` centres a lone one.
|
|
33
|
+
const cols = Math.min(columns, Math.max(cards.length, 1)) as 1 | 2 | 3;
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<section class="b-media-cards">
|
|
37
|
+
{(title || intro) && (
|
|
38
|
+
<div class="b-media-cards__head">
|
|
39
|
+
{title && <Heading text={title} level={2} />}
|
|
40
|
+
{intro && <p class="b-media-cards__intro" set:html={inlineMarkdown(intro)} />}
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
<div class="b-media-cards__grid" data-cols={cols} data-single={cards.length === 1 ? 'true' : undefined}>
|
|
44
|
+
{cards.map((card) => {
|
|
45
|
+
// Surface a mailto:/tel: as visible, copyable text — a plain mailto does
|
|
46
|
+
// nothing on a device with no mail app, so the address must be readable too.
|
|
47
|
+
const email = card.href?.startsWith('mailto:') ? card.href.slice(7).split('?')[0] : null;
|
|
48
|
+
const phone = card.href?.startsWith('tel:') ? card.href.slice(4) : null;
|
|
49
|
+
return (
|
|
50
|
+
<article class:list={['b-media-cards__card', 'ferst-tile', 'ferst-tile--sm', card.href && 'is-link']}>
|
|
51
|
+
{card.image && (
|
|
52
|
+
<div class="b-media-cards__media">
|
|
53
|
+
<img src={card.image.src} alt={card.image.alt ?? ''} loading="lazy" />
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
<div class="b-media-cards__body">
|
|
57
|
+
{card.icon && isIconKey(card.icon) && (
|
|
58
|
+
<span class="b-media-cards__badge" aria-hidden="true"><Icon name={card.icon} size={20} /></span>
|
|
59
|
+
)}
|
|
60
|
+
<h3 class="b-media-cards__title">
|
|
61
|
+
{card.href ? <a href={card.href}>{card.title}</a> : card.title}
|
|
62
|
+
</h3>
|
|
63
|
+
{card.meta && <p class="b-media-cards__meta">{card.meta}</p>}
|
|
64
|
+
{card.text && <p class="b-media-cards__text" set:html={card.href ? inlineMarkdownNoLinks(card.text) : inlineMarkdown(card.text)} />}
|
|
65
|
+
{(email || phone) && (
|
|
66
|
+
<span class="b-media-cards__contact">{email ?? phone}</span>
|
|
67
|
+
)}
|
|
68
|
+
{card.href && !email && !phone && (
|
|
69
|
+
<span class="b-media-cards__cta" aria-hidden="true">{card.linkLabel ?? 'Learn more'} →</span>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</article>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.b-media-cards {
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
gap: 2rem;
|
|
83
|
+
max-width: 72rem;
|
|
84
|
+
margin-inline: auto;
|
|
85
|
+
padding-inline: 1rem;
|
|
86
|
+
}
|
|
87
|
+
.b-media-cards__head {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
gap: 0.75rem;
|
|
91
|
+
}
|
|
92
|
+
.b-media-cards__intro {
|
|
93
|
+
margin: 0;
|
|
94
|
+
color: var(--muted);
|
|
95
|
+
font-size: 1.0625rem;
|
|
96
|
+
line-height: 1.6;
|
|
97
|
+
max-width: var(--max);
|
|
98
|
+
}
|
|
99
|
+
.b-media-cards__grid {
|
|
100
|
+
display: grid;
|
|
101
|
+
grid-template-columns: 1fr;
|
|
102
|
+
gap: 1.5rem;
|
|
103
|
+
}
|
|
104
|
+
/* A solitary card is a centred feature, not a full-width slab. */
|
|
105
|
+
.b-media-cards__grid[data-single] { max-width: 28rem; margin-inline: auto; }
|
|
106
|
+
@media (min-width: 40em) {
|
|
107
|
+
.b-media-cards__grid[data-cols='2'] { grid-template-columns: repeat(2, 1fr); }
|
|
108
|
+
.b-media-cards__grid[data-cols='3'] { grid-template-columns: repeat(2, 1fr); }
|
|
109
|
+
}
|
|
110
|
+
@media (min-width: 60em) {
|
|
111
|
+
.b-media-cards__grid[data-cols='3'] { grid-template-columns: repeat(3, 1fr); }
|
|
112
|
+
}
|
|
113
|
+
.b-media-cards__card {
|
|
114
|
+
position: relative;
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-direction: column;
|
|
117
|
+
/* surface comes from .ferst-tile .ferst-tile--sm (bg/border/radius/shadow-sm) */
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
|
120
|
+
}
|
|
121
|
+
.b-media-cards__card.is-link:hover {
|
|
122
|
+
transform: translateY(-2px);
|
|
123
|
+
box-shadow: var(--shadow-md);
|
|
124
|
+
border-color: var(--gold);
|
|
125
|
+
}
|
|
126
|
+
/* Image header — cover-cropped to a uniform 16:9 so any source ratio fits (the
|
|
127
|
+
PostCard technique; also out-specifies the global img{height:auto}). */
|
|
128
|
+
.b-media-cards__media {
|
|
129
|
+
position: relative;
|
|
130
|
+
aspect-ratio: 16 / 9;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
background: var(--bg-section);
|
|
133
|
+
}
|
|
134
|
+
.b-media-cards .b-media-cards__media img {
|
|
135
|
+
position: absolute;
|
|
136
|
+
inset: 0;
|
|
137
|
+
width: 100%;
|
|
138
|
+
height: 100%;
|
|
139
|
+
object-fit: cover;
|
|
140
|
+
display: block;
|
|
141
|
+
}
|
|
142
|
+
.b-media-cards__body {
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-direction: column;
|
|
145
|
+
align-items: var(--content-items, stretch); /* follows the page / section alignment */
|
|
146
|
+
gap: 0.5rem;
|
|
147
|
+
padding: 1.1rem 1.2rem 1.3rem;
|
|
148
|
+
flex: 1;
|
|
149
|
+
}
|
|
150
|
+
/* Icon badge — a small brand chip; slightly lifted over the image edge when an
|
|
151
|
+
image is present (the negative margin only bites inside a card with a header). */
|
|
152
|
+
.b-media-cards__badge {
|
|
153
|
+
display: inline-flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
justify-content: center;
|
|
156
|
+
width: 2.25rem;
|
|
157
|
+
height: 2.25rem;
|
|
158
|
+
border-radius: 999px;
|
|
159
|
+
background: var(--gold-light);
|
|
160
|
+
color: var(--gold);
|
|
161
|
+
margin-top: -2.3rem;
|
|
162
|
+
margin-bottom: 0.2rem;
|
|
163
|
+
border: 3px solid var(--bg-surface);
|
|
164
|
+
}
|
|
165
|
+
/* No image → the badge sits normally at the top of the body. */
|
|
166
|
+
.b-media-cards__card:not(:has(.b-media-cards__media)) .b-media-cards__badge {
|
|
167
|
+
margin-top: 0;
|
|
168
|
+
border: none;
|
|
169
|
+
}
|
|
170
|
+
.b-media-cards__title {
|
|
171
|
+
margin: 0;
|
|
172
|
+
font-size: 1.2rem;
|
|
173
|
+
color: var(--fg);
|
|
174
|
+
}
|
|
175
|
+
.b-media-cards__card.is-link:hover .b-media-cards__title a { color: var(--gold); }
|
|
176
|
+
.b-media-cards__title a { color: var(--fg); }
|
|
177
|
+
/* Stretch the title link over the whole card so it's clickable. */
|
|
178
|
+
.b-media-cards__title a::after { content: ''; position: absolute; inset: 0; }
|
|
179
|
+
.b-media-cards__meta {
|
|
180
|
+
margin: 0;
|
|
181
|
+
align-self: var(--content-self, flex-start); /* a pill keeps its width, follows the alignment */
|
|
182
|
+
padding: 0.2rem 0.6rem;
|
|
183
|
+
border-radius: 999px;
|
|
184
|
+
background: var(--bg-section);
|
|
185
|
+
color: var(--muted);
|
|
186
|
+
font-family: var(--font-heading);
|
|
187
|
+
font-size: 0.72rem;
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
letter-spacing: 0.03em;
|
|
190
|
+
}
|
|
191
|
+
.b-media-cards__text {
|
|
192
|
+
margin: 0;
|
|
193
|
+
color: var(--muted);
|
|
194
|
+
line-height: 1.6;
|
|
195
|
+
}
|
|
196
|
+
.b-media-cards__cta {
|
|
197
|
+
margin-top: auto;
|
|
198
|
+
padding-top: 0.5rem;
|
|
199
|
+
font-family: var(--font-heading);
|
|
200
|
+
font-size: 0.85rem;
|
|
201
|
+
font-weight: 600;
|
|
202
|
+
color: var(--gold);
|
|
203
|
+
}
|
|
204
|
+
/* A mailto/tel shown as visible, copyable text (below any description). */
|
|
205
|
+
.b-media-cards__contact {
|
|
206
|
+
margin-top: auto;
|
|
207
|
+
padding-top: 0.5rem;
|
|
208
|
+
font-family: var(--font-heading);
|
|
209
|
+
font-size: 0.85rem;
|
|
210
|
+
font-weight: 600;
|
|
211
|
+
color: var(--gold);
|
|
212
|
+
overflow-wrap: anywhere;
|
|
213
|
+
}
|
|
214
|
+
</style>
|
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Notice — a Level-2 recipe: an inline highlighted info box (optional icon +
|
|
4
|
-
* title + body + optional link) that sets a passage apart from the surrounding
|
|
5
|
-
* prose — a "reporting a concern", "please note", "how to book" callout. Two
|
|
6
|
-
* tones (accent / neutral) in keeping with badge/announcement; token-driven, so
|
|
7
|
-
* it follows the brand and both themes. The page-level sibling of the corner
|
|
8
|
-
* StickyCallout (this one sits in the content flow).
|
|
9
|
-
*/
|
|
10
|
-
import Icon from '../Icon.astro';
|
|
11
|
-
import { isIconKey } from '../../lib/icons';
|
|
12
|
-
|
|
13
|
-
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
14
|
-
interface Props {
|
|
15
|
-
tone?: 'accent' | 'neutral';
|
|
16
|
-
icon?: string;
|
|
17
|
-
title?: string;
|
|
18
|
-
text: string;
|
|
19
|
-
link?: { label: string; href: string };
|
|
20
|
-
}
|
|
21
|
-
const { tone = 'accent', icon, title, text, link } = Astro.props;
|
|
22
|
-
const paras = text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
<aside class="b-notice" data-tone={tone}>
|
|
26
|
-
{icon && isIconKey(icon) && (
|
|
27
|
-
<span class="b-notice__icon" aria-hidden="true"><Icon name={icon} size={22} /></span>
|
|
28
|
-
)}
|
|
29
|
-
<div class="b-notice__body">
|
|
30
|
-
{title && <p class="b-notice__title">{title}</p>}
|
|
31
|
-
{paras.map((p) => <p class="b-notice__text" set:html={inlineMarkdown(p)} />)}
|
|
32
|
-
{link && <a class="b-notice__link" href={link.href}>{link.label}</a>}
|
|
33
|
-
</div>
|
|
34
|
-
</aside>
|
|
35
|
-
|
|
36
|
-
<style>
|
|
37
|
-
.b-notice {
|
|
38
|
-
display: flex;
|
|
39
|
-
gap: 0.85rem;
|
|
40
|
-
align-items: flex-start;
|
|
41
|
-
max-width: var(--max);
|
|
42
|
-
margin-inline: auto;
|
|
43
|
-
padding: 1.1rem 1.25rem;
|
|
44
|
-
border-radius: var(--radius);
|
|
45
|
-
border-inline-start: 4px solid var(--gold);
|
|
46
|
-
}
|
|
47
|
-
.b-notice[data-tone='accent'] {
|
|
48
|
-
background: var(--gold-light);
|
|
49
|
-
border-inline-start-color: var(--gold);
|
|
50
|
-
}
|
|
51
|
-
.b-notice[data-tone='neutral'] {
|
|
52
|
-
background: var(--bg-section);
|
|
53
|
-
border-inline-start-color: var(--accent);
|
|
54
|
-
}
|
|
55
|
-
/* On a centred page, keep the box a readable width but centre it as a block;
|
|
56
|
-
text inside stays left (a centred info box reads badly). */
|
|
57
|
-
:global([data-content-align='center']) .b-notice {
|
|
58
|
-
text-align: start;
|
|
59
|
-
}
|
|
60
|
-
.b-notice__icon {
|
|
61
|
-
display: inline-flex;
|
|
62
|
-
color: var(--gold);
|
|
63
|
-
margin-top: 0.1rem;
|
|
64
|
-
flex-shrink: 0;
|
|
65
|
-
}
|
|
66
|
-
.b-notice__body {
|
|
67
|
-
display: flex;
|
|
68
|
-
flex-direction: column;
|
|
69
|
-
gap: 0.4rem;
|
|
70
|
-
min-width: 0;
|
|
71
|
-
}
|
|
72
|
-
.b-notice__title {
|
|
73
|
-
margin: 0;
|
|
74
|
-
font-family: var(--font-heading);
|
|
75
|
-
font-weight: 600;
|
|
76
|
-
font-size: 1.05rem;
|
|
77
|
-
color: var(--fg);
|
|
78
|
-
}
|
|
79
|
-
.b-notice__text {
|
|
80
|
-
margin: 0;
|
|
81
|
-
color: var(--muted);
|
|
82
|
-
line-height: 1.6;
|
|
83
|
-
}
|
|
84
|
-
.b-notice__link {
|
|
85
|
-
margin-top: 0.1rem;
|
|
86
|
-
align-self: flex-start;
|
|
87
|
-
color: var(--gold);
|
|
88
|
-
font-weight: 600;
|
|
89
|
-
}
|
|
90
|
-
</style>
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Notice — a Level-2 recipe: an inline highlighted info box (optional icon +
|
|
4
|
+
* title + body + optional link) that sets a passage apart from the surrounding
|
|
5
|
+
* prose — a "reporting a concern", "please note", "how to book" callout. Two
|
|
6
|
+
* tones (accent / neutral) in keeping with badge/announcement; token-driven, so
|
|
7
|
+
* it follows the brand and both themes. The page-level sibling of the corner
|
|
8
|
+
* StickyCallout (this one sits in the content flow).
|
|
9
|
+
*/
|
|
10
|
+
import Icon from '../Icon.astro';
|
|
11
|
+
import { isIconKey } from '../../lib/icons';
|
|
12
|
+
|
|
13
|
+
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
14
|
+
interface Props {
|
|
15
|
+
tone?: 'accent' | 'neutral';
|
|
16
|
+
icon?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
text: string;
|
|
19
|
+
link?: { label: string; href: string };
|
|
20
|
+
}
|
|
21
|
+
const { tone = 'accent', icon, title, text, link } = Astro.props;
|
|
22
|
+
const paras = text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<aside class="b-notice" data-tone={tone}>
|
|
26
|
+
{icon && isIconKey(icon) && (
|
|
27
|
+
<span class="b-notice__icon" aria-hidden="true"><Icon name={icon} size={22} /></span>
|
|
28
|
+
)}
|
|
29
|
+
<div class="b-notice__body">
|
|
30
|
+
{title && <p class="b-notice__title">{title}</p>}
|
|
31
|
+
{paras.map((p) => <p class="b-notice__text" set:html={inlineMarkdown(p)} />)}
|
|
32
|
+
{link && <a class="b-notice__link" href={link.href}>{link.label}</a>}
|
|
33
|
+
</div>
|
|
34
|
+
</aside>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
.b-notice {
|
|
38
|
+
display: flex;
|
|
39
|
+
gap: 0.85rem;
|
|
40
|
+
align-items: flex-start;
|
|
41
|
+
max-width: var(--max);
|
|
42
|
+
margin-inline: auto;
|
|
43
|
+
padding: 1.1rem 1.25rem;
|
|
44
|
+
border-radius: var(--radius);
|
|
45
|
+
border-inline-start: 4px solid var(--gold);
|
|
46
|
+
}
|
|
47
|
+
.b-notice[data-tone='accent'] {
|
|
48
|
+
background: var(--gold-light);
|
|
49
|
+
border-inline-start-color: var(--gold);
|
|
50
|
+
}
|
|
51
|
+
.b-notice[data-tone='neutral'] {
|
|
52
|
+
background: var(--bg-section);
|
|
53
|
+
border-inline-start-color: var(--accent);
|
|
54
|
+
}
|
|
55
|
+
/* On a centred page, keep the box a readable width but centre it as a block;
|
|
56
|
+
text inside stays left (a centred info box reads badly). */
|
|
57
|
+
:global([data-content-align='center']) .b-notice {
|
|
58
|
+
text-align: start;
|
|
59
|
+
}
|
|
60
|
+
.b-notice__icon {
|
|
61
|
+
display: inline-flex;
|
|
62
|
+
color: var(--gold);
|
|
63
|
+
margin-top: 0.1rem;
|
|
64
|
+
flex-shrink: 0;
|
|
65
|
+
}
|
|
66
|
+
.b-notice__body {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
gap: 0.4rem;
|
|
70
|
+
min-width: 0;
|
|
71
|
+
}
|
|
72
|
+
.b-notice__title {
|
|
73
|
+
margin: 0;
|
|
74
|
+
font-family: var(--font-heading);
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
font-size: 1.05rem;
|
|
77
|
+
color: var(--fg);
|
|
78
|
+
}
|
|
79
|
+
.b-notice__text {
|
|
80
|
+
margin: 0;
|
|
81
|
+
color: var(--muted);
|
|
82
|
+
line-height: 1.6;
|
|
83
|
+
}
|
|
84
|
+
.b-notice__link {
|
|
85
|
+
margin-top: 0.1rem;
|
|
86
|
+
align-self: var(--content-self, flex-start);
|
|
87
|
+
color: var(--gold);
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
@@ -21,12 +21,16 @@ interface Props {
|
|
|
21
21
|
plans?: Plan[];
|
|
22
22
|
}
|
|
23
23
|
const { title, intro, plans = [] } = Astro.props;
|
|
24
|
+
// Alignment overrides (BlockLayout): the title + intro default to centred; the block's
|
|
25
|
+
// own `align`, or `headingAlign` for the title alone, overrides it from the page data.
|
|
26
|
+
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
27
|
+
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
24
28
|
---
|
|
25
29
|
|
|
26
30
|
<section class="b-pricing">
|
|
27
31
|
{(title || intro) && (
|
|
28
|
-
<div class="b-pricing__head">
|
|
29
|
-
{title && <Heading text={title} level={2}
|
|
32
|
+
<div class="b-pricing__head" data-recipe-head data-content-align={headAlign}>
|
|
33
|
+
{title && <Heading text={title} level={2} />}
|
|
30
34
|
{intro && <p class="b-pricing__intro">{intro}</p>}
|
|
31
35
|
</div>
|
|
32
36
|
)}
|
|
@@ -69,7 +73,6 @@ const { title, intro, plans = [] } = Astro.props;
|
|
|
69
73
|
.b-pricing__head {
|
|
70
74
|
max-width: var(--max);
|
|
71
75
|
margin-inline: auto;
|
|
72
|
-
text-align: center;
|
|
73
76
|
display: flex;
|
|
74
77
|
flex-direction: column;
|
|
75
78
|
gap: 0.75rem;
|
|
@@ -103,7 +106,7 @@ const { title, intro, plans = [] } = Astro.props;
|
|
|
103
106
|
position: relative;
|
|
104
107
|
}
|
|
105
108
|
.b-pricing__badge {
|
|
106
|
-
align-self: flex-start;
|
|
109
|
+
align-self: var(--content-self, flex-start);
|
|
107
110
|
font-size: 0.7rem;
|
|
108
111
|
font-weight: 700;
|
|
109
112
|
letter-spacing: 0.08em;
|