@cratis/cast 0.1.0 → 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 +3 -1
- package/badges/RegistryMember.ts +2 -0
- 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 +214 -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 +8 -6
- 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 +17 -15
- 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 +7 -2
- 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 +48 -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 +117 -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 +28 -25
- package/writing/fetchWriting.ts +17 -2
- package/writing/writingSnapshotSchema.ts +27 -0
package/routes/cv.astro
CHANGED
|
@@ -2,370 +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
|
|
6
|
-
import
|
|
7
|
-
import Footer from '../site/components/Footer.astro';
|
|
8
|
-
import CastBadge from '../site/components/CastBadge.astro';
|
|
9
|
-
import { config, person, careerEvents, cratis, skills, education, certifications, talks, firstName, githubUrl, profileLinks, tier } from '../site/data';
|
|
10
|
-
import { liveProjects as projects, mergedPrFloor, mergedPrTotal, cratisRepos } from '../site/lib/github';
|
|
11
|
-
import { project, formatMonth, normalizeDate } from '../site/lib/career';
|
|
12
|
-
import { plainText, renderInline } from '../site/lib/inline';
|
|
13
|
-
|
|
14
|
-
const projection = project(careerEvents);
|
|
15
|
-
|
|
16
|
-
// Milestones that happened during a role are listed under it.
|
|
17
|
-
const MILESTONES = new Set(['SystemDeployed', 'LeadershipAssigned']);
|
|
18
|
-
const roles = [...projection.roles].reverse().map((r) => ({
|
|
19
|
-
...r,
|
|
20
|
-
milestones: careerEvents.filter((e) => {
|
|
21
|
-
const d = normalizeDate(e.date);
|
|
22
|
-
// Same organization (an assignment such as 'Azets (via Everdeen)' counts) and within the role's dates.
|
|
23
|
-
return MILESTONES.has(e.type) && !!e.org?.includes(r.org) && d >= normalizeDate(r.from) && (!r.to || d < normalizeDate(r.to));
|
|
24
|
-
}),
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
const EARLIER = new Set(['VolunteerTeamJoined', 'PartTimeRoleStarted', 'ContractStarted']);
|
|
28
|
-
const earlier = careerEvents.filter((e) => EARLIER.has(e.type)).reverse();
|
|
29
|
-
|
|
30
|
-
const range = (from: string, to?: string) => `${formatMonth(from)} – ${to ? formatMonth(to) : 'present'}`;
|
|
31
|
-
const host = (url: string) => url.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '');
|
|
32
|
-
const siteHost = host(config.site.url);
|
|
33
|
-
const list = (names: string[], joiner = ', ') => names.join(joiner);
|
|
34
|
-
// One factual line for the Cratis role, built only from what the data shows.
|
|
35
|
-
const cratisFacts = (() => {
|
|
36
|
-
if (!cratis) return '';
|
|
37
|
-
const founded = cratis.repos.filter((x) => x.founded).map((x) => x.name);
|
|
38
|
-
const leads = cratisRepos.filter((x) => x.rank === 1 && !x.founded).map((x) => x.name);
|
|
39
|
-
const seconds = cratisRepos.filter((x) => x.rank === 2 && x.group === 'pipeline').map((x) => x.name);
|
|
40
|
-
const parts = [
|
|
41
|
-
founded.length ? `Founded ${list(founded)}` : '',
|
|
42
|
-
leads.length ? `${founded.length ? 'also ' : ''}#1 committer on ${list(leads)}` : '',
|
|
43
|
-
seconds.length ? `#2 all-time on ${list(seconds, ' and ')}` : '',
|
|
44
|
-
].filter(Boolean);
|
|
45
|
-
const sentence = parts.length ? `${parts.join('; ')}.` : '';
|
|
46
|
-
const prs = mergedPrTotal > 0 ? ` ${new Intl.NumberFormat('en-US').format(mergedPrFloor)}${mergedPrTotal >= 100 ? '+' : ''} merged pull requests in the Cratis organization.` : '';
|
|
47
|
-
return `${sentence}${prs}`.trim();
|
|
48
|
-
})();
|
|
49
|
-
const talkYear = (date: string) => date.slice(0, 4);
|
|
5
|
+
import CvPage from '../site/pages/CvPage.astro';
|
|
6
|
+
import { cast } from '../site/data';
|
|
50
7
|
---
|
|
51
8
|
|
|
52
|
-
<
|
|
53
|
-
title={`CV · ${person.name}`}
|
|
54
|
-
description={`Curriculum vitae of ${person.fullName ?? person.name}, ${person.title}${person.location ? `, ${person.location}` : ''}.`}
|
|
55
|
-
bodyClass="cv-page"
|
|
56
|
-
>
|
|
57
|
-
<Nav slot="nav" home={false} />
|
|
58
|
-
|
|
59
|
-
<div class="container-x max-w-5xl pt-[calc(var(--nav-h)+2.5rem)] pb-24 print:p-0">
|
|
60
|
-
<div class="no-print mb-6 flex flex-wrap items-center justify-between gap-3">
|
|
61
|
-
<a href="/" class="link-quiet font-mono text-xs">← {siteHost}</a>
|
|
62
|
-
<div class="flex flex-wrap gap-2">
|
|
63
|
-
{careerEvents.length > 0 && <a href="/#career" class="btn btn-sm">Replay as event stream</a>}
|
|
64
|
-
<button type="button" class="btn btn-sm btn-primary" data-print>
|
|
65
|
-
<svg viewBox="0 0 16 16" class="size-4" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M4.5 6V2.5h7V6M4.5 11.5h-2v-5h11v5h-2M4.5 9.5h7v4h-7z" stroke-linejoin="round"></path></svg>
|
|
66
|
-
Print / Save as PDF
|
|
67
|
-
</button>
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
|
-
<article class="cv panel p-6 sm:p-10 md:p-14" aria-labelledby="cv-name">
|
|
72
|
-
<header class="cv-head flex flex-col gap-6 border-b border-line pb-8 md:flex-row md:items-end md:justify-between">
|
|
73
|
-
<div>
|
|
74
|
-
<p class="no-print mb-4 flex flex-wrap items-center gap-3"><span class="note">Projection: {firstName}.CV</span><CastBadge tier={tier.tier} handle={person.handle} size="compact" /></p>
|
|
75
|
-
<h1 id="cv-name" transition:name="person-name" class="text-[clamp(2.25rem,5vw,3.5rem)] leading-none font-[800] tracking-[-0.045em]">{person.fullName ?? person.name}</h1>
|
|
76
|
-
<p class="mt-3 text-lg text-fg-muted">{person.title}</p>
|
|
77
|
-
</div>
|
|
78
|
-
<ul class="cv-contact space-y-1 font-mono text-[0.8125rem] text-fg-muted md:text-right">
|
|
79
|
-
{person.location && <li>{person.location}</li>}
|
|
80
|
-
{person.email && <li><a href={`mailto:${person.email}`} class="hover:text-fg" data-no-url>{person.email}</a></li>}
|
|
81
|
-
<li><a href={config.site.url} class="hover:text-fg" data-no-url>{siteHost}</a></li>
|
|
82
|
-
<li><a href={githubUrl} class="hover:text-fg" data-no-url>{host(githubUrl)}</a></li>
|
|
83
|
-
{profileLinks.filter((link) => link.key !== 'github').slice(0, 2).map((link) => <li><a href={link.url} class="hover:text-fg" data-no-url>{host(link.url)}</a></li>)}
|
|
84
|
-
</ul>
|
|
85
|
-
</header>
|
|
86
|
-
|
|
87
|
-
<div class="cv-grid mt-10 grid gap-12 md:grid-cols-[1fr_15rem]">
|
|
88
|
-
<div class="cv-main space-y-12">
|
|
89
|
-
<section aria-labelledby="cv-summary">
|
|
90
|
-
<h2 id="cv-summary" class="cv-h">Summary</h2>
|
|
91
|
-
<div class="space-y-3 leading-relaxed text-fg-muted">
|
|
92
|
-
{(person.about.length ? person.about : [person.intro]).map((p) => <p set:html={renderInline(p)} />)}
|
|
93
|
-
</div>
|
|
94
|
-
</section>
|
|
95
|
-
|
|
96
|
-
<section aria-labelledby="cv-exp">
|
|
97
|
-
<h2 id="cv-exp" class="cv-h">Experience</h2>
|
|
98
|
-
<ol class="space-y-8">
|
|
99
|
-
{
|
|
100
|
-
roles.map((r) => (
|
|
101
|
-
<li class="cv-item">
|
|
102
|
-
<div class="flex flex-wrap items-baseline justify-between gap-x-4">
|
|
103
|
-
<h3 class="text-lg font-[640] tracking-[-0.01em]">
|
|
104
|
-
{r.title}
|
|
105
|
-
<span class="font-sans font-normal text-fg-muted">
|
|
106
|
-
{' · '}
|
|
107
|
-
{r.orgUrl ? <a href={r.orgUrl} data-print-url={host(r.orgUrl)} class="underline decoration-line-strong hover:text-fg">{r.org}</a> : r.org}
|
|
108
|
-
</span>
|
|
109
|
-
</h3>
|
|
110
|
-
<p class="font-mono text-xs whitespace-nowrap text-fg-faint">
|
|
111
|
-
{range(r.from, r.to)}
|
|
112
|
-
{r.location && ` · ${r.location}`}
|
|
113
|
-
</p>
|
|
114
|
-
</div>
|
|
115
|
-
<p class="mt-2 leading-relaxed text-fg-muted">{plainText(r.summary)}</p>
|
|
116
|
-
{r.org === 'Cratis' && cratisFacts && (
|
|
117
|
-
<p class="mt-2 leading-relaxed text-fg-muted">{cratisFacts}</p>
|
|
118
|
-
)}
|
|
119
|
-
{r.milestones.length > 0 && (
|
|
120
|
-
<ul class="mt-2 space-y-1 text-sm text-fg-muted">
|
|
121
|
-
{r.milestones.map((m) => (
|
|
122
|
-
<li class="flex gap-2">
|
|
123
|
-
<span class="font-mono text-xs leading-5 whitespace-nowrap text-event">{formatMonth(m.date)}</span>
|
|
124
|
-
<span><span class="text-fg">{m.title}.</span> {plainText(m.summary)}</span>
|
|
125
|
-
</li>
|
|
126
|
-
))}
|
|
127
|
-
</ul>
|
|
128
|
-
)}
|
|
129
|
-
{r.skills.length > 0 && <p class="mt-2 font-mono text-[0.75rem] text-fg-faint">{r.skills.join(' · ')}</p>}
|
|
130
|
-
</li>
|
|
131
|
-
))
|
|
132
|
-
}
|
|
133
|
-
</ol>
|
|
134
|
-
</section>
|
|
135
|
-
|
|
136
|
-
{earlier.length > 0 && <section aria-labelledby="cv-earlier">
|
|
137
|
-
<h2 id="cv-earlier" class="cv-h">Earlier</h2>
|
|
138
|
-
<ol class="space-y-5">
|
|
139
|
-
{
|
|
140
|
-
earlier.map((e) => (
|
|
141
|
-
<li class="cv-item">
|
|
142
|
-
<div class="flex flex-wrap items-baseline justify-between gap-x-4">
|
|
143
|
-
<h3 class="font-[620]">
|
|
144
|
-
{e.title}
|
|
145
|
-
<span class="font-sans font-normal text-fg-muted">
|
|
146
|
-
{' · '}
|
|
147
|
-
{e.orgUrl ? <a href={e.orgUrl} data-print-url={host(e.orgUrl)} class="underline decoration-line-strong hover:text-fg">{e.org}</a> : e.org}
|
|
148
|
-
</span>
|
|
149
|
-
</h3>
|
|
150
|
-
<p class="font-mono text-xs whitespace-nowrap text-fg-faint">{e.endDate ? range(e.date, e.endDate) : `from ${formatMonth(e.date)}`}</p>
|
|
151
|
-
</div>
|
|
152
|
-
<p class="mt-1.5 text-sm leading-relaxed text-fg-muted">{plainText(e.summary)}</p>
|
|
153
|
-
</li>
|
|
154
|
-
))
|
|
155
|
-
}
|
|
156
|
-
</ol>
|
|
157
|
-
</section>}
|
|
158
|
-
|
|
159
|
-
{projects.length > 0 && <section aria-labelledby="cv-projects">
|
|
160
|
-
<h2 id="cv-projects" class="cv-h">Selected projects</h2>
|
|
161
|
-
<ul class="cv-projects grid gap-x-8 gap-y-4 sm:grid-cols-2">
|
|
162
|
-
{
|
|
163
|
-
projects.map((p) => (
|
|
164
|
-
<li class="cv-item">
|
|
165
|
-
<h3 class="font-[620]">
|
|
166
|
-
<a href={p.url} data-print-url={host(p.url)} class="hover:text-event-soft">{p.name}</a>
|
|
167
|
-
{p.version && <span class="ml-1.5 font-mono text-xs font-normal text-fg-faint">{p.version}</span>}
|
|
168
|
-
</h3>
|
|
169
|
-
<p class="mt-0.5 text-sm leading-relaxed text-fg-muted">{plainText(p.pitch)}</p>
|
|
170
|
-
<p class="mt-0.5 font-mono text-[0.75rem] text-fg-faint">{p.stack.join(' · ')}</p>
|
|
171
|
-
</li>
|
|
172
|
-
))
|
|
173
|
-
}
|
|
174
|
-
</ul>
|
|
175
|
-
</section>}
|
|
176
|
-
</div>
|
|
177
|
-
|
|
178
|
-
<aside class="cv-side space-y-10 text-sm">
|
|
179
|
-
{Object.keys(skills).length > 0 && <section aria-labelledby="cv-skills">
|
|
180
|
-
<h2 id="cv-skills" class="cv-h">Skills</h2>
|
|
181
|
-
<dl class="space-y-4">
|
|
182
|
-
{
|
|
183
|
-
Object.entries(skills).map(([group, list]) => (
|
|
184
|
-
<div>
|
|
185
|
-
<dt class="mb-1 font-medium text-fg">{group}</dt>
|
|
186
|
-
<dd class="leading-relaxed text-fg-muted">{list.join(', ')}</dd>
|
|
187
|
-
</div>
|
|
188
|
-
))
|
|
189
|
-
}
|
|
190
|
-
</dl>
|
|
191
|
-
</section>}
|
|
192
|
-
|
|
193
|
-
{education.length > 0 && <section aria-labelledby="cv-edu">
|
|
194
|
-
<h2 id="cv-edu" class="cv-h">Education</h2>
|
|
195
|
-
{
|
|
196
|
-
education.map((e) => (
|
|
197
|
-
<div>
|
|
198
|
-
<p class="font-medium text-fg">{e.degree}</p>
|
|
199
|
-
<p class="text-fg-muted">{e.school}</p>
|
|
200
|
-
<p class="font-mono text-xs text-fg-faint">{e.from}{e.to ? ` – ${e.to}` : ''}</p>
|
|
201
|
-
</div>
|
|
202
|
-
))
|
|
203
|
-
}
|
|
204
|
-
</section>}
|
|
205
|
-
|
|
206
|
-
{certifications.length > 0 && <section aria-labelledby="cv-certs">
|
|
207
|
-
<h2 id="cv-certs" class="cv-h">Certifications</h2>
|
|
208
|
-
<ul class="space-y-3">
|
|
209
|
-
{
|
|
210
|
-
certifications.map((c) => (
|
|
211
|
-
<li>
|
|
212
|
-
{c.url ? <a href={c.url} class="font-medium text-fg hover:text-event-soft" data-no-url>{c.title}</a> : <span class="font-medium text-fg">{c.title}</span>}
|
|
213
|
-
<p class="font-mono text-xs text-fg-faint">{c.issuer} · {c.year}</p>
|
|
214
|
-
</li>
|
|
215
|
-
))
|
|
216
|
-
}
|
|
217
|
-
</ul>
|
|
218
|
-
</section>}
|
|
219
|
-
|
|
220
|
-
{talks.length > 0 && <section aria-labelledby="cv-talks">
|
|
221
|
-
<h2 id="cv-talks" class="cv-h">Talks</h2>
|
|
222
|
-
{
|
|
223
|
-
talks.map((t) => (
|
|
224
|
-
<div>
|
|
225
|
-
{t.url ? <a href={t.url} class="font-medium text-fg hover:text-event-soft" data-no-url>{t.title}</a> : <span class="font-medium text-fg">{t.title}</span>}
|
|
226
|
-
<p class="text-fg-muted">{t.host}{t.with && `, with ${t.with}`}</p>
|
|
227
|
-
<p class="font-mono text-xs text-fg-faint">{talkYear(t.date)}</p>
|
|
228
|
-
</div>
|
|
229
|
-
))
|
|
230
|
-
}
|
|
231
|
-
</section>}
|
|
232
|
-
|
|
233
|
-
{person.languages.length > 0 && <section aria-labelledby="cv-lang">
|
|
234
|
-
<h2 id="cv-lang" class="cv-h">Languages</h2>
|
|
235
|
-
<ul class="space-y-1 text-fg-muted">
|
|
236
|
-
{person.languages.map((l) => <li>{l}</li>)}
|
|
237
|
-
</ul>
|
|
238
|
-
</section>}
|
|
239
|
-
</aside>
|
|
240
|
-
</div>
|
|
241
|
-
</article>
|
|
242
|
-
</div>
|
|
243
|
-
|
|
244
|
-
<Footer slot="footer" />
|
|
245
|
-
</Base>
|
|
246
|
-
|
|
247
|
-
<style>
|
|
248
|
-
.cv {
|
|
249
|
-
backdrop-filter: none;
|
|
250
|
-
-webkit-backdrop-filter: none;
|
|
251
|
-
}
|
|
252
|
-
.cv-h {
|
|
253
|
-
margin-bottom: 1.25rem;
|
|
254
|
-
font-family: var(--font-mono);
|
|
255
|
-
font-size: 0.75rem;
|
|
256
|
-
font-weight: 500;
|
|
257
|
-
letter-spacing: 0.1em;
|
|
258
|
-
text-transform: uppercase;
|
|
259
|
-
color: var(--color-event);
|
|
260
|
-
}
|
|
261
|
-
.cv h3 {
|
|
262
|
-
font-family: var(--font-display);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
@media print {
|
|
266
|
-
@page {
|
|
267
|
-
size: A4;
|
|
268
|
-
margin: 13mm 14mm;
|
|
269
|
-
}
|
|
270
|
-
:global(.grain::after),
|
|
271
|
-
:global(header[data-nav]),
|
|
272
|
-
:global(footer),
|
|
273
|
-
:global(dialog),
|
|
274
|
-
:global(.sr-only-focusable),
|
|
275
|
-
.no-print {
|
|
276
|
-
display: none !important;
|
|
277
|
-
}
|
|
278
|
-
.cv {
|
|
279
|
-
padding: 0 !important;
|
|
280
|
-
border: 0 !important;
|
|
281
|
-
background: none !important;
|
|
282
|
-
box-shadow: none !important;
|
|
283
|
-
backdrop-filter: none !important;
|
|
284
|
-
color: #111;
|
|
285
|
-
}
|
|
286
|
-
.cv :global(*) {
|
|
287
|
-
color: #111 !important;
|
|
288
|
-
border-color: #d4d4d4 !important;
|
|
289
|
-
}
|
|
290
|
-
.cv :global(.text-fg-muted),
|
|
291
|
-
.cv :global(.text-fg-faint) {
|
|
292
|
-
color: #444 !important;
|
|
293
|
-
}
|
|
294
|
-
.cv-h {
|
|
295
|
-
color: #b4520d !important;
|
|
296
|
-
margin-bottom: 0.6rem;
|
|
297
|
-
font-size: 7.5pt;
|
|
298
|
-
}
|
|
299
|
-
.cv-head {
|
|
300
|
-
padding-bottom: 1rem;
|
|
301
|
-
}
|
|
302
|
-
.cv-head h1 {
|
|
303
|
-
font-size: 24pt;
|
|
304
|
-
}
|
|
305
|
-
.cv-grid {
|
|
306
|
-
display: grid !important;
|
|
307
|
-
grid-template-columns: 1fr 13.5rem !important;
|
|
308
|
-
gap: 1.75rem !important;
|
|
309
|
-
margin-top: 1.25rem !important;
|
|
310
|
-
}
|
|
311
|
-
.cv-main {
|
|
312
|
-
row-gap: 0;
|
|
313
|
-
}
|
|
314
|
-
.cv-main > :global(* + *) {
|
|
315
|
-
margin-top: 1.4rem !important;
|
|
316
|
-
}
|
|
317
|
-
.cv-main :global(ol.space-y-8 > * + *) {
|
|
318
|
-
margin-top: 0.9rem !important;
|
|
319
|
-
}
|
|
320
|
-
.cv-side > :global(* + *) {
|
|
321
|
-
margin-top: 1.25rem !important;
|
|
322
|
-
}
|
|
323
|
-
.cv-item,
|
|
324
|
-
.cv-side > section {
|
|
325
|
-
break-inside: avoid;
|
|
326
|
-
}
|
|
327
|
-
.cv-h {
|
|
328
|
-
break-after: avoid;
|
|
329
|
-
}
|
|
330
|
-
.cv :global(a) {
|
|
331
|
-
text-decoration: none !important;
|
|
332
|
-
}
|
|
333
|
-
.cv-main :global(a[data-print-url])::after {
|
|
334
|
-
content: ' ' attr(data-print-url);
|
|
335
|
-
font-family: var(--font-mono);
|
|
336
|
-
font-size: 7pt;
|
|
337
|
-
color: #555;
|
|
338
|
-
word-break: break-all;
|
|
339
|
-
}
|
|
340
|
-
.cv :global(.text-lg) {
|
|
341
|
-
font-size: 10pt;
|
|
342
|
-
}
|
|
343
|
-
.cv :global(.leading-relaxed) {
|
|
344
|
-
line-height: 1.42;
|
|
345
|
-
}
|
|
346
|
-
.cv-projects :global(a[data-print-url])::after {
|
|
347
|
-
display: block;
|
|
348
|
-
word-break: normal;
|
|
349
|
-
}
|
|
350
|
-
.cv-projects {
|
|
351
|
-
grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
|
|
352
|
-
column-gap: 1rem !important;
|
|
353
|
-
row-gap: 0.6rem !important;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
</style>
|
|
357
|
-
|
|
358
|
-
<script>
|
|
359
|
-
import { onPageLoad } from '../site/lib/lifecycle';
|
|
360
|
-
onPageLoad(() => {
|
|
361
|
-
if (!document.querySelector('.cv-page')) return;
|
|
362
|
-
document.querySelector('[data-print]')?.addEventListener('click', () => window.print());
|
|
363
|
-
const url = new URL(location.href);
|
|
364
|
-
if (url.searchParams.has('print')) {
|
|
365
|
-
history.replaceState(history.state, '', url.pathname);
|
|
366
|
-
const print = () => window.setTimeout(() => window.print(), 400);
|
|
367
|
-
if (document.readyState === 'complete') print();
|
|
368
|
-
else window.addEventListener('load', print, { once: true });
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
</script>
|
|
9
|
+
<CvPage cast={cast} />
|
package/routes/index.astro
CHANGED
|
@@ -2,60 +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
|
|
6
|
-
import
|
|
7
|
-
import Hero from '../site/components/Hero.astro';
|
|
8
|
-
import Now from '../site/components/Now.astro';
|
|
9
|
-
import Stats from '../site/components/Stats.astro';
|
|
10
|
-
import CareerReplay from '../site/components/CareerReplay.astro';
|
|
11
|
-
import EventModel from '../site/components/EventModel.astro';
|
|
12
|
-
import Cratis from '../site/components/Cratis.astro';
|
|
13
|
-
import Activity from '../site/components/Activity.astro';
|
|
14
|
-
import Projects from '../site/components/Projects.astro';
|
|
15
|
-
import Agentic from '../site/components/Agentic.astro';
|
|
16
|
-
import Writing from '../site/components/Writing.astro';
|
|
17
|
-
import Talks from '../site/components/Talks.astro';
|
|
18
|
-
import Skills from '../site/components/Skills.astro';
|
|
19
|
-
import Contact from '../site/components/Contact.astro';
|
|
20
|
-
import Footer from '../site/components/Footer.astro';
|
|
21
|
-
import { config, person, sections } from '../site/data';
|
|
22
|
-
import { plainText } from '../site/lib/inline';
|
|
23
|
-
|
|
24
|
-
const components = {
|
|
25
|
-
hero: Hero,
|
|
26
|
-
now: Now,
|
|
27
|
-
stats: Stats,
|
|
28
|
-
career: CareerReplay,
|
|
29
|
-
model: EventModel,
|
|
30
|
-
cratis: Cratis,
|
|
31
|
-
activity: Activity,
|
|
32
|
-
agentic: Agentic,
|
|
33
|
-
projects: Projects,
|
|
34
|
-
writing: Writing,
|
|
35
|
-
talks: Talks,
|
|
36
|
-
skills: Skills,
|
|
37
|
-
contact: Contact,
|
|
38
|
-
} as const;
|
|
39
|
-
|
|
40
|
-
const description = config.site.description ?? `${plainText(person.tagline)} ${plainText(person.intro)}`;
|
|
5
|
+
import ProfilePage from '../site/pages/ProfilePage.astro';
|
|
6
|
+
import { cast } from '../site/data';
|
|
41
7
|
---
|
|
42
8
|
|
|
43
|
-
<
|
|
44
|
-
<Nav slot="nav" />
|
|
45
|
-
{
|
|
46
|
-
sections.map((section, position) => {
|
|
47
|
-
const Section = components[section.id];
|
|
48
|
-
const previous = sections[position - 1];
|
|
49
|
-
return (
|
|
50
|
-
<Section
|
|
51
|
-
index={section.index}
|
|
52
|
-
{...(section.title ? { title: section.title } : {})}
|
|
53
|
-
{...(section.label ? { label: section.label } : {})}
|
|
54
|
-
{...(section.intro ? { intro: section.intro } : {})}
|
|
55
|
-
compact={section.id === 'skills' && previous?.id === 'talks'}
|
|
56
|
-
/>
|
|
57
|
-
);
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
<Footer slot="footer" />
|
|
61
|
-
</Base>
|
|
9
|
+
<ProfilePage cast={cast} />
|
|
@@ -2,19 +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 SectionHeader from './SectionHeader.astro';
|
|
6
8
|
import PullRequestFeed from './PullRequestFeed.astro';
|
|
7
|
-
import { cratis } from '../data';
|
|
8
|
-
import { renderInline } from '../lib/inline';
|
|
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
|
}
|
|
16
17
|
const { index, title = 'Every contribution, *appended.*', label = 'Event store', intro } = Astro.props;
|
|
17
|
-
|
|
18
|
+
const { cast } = Astro.props;
|
|
19
|
+
const { cratis, github, snapshotDate } = cast;
|
|
18
20
|
import { foldCalendar, formatDay, formatMonthLong } from '../lib/contributions';
|
|
19
21
|
|
|
20
22
|
const fmt = new Intl.NumberFormat('en-US');
|
|
@@ -41,9 +43,9 @@ const summary = `Contribution heatmap: ${fmt.format(total)} contributions betwee
|
|
|
41
43
|
|
|
42
44
|
<section id="activity" class="relative py-24 md:py-36" aria-labelledby="activity-title">
|
|
43
45
|
<div class="container-x">
|
|
44
|
-
<SectionHeader index={index} label={label} title={title} id="activity-title">
|
|
46
|
+
<SectionHeader host={cast.host} index={index} label={label} title={title} id="activity-title">
|
|
45
47
|
<p class="mt-6 max-w-[60ch] text-lg leading-relaxed text-fg-muted">
|
|
46
|
-
{intro ? <Fragment
|
|
48
|
+
{intro ? <Fragment><Inline text={intro} host={cast.host} /></Fragment> : <>My GitHub contribution calendar, re-projected as an event store: one partition per day, brighter means more events appended.
|
|
47
49
|
Captured when the site was built.</>}
|
|
48
50
|
</p>
|
|
49
51
|
</SectionHeader>
|
|
@@ -121,7 +123,7 @@ const summary = `Contribution heatmap: ${fmt.format(total)} contributions betwee
|
|
|
121
123
|
</tbody>
|
|
122
124
|
</table>
|
|
123
125
|
</div>}
|
|
124
|
-
{!cratis && <PullRequestFeed class="mt-6" />}
|
|
126
|
+
{!cratis && <PullRequestFeed cast={cast} class="mt-6" />}
|
|
125
127
|
</div>
|
|
126
128
|
<div class="hm-tip" role="presentation" data-heatmap-tip hidden></div>
|
|
127
129
|
</section>
|
|
@@ -2,17 +2,18 @@
|
|
|
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 { agentic as agenticData, config } from '../data';
|
|
7
|
-
import { github } from '../lib/github';
|
|
8
|
-
import { postsFor, themes } from '../lib/blog';
|
|
9
|
-
import { renderInline } from '../lib/inline';
|
|
10
8
|
|
|
11
9
|
interface Props {
|
|
10
|
+
cast: CastView;
|
|
12
11
|
index: string;
|
|
13
12
|
title?: string;
|
|
14
13
|
label?: string;
|
|
15
14
|
}
|
|
15
|
+
const { cast } = Astro.props;
|
|
16
|
+
const { agentic: agenticData, config, github, postsFor, themes } = cast;
|
|
16
17
|
const agentic = agenticData!;
|
|
17
18
|
const { index, title = agentic.title, label = agentic.label } = Astro.props;
|
|
18
19
|
const fmt = new Intl.NumberFormat('en-US');
|
|
@@ -25,8 +26,8 @@ const kinds = ['command', 'event', 'view', 'event', 'command'] as const;
|
|
|
25
26
|
<section id="agentic" class="relative overflow-hidden py-24 md:py-36" aria-labelledby="agentic-title">
|
|
26
27
|
<div class="pointer-events-none absolute top-1/4 -left-40 size-[36rem] rounded-full bg-view/8 blur-[120px]" aria-hidden="true"></div>
|
|
27
28
|
<div class="container-x relative">
|
|
28
|
-
<SectionHeader index={index} kind="view" label={label} title={title} id="agentic-title">
|
|
29
|
-
<p class="mt-6 max-w-[62ch] text-lg leading-relaxed text-fg-muted"
|
|
29
|
+
<SectionHeader host={cast.host} index={index} kind="view" label={label} title={title} id="agentic-title">
|
|
30
|
+
<p class="mt-6 max-w-[62ch] text-lg leading-relaxed text-fg-muted"><Inline text={agentic.pitch} host={cast.host} /></p>
|
|
30
31
|
</SectionHeader>
|
|
31
32
|
|
|
32
33
|
<div class="panel overflow-hidden" data-reveal data-harness>
|
|
@@ -46,7 +47,7 @@ const kinds = ['command', 'event', 'view', 'event', 'command'] as const;
|
|
|
46
47
|
</span>
|
|
47
48
|
<span class="mt-4 block font-display text-2xl font-[650] tracking-[-0.02em]">{w.step}</span>
|
|
48
49
|
<span class={`note mt-1 ${kinds[i] === 'command' ? 'note-command' : kinds[i] === 'view' ? 'note-view' : ''}`}>{w.event}</span>
|
|
49
|
-
<span class="mt-3 block text-sm leading-relaxed text-fg-muted"
|
|
50
|
+
<span class="mt-3 block text-sm leading-relaxed text-fg-muted"><Inline text={w.text} host={cast.host} /></span>
|
|
50
51
|
{w.loop && (
|
|
51
52
|
<span class="loop mt-4 inline-flex whitespace-nowrap items-center gap-1.5 rounded-full border border-event/30 bg-event/8 px-2.5 py-0.5 font-mono text-[0.6875rem] text-event-soft">
|
|
52
53
|
↺ {w.loop}
|
|
@@ -65,10 +66,10 @@ const kinds = ['command', 'event', 'view', 'event', 'command'] as const;
|
|
|
65
66
|
const Tag = item.url ? 'a' : 'div';
|
|
66
67
|
return (
|
|
67
68
|
<li class:list={[item.list && 'md:col-span-2']} data-reveal style={`--reveal-delay:${(i % 3) * 60}ms`}>
|
|
68
|
-
<Tag href={item.url} rel={item.url ?
|
|
69
|
+
<Tag href={item.url} rel={item.url ? cast.host.rel : undefined} class:list={['panel flex h-full flex-col p-5 md:p-6', item.url && 'glow group']} style="--glow: var(--color-view)">
|
|
69
70
|
<span class="note note-view">{item.kind}</span>
|
|
70
71
|
<span class="mt-4 font-display text-2xl font-[650] tracking-[-0.02em] transition-colors group-hover:text-view-soft">{item.name}</span>
|
|
71
|
-
<span class="mt-2 text-sm leading-relaxed text-fg-muted"
|
|
72
|
+
<span class="mt-2 text-sm leading-relaxed text-fg-muted"><Inline text={item.text} host={cast.host} /></span>
|
|
72
73
|
{item.list && (
|
|
73
74
|
<span class="mt-5 block">
|
|
74
75
|
<span class="block font-mono text-[0.6875rem] tracking-[0.08em] text-fg-faint uppercase">{item.listLabel}</span>
|
|
@@ -89,7 +90,7 @@ const kinds = ['command', 'event', 'view', 'event', 'command'] as const;
|
|
|
89
90
|
<ul class="mt-3 grid gap-x-8 gap-y-2 md:grid-cols-2">
|
|
90
91
|
{aiPosts.map((p) => (
|
|
91
92
|
<li>
|
|
92
|
-
<a href={p.url} rel=
|
|
93
|
+
<a href={p.url} rel={cast.host.rel} class="group flex items-baseline gap-3 text-[0.9375rem]">
|
|
93
94
|
<span class="text-view" aria-hidden="true">→</span>
|
|
94
95
|
<span class="text-fg group-hover:underline">{p.headline}</span>
|
|
95
96
|
</a>
|
|
@@ -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 {
|
|
6
|
-
import
|
|
5
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
import Inline from './Inline.astro';
|
|
7
|
+
import { project, formatMonth, yearsBetween, alsoText } from '../lib/career';
|
|
8
|
+
import SectionHeader from './SectionHeader.astro';
|
|
7
9
|
|
|
8
10
|
interface Props {
|
|
11
|
+
cast: CastView;
|
|
9
12
|
index: string;
|
|
10
13
|
title?: string;
|
|
11
14
|
label?: string;
|
|
12
15
|
intro?: string;
|
|
13
16
|
}
|
|
14
17
|
const { index, title = 'Career, replayed from the *event log.*', label = 'Event stream', intro } = Astro.props;
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
const { cast } = Astro.props;
|
|
19
|
+
const { careerEvents, firstName } = cast;
|
|
17
20
|
|
|
18
21
|
const total = careerEvents.length;
|
|
19
22
|
const final = project(careerEvents);
|
|
@@ -27,9 +30,9 @@ const icon = {
|
|
|
27
30
|
|
|
28
31
|
<section id="career" class="relative py-24 md:py-36" aria-labelledby="career-title" data-replay data-total={total}>
|
|
29
32
|
<div class="container-x">
|
|
30
|
-
<SectionHeader index={index} label={label} title={title} id="career-title">
|
|
33
|
+
<SectionHeader host={cast.host} index={index} label={label} title={title} id="career-title">
|
|
31
34
|
<p class="mt-6 max-w-[60ch] text-lg text-fg-muted" data-reveal>
|
|
32
|
-
{intro ? <Fragment
|
|
35
|
+
{intro ? <Fragment><Inline text={intro} host={cast.host} /></Fragment> : <>Every role, skill and organization below is a projection, computed by folding these {total} events in order.
|
|
33
36
|
Scroll, scrub or press play to watch the state rebuild itself.</>}
|
|
34
37
|
</p>
|
|
35
38
|
</SectionHeader>
|
|
@@ -176,7 +179,7 @@ const icon = {
|
|
|
176
179
|
{e.org && (
|
|
177
180
|
<p class="mt-1 text-sm text-fg-muted">
|
|
178
181
|
{e.orgUrl ? (
|
|
179
|
-
<a href={e.orgUrl} class="underline decoration-line-strong hover:text-fg hover:decoration-event" rel=
|
|
182
|
+
<a href={e.orgUrl} class="underline decoration-line-strong hover:text-fg hover:decoration-event" rel={cast.host.rel}>
|
|
180
183
|
{e.org}
|
|
181
184
|
</a>
|
|
182
185
|
) : (
|
|
@@ -185,7 +188,7 @@ const icon = {
|
|
|
185
188
|
{e.location && <span class="text-fg-faint"> · {e.location}</span>}
|
|
186
189
|
</p>
|
|
187
190
|
)}
|
|
188
|
-
<p class="mt-3 text-[0.95rem] leading-relaxed text-fg-muted"
|
|
191
|
+
<p class="mt-3 text-[0.95rem] leading-relaxed text-fg-muted"><Inline text={e.summary} host={cast.host} /></p>
|
|
189
192
|
{e.skills && (
|
|
190
193
|
<ul class="mt-4 flex flex-wrap gap-1.5" aria-label="Skills">
|
|
191
194
|
{e.skills.map((s) => (
|
|
@@ -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
|
-
|
|
5
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
cast: CastView;
|
|
9
|
+
}
|
|
10
|
+
const { cast } = Astro.props;
|
|
11
|
+
const host = new URL(cast.config.site.url).host;
|
|
7
12
|
---
|
|
8
13
|
|
|
9
|
-
<dialog class="palette" data-palette aria-label="Command palette" transition:persist=
|
|
14
|
+
<dialog class="palette" data-palette aria-label="Command palette" transition:persist={`palette-${cast.person.handle.toLowerCase()}`}>
|
|
10
15
|
<div class="sheet panel overflow-hidden">
|
|
11
16
|
<div class="flex items-center gap-3 border-b border-line px-4">
|
|
12
17
|
<svg viewBox="0 0 20 20" class="size-4 shrink-0 text-fg-faint" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
|
|
@@ -111,6 +116,7 @@ const host = new URL(config.site.url).host;
|
|
|
111
116
|
</style>
|
|
112
117
|
|
|
113
118
|
<script>
|
|
119
|
+
import { onPageLoad } from '../lib/lifecycle';
|
|
114
120
|
import { mountPalette } from '../scripts/palette';
|
|
115
|
-
mountPalette();
|
|
121
|
+
onPageLoad(() => mountPalette());
|
|
116
122
|
</script>
|