@the-smithy/create-hearth-site 0.30.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 { directionsUrl, pinned, type BlockOf } from "@the-smithy/hearth";
2
2
  import { theme } from "@/theme";
3
+ import { Part } from "../parts";
3
4
  import { ExternalLink, fill, Markdown, Section, SectionHeading, SmartLink } from "../ui";
4
5
  import { OpenMap } from "./OpenMap";
5
6
 
@@ -9,16 +10,20 @@ export function Location({ block }: { block: BlockOf<"location"> }) {
9
10
  const Title = d.heading ? "h3" : "h2";
10
11
  return (
11
12
  <Section block="location" id={block.anchor}>
12
- <SectionHeading eyebrow={d.eyebrow} heading={d.heading} />
13
+ <Part name="location.heading" props={{ block }}>
14
+ <SectionHeading eyebrow={d.eyebrow} heading={d.heading} />
15
+ </Part>
13
16
  <div className={t.grid}>
14
17
  <div className={t.details}>
15
- <Title className={t.name}>{d.name}</Title>
16
- <address className={t.address}>{d.address}</address>
17
- {d.hours ? <p className={t.hours}>{d.hours}</p> : null}
18
- {d.notes ? <Markdown className={t.notes}>{d.notes}</Markdown> : null}
19
- <ExternalLink href={directionsUrl(d)} rel="noopener" className={t.directions}>
20
- {theme.text.directions}
21
- </ExternalLink>
18
+ <Part name="location.details" props={{ block }}>
19
+ <Title className={t.name}>{d.name}</Title>
20
+ <address className={t.address}>{d.address}</address>
21
+ {d.hours ? <p className={t.hours}>{d.hours}</p> : null}
22
+ {d.notes ? <Markdown className={t.notes}>{d.notes}</Markdown> : null}
23
+ <ExternalLink href={directionsUrl(d)} rel="noopener" className={t.directions}>
24
+ {theme.text.directions}
25
+ </ExternalLink>
26
+ </Part>
22
27
  </div>
23
28
  {/* Content from before map pins has none: it shows no map until the pin is set in Hearth. */}
24
29
  {d.showMap && d.position ? <OpenMap places={[{ name: d.name, address: d.address, position: d.position }]} label={fill(theme.text.mapOf, { name: d.name })} className={t.map} /> : null}
@@ -35,27 +40,31 @@ export function Locations({ block }: { block: BlockOf<"locations"> }) {
35
40
  const Title = d.heading ? "h3" : "h2";
36
41
  return (
37
42
  <Section block="locations" id={block.anchor}>
38
- <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
43
+ <Part name="locations.heading" props={{ block }}>
44
+ <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
45
+ </Part>
39
46
  <div className={t.grid} data-map={onMap.length ? "" : undefined}>
40
47
  {onMap.length ? <OpenMap places={onMap} label={d.heading ? fill(theme.text.mapHeading, { heading: d.heading }) : theme.text.mapPlaces} className={t.map} /> : null}
41
48
  <ul className={onMap.length ? t.list : t.listNoMap}>
42
49
  {d.items.map((place, i) => (
43
50
  <li key={i} className={t.item}>
44
- <Title className={t.name}>{place.name}</Title>
45
- <address className={t.address}>{place.address}</address>
46
- {place.hours ? <p className={t.hours}>{place.hours}</p> : null}
47
- {place.notes ? <p className={t.notes}>{place.notes}</p> : null}
48
- <p className={t.links}>
49
- <ExternalLink href={directionsUrl(place)} rel="noopener" className={t.link}>
50
- {theme.text.directions}
51
- <span className="sr-only"> {fill(theme.text.directionsTo, { name: place.name })}</span>
52
- </ExternalLink>
53
- {place.link ? (
54
- <SmartLink href={place.link.href} className={t.link}>
55
- {place.link.label}
56
- </SmartLink>
57
- ) : null}
58
- </p>
51
+ <Part name="locations.item" props={{ block, item: place, index: i, heading: Title }}>
52
+ <Title className={t.name}>{place.name}</Title>
53
+ <address className={t.address}>{place.address}</address>
54
+ {place.hours ? <p className={t.hours}>{place.hours}</p> : null}
55
+ {place.notes ? <p className={t.notes}>{place.notes}</p> : null}
56
+ <p className={t.links}>
57
+ <ExternalLink href={directionsUrl(place)} rel="noopener" className={t.link}>
58
+ {theme.text.directions}
59
+ <span className="sr-only"> {fill(theme.text.directionsTo, { name: place.name })}</span>
60
+ </ExternalLink>
61
+ {place.link ? (
62
+ <SmartLink href={place.link.href} className={t.link}>
63
+ {place.link.label}
64
+ </SmartLink>
65
+ ) : null}
66
+ </p>
67
+ </Part>
59
68
  </li>
60
69
  ))}
61
70
  </ul>
@@ -69,15 +78,19 @@ export function Schedule({ block }: { block: BlockOf<"schedule"> }) {
69
78
  const t = theme.schedule;
70
79
  return (
71
80
  <Section block="schedule" id={block.anchor}>
72
- <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
81
+ <Part name="schedule.heading" props={{ block }}>
82
+ <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
83
+ </Part>
73
84
  <ol className={t.list}>
74
85
  {d.items.map((item, i) => (
75
86
  <li key={i} className={t.item}>
76
- <p className={t.time}>{item.time}</p>
77
- <div>
78
- <p className={t.title}>{item.title}</p>
79
- {item.details ? <p className={t.details}>{item.details}</p> : null}
80
- </div>
87
+ <Part name="schedule.item" props={{ block, item, index: i }}>
88
+ <p className={t.time}>{item.time}</p>
89
+ <div>
90
+ <p className={t.title}>{item.title}</p>
91
+ {item.details ? <p className={t.details}>{item.details}</p> : null}
92
+ </div>
93
+ </Part>
81
94
  </li>
82
95
  ))}
83
96
  </ol>
@@ -1,6 +1,7 @@
1
1
  import { videoEmbedUrl, type BlockOf } from "@the-smithy/hearth";
2
2
  import { HearthEmbed } from "@the-smithy/hearth/react";
3
3
  import { theme } from "@/theme";
4
+ import { Part } from "../parts";
4
5
  import { ExternalLink, Img, Section, SectionHeading } from "../ui";
5
6
 
6
7
  export function Gallery({ block }: { block: BlockOf<"gallery"> }) {
@@ -8,14 +9,18 @@ export function Gallery({ block }: { block: BlockOf<"gallery"> }) {
8
9
  const t = theme.gallery;
9
10
  return (
10
11
  <Section block="gallery" id={block.anchor}>
11
- <SectionHeading heading={d.heading} intro={d.intro} />
12
+ <Part name="gallery.heading" props={{ block }}>
13
+ <SectionHeading heading={d.heading} intro={d.intro} />
14
+ </Part>
12
15
  <ul className={t.list} data-columns={d.columns}>
13
16
  {d.images.map((item, i) => (
14
17
  <li key={i}>
15
- <figure>
16
- <Img image={item.image} crop={4 / 3} className={t.image} />
17
- {item.caption ? <figcaption className={t.caption}>{item.caption}</figcaption> : null}
18
- </figure>
18
+ <Part name="gallery.item" props={{ block, item, index: i }}>
19
+ <figure>
20
+ <Img image={item.image} crop={4 / 3} className={t.image} />
21
+ {item.caption ? <figcaption className={t.caption}>{item.caption}</figcaption> : null}
22
+ </figure>
23
+ </Part>
19
24
  </li>
20
25
  ))}
21
26
  </ul>
@@ -30,7 +35,9 @@ export function Video({ block }: { block: BlockOf<"video"> }) {
30
35
  if (!src) return null;
31
36
  return (
32
37
  <Section block="video" id={block.anchor}>
33
- <SectionHeading heading={d.heading} />
38
+ <Part name="video.heading" props={{ block }}>
39
+ <SectionHeading heading={d.heading} />
40
+ </Part>
34
41
  <figure className={t.figure}>
35
42
  <div className={t.frame}>
36
43
  <iframe
@@ -67,16 +74,20 @@ export function People({ block }: { block: BlockOf<"people"> }) {
67
74
  const Title = d.heading ? "h3" : "h2";
68
75
  return (
69
76
  <Section block="people" id={block.anchor}>
70
- <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
77
+ <Part name="people.heading" props={{ block }}>
78
+ <SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
79
+ </Part>
71
80
  <ul className={t.list}>
72
81
  {d.people.map((p, i) => (
73
82
  <li key={i} className={t.person}>
74
- {p.image ? <Img image={p.image} crop={1} className={t.image} /> : null}
75
- <div>
76
- <Title className={t.name}>{p.name}</Title>
77
- {p.role ? <p className={t.role}>{p.role}</p> : null}
78
- </div>
79
- {p.bio ? <p className={t.bio}>{p.bio}</p> : null}
83
+ <Part name="people.item" props={{ block, item: p, index: i, heading: Title }}>
84
+ {p.image ? <Img image={p.image} crop={1} className={t.image} /> : null}
85
+ <div>
86
+ <Title className={t.name}>{p.name}</Title>
87
+ {p.role ? <p className={t.role}>{p.role}</p> : null}
88
+ </div>
89
+ {p.bio ? <p className={t.bio}>{p.bio}</p> : null}
90
+ </Part>
80
91
  </li>
81
92
  ))}
82
93
  </ul>
@@ -89,19 +100,23 @@ export function Logos({ block }: { block: BlockOf<"logos"> }) {
89
100
  const t = theme.logos;
90
101
  return (
91
102
  <Section block="logos" id={block.anchor}>
92
- <SectionHeading heading={d.heading} intro={d.intro} />
103
+ <Part name="logos.heading" props={{ block }}>
104
+ <SectionHeading heading={d.heading} intro={d.intro} />
105
+ </Part>
93
106
  <ul className={t.list}>
94
107
  {d.logos.map((l, i) => {
95
108
  const mark = l.logo ? <Img image={{ src: l.logo.src, alt: l.logo.alt || l.name }} className={t.image} /> : <span className={t.name}>{l.name}</span>;
96
109
  return (
97
110
  <li key={i}>
98
- {l.url ? (
99
- <ExternalLink href={l.url} rel="noopener" className={t.link}>
100
- {mark}
101
- </ExternalLink>
102
- ) : (
103
- mark
104
- )}
111
+ <Part name="logos.item" props={{ block, item: l, index: i }}>
112
+ {l.url ? (
113
+ <ExternalLink href={l.url} rel="noopener" className={t.link}>
114
+ {mark}
115
+ </ExternalLink>
116
+ ) : (
117
+ mark
118
+ )}
119
+ </Part>
105
120
  </li>
106
121
  );
107
122
  })}
@@ -16,11 +16,17 @@ import { Share } from "./Share";
16
16
 
17
17
  /** An event's page: its structured data, and the site's EventPage override or Hearth's own. Used at /events/… and /<collection>/…. */
18
18
  export function EventView({ event }: { event: HearthEventDetail }) {
19
- const Page = overrides.EventPage ?? EventContent;
19
+ const Page = overrides.EventPage;
20
20
  return (
21
21
  <>
22
22
  <JsonLd data={event.meta.jsonLd} />
23
- <Page event={event} />
23
+ {Page ? (
24
+ <Page event={event}>
25
+ <EventContent event={event} />
26
+ </Page>
27
+ ) : (
28
+ <EventContent event={event} />
29
+ )}
24
30
  </>
25
31
  );
26
32
  }
@@ -13,11 +13,17 @@ import { Share } from "./Share";
13
13
 
14
14
  /** A fundraiser's page: its structured data, and the site's FundraiserPage override or Hearth's own. */
15
15
  export function FundraiserView({ fundraiser }: { fundraiser: FundraiserDetail }) {
16
- const Page = overrides.FundraiserPage ?? FundraiserContent;
16
+ const Page = overrides.FundraiserPage;
17
17
  return (
18
18
  <>
19
19
  <JsonLd data={fundraiser.meta.jsonLd} />
20
- <Page fundraiser={fundraiser} />
20
+ {Page ? (
21
+ <Page fundraiser={fundraiser}>
22
+ <FundraiserContent fundraiser={fundraiser} />
23
+ </Page>
24
+ ) : (
25
+ <FundraiserContent fundraiser={fundraiser} />
26
+ )}
21
27
  </>
22
28
  );
23
29
  }
@@ -7,7 +7,14 @@ import { DonateForm } from "./DonateForm";
7
7
  * offers the active ones to choose from.
8
8
  */
9
9
  export async function DonationForm(props: Omit<DonateFormProps, "fundraisers">) {
10
- const Form = overrides.DonateForm ?? DonateForm;
10
+ const Form = overrides.DonateForm;
11
11
  const fundraisers = props.fundraiser ? [] : await hearthNext.getFundraisers({ status: "active" });
12
- return <Form {...props} fundraisers={fundraisers.map((f) => ({ slug: f.slug, title: f.title }))} />;
12
+ const all = { ...props, fundraisers: fundraisers.map((f) => ({ slug: f.slug, title: f.title })) };
13
+ return Form ? (
14
+ <Form {...all}>
15
+ <DonateForm {...all} />
16
+ </Form>
17
+ ) : (
18
+ <DonateForm {...all} />
19
+ );
13
20
  }
@@ -17,7 +17,7 @@ import { SiteHeader } from "./SiteHeader";
17
17
  export default async function SiteFrame({ children, headerActions }: { children: ReactNode; headerActions?: ReactNode }) {
18
18
  const [site, preview, globals] = await Promise.all([hearthNext.getSite(), hearthNext.isPreview(), hearthNext.getGlobalBlocks()]);
19
19
  const t = theme.page;
20
- const Footer = overrides.Footer ?? SiteFooter;
20
+ const Footer = overrides.Footer;
21
21
  // Global blocks (Global blocks in Hearth) shown outside pages: under menu items, and as pop-ups.
22
22
  const dropdowns = Object.fromEntries(
23
23
  globals.filter((g) => site.settings.navigation.some((n) => n.dropdown === g.key)).map((g) => [g.key, <Blocks key={g.key} blocks={[g.block]} />]),
@@ -33,7 +33,13 @@ export default async function SiteFrame({ children, headerActions }: { children:
33
33
  <main id="main" className={t.main}>
34
34
  {children}
35
35
  </main>
36
- <Footer site={site} />
36
+ {Footer ? (
37
+ <Footer site={site}>
38
+ <SiteFooter site={site} />
39
+ </Footer>
40
+ ) : (
41
+ <SiteFooter site={site} />
42
+ )}
37
43
  {globals.map((g) =>
38
44
  g.popup ? (
39
45
  <Popup key={g.key} popupKey={g.key} name={g.name} title={g.popup.title} button={g.popup.button}>
@@ -1,6 +1,6 @@
1
1
  import type { Fundraiser, FundraiserDetail, HearthEvent, HearthEventDetail, Site } from "@the-smithy/hearth";
2
2
  import type { BlockComponents } from "@the-smithy/hearth/react";
3
- import type { ComponentType } from "react";
3
+ import type { ComponentProps, ComponentType, ReactNode } from "react";
4
4
  import * as site from "@/theme/overrides";
5
5
 
6
6
  /** The level of a card's title: the next one down from what's above the cards, so h3 under a block's heading and h2 without one. */
@@ -18,27 +18,109 @@ export type DonateFormProps = {
18
18
  submitLabel?: string;
19
19
  };
20
20
 
21
+ type BlockType = keyof BlockComponents;
22
+ /** A block of one type, as its component is given it. */
23
+ type Block<T extends BlockType> = ComponentProps<NonNullable<BlockComponents[T]>>["block"];
24
+ type ItemOf<T extends BlockType, K extends keyof Block<T>["data"]> = NonNullable<Block<T>["data"][K]> extends readonly (infer I)[] ? I : never;
25
+
26
+ /** What an override is given as well as its props: Hearth's own version, drawn, to place inside the site's or leave out. */
27
+ type WithHearths<P> = P & { children?: ReactNode };
28
+
29
+ /** A site's component for a whole block: given the block, and Hearth's own block as `children`. */
30
+ export type BlockOverride<T extends BlockType> = ComponentType<WithHearths<{ block: Block<T> }>>;
31
+ export type BlockOverrides = { [T in BlockType]?: BlockOverride<T> };
32
+
33
+ type One<T extends BlockType> = { block: Block<T> };
34
+ /** One item of a list: the item, its place in the list (from 0), and the block. */
35
+ type Item<T extends BlockType, K extends keyof Block<T>["data"]> = { block: Block<T>; item: ItemOf<T, K>; index: number };
36
+ /** An item with a title: `heading` is the title's level, h3 under the block's heading and h2 without one. */
37
+ type TitledItem<T extends BlockType, K extends keyof Block<T>["data"]> = Item<T, K> & { heading: CardHeading };
38
+
39
+ /**
40
+ * The parts of Hearth's blocks a site can replace or add to in src/theme/overrides.tsx (`parts`), keeping the rest of
41
+ * the block and its updates. Each is given these props and Hearth's own part as `children`. A part is what's inside its
42
+ * element, which stays Hearth's, with its classes: an item is a card's contents inside its <li>.
43
+ *
44
+ * - `heading`: the block's heading (eyebrow, heading and intro). The hero's and a CTA's: the text above the buttons.
45
+ * - `item`: one item's contents, inside its element (an <li>; a stat's <div>; a question's <div> or <details>).
46
+ * - `actions`: the block's links, inside its button row. The row is drawn for a site's part even with no links.
47
+ * - `media`: the block's image, where it has one. Without one, children is null and a site's part can still draw something.
48
+ * - `details`: a location's name, address, hours and directions, beside its map.
49
+ */
50
+ export type PartProps = {
51
+ "hero.heading": One<"hero">;
52
+ "hero.actions": One<"hero">;
53
+ "hero.media": One<"hero">;
54
+ "mediaText.heading": One<"mediaText">;
55
+ "mediaText.actions": One<"mediaText">;
56
+ "mediaText.media": One<"mediaText">;
57
+ "stats.heading": One<"stats">;
58
+ "stats.item": Item<"stats", "items">;
59
+ "cardGrid.heading": One<"cardGrid">;
60
+ "cardGrid.item": TitledItem<"cardGrid", "items">;
61
+ "projects.heading": One<"projects">;
62
+ "projects.item": TitledItem<"projects", "items">;
63
+ "pricing.heading": One<"pricing">;
64
+ "pricing.item": TitledItem<"pricing", "plans">;
65
+ "cta.heading": One<"cta">;
66
+ "cta.actions": One<"cta">;
67
+ "cta.media": One<"cta">;
68
+ "faq.heading": One<"faq">;
69
+ "faq.item": Item<"faq", "items">;
70
+ "gallery.heading": One<"gallery">;
71
+ "gallery.item": Item<"gallery", "images">;
72
+ "video.heading": One<"video">;
73
+ "people.heading": One<"people">;
74
+ "people.item": TitledItem<"people", "people">;
75
+ "logos.heading": One<"logos">;
76
+ "logos.item": Item<"logos", "logos">;
77
+ "location.heading": One<"location">;
78
+ "location.details": One<"location">;
79
+ "locations.heading": One<"locations">;
80
+ "locations.item": TitledItem<"locations", "items">;
81
+ "schedule.heading": One<"schedule">;
82
+ "schedule.item": Item<"schedule", "items">;
83
+ "form.heading": One<"form">;
84
+ "upcomingEvents.heading": One<"upcomingEvents">;
85
+ "pastEvents.heading": One<"pastEvents">;
86
+ "eventSpotlight.actions": One<"eventSpotlight">;
87
+ "eventSpotlight.media": One<"eventSpotlight">;
88
+ "collectionList.heading": One<"collectionList">;
89
+ "collectionList.item": TitledItem<"collectionList", "entries">;
90
+ "fundraiserGrid.heading": One<"fundraiserGrid">;
91
+ "fundraiserSpotlight.actions": One<"fundraiserSpotlight">;
92
+ "fundraiserSpotlight.media": One<"fundraiserSpotlight">;
93
+ "donate.heading": One<"donate">;
94
+ };
95
+ export type PartName = keyof PartProps;
96
+ export type PartOverride<N extends PartName> = ComponentType<WithHearths<PartProps[N]>>;
97
+ export type PartOverrides = { [N in PartName]?: PartOverride<N> | null };
98
+
21
99
  /**
22
100
  * Everything src/theme/overrides.tsx can export. All of it is optional, so a site's file, which updates never replace,
23
101
  * exports only what it uses. The types are here for that file to use: `export const Footer: Overrides["Footer"] = …`.
102
+ * Each but Logo is given Hearth's own version as `children`: `<>{children}<Extra /></>` adds to it and keeps it up to
103
+ * date; leaving children out replaces it.
24
104
  */
25
105
  export type Overrides = {
26
- /** Replaces a whole block's component. */
27
- blocks?: Partial<BlockComponents>;
106
+ /** A whole block, by type. */
107
+ blocks?: BlockOverrides;
108
+ /** One part of a block, keeping the rest of it (see PartProps). */
109
+ parts?: PartOverrides;
28
110
  /** The logo in the header, and in the footer if the `footer.logo` slot is set. */
29
111
  Logo?: ComponentType<{ site: Site; place: "header" | "footer" }> | null;
30
- /** Replaces the footer. */
31
- Footer?: ComponentType<{ site: Site }> | null;
32
- /** Replaces an event's page, at /events/… and in collections. Its structured data is drawn around it. */
33
- EventPage?: ComponentType<{ event: HearthEventDetail }> | null;
34
- /** Replaces a fundraiser's page. Its structured data is drawn around it. */
35
- FundraiserPage?: ComponentType<{ fundraiser: FundraiserDetail }> | null;
36
- /** Replaces an event's card, as an <li>. May be async. */
37
- EventCard?: ComponentType<{ event: HearthEvent; heading: CardHeading }> | null;
38
- /** Replaces a fundraiser's card, as an <li>. May be async. */
39
- FundraiserCard?: ComponentType<{ fundraiser: Fundraiser; heading: CardHeading }> | null;
40
- /** Replaces the donation form. */
41
- DonateForm?: ComponentType<DonateFormProps> | null;
112
+ /** The footer. */
113
+ Footer?: ComponentType<WithHearths<{ site: Site }>> | null;
114
+ /** An event's page, at /events/… and in collections. Its structured data is drawn around it. */
115
+ EventPage?: ComponentType<WithHearths<{ event: HearthEventDetail }>> | null;
116
+ /** A fundraiser's page. Its structured data is drawn around it. */
117
+ FundraiserPage?: ComponentType<WithHearths<{ fundraiser: FundraiserDetail }>> | null;
118
+ /** An event's card, as an <li> (Hearth's, in children, is one too). May be async. */
119
+ EventCard?: ComponentType<WithHearths<{ event: HearthEvent; heading: CardHeading }>> | null;
120
+ /** A fundraiser's card, as an <li> (Hearth's, in children, is one too). May be async. */
121
+ FundraiserCard?: ComponentType<WithHearths<{ fundraiser: Fundraiser; heading: CardHeading }>> | null;
122
+ /** The donation form. */
123
+ DonateForm?: ComponentType<WithHearths<DonateFormProps>> | null;
42
124
  };
43
125
 
44
126
  /**
@@ -0,0 +1,17 @@
1
+ import type { ComponentType, ReactNode } from "react";
2
+ import { overrides, type PartName, type PartProps } from "./overrides";
3
+
4
+ /*
5
+ * The named parts of Hearth's blocks (PartProps in ./overrides.ts), which a site can replace or add to in
6
+ * src/theme/overrides.tsx and keep the rest of the block, with its updates. Server components only.
7
+ */
8
+
9
+ /** Has the site its own component for this part? (Read when drawing, not as a file loads.) */
10
+ export const hasPart = (name: PartName) => Boolean(overrides.parts?.[name]);
11
+
12
+ /** One part of a block: Hearth's (`children`), or the site's, given Hearth's as its children to keep, add to or leave out. */
13
+ export function Part<N extends PartName>({ name, props, children }: { name: N; props: PartProps[N]; children?: ReactNode }) {
14
+ // Typed by name in PartOverrides; here every part is one shape.
15
+ const Site = overrides.parts?.[name] as ComponentType<Record<string, unknown> & { children?: ReactNode }> | null | undefined;
16
+ return Site ? <Site {...(props as Record<string, unknown>)}>{children}</Site> : children;
17
+ }
@@ -1,27 +1,42 @@
1
- import type { BlockComponents } from "@the-smithy/hearth/react";
2
1
  import type { ComponentType } from "react";
3
2
  import type { Site } from "@the-smithy/hearth";
3
+ import type { Overrides } from "@/hearth/overrides";
4
4
 
5
5
  /**
6
- * The last resort, when a design can't be reached with slots and tokens: replace a whole block's component, or the
7
- * parts below. Replaced components are this site's own code: `pnpm hearth:update` won't update them, so check them
8
- * against src/hearth after each update. Build them from src/hearth/ui.tsx and the theme, and say why in SITE.md.
6
+ * When slots, tokens and CSS can't reach a design: add to Hearth's components, replace one part of a block, or, last,
7
+ * replace a whole one. Each is given Hearth's own version as `children`. Keep it, and the site's code adds to Hearth's,
8
+ * which updates keep current; leave it out, and the site's replaces it and falls behind. `pnpm hearth:update` lists
9
+ * what's overridden here whose Hearth version changed (`pnpm hearth:overrides` checks any time). Build from
10
+ * src/hearth/ui.tsx and the theme, and say why in SITE.md. The types are in src/hearth/overrides.ts.
9
11
  *
10
- * import { CountingStats } from "./CountingStats";
11
- * export const blocks: Partial<BlockComponents> = { stats: CountingStats };
12
+ * A part of a block, keeping the rest of it (all of them: PartProps in src/hearth/overrides.ts). A part is what's
13
+ * inside its element: `heading`, an `item`'s contents inside its <li>, the `actions` inside the button row, `media`.
12
14
  *
13
- * Export any of these too, used everywhere the template draws them (types in src/hearth/overrides.ts). They can import
14
- * Hearth's components, Blocks too. Words, dates and hiding things are slots: try those first.
15
+ * export const parts: Overrides["parts"] = {
16
+ * "hero.actions": ({ children }) => <>{children}<a href="tel:…" className={theme.button.secondary}>Call us</a></>,
17
+ * "cardGrid.item": ({ item, children }) => <>{children}{item.link ? null : <Soon />}</>,
18
+ * };
15
19
  *
16
- * export const Footer: Overrides["Footer"] = ({ site }) => … the whole footer
17
- * export const EventPage: Overrides["EventPage"] = ({ event }) => … an event's page, reusing EventRegisterSection
18
- * and EventVolunteerSection from EventView
19
- * export const FundraiserPage: Overrides["FundraiserPage"] = ({ fundraiser }) => …
20
+ * A whole block, by type: add to Hearth's, or leave children out to replace it.
21
+ *
22
+ * export const blocks: Overrides["blocks"] = {
23
+ * stats: ({ children }) => <>{children}<p className="…">Figures audited in 2026.</p></>,
24
+ * };
25
+ *
26
+ * And these, everywhere the template draws them. They can import Hearth's components, Blocks too. Words, dates and
27
+ * hiding things are slots: try those first.
28
+ *
29
+ * export const Footer: Overrides["Footer"] = ({ site, children }) => … the footer
30
+ * export const EventPage: Overrides["EventPage"] = ({ event, children }) => … an event's page; or build one from
31
+ * EventRegisterSection and EventVolunteerSection
32
+ * export const FundraiserPage: Overrides["FundraiserPage"] = ({ fundraiser, children }) => …
20
33
  * export const EventCard: Overrides["EventCard"] = async ({ event, heading }) => <li>…</li>
21
34
  * export const FundraiserCard: Overrides["FundraiserCard"] = ({ fundraiser, heading }) => <li>…</li>
22
- * export const DonateForm: Overrides["DonateForm"] = (props) => …
35
+ * export const DonateForm: Overrides["DonateForm"] = ({ children, ...props }) => …
23
36
  */
24
- export const blocks: Partial<BlockComponents> = {};
37
+ export const parts: Overrides["parts"] = {};
38
+
39
+ export const blocks: Overrides["blocks"] = {};
25
40
 
26
41
  /**
27
42
  * The header's logo; leave null to show the logo uploaded in Hearth (Settings → Search & sharing) and the site name.