ferst-core 0.7.7 → 0.7.9
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/CalendarEmbed.astro +2 -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 +521 -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 +88 -52
- package/layouts/Base.astro +496 -484
- package/lib/inline-markdown.ts +13 -0
- package/lib/slug.ts +1 -1
- package/package.json +1 -1
- package/routes/calendar/index.astro +2 -1
package/bin/ferst-modules.mjs
CHANGED
|
@@ -9,9 +9,25 @@
|
|
|
9
9
|
// Forms and calendars live in the portal, never in the repo: place a form with a
|
|
10
10
|
// `{ "type": "form", "form": "<id>" }` block, and show the calendar by setting
|
|
11
11
|
// calendarSettings.embedUrl to the address printed here.
|
|
12
|
-
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
13
13
|
import { resolve } from 'node:path';
|
|
14
|
-
import {
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
import { resolveGitBase } from './cms-config.mjs';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* True when THIS file is run as a CLI (not imported). Each bin needs its own copy:
|
|
19
|
+
* `import.meta.url` is per file, so one borrowed from another bin only ever matches that
|
|
20
|
+
* bin (0.7.7 printed nothing). Handles npm's `node_modules/.bin` symlink via realpath.
|
|
21
|
+
*/
|
|
22
|
+
export function isDirectInvocation(argv1) {
|
|
23
|
+
if (!argv1) return false;
|
|
24
|
+
if (import.meta.url === pathToFileURL(argv1).href) return true;
|
|
25
|
+
try {
|
|
26
|
+
return import.meta.url === pathToFileURL(realpathSync(argv1)).href;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
15
31
|
|
|
16
32
|
/** The site to ask about: `--site <ref>`, else ferst-site.json's siteId, else null. */
|
|
17
33
|
export function moduleRef(argv, siteMeta) {
|
|
@@ -1,119 +1,125 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Renders an ordered list of blocks by mapping each block's `type` to its
|
|
4
|
-
* component via a registry. Disabled blocks are skipped. The registry is typed
|
|
5
|
-
* `Record<BlockType, …>`, so adding a block type to the union without a renderer
|
|
6
|
-
* here is a compile error — no silent `undefined` component at runtime.
|
|
7
|
-
*/
|
|
8
|
-
import type { Block, BlockType } from '../content/blocks';
|
|
9
|
-
import Heading from './blocks/Heading.astro';
|
|
10
|
-
import Prose from './blocks/Prose.astro';
|
|
11
|
-
import ImageBlock from './blocks/ImageBlock.astro';
|
|
12
|
-
import ButtonBlock from './blocks/ButtonBlock.astro';
|
|
13
|
-
import Quote from './blocks/Quote.astro';
|
|
14
|
-
import Divider from './blocks/Divider.astro';
|
|
15
|
-
import Spacer from './blocks/Spacer.astro';
|
|
16
|
-
import Badge from './blocks/Badge.astro';
|
|
17
|
-
import Stat from './blocks/Stat.astro';
|
|
18
|
-
import List from './blocks/List.astro';
|
|
19
|
-
import Section from './blocks/Section.astro';
|
|
20
|
-
import Grid from './blocks/Grid.astro';
|
|
21
|
-
import Stack from './blocks/Stack.astro';
|
|
22
|
-
import Hero from './recipes/Hero.astro';
|
|
23
|
-
import FeatureCards from './recipes/FeatureCards.astro';
|
|
24
|
-
import Cta from './recipes/Cta.astro';
|
|
25
|
-
import Faq from './recipes/Faq.astro';
|
|
26
|
-
import Gallery from './recipes/Gallery.astro';
|
|
27
|
-
import Testimonial from './recipes/Testimonial.astro';
|
|
28
|
-
import ContactForm from './recipes/ContactForm.astro';
|
|
29
|
-
import Form from './recipes/Form.astro';
|
|
30
|
-
import Accordion from './recipes/Accordion.astro';
|
|
31
|
-
import Tiles from './recipes/Tiles.astro';
|
|
32
|
-
import Banner from './recipes/Banner.astro';
|
|
33
|
-
import Stats from './recipes/Stats.astro';
|
|
34
|
-
import Steps from './recipes/Steps.astro';
|
|
35
|
-
import Pricing from './recipes/Pricing.astro';
|
|
36
|
-
import Logos from './recipes/Logos.astro';
|
|
37
|
-
import Tabs from './recipes/Tabs.astro';
|
|
38
|
-
import Announcement from './recipes/Announcement.astro';
|
|
39
|
-
import Bento from './recipes/Bento.astro';
|
|
40
|
-
import Marquee from './recipes/Marquee.astro';
|
|
41
|
-
import LatestPosts from './recipes/LatestPosts.astro';
|
|
42
|
-
import MapEmbed from './recipes/MapEmbed.astro';
|
|
43
|
-
import Table from './recipes/Table.astro';
|
|
44
|
-
import Notice from './recipes/Notice.astro';
|
|
45
|
-
import MediaCards from './recipes/MediaCards.astro';
|
|
46
|
-
import ContactCard from './recipes/ContactCard.astro';
|
|
47
|
-
import Locations from './recipes/Locations.astro';
|
|
48
|
-
import Video from './recipes/Video.astro';
|
|
49
|
-
import Documents from './recipes/Documents.astro';
|
|
50
|
-
import LinkRow from './recipes/LinkRow.astro';
|
|
51
|
-
import DocumentRecipe from './recipes/Document.astro';
|
|
52
|
-
import Timetable from './recipes/Timetable.astro';
|
|
53
|
-
import Contacts from './recipes/Contacts.astro';
|
|
54
|
-
import MediaText from './recipes/MediaText.astro';
|
|
55
|
-
|
|
56
|
-
interface Props {
|
|
57
|
-
blocks: Block[];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const { blocks } = Astro.props;
|
|
61
|
-
|
|
62
|
-
const registry: Record<BlockType, any> = {
|
|
63
|
-
heading: Heading,
|
|
64
|
-
prose: Prose,
|
|
65
|
-
image: ImageBlock,
|
|
66
|
-
button: ButtonBlock,
|
|
67
|
-
quote: Quote,
|
|
68
|
-
divider: Divider,
|
|
69
|
-
spacer: Spacer,
|
|
70
|
-
badge: Badge,
|
|
71
|
-
stat: Stat,
|
|
72
|
-
list: List,
|
|
73
|
-
section: Section,
|
|
74
|
-
grid: Grid,
|
|
75
|
-
stack: Stack,
|
|
76
|
-
hero: Hero,
|
|
77
|
-
featureCards: FeatureCards,
|
|
78
|
-
cta: Cta,
|
|
79
|
-
faq: Faq,
|
|
80
|
-
gallery: Gallery,
|
|
81
|
-
testimonial: Testimonial,
|
|
82
|
-
contactForm: ContactForm,
|
|
83
|
-
form: Form,
|
|
84
|
-
accordion: Accordion,
|
|
85
|
-
tiles: Tiles,
|
|
86
|
-
banner: Banner,
|
|
87
|
-
stats: Stats,
|
|
88
|
-
steps: Steps,
|
|
89
|
-
pricing: Pricing,
|
|
90
|
-
logos: Logos,
|
|
91
|
-
tabs: Tabs,
|
|
92
|
-
announcement: Announcement,
|
|
93
|
-
bento: Bento,
|
|
94
|
-
marquee: Marquee,
|
|
95
|
-
latestPosts: LatestPosts,
|
|
96
|
-
mapEmbed: MapEmbed,
|
|
97
|
-
table: Table,
|
|
98
|
-
notice: Notice,
|
|
99
|
-
mediaCards: MediaCards,
|
|
100
|
-
contactCard: ContactCard,
|
|
101
|
-
locations: Locations,
|
|
102
|
-
video: Video,
|
|
103
|
-
documents: Documents,
|
|
104
|
-
linkRow: LinkRow,
|
|
105
|
-
document: DocumentRecipe,
|
|
106
|
-
timetable: Timetable,
|
|
107
|
-
contacts: Contacts,
|
|
108
|
-
mediaText: MediaText,
|
|
109
|
-
};
|
|
110
|
-
---
|
|
111
|
-
|
|
112
|
-
{
|
|
113
|
-
blocks
|
|
114
|
-
.filter((block) => block.enabled !== false)
|
|
115
|
-
.map((block) => {
|
|
116
|
-
const Component = registry[block.type];
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Renders an ordered list of blocks by mapping each block's `type` to its
|
|
4
|
+
* component via a registry. Disabled blocks are skipped. The registry is typed
|
|
5
|
+
* `Record<BlockType, …>`, so adding a block type to the union without a renderer
|
|
6
|
+
* here is a compile error — no silent `undefined` component at runtime.
|
|
7
|
+
*/
|
|
8
|
+
import type { Block, BlockType } from '../content/blocks';
|
|
9
|
+
import Heading from './blocks/Heading.astro';
|
|
10
|
+
import Prose from './blocks/Prose.astro';
|
|
11
|
+
import ImageBlock from './blocks/ImageBlock.astro';
|
|
12
|
+
import ButtonBlock from './blocks/ButtonBlock.astro';
|
|
13
|
+
import Quote from './blocks/Quote.astro';
|
|
14
|
+
import Divider from './blocks/Divider.astro';
|
|
15
|
+
import Spacer from './blocks/Spacer.astro';
|
|
16
|
+
import Badge from './blocks/Badge.astro';
|
|
17
|
+
import Stat from './blocks/Stat.astro';
|
|
18
|
+
import List from './blocks/List.astro';
|
|
19
|
+
import Section from './blocks/Section.astro';
|
|
20
|
+
import Grid from './blocks/Grid.astro';
|
|
21
|
+
import Stack from './blocks/Stack.astro';
|
|
22
|
+
import Hero from './recipes/Hero.astro';
|
|
23
|
+
import FeatureCards from './recipes/FeatureCards.astro';
|
|
24
|
+
import Cta from './recipes/Cta.astro';
|
|
25
|
+
import Faq from './recipes/Faq.astro';
|
|
26
|
+
import Gallery from './recipes/Gallery.astro';
|
|
27
|
+
import Testimonial from './recipes/Testimonial.astro';
|
|
28
|
+
import ContactForm from './recipes/ContactForm.astro';
|
|
29
|
+
import Form from './recipes/Form.astro';
|
|
30
|
+
import Accordion from './recipes/Accordion.astro';
|
|
31
|
+
import Tiles from './recipes/Tiles.astro';
|
|
32
|
+
import Banner from './recipes/Banner.astro';
|
|
33
|
+
import Stats from './recipes/Stats.astro';
|
|
34
|
+
import Steps from './recipes/Steps.astro';
|
|
35
|
+
import Pricing from './recipes/Pricing.astro';
|
|
36
|
+
import Logos from './recipes/Logos.astro';
|
|
37
|
+
import Tabs from './recipes/Tabs.astro';
|
|
38
|
+
import Announcement from './recipes/Announcement.astro';
|
|
39
|
+
import Bento from './recipes/Bento.astro';
|
|
40
|
+
import Marquee from './recipes/Marquee.astro';
|
|
41
|
+
import LatestPosts from './recipes/LatestPosts.astro';
|
|
42
|
+
import MapEmbed from './recipes/MapEmbed.astro';
|
|
43
|
+
import Table from './recipes/Table.astro';
|
|
44
|
+
import Notice from './recipes/Notice.astro';
|
|
45
|
+
import MediaCards from './recipes/MediaCards.astro';
|
|
46
|
+
import ContactCard from './recipes/ContactCard.astro';
|
|
47
|
+
import Locations from './recipes/Locations.astro';
|
|
48
|
+
import Video from './recipes/Video.astro';
|
|
49
|
+
import Documents from './recipes/Documents.astro';
|
|
50
|
+
import LinkRow from './recipes/LinkRow.astro';
|
|
51
|
+
import DocumentRecipe from './recipes/Document.astro';
|
|
52
|
+
import Timetable from './recipes/Timetable.astro';
|
|
53
|
+
import Contacts from './recipes/Contacts.astro';
|
|
54
|
+
import MediaText from './recipes/MediaText.astro';
|
|
55
|
+
|
|
56
|
+
interface Props {
|
|
57
|
+
blocks: Block[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { blocks } = Astro.props;
|
|
61
|
+
|
|
62
|
+
const registry: Record<BlockType, any> = {
|
|
63
|
+
heading: Heading,
|
|
64
|
+
prose: Prose,
|
|
65
|
+
image: ImageBlock,
|
|
66
|
+
button: ButtonBlock,
|
|
67
|
+
quote: Quote,
|
|
68
|
+
divider: Divider,
|
|
69
|
+
spacer: Spacer,
|
|
70
|
+
badge: Badge,
|
|
71
|
+
stat: Stat,
|
|
72
|
+
list: List,
|
|
73
|
+
section: Section,
|
|
74
|
+
grid: Grid,
|
|
75
|
+
stack: Stack,
|
|
76
|
+
hero: Hero,
|
|
77
|
+
featureCards: FeatureCards,
|
|
78
|
+
cta: Cta,
|
|
79
|
+
faq: Faq,
|
|
80
|
+
gallery: Gallery,
|
|
81
|
+
testimonial: Testimonial,
|
|
82
|
+
contactForm: ContactForm,
|
|
83
|
+
form: Form,
|
|
84
|
+
accordion: Accordion,
|
|
85
|
+
tiles: Tiles,
|
|
86
|
+
banner: Banner,
|
|
87
|
+
stats: Stats,
|
|
88
|
+
steps: Steps,
|
|
89
|
+
pricing: Pricing,
|
|
90
|
+
logos: Logos,
|
|
91
|
+
tabs: Tabs,
|
|
92
|
+
announcement: Announcement,
|
|
93
|
+
bento: Bento,
|
|
94
|
+
marquee: Marquee,
|
|
95
|
+
latestPosts: LatestPosts,
|
|
96
|
+
mapEmbed: MapEmbed,
|
|
97
|
+
table: Table,
|
|
98
|
+
notice: Notice,
|
|
99
|
+
mediaCards: MediaCards,
|
|
100
|
+
contactCard: ContactCard,
|
|
101
|
+
locations: Locations,
|
|
102
|
+
video: Video,
|
|
103
|
+
documents: Documents,
|
|
104
|
+
linkRow: LinkRow,
|
|
105
|
+
document: DocumentRecipe,
|
|
106
|
+
timetable: Timetable,
|
|
107
|
+
contacts: Contacts,
|
|
108
|
+
mediaText: MediaText,
|
|
109
|
+
};
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
blocks
|
|
114
|
+
.filter((block) => block.enabled !== false)
|
|
115
|
+
.map((block) => {
|
|
116
|
+
const Component = registry[block.type];
|
|
117
|
+
// A block's own `align` sets the alignment cascade for everything inside it. The
|
|
118
|
+
// wrapper is `display: contents`, so it adds no box and the layout is unchanged;
|
|
119
|
+
// heading / banner / section handle their own `align`.
|
|
120
|
+
const own = block.type === 'heading' || block.type === 'banner' || block.type === 'section';
|
|
121
|
+
return block.align && !own
|
|
122
|
+
? <div class="block-align" data-content-align={block.align}><Component {...block} /></div>
|
|
123
|
+
: <Component {...block} />;
|
|
124
|
+
})
|
|
125
|
+
}
|
package/components/Button.astro
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { inlineMarkdown } from '../lib/inline-markdown';
|
|
2
3
|
/**
|
|
3
4
|
* CalendarEmbed — a consent-gated external calendar embed (e.g. Google Calendar).
|
|
4
5
|
* Nothing loads from the provider until the visitor clicks "Load calendar", so no
|
|
@@ -38,7 +39,7 @@ const safe = isSafeEmbedUrl(finalEmbedUrl);
|
|
|
38
39
|
|
|
39
40
|
<section class="b-calendar">
|
|
40
41
|
{title && <h2 class="b-calendar__title">{title}</h2>}
|
|
41
|
-
{intro && <p class="b-calendar__intro"
|
|
42
|
+
{intro && <p class="b-calendar__intro" set:html={inlineMarkdown(intro)} />}
|
|
42
43
|
|
|
43
44
|
{!finalEmbedUrl ? (
|
|
44
45
|
<p class="b-calendar__note">The calendar isn't set up yet.</p>
|
package/components/Card.astro
CHANGED
|
@@ -27,6 +27,8 @@ const { as: Tag = 'div', hover = true, padding = '1.25rem 1.5rem', overflow = 'h
|
|
|
27
27
|
padding: var(--card-padding);
|
|
28
28
|
display: flex;
|
|
29
29
|
flex-direction: column;
|
|
30
|
+
/* Children follow the page / section alignment (Base: --content-items). */
|
|
31
|
+
align-items: var(--content-items, stretch);
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
.card--hover {
|
|
@@ -1026,6 +1026,29 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
1026
1026
|
window.addEventListener('scroll', onScrollCompact, { passive: true });
|
|
1027
1027
|
}
|
|
1028
1028
|
|
|
1029
|
+
// The header's full height, published as --header-h on <html> so a full-screen opener
|
|
1030
|
+
// fills the screen BELOW the sticky header (Hero size "screen"). Only the full-size header
|
|
1031
|
+
// counts: while it's condensed on scroll, or animating between sizes, the last full value
|
|
1032
|
+
// stays, so the opener never resizes as the page scrolls.
|
|
1033
|
+
let headerAnimating = false;
|
|
1034
|
+
function syncHeaderHeight() {
|
|
1035
|
+
if (!siteHeader || headerAnimating || siteHeader.classList.contains('is-compact')) return;
|
|
1036
|
+
document.documentElement.style.setProperty('--header-h', `${siteHeader.getBoundingClientRect().height}px`);
|
|
1037
|
+
}
|
|
1038
|
+
if (siteHeader) {
|
|
1039
|
+
siteHeader.addEventListener('transitionrun', (e) => {
|
|
1040
|
+
if (e.target === siteHeader && e.propertyName === 'height') headerAnimating = true;
|
|
1041
|
+
});
|
|
1042
|
+
const settled = (e: TransitionEvent) => {
|
|
1043
|
+
if (e.target !== siteHeader || e.propertyName !== 'height') return;
|
|
1044
|
+
headerAnimating = false;
|
|
1045
|
+
syncHeaderHeight();
|
|
1046
|
+
};
|
|
1047
|
+
siteHeader.addEventListener('transitionend', settled);
|
|
1048
|
+
siteHeader.addEventListener('transitioncancel', settled);
|
|
1049
|
+
syncHeaderHeight();
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1029
1052
|
// Crowding (#11): on desktop the menu is centred on the header. When the site name (or
|
|
1030
1053
|
// the right-hand buttons) would run into it, give the menu its own row instead. Decided
|
|
1031
1054
|
// by measuring the real layout, so it adapts to any name and any number of links.
|
|
@@ -1048,6 +1071,7 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
1048
1071
|
const queue = () => { if (!queued) { queued = true; requestAnimationFrame(() => { queued = false; fitCheck(); }); } };
|
|
1049
1072
|
if (brand) new ResizeObserver(queue).observe(brand);
|
|
1050
1073
|
new ResizeObserver(queue).observe(siteHeader);
|
|
1074
|
+
new ResizeObserver(syncHeaderHeight).observe(siteHeader);
|
|
1051
1075
|
desktop.addEventListener('change', queue);
|
|
1052
1076
|
document.fonts?.ready.then(queue);
|
|
1053
1077
|
fitCheck();
|
|
@@ -1,87 +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 ProseParts from '../ProseParts.astro';
|
|
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
|
-
</
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
.b-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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>
|
|
@@ -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">
|