@the-smithy/create-hearth-site 0.31.0 → 0.33.0

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.
@@ -1,5 +1,6 @@
1
1
  import type { BlockOf } from "@the-smithy/hearth";
2
2
  import { theme } from "@/theme";
3
+ import { hasPart, Part } from "../parts";
3
4
  import { cx } from "../theme";
4
5
  import { Button, Emphasis, fill, ground, Img, Markdown, plain, Section, SectionHeading, SmartLink, toTone } from "../ui";
5
6
 
@@ -10,26 +11,32 @@ export function Hero({ block }: { block: BlockOf<"hero"> }) {
10
11
  // Without a tone it's the design's own ground, dark, in `hero.root`. A tone sets the ground, which turns on-dark: on or off.
11
12
  return (
12
13
  <section id={block.anchor} data-block="hero" data-size={d.size} {...ground(d.tone ?? "inverse")} className={cx(d.tone ? `${theme.tones[d.tone]} ${t.toned}` : t.root, block.anchor && theme.section.anchored)}>
13
- {d.image ? <Img image={d.image} crop priority className={t.image} /> : null}
14
+ <Part name="hero.media" props={{ block }}>
15
+ {d.image ? <Img image={d.image} crop priority className={t.image} /> : null}
16
+ </Part>
14
17
  <div className={t.inner} data-size={d.size}>
15
- {d.logo ? <Img image={d.logo} priority className={t.logo} /> : null}
16
- {d.eyebrow ? <p className={t.eyebrow}>{d.eyebrow}</p> : null}
17
- <h1 className={t.title}>
18
- <Emphasis text={d.heading} className={t.emphasis} />
19
- </h1>
20
- {paragraphs.length > 1 ? (
21
- paragraphs.map((p, i) => (
22
- <p key={i} className={t.body}>
23
- {p}
24
- </p>
25
- ))
26
- ) : d.body ? (
27
- <p className={t.body}>{d.body}</p>
28
- ) : null}
29
- {d.primaryCta || d.secondaryCta ? (
18
+ <Part name="hero.heading" props={{ block }}>
19
+ {d.logo ? <Img image={d.logo} priority className={t.logo} /> : null}
20
+ {d.eyebrow ? <p className={t.eyebrow}>{d.eyebrow}</p> : null}
21
+ <h1 className={t.title}>
22
+ <Emphasis text={d.heading} className={t.emphasis} />
23
+ </h1>
24
+ {paragraphs.length > 1 ? (
25
+ paragraphs.map((p, i) => (
26
+ <p key={i} className={t.body}>
27
+ {p}
28
+ </p>
29
+ ))
30
+ ) : d.body ? (
31
+ <p className={t.body}>{d.body}</p>
32
+ ) : null}
33
+ </Part>
34
+ {d.primaryCta || d.secondaryCta || hasPart("hero.actions") ? (
30
35
  <div className={t.actions}>
31
- <Button link={d.primaryCta} />
32
- <Button link={d.secondaryCta} variant="secondary" />
36
+ <Part name="hero.actions" props={{ block }}>
37
+ <Button link={d.primaryCta} />
38
+ <Button link={d.secondaryCta} variant="secondary" />
39
+ </Part>
33
40
  </div>
34
41
  ) : null}
35
42
  </div>
@@ -54,15 +61,21 @@ export function MediaText({ block }: { block: BlockOf<"mediaText"> }) {
54
61
  <Section block="mediaText" id={block.anchor}>
55
62
  <div className={t.grid} data-image={d.imagePosition}>
56
63
  <div className={t.text}>
57
- <SectionHeading eyebrow={d.eyebrow} heading={d.heading} />
64
+ <Part name="mediaText.heading" props={{ block }}>
65
+ <SectionHeading eyebrow={d.eyebrow} heading={d.heading} />
66
+ </Part>
58
67
  <Markdown className={t.body}>{d.markdown}</Markdown>
59
- {d.cta ? (
68
+ {d.cta || hasPart("mediaText.actions") ? (
60
69
  <div className={t.actions}>
61
- <Button link={d.cta} />
70
+ <Part name="mediaText.actions" props={{ block }}>
71
+ <Button link={d.cta} />
72
+ </Part>
62
73
  </div>
63
74
  ) : null}
64
75
  </div>
65
- {d.image ? <Img image={d.image} crop={d.imageFit === "contain" ? undefined : 4 / 3} className={t.image} data-fit={d.imageFit} /> : null}
76
+ <Part name="mediaText.media" props={{ block }}>
77
+ {d.image ? <Img image={d.image} crop={d.imageFit === "contain" ? undefined : 4 / 3} className={t.image} data-fit={d.imageFit} /> : null}
78
+ </Part>
66
79
  </div>
67
80
  </Section>
68
81
  );
@@ -72,12 +85,16 @@ export function Stats({ block }: { block: BlockOf<"stats"> }) {
72
85
  const t = theme.stats;
73
86
  return (
74
87
  <Section block="stats" id={block.anchor} tone={toTone(t.tone)}>
75
- <SectionHeading heading={block.data.heading} />
88
+ <Part name="stats.heading" props={{ block }}>
89
+ <SectionHeading heading={block.data.heading} />
90
+ </Part>
76
91
  <dl className={t.list}>
77
92
  {block.data.items.map((s, i) => (
78
93
  <div key={i} className={t.item}>
79
- <dt className={t.label}>{s.label}</dt>
80
- <dd className={t.value}>{s.value}</dd>
94
+ <Part name="stats.item" props={{ block, item: s, index: i }}>
95
+ <dt className={t.label}>{s.label}</dt>
96
+ <dd className={t.value}>{s.value}</dd>
97
+ </Part>
81
98
  </div>
82
99
  ))}
83
100
  </dl>
@@ -98,20 +115,24 @@ export function CardGrid({ block }: { block: BlockOf<"cardGrid"> }) {
98
115
  const Title = d.heading ? "h3" : "h2";
99
116
  return (
100
117
  <Section block="cardGrid" id={block.anchor} tone={d.tone}>
101
- <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
118
+ <Part name="cardGrid.heading" props={{ block }}>
119
+ <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
120
+ </Part>
102
121
  <ul className={t.list}>
103
122
  {d.items.map((c, i) => (
104
123
  <li key={i} className={t.card} data-card={cardKey(c.title)} data-icon={c.icon}>
105
- {c.image ? <Img image={c.image} crop={16 / 10} className={t.image} /> : null}
106
- <Title className={t.title}>
107
- <Emphasis text={c.title} />
108
- </Title>
109
- {c.body ? <p className={t.body}>{c.body}</p> : null}
110
- {c.link ? (
111
- <SmartLink href={c.link.href} className={t.link}>
112
- {c.link.label}
113
- </SmartLink>
114
- ) : null}
124
+ <Part name="cardGrid.item" props={{ block, item: c, index: i, heading: Title }}>
125
+ {c.image ? <Img image={c.image} crop={16 / 10} className={t.image} /> : null}
126
+ <Title className={t.title}>
127
+ <Emphasis text={c.title} />
128
+ </Title>
129
+ {c.body ? <p className={t.body}>{c.body}</p> : null}
130
+ {c.link ? (
131
+ <SmartLink href={c.link.href} className={t.link}>
132
+ {c.link.label}
133
+ </SmartLink>
134
+ ) : null}
135
+ </Part>
115
136
  </li>
116
137
  ))}
117
138
  </ul>
@@ -126,33 +147,37 @@ export function Projects({ block }: { block: BlockOf<"projects"> }) {
126
147
  const Title = d.heading ? "h3" : "h2";
127
148
  return (
128
149
  <Section block="projects" id={block.anchor} tone={d.tone}>
129
- <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
150
+ <Part name="projects.heading" props={{ block }}>
151
+ <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
152
+ </Part>
130
153
  <ul className={grid ? t.gridList : t.showcaseList} data-layout={d.layout}>
131
154
  {d.items.map((p, i) => (
132
155
  <li key={i} className={grid ? t.gridItem : t.showcaseItem} data-flip={!grid && i % 2 ? "" : undefined}>
133
- {p.image ? <Img image={p.image} crop={16 / 10} className={t.image} /> : null}
134
- <div className={t.text}>
135
- <div className={t.top}>
136
- <Title className={t.name}>{p.name}</Title>
137
- {p.status ? <span className={t.status}>{p.status}</span> : null}
156
+ <Part name="projects.item" props={{ block, item: p, index: i, heading: Title }}>
157
+ {p.image ? <Img image={p.image} crop={16 / 10} className={t.image} /> : null}
158
+ <div className={t.text}>
159
+ <div className={t.top}>
160
+ <Title className={t.name}>{p.name}</Title>
161
+ {p.status ? <span className={t.status}>{p.status}</span> : null}
162
+ </div>
163
+ {p.role ? <p className={t.role}>{p.role}</p> : null}
164
+ {p.description ? <Markdown className={t.description}>{p.description}</Markdown> : null}
165
+ {p.stack.length ? (
166
+ <ul className={t.stack}>
167
+ {p.stack.map((s, j) => (
168
+ <li key={j} className={t.stackItem}>
169
+ {s}
170
+ </li>
171
+ ))}
172
+ </ul>
173
+ ) : null}
174
+ {p.url ? (
175
+ <SmartLink href={p.url} className={t.link}>
176
+ {fill(theme.text.visitProject, { name: p.name })}
177
+ </SmartLink>
178
+ ) : null}
138
179
  </div>
139
- {p.role ? <p className={t.role}>{p.role}</p> : null}
140
- {p.description ? <Markdown className={t.description}>{p.description}</Markdown> : null}
141
- {p.stack.length ? (
142
- <ul className={t.stack}>
143
- {p.stack.map((s, j) => (
144
- <li key={j} className={t.stackItem}>
145
- {s}
146
- </li>
147
- ))}
148
- </ul>
149
- ) : null}
150
- {p.url ? (
151
- <SmartLink href={p.url} className={t.link}>
152
- {fill(theme.text.visitProject, { name: p.name })}
153
- </SmartLink>
154
- ) : null}
155
- </div>
180
+ </Part>
156
181
  </li>
157
182
  ))}
158
183
  </ul>
@@ -166,32 +191,36 @@ export function Pricing({ block }: { block: BlockOf<"pricing"> }) {
166
191
  const Title = d.heading ? "h3" : "h2";
167
192
  return (
168
193
  <Section block="pricing" id={block.anchor} tone={d.tone}>
169
- <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
194
+ <Part name="pricing.heading" props={{ block }}>
195
+ <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
196
+ </Part>
170
197
  <ul className={t.list} data-count={d.plans.length >= 4 ? "many" : "few"}>
171
198
  {d.plans.map((p, i) => (
172
199
  <li key={i} className={t.plan} data-highlight={p.highlight ? "" : undefined}>
173
- {p.highlight ? <p className={t.highlight}>{p.highlight}</p> : null}
174
- <Title className={t.name}>{p.name}</Title>
175
- <p className={t.price}>
176
- <span className={t.amount}>{p.price}</span>
177
- {p.period ? <span className={t.period}>{p.period}</span> : null}
178
- </p>
179
- {p.description ? <p className={t.description}>{p.description}</p> : null}
180
- {p.features.length ? (
181
- <ul className={t.features}>
182
- {p.features.map((f, j) => (
183
- <li key={j} className={t.feature}>
184
- <span aria-hidden className={t.check} />
185
- {f}
186
- </li>
187
- ))}
188
- </ul>
189
- ) : null}
190
- {p.cta ? (
191
- <div className={t.actions}>
192
- <Button link={p.cta} variant={p.highlight ? "primary" : "secondary"} />
193
- </div>
194
- ) : null}
200
+ <Part name="pricing.item" props={{ block, item: p, index: i, heading: Title }}>
201
+ {p.highlight ? <p className={t.highlight}>{p.highlight}</p> : null}
202
+ <Title className={t.name}>{p.name}</Title>
203
+ <p className={t.price}>
204
+ <span className={t.amount}>{p.price}</span>
205
+ {p.period ? <span className={t.period}>{p.period}</span> : null}
206
+ </p>
207
+ {p.description ? <p className={t.description}>{p.description}</p> : null}
208
+ {p.features.length ? (
209
+ <ul className={t.features}>
210
+ {p.features.map((f, j) => (
211
+ <li key={j} className={t.feature}>
212
+ <span aria-hidden className={t.check} />
213
+ {f}
214
+ </li>
215
+ ))}
216
+ </ul>
217
+ ) : null}
218
+ {p.cta ? (
219
+ <div className={t.actions}>
220
+ <Button link={p.cta} variant={p.highlight ? "primary" : "secondary"} />
221
+ </div>
222
+ ) : null}
223
+ </Part>
195
224
  </li>
196
225
  ))}
197
226
  </ul>
@@ -206,14 +235,18 @@ export function CallToAction({ block }: { block: BlockOf<"cta"> }) {
206
235
  const content = (
207
236
  <div className={t.row}>
208
237
  <div className={t.text}>
209
- <h2 className={t.title}>
210
- <Emphasis text={d.heading} />
211
- </h2>
212
- {d.body ? <p className={t.body}>{d.body}</p> : null}
238
+ <Part name="cta.heading" props={{ block }}>
239
+ <h2 className={t.title}>
240
+ <Emphasis text={d.heading} />
241
+ </h2>
242
+ {d.body ? <p className={t.body}>{d.body}</p> : null}
243
+ </Part>
213
244
  </div>
214
245
  <div className={t.actions}>
215
- <Button link={d.primary} />
216
- <Button link={d.secondary} variant="secondary" />
246
+ <Part name="cta.actions" props={{ block }}>
247
+ <Button link={d.primary} />
248
+ <Button link={d.secondary} variant="secondary" />
249
+ </Part>
217
250
  </div>
218
251
  </div>
219
252
  );
@@ -225,7 +258,9 @@ export function CallToAction({ block }: { block: BlockOf<"cta"> }) {
225
258
  );
226
259
  return (
227
260
  <section id={block.anchor} data-block="cta" {...ground("inverse")} className={cx(t.imageRoot, block.anchor && theme.section.anchored)}>
228
- <Img image={d.image} crop className={t.image} />
261
+ <Part name="cta.media" props={{ block }}>
262
+ <Img image={d.image} crop className={t.image} />
263
+ </Part>
229
264
  <div className={t.overlay}>
230
265
  <div className={t.overlayInner}>{content}</div>
231
266
  </div>
@@ -237,15 +272,19 @@ export function Faq({ block }: { block: BlockOf<"faq"> }) {
237
272
  const t = theme.faq;
238
273
  return (
239
274
  <Section block="faq" id={block.anchor}>
240
- <SectionHeading heading={block.data.heading} />
275
+ <Part name="faq.heading" props={{ block }}>
276
+ <SectionHeading heading={block.data.heading} />
277
+ </Part>
241
278
  {block.data.display === "list" ? (
242
279
  <dl className={t.terms}>
243
280
  {block.data.items.map((q, i) => (
244
281
  <div key={i} className={t.term}>
245
- <dt className={t.termQuestion}>{q.question}</dt>
246
- <dd>
247
- <Markdown className={t.termAnswer}>{q.answer}</Markdown>
248
- </dd>
282
+ <Part name="faq.item" props={{ block, item: q, index: i }}>
283
+ <dt className={t.termQuestion}>{q.question}</dt>
284
+ <dd>
285
+ <Markdown className={t.termAnswer}>{q.answer}</Markdown>
286
+ </dd>
287
+ </Part>
249
288
  </div>
250
289
  ))}
251
290
  </dl>
@@ -253,11 +292,13 @@ export function Faq({ block }: { block: BlockOf<"faq"> }) {
253
292
  <div className={t.list}>
254
293
  {block.data.items.map((q, i) => (
255
294
  <details key={i} className={t.item}>
256
- <summary className={t.question}>
257
- {q.question}
258
- <span aria-hidden className={t.icon} />
259
- </summary>
260
- <Markdown className={t.answer}>{q.answer}</Markdown>
295
+ <Part name="faq.item" props={{ block, item: q, index: i }}>
296
+ <summary className={t.question}>
297
+ {q.question}
298
+ <span aria-hidden className={t.icon} />
299
+ </summary>
300
+ <Markdown className={t.answer}>{q.answer}</Markdown>
301
+ </Part>
261
302
  </details>
262
303
  ))}
263
304
  </div>
@@ -6,6 +6,7 @@ import { Registration } from "../components/Registration";
6
6
  import { VolunteerForm } from "../forms/VolunteerForm";
7
7
  import { formatEventDate } from "../format";
8
8
  import { overrides, type CardHeading } from "../overrides";
9
+ import { Part } from "../parts";
9
10
  import { fill, Img, Section, SectionHeading, toTone } from "../ui";
10
11
 
11
12
  export { formatEventDate };
@@ -39,12 +40,18 @@ export function EventCard({ event, heading: Heading = "h3" }: { event: HearthEve
39
40
  /** Event cards (the site's own, if it replaces them). `heading` is the level of their titles; `emptyText` is what to say when there are none. */
40
41
  export function EventList({ events, heading = "h3", emptyText }: { events: HearthEvent[]; heading?: CardHeading; emptyText?: string }) {
41
42
  if (!events.length) return <p className={theme.eventCard.empty}>{emptyText || theme.text.noEvents}</p>;
42
- const Card = overrides.EventCard ?? EventCard;
43
+ const Card = overrides.EventCard;
43
44
  return (
44
45
  <ul className={theme.eventCard.list}>
45
- {events.map((e) => (
46
- <Card key={e.slug} event={e} heading={heading} />
47
- ))}
46
+ {events.map((e) =>
47
+ Card ? (
48
+ <Card key={e.slug} event={e} heading={heading}>
49
+ <EventCard event={e} heading={heading} />
50
+ </Card>
51
+ ) : (
52
+ <EventCard key={e.slug} event={e} heading={heading} />
53
+ ),
54
+ )}
48
55
  </ul>
49
56
  );
50
57
  }
@@ -54,7 +61,9 @@ export function UpcomingEvents({ block }: { block: BlockOf<"upcomingEvents"> })
54
61
  const href = d.collection?.path ?? "/events";
55
62
  return (
56
63
  <Section block="upcomingEvents" id={block.anchor}>
57
- <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
64
+ <Part name="upcomingEvents.heading" props={{ block }}>
65
+ <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
66
+ </Part>
58
67
  <EventList events={d.events} heading={d.heading ? "h3" : "h2"} emptyText={d.emptyText} />
59
68
  {/* A limit of 0 is every event already, and on the page it points at the link would go nowhere. */}
60
69
  {d.events.length && d.limit !== 0 ? (
@@ -74,7 +83,9 @@ export function PastEvents({ block }: { block: BlockOf<"pastEvents"> }) {
74
83
  if (!d.events.length && !d.emptyText) return null;
75
84
  return (
76
85
  <Section block="pastEvents" id={block.anchor} tone={toTone(theme.pastEvents.tone)}>
77
- <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
86
+ <Part name="pastEvents.heading" props={{ block }}>
87
+ <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
88
+ </Part>
78
89
  <EventList events={d.events} heading={d.heading ? "h3" : "h2"} emptyText={d.emptyText} />
79
90
  </Section>
80
91
  );
@@ -94,12 +105,16 @@ export function EventSpotlight({ block }: { block: BlockOf<"eventSpotlight"> })
94
105
  {e.location.name ? <p className={t.place}>{e.location.name}</p> : null}
95
106
  {e.summary ? <p className={t.summary}>{e.summary}</p> : null}
96
107
  <div className={t.actions}>
97
- <Link href={e.path} className={theme.button.primary}>
98
- {theme.text.eventDetails}
99
- </Link>
108
+ <Part name="eventSpotlight.actions" props={{ block }}>
109
+ <Link href={e.path} className={theme.button.primary}>
110
+ {theme.text.eventDetails}
111
+ </Link>
112
+ </Part>
100
113
  </div>
101
114
  </div>
102
- {e.image ? <Img image={e.image} crop={4 / 3} className={t.image} /> : null}
115
+ <Part name="eventSpotlight.media" props={{ block }}>
116
+ {e.image ? <Img image={e.image} crop={4 / 3} className={t.image} /> : null}
117
+ </Part>
103
118
  </div>
104
119
  </Section>
105
120
  );
@@ -5,6 +5,7 @@ import { NotHere } from "../components/NotHere";
5
5
  import { DonationForm } from "../forms/DonationForm";
6
6
  import { money } from "../format";
7
7
  import { overrides, type CardHeading } from "../overrides";
8
+ import { Part } from "../parts";
8
9
  import { fill, Img, Section, SectionHeading, toTone } from "../ui";
9
10
 
10
11
  export { money };
@@ -56,12 +57,18 @@ export function FundraiserCard({ fundraiser, heading: Heading = "h3" }: { fundra
56
57
  /** Fundraiser cards (the site's own, if it replaces them). `heading` is the level of their titles; `emptyText` is what to say when there are none. */
57
58
  export function FundraiserList({ fundraisers, heading = "h3", emptyText }: { fundraisers: Fundraiser[]; heading?: CardHeading; emptyText?: string }) {
58
59
  if (!fundraisers.length) return <p className={theme.fundraiserCard.empty}>{emptyText || theme.text.noFundraisers}</p>;
59
- const Card = overrides.FundraiserCard ?? FundraiserCard;
60
+ const Card = overrides.FundraiserCard;
60
61
  return (
61
62
  <ul className={theme.fundraiserCard.list}>
62
- {fundraisers.map((f) => (
63
- <Card key={f.slug} fundraiser={f} heading={heading} />
64
- ))}
63
+ {fundraisers.map((f) =>
64
+ Card ? (
65
+ <Card key={f.slug} fundraiser={f} heading={heading}>
66
+ <FundraiserCard fundraiser={f} heading={heading} />
67
+ </Card>
68
+ ) : (
69
+ <FundraiserCard key={f.slug} fundraiser={f} heading={heading} />
70
+ ),
71
+ )}
65
72
  </ul>
66
73
  );
67
74
  }
@@ -70,7 +77,9 @@ export function FundraiserGrid({ block }: { block: BlockOf<"fundraiserGrid"> })
70
77
  const d = block.data;
71
78
  return (
72
79
  <Section block="fundraiserGrid" id={block.anchor}>
73
- <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
80
+ <Part name="fundraiserGrid.heading" props={{ block }}>
81
+ <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
82
+ </Part>
74
83
  <FundraiserList fundraisers={d.fundraisers} heading={d.heading ? "h3" : "h2"} emptyText={d.emptyText} />
75
84
  {d.fundraisers.length ? (
76
85
  <NotHere href="/fundraisers">
@@ -96,17 +105,21 @@ export function FundraiserSpotlight({ block }: { block: BlockOf<"fundraiserSpotl
96
105
  {f.summary ? <p className={t.summary}>{f.summary}</p> : null}
97
106
  <Progress fundraiser={f} large />
98
107
  <p className={t.actions}>
99
- {f.status === "active" ? (
100
- <Link href={`${f.path}#donate`} className={theme.button.primary}>
101
- {theme.text.donate}
108
+ <Part name="fundraiserSpotlight.actions" props={{ block }}>
109
+ {f.status === "active" ? (
110
+ <Link href={`${f.path}#donate`} className={theme.button.primary}>
111
+ {theme.text.donate}
112
+ </Link>
113
+ ) : null}
114
+ <Link href={f.path} className={t.learnMore}>
115
+ {theme.text.learnMore}
102
116
  </Link>
103
- ) : null}
104
- <Link href={f.path} className={t.learnMore}>
105
- {theme.text.learnMore}
106
- </Link>
117
+ </Part>
107
118
  </p>
108
119
  </div>
109
- {f.image ? <Img image={f.image} crop={4 / 3} className={t.image} /> : null}
120
+ <Part name="fundraiserSpotlight.media" props={{ block }}>
121
+ {f.image ? <Img image={f.image} crop={4 / 3} className={t.image} /> : null}
122
+ </Part>
110
123
  </div>
111
124
  </Section>
112
125
  );
@@ -118,7 +131,9 @@ export function Donate({ block }: { block: BlockOf<"donate"> }) {
118
131
  return (
119
132
  <Section block="donate" id={block.anchor}>
120
133
  <div className={t.wrap}>
121
- <SectionHeading heading={d.heading} intro={d.intro} />
134
+ <Part name="donate.heading" props={{ block }}>
135
+ <SectionHeading heading={d.heading} intro={d.intro} />
136
+ </Part>
122
137
  {d.fundraiser ? <p className={t.supporting}>{fill(theme.text.supporting, { fundraiser: d.fundraiser.title })}</p> : null}
123
138
  <DonationForm presetsCents={d.presetsCents} minCents={d.minCents} maxCents={d.maxCents} fundraiser={d.fundraiser?.slug} submitLabel={d.submitLabel} />
124
139
  </div>
@@ -1,10 +1,11 @@
1
1
  import type { BlockOf, ResolvedBlock } from "@the-smithy/hearth";
2
2
  import { HearthBlocks, type BlockComponents } from "@the-smithy/hearth/react";
3
- import type { ComponentType } from "react";
3
+ import type { ComponentType, ReactNode } from "react";
4
4
  import { theme } from "@/theme";
5
5
  import { EmberFormBlock } from "../forms/EmberFormBlock";
6
6
  import { HearthForm } from "../forms/ContactForm";
7
7
  import { overrides } from "../overrides";
8
+ import { Part } from "../parts";
8
9
  import { Section, SectionHeading } from "../ui";
9
10
  import { CollectionList } from "./collections";
10
11
  import { CallToAction, CardGrid, Faq, Hero, MediaText, Pricing, Projects, Quote, RichText, Stats } from "./content";
@@ -18,7 +19,9 @@ function FormBlock({ block }: { block: BlockOf<"form"> }) {
18
19
  return (
19
20
  <Section block="form" id={block.anchor}>
20
21
  <div className={theme.formBlock.wrap}>
21
- <SectionHeading heading={block.data.heading} intro={block.data.intro} />
22
+ <Part name="form.heading" props={{ block }}>
23
+ <SectionHeading heading={block.data.heading} intro={block.data.intro} />
24
+ </Part>
22
25
  {/* Set to an Ember form in Hearth: Ember renders it and keeps the leads; the site's form is the fallback. */}
23
26
  {block.data.emberFormId ? (
24
27
  <EmberFormBlock formId={block.data.emberFormId} fallback={<HearthForm form={block.data} />} />
@@ -32,7 +35,7 @@ function FormBlock({ block }: { block: BlockOf<"form"> }) {
32
35
 
33
36
  /**
34
37
  * One component per Hearth block type. Sites style them through the theme (src/theme/index.ts); a site that has to
35
- * replace one entirely does it in src/theme/overrides.tsx. Types without a component (including ones Hearth adds
38
+ * change one adds to it, replaces one of its parts or replaces it in src/theme/overrides.tsx. Types without a component (including ones Hearth adds
36
39
  * later) render nothing.
37
40
  */
38
41
  export const blocks: BlockComponents = {
@@ -66,15 +69,21 @@ export const blocks: BlockComponents = {
66
69
  custom: CustomBlock,
67
70
  };
68
71
 
69
- /** A site's replaced block keeps working with anchors from Hearth, whether or not its code knows about them. */
70
- const anchored = (Component: ComponentType<{ block: ResolvedBlock }>) =>
71
- function Anchored({ block }: { block: ResolvedBlock }) {
72
+ type Drawn = ComponentType<{ block: ResolvedBlock; children?: ReactNode }>;
73
+
74
+ /**
75
+ * A site's block, given Hearth's own as its children, to keep, add to or leave out. Anchors from Hearth keep working
76
+ * around it, whether or not its code knows about them, so Hearth's inside it goes without one.
77
+ */
78
+ const extended = (Component: Drawn, Hearths: Drawn | undefined) =>
79
+ function Extended({ block }: { block: ResolvedBlock }) {
80
+ const drawn = <Component block={block}>{Hearths ? <Hearths block={{ ...block, anchor: undefined }} /> : null}</Component>;
72
81
  return block.anchor ? (
73
82
  <div id={block.anchor} className={theme.section.anchored}>
74
- <Component block={block} />
83
+ {drawn}
75
84
  </div>
76
85
  ) : (
77
- <Component block={block} />
86
+ drawn
78
87
  );
79
88
  };
80
89
 
@@ -87,7 +96,9 @@ let components: BlockComponents | undefined;
87
96
  function allBlocks() {
88
97
  components ??= {
89
98
  ...blocks,
90
- ...Object.fromEntries(Object.entries(overrides.blocks ?? {}).map(([type, component]) => [type, anchored(component as ComponentType<{ block: ResolvedBlock }>)])),
99
+ ...Object.fromEntries(
100
+ Object.entries(overrides.blocks ?? {}).map(([type, component]) => [type, extended(component as Drawn, blocks[type as keyof BlockComponents] as Drawn | undefined)]),
101
+ ),
91
102
  };
92
103
  return components;
93
104
  }