@cratis/cast 0.0.1 → 0.1.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 +11 -1
- package/badges/RegistryMember.ts +16 -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 +63 -0
- package/badges/resolveTier.ts +24 -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/castSchema.ts +262 -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/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 +64 -0
- package/machine/llmsText.ts +69 -0
- package/machine/ogImageSvg.ts +79 -0
- package/machine/resumeJson.ts +87 -0
- package/package.json +76 -8
- package/registry/members.yaml +16 -0
- package/routes/404.astro +40 -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 +11 -0
- package/routes/cv.astro +371 -0
- package/routes/favicon.svg.ts +16 -0
- package/routes/index.astro +61 -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 +194 -0
- package/site/components/Agentic.astro +143 -0
- package/site/components/CareerReplay.astro +412 -0
- package/site/components/CastBadge.astro +132 -0
- package/site/components/CommandPalette.astro +116 -0
- package/site/components/Contact.astro +163 -0
- package/site/components/Cratis.astro +233 -0
- package/site/components/CratisMark.astro +18 -0
- package/site/components/EventModel.astro +262 -0
- package/site/components/Footer.astro +28 -0
- package/site/components/Hero.astro +208 -0
- package/site/components/Monogram.astro +25 -0
- package/site/components/Nav.astro +124 -0
- package/site/components/Now.astro +113 -0
- package/site/components/Projects.astro +188 -0
- package/site/components/PullRequestFeed.astro +86 -0
- package/site/components/SectionHeader.astro +27 -0
- package/site/components/Skills.astro +41 -0
- package/site/components/Stats.astro +101 -0
- package/site/components/Talks.astro +73 -0
- package/site/components/Terminal.astro +159 -0
- package/site/components/Writing.astro +126 -0
- package/site/data.ts +48 -0
- package/site/layouts/Base.astro +144 -0
- package/site/lib/accent.ts +52 -0
- package/site/lib/blog.ts +32 -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 +55 -0
- package/site/lib/inline.ts +23 -0
- package/site/lib/lifecycle.ts +44 -0
- package/site/lib/motion.ts +43 -0
- package/site/lib/nav.ts +38 -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/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 +145 -0
- package/site/scripts/palette.ts +296 -0
- package/site/scripts/replay.ts +251 -0
- package/site/scripts/terminal.ts +556 -0
- package/site/styles/global.css +508 -0
- package/virtual.d.ts +15 -0
- package/writing/WritingSnapshot.ts +20 -0
- package/writing/fetchWriting.ts +54 -0
- package/writing/parse.ts +72 -0
package/routes/cv.astro
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
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 Base from '../site/layouts/Base.astro';
|
|
6
|
+
import Nav from '../site/components/Nav.astro';
|
|
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);
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
<Base
|
|
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>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { APIRoute } from 'astro';
|
|
5
|
+
import { config } from 'virtual:cast';
|
|
6
|
+
import { faviconSvg } from '../machine/ogImageSvg';
|
|
7
|
+
import { accentTokens } from '../site/lib/accent';
|
|
8
|
+
|
|
9
|
+
export const prerender = true;
|
|
10
|
+
|
|
11
|
+
export const GET: APIRoute = () => {
|
|
12
|
+
const initial = (config.person.name.trim()[0] ?? 'C').toUpperCase();
|
|
13
|
+
return new Response(faviconSvg(initial, config.theme.accent, accentTokens(config.theme.accent).ink), {
|
|
14
|
+
headers: { 'content-type': 'image/svg+xml; charset=utf-8' },
|
|
15
|
+
});
|
|
16
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
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 Base from '../site/layouts/Base.astro';
|
|
6
|
+
import Nav from '../site/components/Nav.astro';
|
|
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)}`;
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
<Base title={`${person.name} · ${person.title}`} description={description}>
|
|
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>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { APIRoute } from 'astro';
|
|
5
|
+
import * as data from 'virtual:cast';
|
|
6
|
+
import { llmsText } from '../machine/llmsText';
|
|
7
|
+
|
|
8
|
+
export const prerender = true;
|
|
9
|
+
|
|
10
|
+
export const GET: APIRoute = () => new Response(llmsText(data), { headers: { 'content-type': 'text/plain; charset=utf-8' } });
|
package/routes/og.png.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { APIRoute } from 'astro';
|
|
5
|
+
import sharp from 'sharp';
|
|
6
|
+
import { config, tier } from 'virtual:cast';
|
|
7
|
+
import { ogImageSvg } from '../machine/ogImageSvg';
|
|
8
|
+
import { accentTokens } from '../site/lib/accent';
|
|
9
|
+
|
|
10
|
+
export const prerender = true;
|
|
11
|
+
|
|
12
|
+
export const GET: APIRoute = async () => {
|
|
13
|
+
const svg = ogImageSvg({
|
|
14
|
+
name: config.person.name,
|
|
15
|
+
title: config.person.title,
|
|
16
|
+
tagline: config.person.tagline,
|
|
17
|
+
host: new URL(config.site.url).host,
|
|
18
|
+
accent: accentTokens(config.theme.accent).darkSoft,
|
|
19
|
+
tier: tier.tier,
|
|
20
|
+
});
|
|
21
|
+
const png = await sharp(Buffer.from(svg)).png().toBuffer();
|
|
22
|
+
return new Response(new Uint8Array(png), { headers: { 'content-type': 'image/png' } });
|
|
23
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { APIRoute } from 'astro';
|
|
5
|
+
import { config } from 'virtual:cast';
|
|
6
|
+
import { resumeJson } from '../machine/resumeJson';
|
|
7
|
+
|
|
8
|
+
export const prerender = true;
|
|
9
|
+
|
|
10
|
+
export const GET: APIRoute = ({ site }) =>
|
|
11
|
+
new Response(JSON.stringify(resumeJson(config, new URL('/apple-touch-icon.png', site ?? config.site.url).toString()), null, 2), {
|
|
12
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
13
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { APIRoute } from 'astro';
|
|
5
|
+
import { config } from 'virtual:cast';
|
|
6
|
+
|
|
7
|
+
export const prerender = true;
|
|
8
|
+
|
|
9
|
+
export const GET: APIRoute = () =>
|
|
10
|
+
new Response(`User-agent: *\nAllow: /\n\nSitemap: ${new URL('/sitemap-index.xml', config.site.url)}\n`, { headers: { 'content-type': 'text/plain; charset=utf-8' } });
|