ferst-core 0.7.5 → 0.7.6
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/cms/config.template.yml +13 -0
- package/components/SiteFooter.astro +52 -1
- package/components/SiteHeader.astro +1246 -1243
- package/components/recipes/Contacts.astro +3 -3
- package/components/recipes/Documents.astro +2 -2
- package/components/recipes/Steps.astro +121 -120
- package/components/recipes/Table.astro +2 -1
- package/components/recipes/Video.astro +1 -1
- package/content/blocks.ts +16 -8
- package/content/schemas.ts +5 -0
- package/fonts/OFL.txt +287 -0
- package/fonts/inter-latin-400-normal.woff2 +0 -0
- package/fonts/inter-latin-500-normal.woff2 +0 -0
- package/fonts/inter-latin-600-normal.woff2 +0 -0
- package/fonts/playfair-display-latin-700-normal.woff2 +0 -0
- package/fonts/poppins-latin-300-normal.woff2 +0 -0
- package/fonts/poppins-latin-400-normal.woff2 +0 -0
- package/fonts/poppins-latin-500-normal.woff2 +0 -0
- package/fonts/poppins-latin-600-normal.woff2 +0 -0
- package/fonts/poppins-latin-700-normal.woff2 +0 -0
- package/integration.mjs +41 -1
- package/layouts/Base.astro +1 -0
- package/lib/icons.ts +10 -0
- package/lib/links.ts +24 -22
- package/lib/nav.ts +12 -0
- package/lib/siteSettings.ts +2 -1
- package/package.json +69 -68
|
@@ -11,7 +11,7 @@ import Icon from '../Icon.astro';
|
|
|
11
11
|
import { inlineMarkdown, textWithBreaks } from '../../lib/inline-markdown';
|
|
12
12
|
import { mailtoHref, telHref } from '../../lib/links';
|
|
13
13
|
|
|
14
|
-
interface Contact { role
|
|
14
|
+
interface Contact { role?: string; name?: string; phone?: string; email?: string; emailSubject?: string; address?: string; hours?: string }
|
|
15
15
|
interface Props {
|
|
16
16
|
title?: string;
|
|
17
17
|
intro?: string;
|
|
@@ -38,8 +38,8 @@ const { title, intro, groups = [] } = Astro.props;
|
|
|
38
38
|
<ul class="b-contacts__grid">
|
|
39
39
|
{g.contacts.map((c) => (
|
|
40
40
|
<li class="b-contacts__card">
|
|
41
|
-
<h3 class="b-contacts__role">{c.role}</h3>
|
|
42
|
-
{c.name && <p class="b-contacts__name">{c.name}</p>}
|
|
41
|
+
<h3 class="b-contacts__role">{c.role ?? c.name}</h3>
|
|
42
|
+
{c.role && c.name && <p class="b-contacts__name">{c.name}</p>}
|
|
43
43
|
<div class="b-contacts__lines">
|
|
44
44
|
{c.phone && <a class="b-contacts__line" href={telHref(c.phone)}><Icon name="phone" size={16} /><span>{c.phone}</span></a>}
|
|
45
45
|
{c.email && <a class="b-contacts__line" href={mailtoHref(c.email, c.emailSubject)}><Icon name="email" size={16} /><span>{c.email}</span></a>}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import Heading from '../blocks/Heading.astro';
|
|
9
9
|
import Icon from '../Icon.astro';
|
|
10
10
|
import { documentGroups, type DocumentItem } from '../../lib/documents';
|
|
11
|
-
|
|
11
|
+
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
12
12
|
interface Props {
|
|
13
13
|
title?: string;
|
|
14
14
|
intro?: string;
|
|
@@ -23,7 +23,7 @@ const groups = documentGroups(items, groupByYear);
|
|
|
23
23
|
{(title || intro) && (
|
|
24
24
|
<div class="b-docs__head">
|
|
25
25
|
{title && <Heading text={title} level={2} />}
|
|
26
|
-
{intro && <p class="b-docs__intro"
|
|
26
|
+
{intro && <p class="b-docs__intro" set:html={inlineMarkdown(intro)} />}
|
|
27
27
|
</div>
|
|
28
28
|
)}
|
|
29
29
|
{items.length === 0 ? (
|
|
@@ -1,120 +1,121 @@
|
|
|
1
|
-
---
|
|
2
|
-
/** Steps — a Level-2 recipe: a numbered process. `horizontal` lays connected
|
|
3
|
-
* cards in a row; `vertical` a stacked timeline. Composes Heading + Icon. */
|
|
4
|
-
import Heading from '../blocks/Heading.astro';
|
|
5
|
-
import Icon from '../Icon.astro';
|
|
6
|
-
import { isIconKey } from '../../lib/icons';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
interface
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.b-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
font-
|
|
83
|
-
font-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
.b-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
grid-auto-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
1
|
+
---
|
|
2
|
+
/** Steps — a Level-2 recipe: a numbered process. `horizontal` lays connected
|
|
3
|
+
* cards in a row; `vertical` a stacked timeline. Composes Heading + Icon. */
|
|
4
|
+
import Heading from '../blocks/Heading.astro';
|
|
5
|
+
import Icon from '../Icon.astro';
|
|
6
|
+
import { isIconKey } from '../../lib/icons';
|
|
7
|
+
|
|
8
|
+
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
9
|
+
interface Item { title: string; text?: string; icon?: string }
|
|
10
|
+
interface Props {
|
|
11
|
+
title?: string;
|
|
12
|
+
intro?: string;
|
|
13
|
+
layout?: 'horizontal' | 'vertical';
|
|
14
|
+
items?: Item[];
|
|
15
|
+
}
|
|
16
|
+
const { title, intro, layout = 'horizontal', items = [] } = Astro.props;
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<section class="b-steps" data-layout={layout}>
|
|
20
|
+
{(title || intro) && (
|
|
21
|
+
<div class="b-steps__head">
|
|
22
|
+
{title && <Heading text={title} level={2} align="center" />}
|
|
23
|
+
{intro && <p class="b-steps__intro" set:html={inlineMarkdown(intro)} />}
|
|
24
|
+
</div>
|
|
25
|
+
)}
|
|
26
|
+
<ol class="b-steps__list">
|
|
27
|
+
{items.map((item, i) => (
|
|
28
|
+
<li class="b-steps__item">
|
|
29
|
+
<span class="b-steps__marker" aria-hidden="true">
|
|
30
|
+
{item.icon && isIconKey(item.icon) ? <Icon name={item.icon} size={20} /> : i + 1}
|
|
31
|
+
</span>
|
|
32
|
+
<div class="b-steps__body">
|
|
33
|
+
<h3 class="b-steps__title">{item.title}</h3>
|
|
34
|
+
{item.text && <p class="b-steps__text">{item.text}</p>}
|
|
35
|
+
</div>
|
|
36
|
+
</li>
|
|
37
|
+
))}
|
|
38
|
+
</ol>
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<style>
|
|
42
|
+
.b-steps {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
gap: 2rem;
|
|
46
|
+
max-width: 72rem;
|
|
47
|
+
margin-inline: auto;
|
|
48
|
+
padding-inline: 1rem;
|
|
49
|
+
}
|
|
50
|
+
.b-steps__head {
|
|
51
|
+
max-width: var(--max);
|
|
52
|
+
margin-inline: auto;
|
|
53
|
+
text-align: center;
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
gap: 0.75rem;
|
|
57
|
+
}
|
|
58
|
+
.b-steps__intro { margin: 0; color: var(--muted); line-height: 1.6; }
|
|
59
|
+
.b-steps__list {
|
|
60
|
+
list-style: none;
|
|
61
|
+
margin: 0;
|
|
62
|
+
padding: 0;
|
|
63
|
+
display: grid;
|
|
64
|
+
gap: 1.5rem;
|
|
65
|
+
}
|
|
66
|
+
/* Base (and mobile): marker left, body right. */
|
|
67
|
+
.b-steps__item {
|
|
68
|
+
display: flex;
|
|
69
|
+
gap: 1rem;
|
|
70
|
+
align-items: flex-start;
|
|
71
|
+
}
|
|
72
|
+
.b-steps__marker {
|
|
73
|
+
flex-shrink: 0;
|
|
74
|
+
width: 2.5rem;
|
|
75
|
+
height: 2.5rem;
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
border-radius: var(--radius-pill);
|
|
80
|
+
background: var(--gold);
|
|
81
|
+
color: var(--text-light);
|
|
82
|
+
font-family: var(--font-heading);
|
|
83
|
+
font-weight: 700;
|
|
84
|
+
font-size: 1.05rem;
|
|
85
|
+
}
|
|
86
|
+
.b-steps__title { margin: 0 0 0.35rem; font-size: 1.1rem; color: var(--fg); }
|
|
87
|
+
.b-steps__text { margin: 0; color: var(--muted); line-height: 1.6; }
|
|
88
|
+
|
|
89
|
+
/* Vertical: a centred, stacked timeline. */
|
|
90
|
+
.b-steps[data-layout='vertical'] .b-steps__list { max-width: 40rem; margin-inline: auto; }
|
|
91
|
+
|
|
92
|
+
/* Horizontal (≥48em): connected cards in a row, marker over centred body. */
|
|
93
|
+
@media (min-width: 48em) {
|
|
94
|
+
.b-steps[data-layout='horizontal'] .b-steps__list {
|
|
95
|
+
grid-auto-flow: column;
|
|
96
|
+
grid-auto-columns: 1fr;
|
|
97
|
+
gap: 1rem;
|
|
98
|
+
}
|
|
99
|
+
.b-steps[data-layout='horizontal'] .b-steps__item {
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
align-items: center;
|
|
102
|
+
text-align: center;
|
|
103
|
+
gap: 0.85rem;
|
|
104
|
+
position: relative;
|
|
105
|
+
}
|
|
106
|
+
/* Connector from this marker's right edge to the next marker's left edge.
|
|
107
|
+
The +1.5rem / -2rem account for the 2.5rem marker AND the 1rem grid gap,
|
|
108
|
+
so the line sits centred between markers (not falling short). */
|
|
109
|
+
.b-steps[data-layout='horizontal'] .b-steps__item:not(:last-child)::after {
|
|
110
|
+
content: '';
|
|
111
|
+
position: absolute;
|
|
112
|
+
top: calc(1.25rem - 1px);
|
|
113
|
+
left: calc(50% + 1.5rem);
|
|
114
|
+
width: calc(100% - 2rem);
|
|
115
|
+
height: 2px;
|
|
116
|
+
background: var(--border);
|
|
117
|
+
z-index: 0;
|
|
118
|
+
}
|
|
119
|
+
.b-steps[data-layout='horizontal'] .b-steps__marker { position: relative; z-index: 1; }
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
@@ -9,6 +9,7 @@ import Heading from '../blocks/Heading.astro';
|
|
|
9
9
|
|
|
10
10
|
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
11
11
|
import { filledColumns } from '../../lib/table';
|
|
12
|
+
import { proseParts } from '../../lib/prose';
|
|
12
13
|
interface Props {
|
|
13
14
|
title?: string;
|
|
14
15
|
intro?: string;
|
|
@@ -24,7 +25,7 @@ const keep = filledColumns(columns, rows);
|
|
|
24
25
|
{(title || intro) && (
|
|
25
26
|
<div class="b-table__head">
|
|
26
27
|
{title && <Heading text={title} level={2} />}
|
|
27
|
-
{intro && <p class="b-table__intro" set:html={
|
|
28
|
+
{intro && proseParts(intro).map((part) => part.kind === 'p' ? <p class="b-table__intro" set:html={part.html} /> : <ul class="b-table__intro">{part.items.map((x) => <li set:html={x} />)}</ul>)}
|
|
28
29
|
</div>
|
|
29
30
|
)}
|
|
30
31
|
<div class="b-table__scroll">
|
|
@@ -38,7 +38,7 @@ const safeLink = /^https:\/\//i.test(url) ? url : undefined;
|
|
|
38
38
|
</div>
|
|
39
39
|
<div class="b-video__frame"></div>
|
|
40
40
|
</div>
|
|
41
|
-
{caption && <figcaption class="b-video__caption"
|
|
41
|
+
{caption && <figcaption class="b-video__caption" set:html={inlineMarkdown(caption)} />}
|
|
42
42
|
</figure>
|
|
43
43
|
{/* Always visible, before AND after the player loads: the way to the provider. */}
|
|
44
44
|
<div class="b-video__footer">
|
package/content/blocks.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { iconKeys, isIconKey } from '../lib/icons';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The block palette — the heart of the Ferst page model (see
|
|
@@ -14,6 +15,12 @@ import { z } from 'zod';
|
|
|
14
15
|
|
|
15
16
|
/** Every block shares a `type` discriminant and an `enabled` flag (keep a block
|
|
16
17
|
* in the page data without rendering it). */
|
|
18
|
+
/** An icon name from the engine's registry (lib/icons). An unknown name fails validation,
|
|
19
|
+
* and so the build, with the valid names listed, instead of rendering nothing silently. */
|
|
20
|
+
const iconName = z.string().refine(isIconKey, (v) => ({
|
|
21
|
+
message: `Unknown icon "${v}". Use one of: ${iconKeys.join(', ')}.`,
|
|
22
|
+
}));
|
|
23
|
+
|
|
17
24
|
export const headingBlock = z.object({
|
|
18
25
|
type: z.literal('heading'),
|
|
19
26
|
enabled: z.boolean().default(true),
|
|
@@ -159,7 +166,7 @@ export const featureCardsBlock = z.object({
|
|
|
159
166
|
cards: z
|
|
160
167
|
.array(
|
|
161
168
|
z.object({
|
|
162
|
-
icon:
|
|
169
|
+
icon: iconName.optional(),
|
|
163
170
|
title: z.string(),
|
|
164
171
|
text: z.string().optional(),
|
|
165
172
|
href: z.string().optional(),
|
|
@@ -261,7 +268,7 @@ export const tilesBlock = z.object({
|
|
|
261
268
|
.array(
|
|
262
269
|
z
|
|
263
270
|
.object({
|
|
264
|
-
icon:
|
|
271
|
+
icon: iconName.optional(),
|
|
265
272
|
eyebrow: z.string().optional(),
|
|
266
273
|
/** Optional only for a picture tile: with an image and no text, the tile is
|
|
267
274
|
* the picture alone (no scrim) — a linked poster/logo — named by its alt. */
|
|
@@ -319,7 +326,7 @@ export const stepsBlock = z.object({
|
|
|
319
326
|
title: z.string().optional(),
|
|
320
327
|
intro: z.string().optional(),
|
|
321
328
|
layout: z.enum(['horizontal', 'vertical']).default('horizontal'),
|
|
322
|
-
items: z.array(z.object({ title: z.string(), text: z.string().optional(), icon:
|
|
329
|
+
items: z.array(z.object({ title: z.string(), text: z.string().optional(), icon: iconName.optional() })).default([]),
|
|
323
330
|
});
|
|
324
331
|
|
|
325
332
|
/** Pricing — a row of plan cards (name, price, period, feature list, CTA). One
|
|
@@ -388,7 +395,7 @@ export const bentoBlock = z.object({
|
|
|
388
395
|
z.object({
|
|
389
396
|
size: z.enum(['sm', 'wide', 'tall', 'lg']).default('sm'),
|
|
390
397
|
variant: z.enum(['surface', 'filled', 'gradient', 'image']).default('surface'),
|
|
391
|
-
icon:
|
|
398
|
+
icon: iconName.optional(),
|
|
392
399
|
eyebrow: z.string().optional(),
|
|
393
400
|
title: z.string(),
|
|
394
401
|
text: z.string().optional(),
|
|
@@ -471,7 +478,7 @@ export const noticeBlock = z.object({
|
|
|
471
478
|
type: z.literal('notice'),
|
|
472
479
|
enabled: z.boolean().default(true),
|
|
473
480
|
tone: z.enum(['accent', 'neutral']).default('accent'),
|
|
474
|
-
icon:
|
|
481
|
+
icon: iconName.optional(),
|
|
475
482
|
title: z.string().optional(),
|
|
476
483
|
text: z.string(),
|
|
477
484
|
link: z.object({ label: z.string(), href: z.string() }).optional(),
|
|
@@ -492,7 +499,7 @@ export const mediaCardsBlock = z.object({
|
|
|
492
499
|
.array(
|
|
493
500
|
z.object({
|
|
494
501
|
image: z.object({ src: z.string(), alt: z.string().default('') }).optional(),
|
|
495
|
-
icon:
|
|
502
|
+
icon: iconName.optional(),
|
|
496
503
|
title: z.string(),
|
|
497
504
|
/** A short meta line shown as a pill (e.g. "Sundays 10:45 · Main hall"). */
|
|
498
505
|
meta: z.string().optional(),
|
|
@@ -517,7 +524,7 @@ export const contactCardBlock = z.object({
|
|
|
517
524
|
items: z
|
|
518
525
|
.array(
|
|
519
526
|
z.object({
|
|
520
|
-
icon:
|
|
527
|
+
icon: iconName.optional(),
|
|
521
528
|
value: z.string(),
|
|
522
529
|
href: z.string().optional(),
|
|
523
530
|
})
|
|
@@ -647,7 +654,7 @@ export const contactsBlock = z.object({
|
|
|
647
654
|
contacts: z
|
|
648
655
|
.array(
|
|
649
656
|
z.object({
|
|
650
|
-
role: z.string(),
|
|
657
|
+
role: z.string().optional(),
|
|
651
658
|
name: z.string().optional(),
|
|
652
659
|
phone: z.string().optional(),
|
|
653
660
|
email: z.string().optional(),
|
|
@@ -655,6 +662,7 @@ export const contactsBlock = z.object({
|
|
|
655
662
|
address: z.string().optional(),
|
|
656
663
|
hours: z.string().optional(),
|
|
657
664
|
})
|
|
665
|
+
.refine((c) => !!c.role || !!c.name, { message: 'A contact needs a role or a name', path: ['role'] })
|
|
658
666
|
)
|
|
659
667
|
.min(1),
|
|
660
668
|
})
|
package/content/schemas.ts
CHANGED
|
@@ -90,6 +90,11 @@ export const navbarSettingsSchema = z.object({
|
|
|
90
90
|
export const footerSettingsSchema = z.object({
|
|
91
91
|
/** Footer legal/info nav. Empty by default — every client defines its own. */
|
|
92
92
|
footer: z.array(navLinkSchema).default([]),
|
|
93
|
+
/** Headed footer columns for reference material (policies, safeguarding, documents,
|
|
94
|
+
* forms) — where it belongs, so the main menu stays short and clear. */
|
|
95
|
+
groups: z
|
|
96
|
+
.array(z.object({ heading: z.string(), links: z.array(navLinkSchema).min(1) }))
|
|
97
|
+
.default([]),
|
|
93
98
|
});
|
|
94
99
|
|
|
95
100
|
/**
|