@cratis/cast 0.1.0 → 0.2.1
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/README.md +3 -1
- package/badges/RegistryMember.ts +2 -0
- package/badges/badgeSvg.ts +10 -1
- package/badges/registry.ts +5 -0
- package/cast-json.schema.json +1496 -0
- package/config/calendar.ts +34 -0
- package/config/castSchema.ts +3 -2
- package/github/gitHubSnapshotSchema.ts +58 -0
- package/machine/castJson.ts +17 -4
- package/machine/castJsonSchema.ts +35 -0
- package/machine/index.ts +6 -0
- package/machine/profileFromCastJson.ts +215 -0
- package/package.json +5 -1
- package/routes/404.astro +6 -4
- package/routes/cast.json.ts +7 -1
- package/routes/cv.astro +3 -365
- package/routes/index.astro +3 -55
- package/site/components/Activity.astro +9 -8
- package/site/components/Agentic.astro +11 -10
- package/site/components/CareerReplay.astro +11 -8
- package/site/components/CommandPalette.astro +10 -4
- package/site/components/Contact.astro +8 -5
- package/site/components/Cratis.astro +18 -16
- package/site/components/EventModel.astro +8 -5
- package/site/components/Footer.astro +9 -4
- package/site/components/Hero.astro +13 -10
- package/site/components/Inline.astro +45 -0
- package/site/components/Monogram.astro +4 -1
- package/site/components/Nav.astro +12 -9
- package/site/components/Now.astro +13 -10
- package/site/components/Projects.astro +11 -14
- package/site/components/PullRequestFeed.astro +6 -4
- package/site/components/SectionHeader.astro +6 -3
- package/site/components/Skills.astro +5 -2
- package/site/components/Stats.astro +9 -4
- package/site/components/Talks.astro +8 -5
- package/site/components/Terminal.astro +7 -2
- package/site/components/Writing.astro +9 -7
- package/site/data.ts +6 -43
- package/site/layouts/Base.astro +31 -19
- package/site/lib/blog.ts +42 -16
- package/site/lib/github.ts +56 -31
- package/site/lib/inline.ts +65 -9
- package/site/lib/nav.ts +14 -3
- package/site/pages/CvPage.astro +379 -0
- package/site/pages/ProfilePage.astro +78 -0
- package/site/profile/CastHost.ts +32 -0
- package/site/profile/CastView.ts +96 -0
- package/site/profile/assertRenderable.ts +151 -0
- package/site/profile/clientCast.ts +120 -0
- package/site/profile/index.ts +8 -0
- package/site/scripts/live.ts +21 -11
- package/site/scripts/palette.ts +30 -13
- package/site/scripts/replay.ts +2 -1
- package/site/scripts/terminal.ts +40 -29
- package/writing/fetchWriting.ts +17 -2
- package/writing/writingSnapshotSchema.ts +27 -0
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
// Copyright (c) Cratis. All rights reserved.
|
|
3
3
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
5
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
import Inline from './Inline.astro';
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
|
+
cast: CastView;
|
|
9
10
|
index: string;
|
|
10
11
|
title?: string;
|
|
11
12
|
label?: string;
|
|
12
13
|
intro?: string;
|
|
13
14
|
}
|
|
14
15
|
const { index, title = 'Append an event to *my stream.*', label = 'Command', intro = 'Questions about event sourcing, Cratis, a project or a role? Issue the command and it lands in my inbox.' } = Astro.props;
|
|
16
|
+
const { cast } = Astro.props;
|
|
17
|
+
const { person, profileLinks } = cast;
|
|
15
18
|
const mailto = person.email ? `mailto:${person.email}` : undefined;
|
|
16
19
|
const channels = [person.email ? 'email' : undefined, ...profileLinks.slice(0, 3).map((link) => link.label)].filter(Boolean).join(' · ');
|
|
17
20
|
---
|
|
@@ -24,8 +27,8 @@ const channels = [person.email ? 'email' : undefined, ...profileLinks.slice(0, 3
|
|
|
24
27
|
<p class="mb-5 flex items-center gap-3 font-mono text-xs text-fg-faint" data-reveal>
|
|
25
28
|
{index && <span class="text-fg-muted">{index}</span>}{index && <span class="h-px w-8 bg-line-strong" aria-hidden="true"></span>}<span class="note note-command">{label}</span>
|
|
26
29
|
</p>
|
|
27
|
-
<h2 id="contact-title" class="max-w-[14ch] text-[clamp(2.75rem,7.5vw,6.5rem)] leading-[0.92] font-[800] tracking-[-0.045em]" data-reveal
|
|
28
|
-
<p class="mt-6 max-w-[52ch] text-lg text-fg-muted" data-reveal
|
|
30
|
+
<h2 id="contact-title" class="max-w-[14ch] text-[clamp(2.75rem,7.5vw,6.5rem)] leading-[0.92] font-[800] tracking-[-0.045em]" data-reveal><Inline text={title} host={cast.host} /></h2>
|
|
31
|
+
<p class="mt-6 max-w-[52ch] text-lg text-fg-muted" data-reveal><Inline text={intro} host={cast.host} /></p>
|
|
29
32
|
|
|
30
33
|
<div class="mt-14 grid grid-cols-1 items-center gap-6 lg:grid-cols-[1fr_auto_1fr]" data-reveal data-contact>
|
|
31
34
|
<!-- Command sticky note -->
|
|
@@ -71,7 +74,7 @@ const channels = [person.email ? 'email' : undefined, ...profileLinks.slice(0, 3
|
|
|
71
74
|
</span>
|
|
72
75
|
</div>}
|
|
73
76
|
<div class="mt-3 flex flex-wrap gap-2.5">
|
|
74
|
-
{profileLinks.map((link) => <a href={link.url} class="btn btn-sm" rel=
|
|
77
|
+
{profileLinks.map((link) => <a href={link.url} class="btn btn-sm" rel={cast.host.verified ? cast.host.rel : 'me noopener'}>{link.label}</a>)}
|
|
75
78
|
</div>
|
|
76
79
|
</div>
|
|
77
80
|
</div>
|
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
// Copyright (c) Cratis. All rights reserved.
|
|
3
3
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
import Inline from './Inline.astro';
|
|
6
7
|
import SectionHeader from './SectionHeader.astro';
|
|
7
8
|
import PullRequestFeed from './PullRequestFeed.astro';
|
|
8
|
-
import {
|
|
9
|
-
import { renderInline } from '../lib/inline';
|
|
10
|
-
|
|
9
|
+
import type { CratisRepo } from '../lib/github';
|
|
11
10
|
interface Props {
|
|
11
|
+
cast: CastView;
|
|
12
12
|
index: string;
|
|
13
13
|
title?: string;
|
|
14
14
|
label?: string;
|
|
15
15
|
}
|
|
16
|
+
const { cast } = Astro.props;
|
|
17
|
+
const { cratis: cratisData, cratisRepos, mergedPrFloor, mergedPrTotal, mergedPrStatus, snapshotDate, starsFor } = cast;
|
|
16
18
|
const cratis = cratisData!;
|
|
17
19
|
const { index, title = cratis.title, label = cratis.label } = Astro.props;
|
|
18
20
|
|
|
@@ -38,23 +40,23 @@ const badge = (r: CratisRepo) => (r.founded ? 'b-founded' : r.rank === 1 ? 'b-le
|
|
|
38
40
|
<section id="cratis" class="relative overflow-hidden py-24 md:py-36" aria-labelledby="cratis-title">
|
|
39
41
|
<div class="pointer-events-none absolute top-1/3 -right-40 size-[40rem] rounded-full bg-command/10 blur-[120px]" aria-hidden="true"></div>
|
|
40
42
|
<div class="container-x relative">
|
|
41
|
-
<SectionHeader index={index} kind="command" label={label} title={title} id="cratis-title">
|
|
43
|
+
<SectionHeader host={cast.host} index={index} kind="command" label={label} title={title} id="cratis-title">
|
|
42
44
|
<div class="mt-6 grid max-w-[64ch] gap-4 text-lg leading-relaxed">
|
|
43
|
-
{cratis.pitch && <p class="text-fg"
|
|
44
|
-
<p class="text-fg-muted"
|
|
45
|
+
{cratis.pitch && <p class="text-fg"><Inline text={cratis.pitch} host={cast.host} /></p>}
|
|
46
|
+
<p class="text-fg-muted"><Inline text={cratis.role} host={cast.host} /></p>
|
|
45
47
|
</div>
|
|
46
48
|
<div class="mt-6 flex flex-wrap gap-3">
|
|
47
|
-
{Object.entries(cratis.links).map(([text, href]) => <a href={href} class="btn btn-sm" rel=
|
|
49
|
+
{Object.entries(cratis.links).map(([text, href]) => <a href={href} class="btn btn-sm" rel={cast.host.rel}>{text} <span aria-hidden="true">↗</span></a>)}
|
|
48
50
|
</div>
|
|
49
51
|
</SectionHeader>
|
|
50
52
|
|
|
51
|
-
{headline.length > 0 && <dl class:list={['grid grid-cols-2
|
|
53
|
+
{headline.length > 0 && <dl class:list={['grid gap-px', headline.length > 1 ? 'grid-cols-2' : 'grid-cols-1', 'overflow-hidden rounded-2xl border border-line bg-line', headline.length >= 4 ? 'lg:grid-cols-4' : headline.length === 3 ? 'lg:grid-cols-3' : '']} data-reveal>
|
|
52
54
|
{
|
|
53
55
|
headline.map((h) => (
|
|
54
56
|
<div class="flex flex-col bg-ink-900 p-5 md:p-6">
|
|
55
57
|
<dt class="order-2 mt-2 text-sm leading-snug text-fg-muted">
|
|
56
58
|
{h.label}
|
|
57
|
-
{h.live && <> (<span data-merged-prs="exact">{fmt.format(mergedPrTotal)}</span>, <span data-pr-status>
|
|
59
|
+
{h.live && <> (<span data-merged-prs="exact">{fmt.format(mergedPrTotal)}</span>, <span data-pr-status>{mergedPrStatus}</span>)</>}
|
|
58
60
|
</dt>
|
|
59
61
|
<dd data-merged-prs={h.live ? 'floor' : undefined} class={`order-1 font-display text-[clamp(2.25rem,4.4vw,3.5rem)] leading-none font-[750] tracking-[-0.045em] tabular-nums ${h.accent}`}>{h.value}</dd>
|
|
60
62
|
</div>
|
|
@@ -71,7 +73,7 @@ const badge = (r: CratisRepo) => (r.founded ? 'b-founded' : r.rank === 1 ? 'b-le
|
|
|
71
73
|
{
|
|
72
74
|
pipeline.map((r, i) => (
|
|
73
75
|
<li class="relative">
|
|
74
|
-
<a href={r.url} rel=
|
|
76
|
+
<a href={r.url} rel={cast.host.rel} class="panel glow group flex h-full flex-col p-5" style="--glow: var(--color-command)">
|
|
75
77
|
<span class="flex items-center justify-between gap-2 font-mono text-[0.6875rem]">
|
|
76
78
|
<span class="text-fg-faint">{String(i + 1).padStart(2, '0')} · {r.step}</span>
|
|
77
79
|
<span class={`badge ${badge(r)}`}>{r.role}</span>
|
|
@@ -81,7 +83,7 @@ const badge = (r: CratisRepo) => (r.founded ? 'b-founded' : r.rank === 1 ? 'b-le
|
|
|
81
83
|
<span class="mt-3 text-sm leading-relaxed text-fg-muted">{r.desc}</span>
|
|
82
84
|
<span class="mt-auto flex gap-4 pt-5 font-mono text-xs text-fg-faint tabular-nums">
|
|
83
85
|
<span><span class="text-fg">{fmt.format(r.commits)}</span> commits</span>
|
|
84
|
-
<span><span class="text-fg">{fmt.format(r.prs)}</span> PRs</span>
|
|
86
|
+
{r.prs > 0 && <span><span class="text-fg">{fmt.format(r.prs)}</span> PRs</span>}
|
|
85
87
|
{starsFor(`Cratis/${r.name}`) !== undefined && <span class="ml-auto" aria-label={`${starsFor(`Cratis/${r.name}`)} stars on GitHub`}>★ {starsFor(`Cratis/${r.name}`)}</span>}
|
|
86
88
|
</span>
|
|
87
89
|
</a>
|
|
@@ -99,7 +101,7 @@ const badge = (r: CratisRepo) => (r.founded ? 'b-founded' : r.rank === 1 ? 'b-le
|
|
|
99
101
|
const Tag = r.url ? 'a' : 'div';
|
|
100
102
|
return (
|
|
101
103
|
<li>
|
|
102
|
-
<Tag href={r.url} rel={r.url ?
|
|
104
|
+
<Tag href={r.url} rel={r.url ? cast.host.rel : undefined} class:list={['panel flex h-full flex-col p-5', r.url && 'glow group']} style="--glow: var(--color-command)">
|
|
103
105
|
<span class="flex items-center justify-between gap-2 font-mono text-[0.6875rem]">
|
|
104
106
|
<span class="note note-command">{r.kind}</span>
|
|
105
107
|
<span class={`badge ${badge(r)}`}>{r.role}</span>
|
|
@@ -109,7 +111,7 @@ const badge = (r: CratisRepo) => (r.founded ? 'b-founded' : r.rank === 1 ? 'b-le
|
|
|
109
111
|
{r.note && <span class="mt-2 font-mono text-xs text-view">{r.note}</span>}
|
|
110
112
|
<span class="mt-auto flex gap-4 pt-4 font-mono text-xs text-fg-faint tabular-nums">
|
|
111
113
|
<span><span class="text-fg">{fmt.format(r.commits)}</span> commits</span>
|
|
112
|
-
<span><span class="text-fg">{fmt.format(r.prs)}</span> PRs</span>
|
|
114
|
+
{r.prs > 0 && <span><span class="text-fg">{fmt.format(r.prs)}</span> PRs</span>}
|
|
113
115
|
</span>
|
|
114
116
|
</Tag>
|
|
115
117
|
</li>
|
|
@@ -125,7 +127,7 @@ const badge = (r: CratisRepo) => (r.founded ? 'b-founded' : r.rank === 1 ? 'b-le
|
|
|
125
127
|
{
|
|
126
128
|
clients.map((r) => (
|
|
127
129
|
<li>
|
|
128
|
-
<a href={r.url} rel=
|
|
130
|
+
<a href={r.url} rel={cast.host.rel} class="group flex items-start justify-between gap-3 py-3">
|
|
129
131
|
<span class="min-w-0">
|
|
130
132
|
<span class="block font-mono text-sm text-fg group-hover:underline">{r.name}</span>
|
|
131
133
|
<span class="block text-xs leading-relaxed text-fg-muted">{r.desc}</span>
|
|
@@ -143,7 +145,7 @@ const badge = (r: CratisRepo) => (r.founded ? 'b-founded' : r.rank === 1 ? 'b-le
|
|
|
143
145
|
</div>}
|
|
144
146
|
|
|
145
147
|
<div class="mt-16 grid gap-6 md:mt-20 lg:grid-cols-12">
|
|
146
|
-
<PullRequestFeed class="lg:col-span-7" />
|
|
148
|
+
<PullRequestFeed cast={cast} class="lg:col-span-7" />
|
|
147
149
|
|
|
148
150
|
{cratis.highlights.length > 0 && <div class="lg:col-span-5" data-reveal style="--reveal-delay:120ms">
|
|
149
151
|
<h3 class="font-display text-2xl font-[620] tracking-[-0.02em]">Highlights from the log</h3>
|
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
// Copyright (c) Cratis. All rights reserved.
|
|
3
3
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
4
|
|
|
5
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
import Inline from './Inline.astro';
|
|
5
7
|
import SectionHeader from './SectionHeader.astro';
|
|
6
|
-
import { eventModel } from '../data';
|
|
7
|
-
import { renderInline } from '../lib/inline';
|
|
8
8
|
import { safeJson } from '../lib/safeJson';
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
|
+
cast: CastView;
|
|
11
12
|
index: string;
|
|
12
13
|
title?: string;
|
|
13
14
|
label?: string;
|
|
14
15
|
intro?: string;
|
|
15
16
|
}
|
|
17
|
+
const { cast } = Astro.props;
|
|
18
|
+
const { eventModel } = cast;
|
|
16
19
|
const m = eventModel!;
|
|
17
20
|
type ModelCard = (typeof m.cards)[number];
|
|
18
21
|
const { index, title = m.title, label = m.label, intro = m.intro } = Astro.props;
|
|
@@ -68,10 +71,10 @@ const stepsFor = (id: string) => m.steps.flatMap((s, i) => (s.cards.includes(id)
|
|
|
68
71
|
|
|
69
72
|
<section id="model" class="relative py-24 md:py-36" aria-labelledby="model-title">
|
|
70
73
|
<div class="container-x">
|
|
71
|
-
<SectionHeader index={index} kind="command" label={label} title={title} id="model-title">
|
|
74
|
+
<SectionHeader host={cast.host} index={index} kind="command" label={label} title={title} id="model-title">
|
|
72
75
|
<p class="mt-6 max-w-[62ch] text-lg leading-relaxed text-fg-muted">
|
|
73
|
-
{intro ? <Fragment
|
|
74
|
-
This is a real slice from <a href={m.repo} class="text-fg underline decoration-line-strong hover:decoration-fg" rel=
|
|
76
|
+
{intro ? <Fragment><Inline text={intro} host={cast.host} /></Fragment> : <>Before code, I draw how information flows: screens send commands, commands append events, events feed read models, read models feed screens.
|
|
77
|
+
This is a real slice from <a href={m.repo} class="text-fg underline decoration-line-strong hover:decoration-fg" rel={cast.host.rel}>{m.name}</a>, and every name on it is a type in its source.</>}
|
|
75
78
|
</p>
|
|
76
79
|
</SectionHeader>
|
|
77
80
|
|
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
// Copyright (c) Cratis. All rights reserved.
|
|
3
3
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import type { CastView } from '../profile/CastView';
|
|
6
6
|
import { castDirectoryUrl, verificationUrl } from '../../badges/constants';
|
|
7
7
|
import CratisMark from './CratisMark.astro';
|
|
8
|
+
interface Props {
|
|
9
|
+
cast: CastView;
|
|
10
|
+
}
|
|
11
|
+
const { cast } = Astro.props;
|
|
12
|
+
const { config, person, tier } = cast;
|
|
8
13
|
const year = new Date().getFullYear();
|
|
9
14
|
const castUrl = tier.tier ? verificationUrl(person.handle) : castDirectoryUrl;
|
|
10
15
|
---
|
|
11
16
|
|
|
12
17
|
<footer class="border-t border-line print:hidden">
|
|
13
18
|
<div class="container-x flex flex-col gap-6 py-10 text-sm text-fg-muted md:flex-row md:items-center md:justify-between">
|
|
14
|
-
<a href={castUrl} class="cast-link group inline-flex items-center gap-3 rounded-lg" rel=
|
|
19
|
+
<a href={castUrl} class="cast-link group inline-flex items-center gap-3 rounded-lg" rel={cast.host.rel}>
|
|
15
20
|
<span class="grid size-9 place-items-center rounded-xl border border-line bg-ink-900 text-fg transition-colors group-hover:border-line-strong">
|
|
16
21
|
<CratisMark class="size-5" />
|
|
17
22
|
</span>
|
|
@@ -21,8 +26,8 @@ const castUrl = tier.tier ? verificationUrl(person.handle) : castDirectoryUrl;
|
|
|
21
26
|
</span>
|
|
22
27
|
</a>
|
|
23
28
|
<p class="font-mono text-xs">
|
|
24
|
-
© {year} {person.name} · built with <a href="https://github.com/Cratis/Cast" class="underline decoration-line-strong hover:text-fg" rel=
|
|
25
|
-
{config.site.source && <> · <a href={config.site.source} class="underline decoration-line-strong hover:text-fg" rel=
|
|
29
|
+
© {year} {person.name} · built with <a href="https://github.com/Cratis/Cast" class="underline decoration-line-strong hover:text-fg" rel={cast.host.rel}>Cast</a>
|
|
30
|
+
{config.site.source && <> · <a href={config.site.source} class="underline decoration-line-strong hover:text-fg" rel={cast.host.rel}>source</a></>}
|
|
26
31
|
</p>
|
|
27
32
|
</div>
|
|
28
33
|
</footer>
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
// Copyright (c) Cratis. All rights reserved.
|
|
3
3
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
5
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
import Inline from './Inline.astro';
|
|
7
7
|
import CastBadge from './CastBadge.astro';
|
|
8
|
-
import { liveRepos } from '../lib/github';
|
|
9
8
|
import type { BoardLabels } from '../scripts/event-board';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
cast: CastView;
|
|
12
|
+
}
|
|
13
|
+
const { cast } = Astro.props;
|
|
14
|
+
const { person, careerEvents, firstName, githubUrl, tier, sections, liveRepos } = cast;
|
|
10
15
|
const position = careerEvents.length;
|
|
11
16
|
const hasContact = sections.some((section) => section.id === 'contact');
|
|
12
17
|
|
|
@@ -45,7 +50,7 @@ const labels: BoardLabels = {
|
|
|
45
50
|
</span>
|
|
46
51
|
stream <span class="text-fg">{person.handle}</span> · <span data-public-event>position <span class="text-event">{position}</span></span>
|
|
47
52
|
</p>
|
|
48
|
-
<CastBadge tier={tier.tier} handle={person.handle} />
|
|
53
|
+
<CastBadge tier={tier.tier} handle={person.handle} claim={!cast.host.verified} />
|
|
49
54
|
</div>
|
|
50
55
|
|
|
51
56
|
<h1>
|
|
@@ -54,19 +59,17 @@ const labels: BoardLabels = {
|
|
|
54
59
|
</span>
|
|
55
60
|
<span
|
|
56
61
|
class="headline hero-in block max-w-[13ch] text-[clamp(2.85rem,7.4vw,6.5rem)] leading-[0.95] font-[800] tracking-[-0.04em]"
|
|
57
|
-
style="--d:120ms"
|
|
58
|
-
set:html={renderInline(person.tagline, 'serif text-shine')}
|
|
59
|
-
/>
|
|
62
|
+
style="--d:120ms"><Inline text={person.tagline} emphasisClass='serif text-shine' host={cast.host} /></span>
|
|
60
63
|
</h1>
|
|
61
64
|
|
|
62
|
-
<p class="hero-in mt-8 max-w-[58ch] text-lg leading-relaxed text-fg-muted md:text-xl" style="--d:200ms"
|
|
65
|
+
<p class="hero-in mt-8 max-w-[58ch] text-lg leading-relaxed text-fg-muted md:text-xl" style="--d:200ms"><Inline text={person.intro} host={cast.host} /></p>
|
|
63
66
|
|
|
64
67
|
<div class="hero-in mt-10 flex flex-wrap items-center gap-3" style="--d:280ms">
|
|
65
|
-
<a href=
|
|
68
|
+
<a href={cast.href('cv')} class="btn btn-primary" data-magnetic>
|
|
66
69
|
View CV
|
|
67
70
|
<svg viewBox="0 0 16 16" class="size-4" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
|
68
71
|
</a>
|
|
69
|
-
<a href={githubUrl} class="btn" rel=
|
|
72
|
+
<a href={githubUrl} class="btn" rel={cast.host.verified ? cast.host.rel : 'me noopener'}>
|
|
70
73
|
<svg viewBox="0 0 16 16" class="size-4" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
|
|
71
74
|
GitHub
|
|
72
75
|
</a>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
3
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
|
+
|
|
5
|
+
// Inline markup from cast.yaml, rendered as elements with escaped text. Never set:html.
|
|
6
|
+
import { parseInline, type InlineToken } from '../lib/inline';
|
|
7
|
+
import type { CastHost } from '../profile/CastHost';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
text?: string;
|
|
11
|
+
tokens?: InlineToken[];
|
|
12
|
+
/** Class of `*emphasis*`, the italic serif accent by default. */
|
|
13
|
+
emphasisClass?: string;
|
|
14
|
+
/** Where the text is rendered; decides the rel of links and how relative links resolve. */
|
|
15
|
+
host?: CastHost;
|
|
16
|
+
}
|
|
17
|
+
const { text = '', tokens = parseInline(text), emphasisClass = 'serif', host } = Astro.props;
|
|
18
|
+
const linkClass = 'text-fg underline decoration-line-strong hover:decoration-fg';
|
|
19
|
+
|
|
20
|
+
const target = (href: string): string => {
|
|
21
|
+
if (!host?.linkOrigin || !href.startsWith('/')) return href;
|
|
22
|
+
try {
|
|
23
|
+
return new URL(href, host.linkOrigin).toString();
|
|
24
|
+
} catch {
|
|
25
|
+
return '#';
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const relFor = (href: string): string | undefined => (/^https?:/i.test(href) ? (host?.rel ?? 'noopener') : undefined);
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
tokens.map((token) =>
|
|
33
|
+
token.kind === 'text' ? (
|
|
34
|
+
token.text
|
|
35
|
+
) : token.kind === 'code' ? (
|
|
36
|
+
<code class="inline-code">{token.text}</code>
|
|
37
|
+
) : token.kind === 'strong' ? (
|
|
38
|
+
<strong class="font-semibold text-fg"><Astro.self tokens={token.children} emphasisClass={emphasisClass} host={host} /></strong>
|
|
39
|
+
) : token.kind === 'em' ? (
|
|
40
|
+
<em class={emphasisClass}><Astro.self tokens={token.children} emphasisClass={emphasisClass} host={host} /></em>
|
|
41
|
+
) : (
|
|
42
|
+
<a href={target(token.href)} class={linkClass} rel={relFor(target(token.href))}><Astro.self tokens={token.children} emphasisClass={emphasisClass} host={host} /></a>
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
// Copyright (c) Cratis. All rights reserved.
|
|
3
3
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
4
|
|
|
5
|
+
import type { CastView } from '../profile/CastView';
|
|
5
6
|
// The person's monogram: their initial on their accent, the same shape as the favicon.
|
|
6
|
-
import { person } from '../data';
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
|
+
cast: CastView;
|
|
9
10
|
class?: string;
|
|
10
11
|
}
|
|
11
12
|
const { class: className = 'size-7' } = Astro.props;
|
|
13
|
+
const { cast } = Astro.props;
|
|
14
|
+
const { person } = cast;
|
|
12
15
|
const initial = (person.name.trim()[0] ?? person.handle[0] ?? 'C').toUpperCase();
|
|
13
16
|
---
|
|
14
17
|
|
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
// Copyright (c) Cratis. All rights reserved.
|
|
3
3
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import type { CastView } from '../profile/CastView';
|
|
6
6
|
import { navLinks } from '../lib/sections';
|
|
7
7
|
import CastBadge from './CastBadge.astro';
|
|
8
8
|
import Monogram from './Monogram.astro';
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
|
-
|
|
11
|
+
cast: CastView;
|
|
12
|
+
/** Prefix anchor links with the home page path when not on the home page. */
|
|
12
13
|
home?: boolean;
|
|
13
14
|
}
|
|
14
|
-
const { home = true } = Astro.props;
|
|
15
|
-
const
|
|
15
|
+
const { cast, home = true } = Astro.props;
|
|
16
|
+
const { person, sections, tier } = cast;
|
|
17
|
+
const base = home ? '' : cast.href();
|
|
16
18
|
const links = navLinks(sections);
|
|
17
19
|
---
|
|
18
20
|
|
|
@@ -21,12 +23,13 @@ const links = navLinks(sections);
|
|
|
21
23
|
class="group/nav fixed inset-x-0 top-0 z-50 h-[var(--nav-h)] border-b border-transparent transition-[background-color,border-color,backdrop-filter] duration-300 data-[scrolled]:border-line data-[scrolled]:bg-ink-950/70 data-[scrolled]:backdrop-blur-xl data-[scrolled]:backdrop-saturate-150 print:hidden"
|
|
22
24
|
>
|
|
23
25
|
<nav class="container-x flex h-full items-center justify-between gap-4" aria-label="Primary">
|
|
24
|
-
<div class="flex items-center gap-3">
|
|
25
|
-
<
|
|
26
|
-
|
|
26
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
27
|
+
<slot name="lead" />
|
|
28
|
+
<a href={home ? '#top' : cast.href()} class="group flex items-center gap-2.5 rounded-md" aria-label={`${person.handle}, home`}>
|
|
29
|
+
<Monogram cast={cast} class="size-7 shrink-0 transition-transform duration-500 ease-[var(--ease-spring)] group-hover:-rotate-6" />
|
|
27
30
|
<span class="font-mono text-[0.9375rem] font-semibold tracking-tight">{person.handle}</span>
|
|
28
31
|
</a>
|
|
29
|
-
<span class="hidden sm:inline-flex"><CastBadge tier={tier.tier} handle={person.handle} size="compact" /></span>
|
|
32
|
+
<span class="hidden sm:inline-flex"><CastBadge tier={tier.tier} handle={person.handle} size="compact" claim={!cast.host.verified} /></span>
|
|
30
33
|
</div>
|
|
31
34
|
|
|
32
35
|
<ul class="hidden items-center gap-1 md:flex">
|
|
@@ -44,7 +47,7 @@ const links = navLinks(sections);
|
|
|
44
47
|
))
|
|
45
48
|
}
|
|
46
49
|
<li>
|
|
47
|
-
<a href=
|
|
50
|
+
<a href={cast.href('cv')} class="rounded-lg px-3 py-2 text-sm text-fg-muted transition-colors hover:text-fg">CV</a>
|
|
48
51
|
</li>
|
|
49
52
|
</ul>
|
|
50
53
|
|
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
// Copyright (c) Cratis. All rights reserved.
|
|
3
3
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
4
|
|
|
5
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
import Inline from './Inline.astro';
|
|
5
7
|
import { Picture } from 'astro:assets';
|
|
6
|
-
import { person, now, careerEvents, firstName, photo, photoUrl } from '../data';
|
|
7
8
|
import { formatMonth } from '../lib/career';
|
|
8
|
-
import { renderInline } from '../lib/inline';
|
|
9
9
|
import SectionHeader from './SectionHeader.astro';
|
|
10
10
|
|
|
11
11
|
interface Props {
|
|
12
|
+
cast: CastView;
|
|
12
13
|
index: string;
|
|
13
14
|
title?: string;
|
|
14
15
|
label?: string;
|
|
15
16
|
}
|
|
16
17
|
const { index, title = 'Where the stream is *now.*', label = 'Read model' } = Astro.props;
|
|
18
|
+
const { cast } = Astro.props;
|
|
19
|
+
const { person, now, careerEvents, firstName, photo, photoUrl } = cast;
|
|
17
20
|
const rows: { key: string; value: string | string[] }[] = Object.entries(now).map(([key, value]) => ({
|
|
18
21
|
key,
|
|
19
22
|
value: typeof value === 'string' && /^\d{4}-\d{2}$/.test(value) ? formatMonth(value) : value,
|
|
@@ -23,7 +26,7 @@ const hasPortrait = photo !== undefined || photoUrl !== undefined;
|
|
|
23
26
|
|
|
24
27
|
<section id="about" class="relative py-24 md:py-36" aria-labelledby="about-title">
|
|
25
28
|
<div class="container-x">
|
|
26
|
-
<SectionHeader index={index} kind="view" label={label} title={title} id="about-title" />
|
|
29
|
+
<SectionHeader host={cast.host} index={index} kind="view" label={label} title={title} id="about-title" />
|
|
27
30
|
|
|
28
31
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-12">
|
|
29
32
|
{hasPortrait && <figure class="avatar group panel overflow-hidden p-2 lg:col-span-4" data-reveal>
|
|
@@ -69,7 +72,7 @@ const hasPortrait = photo !== undefined || photoUrl !== undefined;
|
|
|
69
72
|
))}
|
|
70
73
|
</ul>
|
|
71
74
|
) : (
|
|
72
|
-
<Fragment
|
|
75
|
+
<Fragment><Inline text={r.value} host={cast.host} /></Fragment>
|
|
73
76
|
)}
|
|
74
77
|
</dd>
|
|
75
78
|
</div>
|
|
@@ -79,16 +82,16 @@ const hasPortrait = photo !== undefined || photoUrl !== undefined;
|
|
|
79
82
|
</div>}
|
|
80
83
|
</div>
|
|
81
84
|
|
|
82
|
-
<div class="mt-16 grid grid-cols-1 gap-10 md:mt-24 md:grid-cols-12">
|
|
83
|
-
<h3 class="eyebrow md:col-span-3" data-reveal>About</h3>
|
|
84
|
-
<div class="text-lg leading-relaxed text-fg-muted md:col-span-9 md:columns-2 md:gap-10 [&>p]:mb-6 [&>p]:break-inside-avoid" data-reveal>
|
|
85
|
-
{person.about.map((p, i) => <p class:list={[i === 0 && 'text-fg']}
|
|
86
|
-
</div>
|
|
85
|
+
{(person.about.length > 0 || person.interests.length > 0) && <div class="mt-16 grid grid-cols-1 gap-10 md:mt-24 md:grid-cols-12">
|
|
86
|
+
{person.about.length > 0 && <h3 class="eyebrow md:col-span-3" data-reveal>About</h3>}
|
|
87
|
+
{person.about.length > 0 && <div class="text-lg leading-relaxed text-fg-muted md:col-span-9 md:columns-2 md:gap-10 [&>p]:mb-6 [&>p]:break-inside-avoid" data-reveal>
|
|
88
|
+
{person.about.map((p, i) => <p class:list={[i === 0 && 'text-fg']}><Inline text={p} host={cast.host} /></p>)}
|
|
89
|
+
</div>}
|
|
87
90
|
{person.interests.length > 0 && <h3 class="eyebrow md:col-span-3" data-reveal>Interests</h3>}
|
|
88
91
|
{person.interests.length > 0 && <ul class="flex flex-wrap gap-2 md:col-span-9" data-reveal>
|
|
89
92
|
{person.interests.map((i) => <li class="tag">{i}</li>)}
|
|
90
93
|
</ul>}
|
|
91
|
-
</div>
|
|
94
|
+
</div>}
|
|
92
95
|
</div>
|
|
93
96
|
</section>
|
|
94
97
|
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
// Copyright (c) Cratis. All rights reserved.
|
|
3
3
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import { renderInline } from '../lib/inline';
|
|
5
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
import Inline from './Inline.astro';
|
|
8
7
|
import SectionHeader from './SectionHeader.astro';
|
|
9
8
|
|
|
10
9
|
interface Props {
|
|
10
|
+
cast: CastView;
|
|
11
11
|
index: string;
|
|
12
12
|
title?: string;
|
|
13
13
|
label?: string;
|
|
14
14
|
intro?: string;
|
|
15
15
|
}
|
|
16
16
|
const { index, title = 'Things I build *after hours.*', label = 'Side projects', intro } = Astro.props;
|
|
17
|
+
const { cast } = Astro.props;
|
|
18
|
+
const { config, liveProjects: projects } = cast;
|
|
17
19
|
// Categories in cast.yaml order, then any others in first-use order.
|
|
18
20
|
const used = [...new Set(projects.map((p) => p.category))];
|
|
19
21
|
const declared = config.projectCategories.filter((c) => used.includes(c.id));
|
|
@@ -21,19 +23,14 @@ const categories = [...declared, ...used.filter((id) => !declared.some((c) => c.
|
|
|
21
23
|
const filters = categories.length > 1 ? [{ id: 'all', label: 'All' }, ...categories.map((c) => ({ id: c.id, label: c.filter ?? c.label }))] : [];
|
|
22
24
|
const countFor = (id: string) => (id === 'all' ? projects.length : projects.filter((p) => p.category === id).length);
|
|
23
25
|
const categoryLabel: Record<string, string> = Object.fromEntries(categories.map((c) => [c.id, c.label]));
|
|
24
|
-
const escape = (t: string) => t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
25
26
|
/** Lets a command wrap only after spaces and slashes, with no added visible characters. */
|
|
26
|
-
const breakable = (cmd: string) =>
|
|
27
|
-
cmd
|
|
28
|
-
.split(/(?<=[/ ])/)
|
|
29
|
-
.map((part) => `<span class="whitespace-nowrap">${escape(part)}</span>`)
|
|
30
|
-
.join('<wbr>');
|
|
27
|
+
const breakable = (cmd: string) => cmd.split(/(?<=[/ ])/);
|
|
31
28
|
---
|
|
32
29
|
|
|
33
30
|
<section id="projects" class="relative py-24 md:py-36" aria-labelledby="projects-title">
|
|
34
31
|
<div class="container-x">
|
|
35
|
-
<SectionHeader index={index} label={label} title={title} id="projects-title">
|
|
36
|
-
{intro && <p class="mt-6 max-w-[62ch] text-lg leading-relaxed text-fg-muted"
|
|
32
|
+
<SectionHeader host={cast.host} index={index} label={label} title={title} id="projects-title">
|
|
33
|
+
{intro && <p class="mt-6 max-w-[62ch] text-lg leading-relaxed text-fg-muted"><Inline text={intro} host={cast.host} /></p>}
|
|
37
34
|
</SectionHeader>
|
|
38
35
|
|
|
39
36
|
<div class="mb-8 flex flex-wrap gap-2" role="group" aria-label="Filter projects by category" data-filters hidden>
|
|
@@ -57,7 +54,7 @@ const breakable = (cmd: string) =>
|
|
|
57
54
|
<div>
|
|
58
55
|
<p class="font-mono text-[0.6875rem] tracking-wider text-fg-faint uppercase">{categoryLabel[p.category]}</p>
|
|
59
56
|
<h3 class="mt-2 flex items-center gap-3 font-display text-[1.75rem] leading-none font-[650] tracking-[-0.03em]">
|
|
60
|
-
<a href={p.url} rel=
|
|
57
|
+
<a href={p.url} rel={cast.host.rel} class="hover:text-event-soft">
|
|
61
58
|
{p.name}
|
|
62
59
|
</a>
|
|
63
60
|
{p.version && (
|
|
@@ -67,7 +64,7 @@ const breakable = (cmd: string) =>
|
|
|
67
64
|
)}
|
|
68
65
|
</h3>
|
|
69
66
|
</div>
|
|
70
|
-
<a href={p.url} rel=
|
|
67
|
+
<a href={p.url} rel={cast.host.rel} class="grid size-9 shrink-0 place-items-center rounded-lg border border-line text-fg-muted transition-colors hover:border-line-strong hover:text-fg" aria-label={`${p.name} on GitHub`}>
|
|
71
68
|
<svg viewBox="0 0 16 16" class="size-4" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M5 11 11 5M6 5h5v5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
72
69
|
</a>
|
|
73
70
|
</header>
|
|
@@ -88,7 +85,7 @@ const breakable = (cmd: string) =>
|
|
|
88
85
|
<div class="term mb-4 flex items-start gap-2.5 rounded-xl border border-line bg-ink-950/80 py-1.5 pr-1.5 pl-3.5 font-mono text-[0.8125rem] leading-5">
|
|
89
86
|
<span class="py-[0.3rem] text-view select-none" aria-hidden="true">$</span>
|
|
90
87
|
<code class="min-w-0 flex-1 py-[0.3rem] [overflow-wrap:anywhere] text-fg">
|
|
91
|
-
|
|
88
|
+
{breakable(p.install).map((part, i) => <>{i > 0 && <wbr />}<span class="whitespace-nowrap">{part}</span></>)}
|
|
92
89
|
</code>
|
|
93
90
|
<button type="button" class="copy" data-copy={p.install} aria-label={`Copy install command for ${p.name}`}>
|
|
94
91
|
<svg viewBox="0 0 16 16" class="ic-copy size-3.5" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="5" y="5" width="9" height="9" rx="2" /><path d="M11 5V3.5A1.5 1.5 0 0 0 9.5 2h-6A1.5 1.5 0 0 0 2 3.5v6A1.5 1.5 0 0 0 3.5 11H5" /></svg>
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
4
|
|
|
5
5
|
// The live feed of merged public pull requests: rendered from the build-time snapshot, refreshed in the browser.
|
|
6
|
-
import {
|
|
7
|
-
import { github, snapshotDate } from '../lib/github';
|
|
6
|
+
import type { CastView } from '../profile/CastView';
|
|
8
7
|
import { relativeTime } from '../lib/reltime';
|
|
9
8
|
|
|
10
9
|
interface Props {
|
|
10
|
+
cast: CastView;
|
|
11
11
|
class?: string;
|
|
12
12
|
}
|
|
13
13
|
const { class: className = '' } = Astro.props;
|
|
14
|
+
const { cast } = Astro.props;
|
|
15
|
+
const { config, github, snapshotDate } = cast;
|
|
14
16
|
// The section shows the newest eight; the terminal's `stream tail` gets the full snapshot.
|
|
15
17
|
const feed = github.mergedPullRequests.latest.slice(0, 8);
|
|
16
18
|
const builtAt = github.fetchedAt ? Date.parse(github.fetchedAt) : Date.now();
|
|
@@ -27,7 +29,7 @@ const orgs = config.github.orgs.join(', ');
|
|
|
27
29
|
</div>
|
|
28
30
|
<template data-pr-template>
|
|
29
31
|
<li class="feed-row">
|
|
30
|
-
<a rel=
|
|
32
|
+
<a rel={cast.host.rel} class="group grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 px-5 py-3.5 transition-colors hover:bg-fg/[0.025] sm:grid-cols-[4.5rem_1fr_auto]">
|
|
31
33
|
<span data-pr-head class="font-mono text-xs leading-6 text-fg-faint tabular-nums"></span>
|
|
32
34
|
<span class="min-w-0">
|
|
33
35
|
<span class="flex flex-wrap items-center gap-x-2.5 gap-y-1 font-mono text-xs">
|
|
@@ -46,7 +48,7 @@ const orgs = config.github.orgs.join(', ');
|
|
|
46
48
|
<ol data-pr-feed class="divide-y divide-line" aria-label={`Latest merged pull requests in ${orgs}`}>
|
|
47
49
|
{feed.map((pr, i) => (
|
|
48
50
|
<li class="feed-row" style={`--i:${i}`}>
|
|
49
|
-
<a href={pr.url} rel=
|
|
51
|
+
<a href={pr.url} rel={cast.host.rel} class="group grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 px-5 py-3.5 transition-colors hover:bg-fg/[0.025] sm:grid-cols-[4.5rem_1fr_auto]">
|
|
50
52
|
<span class="font-mono text-xs leading-6 text-fg-faint tabular-nums">{i === 0 ? 'HEAD' : `HEAD~${i}`}</span>
|
|
51
53
|
<span class="min-w-0">
|
|
52
54
|
<span class="flex flex-wrap items-center gap-x-2.5 gap-y-1 font-mono text-xs">
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Copyright (c) Cratis. All rights reserved.
|
|
3
3
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import type { CastHost } from '../profile/CastHost';
|
|
6
|
+
import Inline from './Inline.astro';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
index: string;
|
|
@@ -11,8 +12,10 @@ interface Props {
|
|
|
11
12
|
/** May use *emphasis* for the italic serif accent. */
|
|
12
13
|
title: string;
|
|
13
14
|
id?: string;
|
|
15
|
+
/** Where the profile is rendered, for links in the title. */
|
|
16
|
+
host?: CastHost;
|
|
14
17
|
}
|
|
15
|
-
const { index, kind = 'event', label, title, id } = Astro.props;
|
|
18
|
+
const { index, kind = 'event', label, title, id, host } = Astro.props;
|
|
16
19
|
const noteClass = kind === 'command' ? 'note note-command' : kind === 'view' ? 'note note-view' : 'note';
|
|
17
20
|
---
|
|
18
21
|
|
|
@@ -22,6 +25,6 @@ const noteClass = kind === 'command' ? 'note note-command' : kind === 'view' ? '
|
|
|
22
25
|
{index && <span class="h-px w-8 bg-line-strong" aria-hidden="true"></span>}
|
|
23
26
|
<span class={noteClass}>{label}</span>
|
|
24
27
|
</p>
|
|
25
|
-
<h2 id={id} class="section-title max-w-[22ch]"
|
|
28
|
+
<h2 id={id} class="section-title max-w-[22ch]"><Inline text={title} host={host} /></h2>
|
|
26
29
|
<slot />
|
|
27
30
|
</header>
|