@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.
- package/bin/create-hearth-site.js +1 -1
- package/package.json +2 -2
- package/template/AGENTS.md +84 -14
- package/template/README.md +16 -5
- package/template/SITE.md +2 -2
- package/template/_gitattributes +2 -0
- package/template/_gitignore +2 -1
- package/template/package.json +1 -0
- package/template/scripts/hearth.mjs +124 -5
- package/template/src/hearth/blocks/collections.tsx +42 -35
- package/template/src/hearth/blocks/content.tsx +140 -99
- package/template/src/hearth/blocks/events.tsx +25 -10
- package/template/src/hearth/blocks/giving.tsx +29 -14
- package/template/src/hearth/blocks/index.tsx +20 -9
- package/template/src/hearth/blocks/info.tsx +43 -30
- package/template/src/hearth/blocks/media.tsx +36 -21
- package/template/src/hearth/components/EventView.tsx +8 -2
- package/template/src/hearth/components/FundraiserView.tsx +8 -2
- package/template/src/hearth/forms/DonationForm.tsx +9 -2
- package/template/src/hearth/layout/SiteFrame.tsx +8 -2
- package/template/src/hearth/overrides.ts +97 -15
- package/template/src/hearth/parts.tsx +17 -0
- package/template/src/theme/overrides.tsx +29 -14
|
@@ -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
|
-
<
|
|
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
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{place.link.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
<
|
|
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
|
|
99
|
-
|
|
100
|
-
{
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
19
|
+
const Page = overrides.EventPage;
|
|
20
20
|
return (
|
|
21
21
|
<>
|
|
22
22
|
<JsonLd data={event.meta.jsonLd} />
|
|
23
|
-
|
|
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
|
|
16
|
+
const Page = overrides.FundraiserPage;
|
|
17
17
|
return (
|
|
18
18
|
<>
|
|
19
19
|
<JsonLd data={fundraiser.meta.jsonLd} />
|
|
20
|
-
|
|
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
|
|
10
|
+
const Form = overrides.DonateForm;
|
|
11
11
|
const fundraisers = props.fundraiser ? [] : await hearthNext.getFundraisers({ status: "active" });
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
27
|
-
blocks?:
|
|
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
|
-
/**
|
|
31
|
-
Footer?: ComponentType<{ site: Site }
|
|
32
|
-
/**
|
|
33
|
-
EventPage?: ComponentType<{ event: HearthEventDetail }
|
|
34
|
-
/**
|
|
35
|
-
FundraiserPage?: ComponentType<{ fundraiser: FundraiserDetail }
|
|
36
|
-
/**
|
|
37
|
-
EventCard?: ComponentType<{ event: HearthEvent; heading: CardHeading }
|
|
38
|
-
/**
|
|
39
|
-
FundraiserCard?: ComponentType<{ fundraiser: Fundraiser; heading: CardHeading }
|
|
40
|
-
/**
|
|
41
|
-
DonateForm?: ComponentType<DonateFormProps
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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.
|