@cratis/cast 0.0.1 → 0.2.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/README.md +13 -1
- package/badges/RegistryMember.ts +18 -0
- package/badges/ResolvedTier.ts +17 -0
- package/badges/Tier.ts +14 -0
- package/badges/TierSource.ts +14 -0
- package/badges/badgeSvg.ts +137 -0
- package/badges/constants.ts +17 -0
- package/badges/cratisMark.ts +15 -0
- package/badges/index.ts +12 -0
- package/badges/registry.ts +68 -0
- package/badges/resolveTier.ts +24 -0
- package/cast-json.schema.json +1496 -0
- package/cast.schema.json +1113 -0
- package/config/CastConfig.ts +17 -0
- package/config/CastConfigError.ts +10 -0
- package/config/LiveStat.ts +17 -0
- package/config/ResolvedSection.ts +13 -0
- package/config/SectionId.ts +21 -0
- package/config/ThemeMode.ts +9 -0
- package/config/calendar.ts +34 -0
- package/config/castSchema.ts +263 -0
- package/config/index.ts +12 -0
- package/config/parseCast.ts +58 -0
- package/config/resolveSections.ts +48 -0
- package/github/GitHubSnapshot.ts +66 -0
- package/github/fetchGitHub.ts +235 -0
- package/github/gitHubSnapshotSchema.ts +58 -0
- package/github/parse.ts +63 -0
- package/index.ts +88 -0
- package/integration/CastData.ts +20 -0
- package/integration/CastOptions.ts +17 -0
- package/integration/Snapshot.ts +12 -0
- package/integration/loadRegistry.ts +37 -0
- package/integration/resolveData.ts +138 -0
- package/integration/snapshotFile.ts +23 -0
- package/integration/virtualModule.ts +43 -0
- package/machine/castJson.ts +77 -0
- package/machine/castJsonSchema.ts +35 -0
- package/machine/index.ts +6 -0
- package/machine/llmsText.ts +69 -0
- package/machine/ogImageSvg.ts +79 -0
- package/machine/profileFromCastJson.ts +214 -0
- package/machine/resumeJson.ts +87 -0
- package/package.json +80 -8
- package/registry/members.yaml +16 -0
- package/routes/404.astro +42 -0
- package/routes/apple-touch-icon.png.ts +17 -0
- package/routes/badge-card.svg.ts +13 -0
- package/routes/badge.svg.ts +11 -0
- package/routes/cast.json.ts +17 -0
- package/routes/cv.astro +9 -0
- package/routes/favicon.svg.ts +16 -0
- package/routes/index.astro +9 -0
- package/routes/llms.txt.ts +10 -0
- package/routes/og.png.ts +23 -0
- package/routes/resume.json.ts +13 -0
- package/routes/robots.txt.ts +10 -0
- package/site/components/Activity.astro +196 -0
- package/site/components/Agentic.astro +144 -0
- package/site/components/CareerReplay.astro +415 -0
- package/site/components/CastBadge.astro +132 -0
- package/site/components/CommandPalette.astro +122 -0
- package/site/components/Contact.astro +166 -0
- package/site/components/Cratis.astro +235 -0
- package/site/components/CratisMark.astro +18 -0
- package/site/components/EventModel.astro +265 -0
- package/site/components/Footer.astro +33 -0
- package/site/components/Hero.astro +211 -0
- package/site/components/Inline.astro +45 -0
- package/site/components/Monogram.astro +28 -0
- package/site/components/Nav.astro +127 -0
- package/site/components/Now.astro +116 -0
- package/site/components/Projects.astro +185 -0
- package/site/components/PullRequestFeed.astro +88 -0
- package/site/components/SectionHeader.astro +30 -0
- package/site/components/Skills.astro +44 -0
- package/site/components/Stats.astro +106 -0
- package/site/components/Talks.astro +76 -0
- package/site/components/Terminal.astro +164 -0
- package/site/components/Writing.astro +128 -0
- package/site/data.ts +11 -0
- package/site/layouts/Base.astro +156 -0
- package/site/lib/accent.ts +52 -0
- package/site/lib/blog.ts +58 -0
- package/site/lib/career.ts +148 -0
- package/site/lib/clipboard.ts +12 -0
- package/site/lib/contributions.ts +63 -0
- package/site/lib/github.ts +72 -0
- package/site/lib/inline.ts +79 -0
- package/site/lib/lifecycle.ts +44 -0
- package/site/lib/motion.ts +43 -0
- package/site/lib/nav.ts +49 -0
- package/site/lib/reltime.ts +33 -0
- package/site/lib/safeJson.ts +15 -0
- package/site/lib/sections.ts +41 -0
- package/site/lib/theme.ts +28 -0
- 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 +117 -0
- package/site/profile/index.ts +8 -0
- package/site/scripts/ambient.ts +93 -0
- package/site/scripts/event-board.ts +403 -0
- package/site/scripts/event-model.ts +155 -0
- package/site/scripts/heatmap.ts +94 -0
- package/site/scripts/live.ts +155 -0
- package/site/scripts/palette.ts +313 -0
- package/site/scripts/replay.ts +252 -0
- package/site/scripts/terminal.ts +559 -0
- package/site/styles/global.css +508 -0
- package/virtual.d.ts +15 -0
- package/writing/WritingSnapshot.ts +20 -0
- package/writing/fetchWriting.ts +69 -0
- package/writing/parse.ts +72 -0
- package/writing/writingSnapshotSchema.ts +27 -0
|
@@ -0,0 +1,265 @@
|
|
|
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
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
import Inline from './Inline.astro';
|
|
7
|
+
import SectionHeader from './SectionHeader.astro';
|
|
8
|
+
import { safeJson } from '../lib/safeJson';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
cast: CastView;
|
|
12
|
+
index: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
intro?: string;
|
|
16
|
+
}
|
|
17
|
+
const { cast } = Astro.props;
|
|
18
|
+
const { eventModel } = cast;
|
|
19
|
+
const m = eventModel!;
|
|
20
|
+
type ModelCard = (typeof m.cards)[number];
|
|
21
|
+
const { index, title = m.title, label = m.label, intro = m.intro } = Astro.props;
|
|
22
|
+
|
|
23
|
+
// Fixed board geometry (px). The board scrolls horizontally when it does not fit.
|
|
24
|
+
const COL = 156;
|
|
25
|
+
const W = 132;
|
|
26
|
+
const H = 66;
|
|
27
|
+
const GUTTER = 20;
|
|
28
|
+
// The first two lanes (screens, commands and read models) are taller; stream lanes below them are compact.
|
|
29
|
+
const laneBand: Record<string, [number, number]> = {};
|
|
30
|
+
const laneY: Record<string, number> = {};
|
|
31
|
+
let laneTop = 0;
|
|
32
|
+
m.lanes.forEach((lane, laneIndex) => {
|
|
33
|
+
const laneHeight = laneIndex < 2 ? 116 : 104;
|
|
34
|
+
laneBand[lane.id] = [laneTop, laneTop + laneHeight];
|
|
35
|
+
laneY[lane.id] = laneTop + Math.round((laneHeight - H) / 2) + 9;
|
|
36
|
+
laneTop += laneHeight;
|
|
37
|
+
});
|
|
38
|
+
const width = GUTTER * 2 + m.columns * COL - (COL - W);
|
|
39
|
+
const height = laneTop;
|
|
40
|
+
|
|
41
|
+
const pos = (c: ModelCard) => ({ x: GUTTER + c.col * COL, y: laneY[c.lane] ?? 0 });
|
|
42
|
+
const byId = new Map(m.cards.map((c) => [c.id, c]));
|
|
43
|
+
|
|
44
|
+
const edges = m.edges.flatMap(([from, to]) => {
|
|
45
|
+
const a = byId.get(from);
|
|
46
|
+
const b = byId.get(to);
|
|
47
|
+
if (!a || !b) return [];
|
|
48
|
+
const pa = pos(a);
|
|
49
|
+
const pb = pos(b);
|
|
50
|
+
let d: string;
|
|
51
|
+
if (pa.y === pb.y) {
|
|
52
|
+
const y = pa.y + H / 2;
|
|
53
|
+
d = `M${pa.x + W},${y} C${pa.x + W + 14},${y} ${pb.x - 14},${y} ${pb.x - 4},${y}`;
|
|
54
|
+
} else {
|
|
55
|
+
const down = pb.y > pa.y;
|
|
56
|
+
const ax = pa.x + W / 2 + (pb.x > pa.x ? W * 0.18 : 0);
|
|
57
|
+
const ay = down ? pa.y + H : pa.y;
|
|
58
|
+
const bx = pb.x + W / 2 - (pb.x > pa.x ? W * 0.18 : 0);
|
|
59
|
+
const by = down ? pb.y - 4 : pb.y + H + 4;
|
|
60
|
+
const my = (ay + by) / 2;
|
|
61
|
+
d = `M${ax},${ay} C${ax},${my} ${bx},${my} ${bx},${by}`;
|
|
62
|
+
}
|
|
63
|
+
return [{ from, to, d }];
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Names never wrap mid-word: long ones get a smaller size that still fits the card.
|
|
67
|
+
const nameSize = (name: string) => (name.length <= 14 ? 12 : name.length <= 17 ? 10.5 : 9.5);
|
|
68
|
+
const kindLabel = { ui: 'Screen', command: 'Command', event: 'Event', view: 'Read model' } as const;
|
|
69
|
+
const stepsFor = (id: string) => m.steps.flatMap((s, i) => (s.cards.includes(id) ? [i] : [])).join(' ');
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
<section id="model" class="relative py-24 md:py-36" aria-labelledby="model-title">
|
|
73
|
+
<div class="container-x">
|
|
74
|
+
<SectionHeader host={cast.host} index={index} kind="command" label={label} title={title} id="model-title">
|
|
75
|
+
<p class="mt-6 max-w-[62ch] text-lg leading-relaxed text-fg-muted">
|
|
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.</>}
|
|
78
|
+
</p>
|
|
79
|
+
</SectionHeader>
|
|
80
|
+
|
|
81
|
+
<div class="panel overflow-hidden" data-reveal data-model>
|
|
82
|
+
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-line px-5 py-3 md:px-7">
|
|
83
|
+
<p class="font-mono text-sm"><span class="text-fg-faint">EventModel:</span> <span class="text-command">{m.name}</span></p>
|
|
84
|
+
<div class="flex items-center gap-2">
|
|
85
|
+
<button type="button" class="btn btn-sm" data-model-play aria-pressed="false">
|
|
86
|
+
<svg viewBox="0 0 16 16" class="size-3.5" fill="currentColor" aria-hidden="true" data-icon-play><path d="M4 2.8v10.4a.6.6 0 0 0 .9.5l8.4-5.2a.6.6 0 0 0 0-1L4.9 2.3a.6.6 0 0 0-.9.5Z"></path></svg>
|
|
87
|
+
<svg viewBox="0 0 16 16" class="hidden size-3.5" fill="currentColor" aria-hidden="true" data-icon-pause><rect x="3.5" y="2.5" width="3" height="11" rx=".8"></rect><rect x="9.5" y="2.5" width="3" height="11" rx=".8"></rect></svg>
|
|
88
|
+
<span data-model-play-label>Play the flow</span>
|
|
89
|
+
</button>
|
|
90
|
+
<button type="button" class="btn btn-sm" data-model-reset hidden>Show all</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="board-scroll overflow-x-auto" data-model-scroll>
|
|
95
|
+
<div class="board relative" style={`width:${width}px;height:${height}px`}>
|
|
96
|
+
{
|
|
97
|
+
m.lanes.map((l) => (
|
|
98
|
+
<div class="lane absolute inset-x-0" style={`top:${laneBand[l.id]![0]}px;height:${laneBand[l.id]![1] - laneBand[l.id]![0]}px`} data-lane={l.id}>
|
|
99
|
+
<span class="absolute top-2 left-5 font-mono text-[0.625rem] tracking-[0.08em] text-fg-faint uppercase">{l.label}</span>
|
|
100
|
+
</div>
|
|
101
|
+
))
|
|
102
|
+
}
|
|
103
|
+
<svg class="pointer-events-none absolute inset-0" width={width} height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden="true">
|
|
104
|
+
<defs>
|
|
105
|
+
<marker id="model-arrow" viewBox="0 0 8 8" refX="6" refY="4" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
106
|
+
<path d="M0 0 8 4 0 8Z" fill="currentColor"></path>
|
|
107
|
+
</marker>
|
|
108
|
+
</defs>
|
|
109
|
+
{edges.map((e) => <path d={e.d} class="edge" data-from={e.from} data-to={e.to} marker-end="url(#model-arrow)"></path>)}
|
|
110
|
+
</svg>
|
|
111
|
+
<ol class="contents" aria-label={`Event model: ${m.name}`}>
|
|
112
|
+
{
|
|
113
|
+
m.cards.map((c) => {
|
|
114
|
+
const p = pos(c);
|
|
115
|
+
return (
|
|
116
|
+
<li class="contents">
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
class={`card card-${c.kind} absolute`}
|
|
120
|
+
style={`left:${p.x}px;top:${p.y}px;width:${W}px;height:${H}px`}
|
|
121
|
+
data-card={c.id}
|
|
122
|
+
data-steps={stepsFor(c.id)}
|
|
123
|
+
aria-describedby="model-detail"
|
|
124
|
+
>
|
|
125
|
+
<span class="block font-mono text-[0.5625rem] tracking-[0.06em] uppercase opacity-70">{kindLabel[c.kind]}</span>
|
|
126
|
+
<span class="mt-1 block font-mono leading-tight font-medium whitespace-nowrap" style={`font-size:${nameSize(c.name)}px`}>{c.name}</span>
|
|
127
|
+
</button>
|
|
128
|
+
</li>
|
|
129
|
+
);
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
</ol>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div class="grid gap-4 border-t border-line px-5 py-4 md:grid-cols-[1fr_auto] md:px-7">
|
|
137
|
+
<div id="model-detail" aria-live="polite" data-model-detail class="min-h-[4.5rem]">
|
|
138
|
+
<p class="font-mono text-xs text-fg-faint" data-model-detail-meta>Hover, focus or tap a card, or play the flow.</p>
|
|
139
|
+
<p class="mt-1 text-[0.95rem] leading-relaxed text-fg" data-model-detail-text>
|
|
140
|
+
{m.summary ?? `${m.steps.length} steps, ${m.cards.length} cards.`}
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
<ol class="flex flex-wrap items-start gap-2 md:max-w-[22rem] md:justify-end" aria-label="Steps">
|
|
144
|
+
{
|
|
145
|
+
m.steps.map((s, i) => (
|
|
146
|
+
<li>
|
|
147
|
+
<button type="button" class="step" data-step={i} aria-pressed="false">
|
|
148
|
+
<span class="text-fg-faint">{String(i + 1).padStart(2, '0')}</span> {s.title}
|
|
149
|
+
</button>
|
|
150
|
+
</li>
|
|
151
|
+
))
|
|
152
|
+
}
|
|
153
|
+
</ol>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<script type="application/json" data-model-json set:html={safeJson({ cards: m.cards.map(({ id, kind, name, source, note }) => ({ id, kind, name, source, note })), steps: m.steps, root: `${m.repo}/blob/${m.branch}/${m.sourceRoot}` })} />
|
|
157
|
+
</div>
|
|
158
|
+
</section>
|
|
159
|
+
|
|
160
|
+
<style>
|
|
161
|
+
.board-scroll {
|
|
162
|
+
scrollbar-width: thin;
|
|
163
|
+
scrollbar-color: var(--color-line-strong) transparent;
|
|
164
|
+
mask-image: linear-gradient(90deg, transparent, #000 1.25rem, #000 calc(100% - 1.25rem), transparent);
|
|
165
|
+
}
|
|
166
|
+
.board {
|
|
167
|
+
margin-inline: auto;
|
|
168
|
+
}
|
|
169
|
+
.lane + .lane {
|
|
170
|
+
border-top: 1px dashed var(--color-line);
|
|
171
|
+
}
|
|
172
|
+
.lane[data-lane='chore'],
|
|
173
|
+
.lane[data-lane='member'] {
|
|
174
|
+
background: color-mix(in oklab, var(--color-event) 3%, transparent);
|
|
175
|
+
}
|
|
176
|
+
.edge {
|
|
177
|
+
fill: none;
|
|
178
|
+
stroke: var(--color-line-strong);
|
|
179
|
+
stroke-width: 1.25;
|
|
180
|
+
color: var(--color-line-strong);
|
|
181
|
+
transition: stroke 300ms ease, opacity 300ms ease;
|
|
182
|
+
}
|
|
183
|
+
.card {
|
|
184
|
+
--c: var(--color-fg-muted);
|
|
185
|
+
border-radius: 0.6rem;
|
|
186
|
+
border: 1px solid color-mix(in oklab, var(--c) 45%, transparent);
|
|
187
|
+
background: color-mix(in oklab, var(--c) 12%, var(--color-ink-900));
|
|
188
|
+
color: color-mix(in oklab, var(--c) 80%, var(--color-fg));
|
|
189
|
+
padding: 0.55rem 0.7rem;
|
|
190
|
+
text-align: left;
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
transition:
|
|
193
|
+
opacity 300ms ease,
|
|
194
|
+
transform 300ms var(--ease-out-expo),
|
|
195
|
+
box-shadow 300ms ease,
|
|
196
|
+
border-color 300ms ease;
|
|
197
|
+
}
|
|
198
|
+
.card::before {
|
|
199
|
+
content: '';
|
|
200
|
+
position: absolute;
|
|
201
|
+
top: 0.5rem;
|
|
202
|
+
right: 0.6rem;
|
|
203
|
+
width: 0.6rem;
|
|
204
|
+
height: 3px;
|
|
205
|
+
border-radius: 2px;
|
|
206
|
+
background: var(--c);
|
|
207
|
+
}
|
|
208
|
+
.card-command { --c: var(--color-command); }
|
|
209
|
+
.card-event { --c: var(--color-event); }
|
|
210
|
+
.card-view { --c: var(--color-view); }
|
|
211
|
+
.card-ui {
|
|
212
|
+
--c: var(--color-fg-muted);
|
|
213
|
+
background: color-mix(in oklab, var(--color-fg) 4%, var(--color-ink-900));
|
|
214
|
+
border-style: dashed;
|
|
215
|
+
}
|
|
216
|
+
[data-focus] .card:not([data-lit]) {
|
|
217
|
+
opacity: 0.28;
|
|
218
|
+
}
|
|
219
|
+
[data-focus] .edge:not([data-lit]) {
|
|
220
|
+
opacity: 0.25;
|
|
221
|
+
}
|
|
222
|
+
.card[data-lit] {
|
|
223
|
+
border-color: var(--c);
|
|
224
|
+
box-shadow: 0 0 0 1px color-mix(in oklab, var(--c) 35%, transparent), 0 12px 32px -12px color-mix(in oklab, var(--c) 60%, transparent);
|
|
225
|
+
transform: translateY(-2px);
|
|
226
|
+
}
|
|
227
|
+
.edge[data-lit] {
|
|
228
|
+
stroke: var(--color-event);
|
|
229
|
+
color: var(--color-event);
|
|
230
|
+
}
|
|
231
|
+
:global(.js-motion) .edge[data-lit] {
|
|
232
|
+
stroke-dasharray: 5 5;
|
|
233
|
+
animation: flow 900ms linear infinite;
|
|
234
|
+
}
|
|
235
|
+
@keyframes flow {
|
|
236
|
+
to {
|
|
237
|
+
stroke-dashoffset: -20;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
.step {
|
|
241
|
+
border-radius: 999px;
|
|
242
|
+
border: 1px solid var(--color-line);
|
|
243
|
+
padding: 0.25rem 0.7rem;
|
|
244
|
+
font-family: var(--font-mono);
|
|
245
|
+
font-size: 0.6875rem;
|
|
246
|
+
color: var(--color-fg-muted);
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
transition: color 150ms ease, border-color 150ms ease, background-color 150ms ease;
|
|
249
|
+
}
|
|
250
|
+
.step:hover {
|
|
251
|
+
color: var(--color-fg);
|
|
252
|
+
border-color: var(--color-line-strong);
|
|
253
|
+
}
|
|
254
|
+
.step[aria-pressed='true'] {
|
|
255
|
+
color: var(--color-fg);
|
|
256
|
+
border-color: color-mix(in oklab, var(--color-event) 55%, transparent);
|
|
257
|
+
background: color-mix(in oklab, var(--color-event) 10%, transparent);
|
|
258
|
+
}
|
|
259
|
+
</style>
|
|
260
|
+
|
|
261
|
+
<script>
|
|
262
|
+
import { initEventModel } from '../scripts/event-model';
|
|
263
|
+
import { onPageLoad } from '../lib/lifecycle';
|
|
264
|
+
onPageLoad(initEventModel);
|
|
265
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
import { castDirectoryUrl, verificationUrl } from '../../badges/constants';
|
|
7
|
+
import CratisMark from './CratisMark.astro';
|
|
8
|
+
interface Props {
|
|
9
|
+
cast: CastView;
|
|
10
|
+
}
|
|
11
|
+
const { cast } = Astro.props;
|
|
12
|
+
const { config, person, tier } = cast;
|
|
13
|
+
const year = new Date().getFullYear();
|
|
14
|
+
const castUrl = tier.tier ? verificationUrl(person.handle) : castDirectoryUrl;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<footer class="border-t border-line print:hidden">
|
|
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">
|
|
19
|
+
<a href={castUrl} class="cast-link group inline-flex items-center gap-3 rounded-lg" rel={cast.host.rel}>
|
|
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">
|
|
21
|
+
<CratisMark class="size-5" />
|
|
22
|
+
</span>
|
|
23
|
+
<span class="leading-tight">
|
|
24
|
+
<span class="block text-[0.9375rem] font-semibold text-fg">Part of the <span class="serif">Cratis Cast</span></span>
|
|
25
|
+
<span class="block font-mono text-[0.6875rem] text-fg-faint">The people behind the play · {castDirectoryUrl.replace('https://', '')}</span>
|
|
26
|
+
</span>
|
|
27
|
+
</a>
|
|
28
|
+
<p class="font-mono text-xs">
|
|
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></>}
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
</footer>
|
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
import Inline from './Inline.astro';
|
|
7
|
+
import CastBadge from './CastBadge.astro';
|
|
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;
|
|
15
|
+
const position = careerEvents.length;
|
|
16
|
+
const hasContact = sections.some((section) => section.id === 'contact');
|
|
17
|
+
|
|
18
|
+
// Board cards use real names only: event types from the career stream, the merged-PR
|
|
19
|
+
// events from the live feed, and read models this site actually projects.
|
|
20
|
+
const commandFor: Record<string, string> = {
|
|
21
|
+
StudiesStarted: 'StartStudies', VolunteerTeamJoined: 'JoinVolunteerTeam', PartTimeRoleStarted: 'StartPartTimeRole',
|
|
22
|
+
ContractStarted: 'StartContract', DegreeCompleted: 'CompleteDegree', EmploymentStarted: 'StartEmployment',
|
|
23
|
+
CertificationsEarned: 'EarnCertifications', CompanyCoFounded: 'FoundCompany', SystemDeployed: 'DeploySystem',
|
|
24
|
+
TalkGiven: 'GiveTalk', LeadershipAssigned: 'AssignLeadership', ToolReleased: 'ReleaseTool',
|
|
25
|
+
};
|
|
26
|
+
const careerFlows = [...new Set(careerEvents.map((e) => e.type))]
|
|
27
|
+
.filter((t) => commandFor[t])
|
|
28
|
+
.map((t): [string, string] => [commandFor[t]!, t]);
|
|
29
|
+
const labels: BoardLabels = {
|
|
30
|
+
// Merged PRs are by far the most common real event, so they show up most.
|
|
31
|
+
flows: [...careerFlows, ...Array.from({ length: Math.ceil(careerFlows.length / 2) }, (): [string, string] => ['MergePullRequest', 'PullRequestMerged'])],
|
|
32
|
+
views: [`${firstName}.Now`, 'CareerReplay', 'Contributions', ...liveRepos],
|
|
33
|
+
};
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<section id="top" data-hero class="relative isolate flex min-h-[min(100svh,64rem)] items-center overflow-hidden pt-[var(--nav-h)]">
|
|
37
|
+
<div class="pointer-events-none absolute inset-0 -z-10" aria-hidden="true">
|
|
38
|
+
<div class="spotlight absolute inset-0" data-spotlight></div>
|
|
39
|
+
<canvas data-board data-labels={JSON.stringify(labels)} class="board absolute inset-0 h-full w-full"></canvas>
|
|
40
|
+
<div class="absolute inset-0 bg-[radial-gradient(60rem_40rem_at_15%_45%,color-mix(in_srgb,var(--color-ink-950)_92%,transparent),transparent_70%)]"></div>
|
|
41
|
+
<div class="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-b from-transparent to-ink-950"></div>
|
|
42
|
+
<div class="absolute -top-40 left-1/2 h-80 w-[60rem] -translate-x-1/2 rounded-full bg-accent/10 blur-3xl"></div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="container-x relative py-20 md:py-28">
|
|
46
|
+
<div class="hero-in mb-8 flex flex-wrap items-center gap-3" style="--d:0ms">
|
|
47
|
+
<p class="inline-flex items-center gap-3 rounded-full border border-line bg-ink-900/70 py-1.5 pr-4 pl-2 font-mono text-xs text-fg-muted backdrop-blur">
|
|
48
|
+
<span class="inline-flex items-center gap-1.5 rounded-full bg-view/10 px-2 py-0.5 text-view">
|
|
49
|
+
<span class="size-1.5 animate-pulse-dot rounded-full bg-view"></span><span data-event-status>career</span>
|
|
50
|
+
</span>
|
|
51
|
+
stream <span class="text-fg">{person.handle}</span> · <span data-public-event>position <span class="text-event">{position}</span></span>
|
|
52
|
+
</p>
|
|
53
|
+
<CastBadge tier={tier.tier} handle={person.handle} claim={!cast.host.verified} />
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<h1>
|
|
57
|
+
<span class="hero-in mb-5 block font-sans text-lg font-medium tracking-normal text-fg-muted md:text-xl" style="--d:60ms">
|
|
58
|
+
<span transition:name="person-name" class="inline-block">{person.name}</span> <span class="text-fg-faint">—</span> <span class="hidden sm:inline">{person.title}</span><span class="sm:hidden">{person.titleShort ?? person.title}</span>
|
|
59
|
+
</span>
|
|
60
|
+
<span
|
|
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]"
|
|
62
|
+
style="--d:120ms"><Inline text={person.tagline} emphasisClass='serif text-shine' host={cast.host} /></span>
|
|
63
|
+
</h1>
|
|
64
|
+
|
|
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>
|
|
66
|
+
|
|
67
|
+
<div class="hero-in mt-10 flex flex-wrap items-center gap-3" style="--d:280ms">
|
|
68
|
+
<a href={cast.href('cv')} class="btn btn-primary" data-magnetic>
|
|
69
|
+
View CV
|
|
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>
|
|
71
|
+
</a>
|
|
72
|
+
<a href={githubUrl} class="btn" rel={cast.host.verified ? cast.host.rel : 'me noopener'}>
|
|
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>
|
|
74
|
+
GitHub
|
|
75
|
+
</a>
|
|
76
|
+
{hasContact && <a href="#contact" class="btn">Contact</a>}
|
|
77
|
+
<span class="ml-1 hidden font-mono text-xs text-fg-faint lg:inline">
|
|
78
|
+
or press <kbd class="kbd" data-mod-key>⌘</kbd> <kbd class="kbd">K</kbd> · <kbd class="kbd">`</kbd> for a terminal
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<dl class="hero-in mt-16 grid max-w-xl grid-cols-3 gap-6 border-t border-line pt-6 font-mono text-xs" style="--d:360ms">
|
|
83
|
+
<div>
|
|
84
|
+
<dt class="note note-command mb-2">Command</dt>
|
|
85
|
+
<dd class="text-fg-muted">intent to change</dd>
|
|
86
|
+
</div>
|
|
87
|
+
<div>
|
|
88
|
+
<dt class="note mb-2">Event</dt>
|
|
89
|
+
<dd class="text-fg-muted">a fact that happened</dd>
|
|
90
|
+
</div>
|
|
91
|
+
<div>
|
|
92
|
+
<dt class="note note-view mb-2">Read model</dt>
|
|
93
|
+
<dd class="text-fg-muted">a projection of facts</dd>
|
|
94
|
+
</div>
|
|
95
|
+
</dl>
|
|
96
|
+
</div>
|
|
97
|
+
</section>
|
|
98
|
+
|
|
99
|
+
<style>
|
|
100
|
+
.board {
|
|
101
|
+
opacity: 0;
|
|
102
|
+
transition: opacity 1.6s ease;
|
|
103
|
+
mask-image: linear-gradient(90deg, rgb(0 0 0 / 0.06) 0%, rgb(0 0 0 / 0.14) 38%, rgb(0 0 0 / 0.85) 58%, #000 72%);
|
|
104
|
+
}
|
|
105
|
+
[data-board-ready] .board {
|
|
106
|
+
opacity: 1;
|
|
107
|
+
}
|
|
108
|
+
@media (max-width: 767px) {
|
|
109
|
+
[data-board-ready] .board {
|
|
110
|
+
opacity: 0.45;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
.spotlight {
|
|
114
|
+
--sx: 70%;
|
|
115
|
+
--sy: 40%;
|
|
116
|
+
background: radial-gradient(34rem circle at var(--sx) var(--sy), color-mix(in oklab, var(--color-accent) 10%, transparent), transparent 60%);
|
|
117
|
+
opacity: 0;
|
|
118
|
+
transition: opacity 600ms ease;
|
|
119
|
+
}
|
|
120
|
+
[data-hero]:hover .spotlight {
|
|
121
|
+
opacity: 1;
|
|
122
|
+
}
|
|
123
|
+
[data-magnetic] {
|
|
124
|
+
transform: translate(var(--tx, 0), var(--ty, 0));
|
|
125
|
+
transition: transform 380ms var(--ease-spring), background-color 180ms ease;
|
|
126
|
+
}
|
|
127
|
+
.headline :global(.serif) {
|
|
128
|
+
font-weight: 520;
|
|
129
|
+
letter-spacing: -0.025em;
|
|
130
|
+
padding-right: 0.06em;
|
|
131
|
+
}
|
|
132
|
+
.headline :global(.text-shine) {
|
|
133
|
+
background: linear-gradient(100deg, var(--color-accent-soft) 0%, var(--color-accent) 35%, #2dd4bf 70%, var(--color-accent-soft) 100%);
|
|
134
|
+
background-size: 200% 100%;
|
|
135
|
+
-webkit-background-clip: text;
|
|
136
|
+
background-clip: text;
|
|
137
|
+
color: transparent;
|
|
138
|
+
text-shadow: none;
|
|
139
|
+
}
|
|
140
|
+
:global(html[data-theme='light']) .headline :global(.text-shine) {
|
|
141
|
+
background-image: linear-gradient(100deg, var(--color-accent) 0%, var(--color-accent-soft) 45%, #0f766e 80%, var(--color-accent) 100%);
|
|
142
|
+
}
|
|
143
|
+
:global(.js-motion) .headline :global(.text-shine) {
|
|
144
|
+
animation: shine 9s ease-in-out infinite alternate;
|
|
145
|
+
}
|
|
146
|
+
@keyframes shine {
|
|
147
|
+
to {
|
|
148
|
+
background-position: 100% 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
:global(.js-motion) .hero-in {
|
|
152
|
+
animation: hero-in 1.1s var(--ease-out-expo) both;
|
|
153
|
+
animation-delay: var(--d, 0ms);
|
|
154
|
+
}
|
|
155
|
+
@keyframes hero-in {
|
|
156
|
+
from {
|
|
157
|
+
opacity: 0;
|
|
158
|
+
transform: translateY(16px);
|
|
159
|
+
filter: blur(6px);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
</style>
|
|
163
|
+
|
|
164
|
+
<script>
|
|
165
|
+
import '../scripts/live';
|
|
166
|
+
import { mountEventBoard, type BoardLabels } from '../scripts/event-board';
|
|
167
|
+
import { onPageLoad } from '../lib/lifecycle';
|
|
168
|
+
import { motionReduced } from '../lib/motion';
|
|
169
|
+
onPageLoad(() => {
|
|
170
|
+
const host = document.querySelector<HTMLElement>('[data-hero]');
|
|
171
|
+
const canvas = host?.querySelector<HTMLCanvasElement>('[data-board]');
|
|
172
|
+
if (!host || !canvas) return;
|
|
173
|
+
let labels: BoardLabels | undefined;
|
|
174
|
+
try {
|
|
175
|
+
labels = JSON.parse(canvas.dataset['labels'] ?? '') as BoardLabels;
|
|
176
|
+
} catch {
|
|
177
|
+
labels = undefined;
|
|
178
|
+
}
|
|
179
|
+
const stopBoard = mountEventBoard(canvas, host, labels);
|
|
180
|
+
|
|
181
|
+
// Pointer spotlight and a magnetic primary CTA (fine pointers, full motion only).
|
|
182
|
+
const spot = host.querySelector<HTMLElement>('[data-spotlight]');
|
|
183
|
+
const cta = host.querySelector<HTMLElement>('[data-magnetic]');
|
|
184
|
+
const fine = window.matchMedia('(pointer: fine)');
|
|
185
|
+
const onMove = (e: PointerEvent) => {
|
|
186
|
+
if (spot) {
|
|
187
|
+
const r = host.getBoundingClientRect();
|
|
188
|
+
spot.style.setProperty('--sx', `${e.clientX - r.left}px`);
|
|
189
|
+
spot.style.setProperty('--sy', `${e.clientY - r.top}px`);
|
|
190
|
+
}
|
|
191
|
+
if (!cta || !fine.matches || motionReduced()) return;
|
|
192
|
+
const b = cta.getBoundingClientRect();
|
|
193
|
+
const dx = e.clientX - (b.left + b.width / 2);
|
|
194
|
+
const dy = e.clientY - (b.top + b.height / 2);
|
|
195
|
+
const near = Math.hypot(dx, dy) < 110;
|
|
196
|
+
cta.style.setProperty('--tx', near ? `${dx * 0.18}px` : '0px');
|
|
197
|
+
cta.style.setProperty('--ty', near ? `${dy * 0.28}px` : '0px');
|
|
198
|
+
};
|
|
199
|
+
const reset = () => {
|
|
200
|
+
cta?.style.setProperty('--tx', '0px');
|
|
201
|
+
cta?.style.setProperty('--ty', '0px');
|
|
202
|
+
};
|
|
203
|
+
host.addEventListener('pointermove', onMove, { passive: true });
|
|
204
|
+
host.addEventListener('pointerleave', reset);
|
|
205
|
+
return () => {
|
|
206
|
+
stopBoard();
|
|
207
|
+
host.removeEventListener('pointermove', onMove);
|
|
208
|
+
host.removeEventListener('pointerleave', reset);
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
</script>
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
// The person's monogram: their initial on their accent, the same shape as the favicon.
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
cast: CastView;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
const { class: className = 'size-7' } = Astro.props;
|
|
13
|
+
const { cast } = Astro.props;
|
|
14
|
+
const { person } = cast;
|
|
15
|
+
const initial = (person.name.trim()[0] ?? person.handle[0] ?? 'C').toUpperCase();
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<svg transition:name="monogram" viewBox="0 0 32 32" class={className} aria-hidden="true">
|
|
19
|
+
<defs>
|
|
20
|
+
<linearGradient id="monogram-sheen" x1="0" x2="0" y1="0" y2="1">
|
|
21
|
+
<stop offset="0" stop-color="#fff" stop-opacity=".22"></stop>
|
|
22
|
+
<stop offset=".6" stop-color="#fff" stop-opacity="0"></stop>
|
|
23
|
+
</linearGradient>
|
|
24
|
+
</defs>
|
|
25
|
+
<rect x="2" y="2" width="28" height="28" rx="7" fill="var(--color-accent)"></rect>
|
|
26
|
+
<rect x="2" y="2" width="28" height="28" rx="7" fill="url(#monogram-sheen)"></rect>
|
|
27
|
+
<text x="16" y="22.2" text-anchor="middle" font-family="var(--font-serif)" font-style="italic" font-weight="600" font-size="19" fill="var(--color-accent-ink)">{initial}</text>
|
|
28
|
+
</svg>
|