@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,127 @@
|
|
|
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 { navLinks } from '../lib/sections';
|
|
7
|
+
import CastBadge from './CastBadge.astro';
|
|
8
|
+
import Monogram from './Monogram.astro';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
cast: CastView;
|
|
12
|
+
/** Prefix anchor links with the home page path when not on the home page. */
|
|
13
|
+
home?: boolean;
|
|
14
|
+
}
|
|
15
|
+
const { cast, home = true } = Astro.props;
|
|
16
|
+
const { person, sections, tier } = cast;
|
|
17
|
+
const base = home ? '' : cast.href();
|
|
18
|
+
const links = navLinks(sections);
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<header
|
|
22
|
+
data-nav
|
|
23
|
+
class="group/nav fixed inset-x-0 top-0 z-50 h-[var(--nav-h)] border-b border-transparent transition-[background-color,border-color,backdrop-filter] duration-300 data-[scrolled]:border-line data-[scrolled]:bg-ink-950/70 data-[scrolled]:backdrop-blur-xl data-[scrolled]:backdrop-saturate-150 print:hidden"
|
|
24
|
+
>
|
|
25
|
+
<nav class="container-x flex h-full items-center justify-between gap-4" aria-label="Primary">
|
|
26
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
27
|
+
<slot name="lead" />
|
|
28
|
+
<a href={home ? '#top' : cast.href()} class="group flex items-center gap-2.5 rounded-md" aria-label={`${person.handle}, home`}>
|
|
29
|
+
<Monogram cast={cast} class="size-7 shrink-0 transition-transform duration-500 ease-[var(--ease-spring)] group-hover:-rotate-6" />
|
|
30
|
+
<span class="font-mono text-[0.9375rem] font-semibold tracking-tight">{person.handle}</span>
|
|
31
|
+
</a>
|
|
32
|
+
<span class="hidden sm:inline-flex"><CastBadge tier={tier.tier} handle={person.handle} size="compact" claim={!cast.host.verified} /></span>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<ul class="hidden items-center gap-1 md:flex">
|
|
36
|
+
{
|
|
37
|
+
links.map((l) => (
|
|
38
|
+
<li>
|
|
39
|
+
<a
|
|
40
|
+
href={`${base}#${l.id}`}
|
|
41
|
+
data-section={home ? l.id : undefined}
|
|
42
|
+
class="relative rounded-lg px-3 py-2 text-sm text-fg-muted transition-colors hover:text-fg aria-[current=location]:text-fg"
|
|
43
|
+
>
|
|
44
|
+
{l.label}
|
|
45
|
+
</a>
|
|
46
|
+
</li>
|
|
47
|
+
))
|
|
48
|
+
}
|
|
49
|
+
<li>
|
|
50
|
+
<a href={cast.href('cv')} class="rounded-lg px-3 py-2 text-sm text-fg-muted transition-colors hover:text-fg">CV</a>
|
|
51
|
+
</li>
|
|
52
|
+
</ul>
|
|
53
|
+
|
|
54
|
+
<div class="flex items-center gap-2">
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
data-theme-toggle
|
|
58
|
+
class="btn btn-sm w-9 px-0 text-fg-muted hover:text-fg"
|
|
59
|
+
aria-label="Light theme"
|
|
60
|
+
aria-pressed="false"
|
|
61
|
+
title="Toggle light theme"
|
|
62
|
+
>
|
|
63
|
+
<svg viewBox="0 0 20 20" class="theme-sun size-4" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
|
|
64
|
+
<circle cx="10" cy="10" r="3.5"></circle><path d="M10 2v2M10 16v2M2 10h2M16 10h2M4.3 4.3l1.4 1.4M14.3 14.3l1.4 1.4M4.3 15.7l1.4-1.4M14.3 5.7l1.4-1.4" stroke-linecap="round"></path>
|
|
65
|
+
</svg>
|
|
66
|
+
<svg viewBox="0 0 20 20" class="theme-moon size-4" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
|
|
67
|
+
<path d="M16 12.5A6.5 6.5 0 0 1 7.5 4a6.5 6.5 0 1 0 8.5 8.5Z" stroke-linejoin="round"></path>
|
|
68
|
+
</svg>
|
|
69
|
+
</button>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
data-terminal-open
|
|
73
|
+
class="btn btn-sm w-9 px-0 text-fg-muted hover:text-fg"
|
|
74
|
+
aria-label="Open terminal"
|
|
75
|
+
aria-haspopup="dialog"
|
|
76
|
+
aria-keyshortcuts="`"
|
|
77
|
+
title="Terminal ( ` )"
|
|
78
|
+
>
|
|
79
|
+
<svg viewBox="0 0 20 20" class="size-4" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
|
|
80
|
+
<rect x="2.5" y="3.5" width="15" height="13" rx="2.5"></rect><path d="m6 8 2.5 2L6 12M10.5 12.5H14" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
81
|
+
</svg>
|
|
82
|
+
</button>
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
data-palette-open
|
|
86
|
+
class="btn btn-sm gap-2.5 pr-1.5 text-fg-muted hover:text-fg"
|
|
87
|
+
aria-haspopup="dialog"
|
|
88
|
+
aria-keyshortcuts="Meta+K Control+K"
|
|
89
|
+
>
|
|
90
|
+
<svg viewBox="0 0 20 20" class="size-4" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
|
|
91
|
+
<circle cx="9" cy="9" r="5.5"></circle><path d="m13.2 13.2 3.3 3.3" stroke-linecap="round"></path>
|
|
92
|
+
</svg>
|
|
93
|
+
<span class="md:hidden">Menu</span>
|
|
94
|
+
<span class="hidden md:inline">Commands</span>
|
|
95
|
+
<span class="hidden items-center gap-0.5 sm:flex" aria-hidden="true">
|
|
96
|
+
<kbd class="kbd" data-mod-key>⌘</kbd><kbd class="kbd">K</kbd>
|
|
97
|
+
</span>
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</nav>
|
|
101
|
+
</header>
|
|
102
|
+
|
|
103
|
+
<script>
|
|
104
|
+
import { onPageLoad } from '../lib/lifecycle';
|
|
105
|
+
import { currentTheme, toggleTheme } from '../lib/theme';
|
|
106
|
+
// Bound once per full load; the button itself is re-rendered on every navigation.
|
|
107
|
+
document.addEventListener('click', (e) => {
|
|
108
|
+
if ((e.target as Element | null)?.closest('[data-theme-toggle]')) toggleTheme();
|
|
109
|
+
});
|
|
110
|
+
onPageLoad(() => {
|
|
111
|
+
document.querySelectorAll('[data-theme-toggle]').forEach((b) => b.setAttribute('aria-pressed', String(currentTheme() === 'light')));
|
|
112
|
+
const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
113
|
+
if (!isMac) document.querySelectorAll('[data-mod-key]').forEach((el) => (el.textContent = 'Ctrl'));
|
|
114
|
+
});
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<style>
|
|
118
|
+
.theme-moon {
|
|
119
|
+
display: none;
|
|
120
|
+
}
|
|
121
|
+
:global(html[data-theme='light']) .theme-sun {
|
|
122
|
+
display: none;
|
|
123
|
+
}
|
|
124
|
+
:global(html[data-theme='light']) .theme-moon {
|
|
125
|
+
display: block;
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
@@ -0,0 +1,116 @@
|
|
|
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 { Picture } from 'astro:assets';
|
|
8
|
+
import { formatMonth } from '../lib/career';
|
|
9
|
+
import SectionHeader from './SectionHeader.astro';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
cast: CastView;
|
|
13
|
+
index: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
}
|
|
17
|
+
const { index, title = 'Where the stream is *now.*', label = 'Read model' } = Astro.props;
|
|
18
|
+
const { cast } = Astro.props;
|
|
19
|
+
const { person, now, careerEvents, firstName, photo, photoUrl } = cast;
|
|
20
|
+
const rows: { key: string; value: string | string[] }[] = Object.entries(now).map(([key, value]) => ({
|
|
21
|
+
key,
|
|
22
|
+
value: typeof value === 'string' && /^\d{4}-\d{2}$/.test(value) ? formatMonth(value) : value,
|
|
23
|
+
}));
|
|
24
|
+
const hasPortrait = photo !== undefined || photoUrl !== undefined;
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<section id="about" class="relative py-24 md:py-36" aria-labelledby="about-title">
|
|
28
|
+
<div class="container-x">
|
|
29
|
+
<SectionHeader host={cast.host} index={index} kind="view" label={label} title={title} id="about-title" />
|
|
30
|
+
|
|
31
|
+
<div class="grid grid-cols-1 gap-6 lg:grid-cols-12">
|
|
32
|
+
{hasPortrait && <figure class="avatar group panel overflow-hidden p-2 lg:col-span-4" data-reveal>
|
|
33
|
+
<div class="relative aspect-square overflow-hidden rounded-[0.9rem]">
|
|
34
|
+
{photo ? <Picture
|
|
35
|
+
src={photo}
|
|
36
|
+
alt={`Portrait of ${person.name}`}
|
|
37
|
+
widths={[320, 500]}
|
|
38
|
+
sizes="(min-width: 1024px) 380px, 90vw"
|
|
39
|
+
formats={['avif', 'webp']}
|
|
40
|
+
loading="lazy"
|
|
41
|
+
class="size-full object-cover"
|
|
42
|
+
/> : <img src={photoUrl} alt={`Portrait of ${person.name}`} width="460" height="460" loading="lazy" class="size-full object-cover" />}
|
|
43
|
+
<div class="tint pointer-events-none absolute inset-0" aria-hidden="true"></div>
|
|
44
|
+
<div class="pointer-events-none absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-ink-950/80 to-transparent" aria-hidden="true"></div>
|
|
45
|
+
</div>
|
|
46
|
+
<figcaption class="flex items-center justify-between px-2 pt-3 pb-1 font-mono text-xs text-fg-muted">
|
|
47
|
+
<span>{person.fullName ?? person.name}</span>
|
|
48
|
+
<span class="text-fg-faint">{person.location}</span>
|
|
49
|
+
</figcaption>
|
|
50
|
+
</figure>}
|
|
51
|
+
|
|
52
|
+
{rows.length > 0 && <div class:list={['panel overflow-hidden', hasPortrait ? 'lg:col-span-8' : 'lg:col-span-12']} data-reveal style="--reveal-delay:120ms">
|
|
53
|
+
<div class="flex items-center justify-between gap-4 border-b border-line px-5 py-3.5 md:px-7">
|
|
54
|
+
<p class="font-mono text-sm">
|
|
55
|
+
<span class="text-fg-faint">Projection:</span> <span class="text-view">{firstName}.Now</span>
|
|
56
|
+
</p>
|
|
57
|
+
<p class="flex items-center gap-2 font-mono text-xs text-fg-muted">
|
|
58
|
+
<span class="size-1.5 animate-pulse-dot rounded-full bg-view" aria-hidden="true"></span>
|
|
59
|
+
projected from {careerEvents.length} events
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
<dl class="divide-y divide-line">
|
|
63
|
+
{
|
|
64
|
+
rows.map((r) => (
|
|
65
|
+
<div class="grid gap-1 px-5 py-4 sm:grid-cols-[9rem_1fr] sm:gap-6 md:px-7">
|
|
66
|
+
<dt class="font-mono text-xs leading-6 text-fg-faint">{r.key}</dt>
|
|
67
|
+
<dd class="text-[0.975rem] leading-6 text-fg">
|
|
68
|
+
{Array.isArray(r.value) ? (
|
|
69
|
+
<ul class="flex flex-wrap gap-2" aria-label={r.key}>
|
|
70
|
+
{r.value.map((v) => (
|
|
71
|
+
<li class="tag border-view/25 bg-view/5 text-view-soft">{v}</li>
|
|
72
|
+
))}
|
|
73
|
+
</ul>
|
|
74
|
+
) : (
|
|
75
|
+
<Fragment><Inline text={r.value} host={cast.host} /></Fragment>
|
|
76
|
+
)}
|
|
77
|
+
</dd>
|
|
78
|
+
</div>
|
|
79
|
+
))
|
|
80
|
+
}
|
|
81
|
+
</dl>
|
|
82
|
+
</div>}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{(person.about.length > 0 || person.interests.length > 0) && <div class="mt-16 grid grid-cols-1 gap-10 md:mt-24 md:grid-cols-12">
|
|
86
|
+
{person.about.length > 0 && <h3 class="eyebrow md:col-span-3" data-reveal>About</h3>}
|
|
87
|
+
{person.about.length > 0 && <div class="text-lg leading-relaxed text-fg-muted md:col-span-9 md:columns-2 md:gap-10 [&>p]:mb-6 [&>p]:break-inside-avoid" data-reveal>
|
|
88
|
+
{person.about.map((p, i) => <p class:list={[i === 0 && 'text-fg']}><Inline text={p} host={cast.host} /></p>)}
|
|
89
|
+
</div>}
|
|
90
|
+
{person.interests.length > 0 && <h3 class="eyebrow md:col-span-3" data-reveal>Interests</h3>}
|
|
91
|
+
{person.interests.length > 0 && <ul class="flex flex-wrap gap-2 md:col-span-9" data-reveal>
|
|
92
|
+
{person.interests.map((i) => <li class="tag">{i}</li>)}
|
|
93
|
+
</ul>}
|
|
94
|
+
</div>}
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<style>
|
|
99
|
+
.avatar :global(img) {
|
|
100
|
+
filter: grayscale(1) contrast(1.08) brightness(0.95);
|
|
101
|
+
transition: filter 900ms var(--ease-out-expo), transform 900ms var(--ease-out-expo);
|
|
102
|
+
}
|
|
103
|
+
.tint {
|
|
104
|
+
background: linear-gradient(160deg, var(--color-accent) 0%, #2dd4bf 100%);
|
|
105
|
+
mix-blend-mode: color;
|
|
106
|
+
opacity: 0.55;
|
|
107
|
+
transition: opacity 900ms var(--ease-out-expo);
|
|
108
|
+
}
|
|
109
|
+
.avatar:hover :global(img) {
|
|
110
|
+
filter: none;
|
|
111
|
+
transform: scale(1.03);
|
|
112
|
+
}
|
|
113
|
+
.avatar:hover .tint {
|
|
114
|
+
opacity: 0;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -0,0 +1,185 @@
|
|
|
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
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
cast: CastView;
|
|
11
|
+
index: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
intro?: string;
|
|
15
|
+
}
|
|
16
|
+
const { index, title = 'Things I build *after hours.*', label = 'Side projects', intro } = Astro.props;
|
|
17
|
+
const { cast } = Astro.props;
|
|
18
|
+
const { config, liveProjects: projects } = cast;
|
|
19
|
+
// Categories in cast.yaml order, then any others in first-use order.
|
|
20
|
+
const used = [...new Set(projects.map((p) => p.category))];
|
|
21
|
+
const declared = config.projectCategories.filter((c) => used.includes(c.id));
|
|
22
|
+
const categories = [...declared, ...used.filter((id) => !declared.some((c) => c.id === id)).map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1), filter: undefined }))];
|
|
23
|
+
const filters = categories.length > 1 ? [{ id: 'all', label: 'All' }, ...categories.map((c) => ({ id: c.id, label: c.filter ?? c.label }))] : [];
|
|
24
|
+
const countFor = (id: string) => (id === 'all' ? projects.length : projects.filter((p) => p.category === id).length);
|
|
25
|
+
const categoryLabel: Record<string, string> = Object.fromEntries(categories.map((c) => [c.id, c.label]));
|
|
26
|
+
/** Lets a command wrap only after spaces and slashes, with no added visible characters. */
|
|
27
|
+
const breakable = (cmd: string) => cmd.split(/(?<=[/ ])/);
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<section id="projects" class="relative py-24 md:py-36" aria-labelledby="projects-title">
|
|
31
|
+
<div class="container-x">
|
|
32
|
+
<SectionHeader host={cast.host} index={index} label={label} title={title} id="projects-title">
|
|
33
|
+
{intro && <p class="mt-6 max-w-[62ch] text-lg leading-relaxed text-fg-muted"><Inline text={intro} host={cast.host} /></p>}
|
|
34
|
+
</SectionHeader>
|
|
35
|
+
|
|
36
|
+
<div class="mb-8 flex flex-wrap gap-2" role="group" aria-label="Filter projects by category" data-filters hidden>
|
|
37
|
+
{
|
|
38
|
+
filters.map((f, i) => (
|
|
39
|
+
<button type="button" class="filter" data-filter={f.id} aria-pressed={i === 0 ? 'true' : 'false'}>
|
|
40
|
+
{f.label}
|
|
41
|
+
<span class="font-mono text-[0.6875rem] opacity-60">{countFor(f.id)}</span>
|
|
42
|
+
</button>
|
|
43
|
+
))
|
|
44
|
+
}
|
|
45
|
+
</div>
|
|
46
|
+
<p class="sr-only" aria-live="polite" data-filter-status></p>
|
|
47
|
+
|
|
48
|
+
<ul class="bento grid grid-cols-1 gap-4 md:grid-cols-3" data-bento>
|
|
49
|
+
{
|
|
50
|
+
projects.map((p, i) => (
|
|
51
|
+
<li class:list={['proj', p.featured && 'md:col-span-2']} data-category={p.category} data-reveal style={`--reveal-delay:${(i % 3) * 70}ms`}>
|
|
52
|
+
<article class="panel glow flex h-full flex-col p-6 md:p-7">
|
|
53
|
+
<header class="flex items-start justify-between gap-4">
|
|
54
|
+
<div>
|
|
55
|
+
<p class="font-mono text-[0.6875rem] tracking-wider text-fg-faint uppercase">{categoryLabel[p.category]}</p>
|
|
56
|
+
<h3 class="mt-2 flex items-center gap-3 font-display text-[1.75rem] leading-none font-[650] tracking-[-0.03em]">
|
|
57
|
+
<a href={p.url} rel={cast.host.rel} class="hover:text-event-soft">
|
|
58
|
+
{p.name}
|
|
59
|
+
</a>
|
|
60
|
+
{p.version && (
|
|
61
|
+
<span class="rounded-md border border-event/30 bg-event/10 px-1.5 py-0.5 font-mono text-[0.6875rem] font-medium tracking-normal text-event-soft">
|
|
62
|
+
{p.version}
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</h3>
|
|
66
|
+
</div>
|
|
67
|
+
<a href={p.url} rel={cast.host.rel} class="grid size-9 shrink-0 place-items-center rounded-lg border border-line text-fg-muted transition-colors hover:border-line-strong hover:text-fg" aria-label={`${p.name} on GitHub`}>
|
|
68
|
+
<svg viewBox="0 0 16 16" class="size-4" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M5 11 11 5M6 5h5v5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
69
|
+
</a>
|
|
70
|
+
</header>
|
|
71
|
+
|
|
72
|
+
<p class="mt-4 text-[1.0625rem] leading-relaxed text-fg">{p.pitch}</p>
|
|
73
|
+
|
|
74
|
+
<ul class:list={['mt-5 grid gap-2 text-sm text-fg-muted', p.featured && 'sm:grid-cols-2 sm:gap-x-6']}>
|
|
75
|
+
{p.features.map((f) => (
|
|
76
|
+
<li class="flex gap-2.5">
|
|
77
|
+
<span class="mt-[0.45rem] size-1.5 shrink-0 rounded-[2px] bg-view/80" aria-hidden="true" />
|
|
78
|
+
{f}
|
|
79
|
+
</li>
|
|
80
|
+
))}
|
|
81
|
+
</ul>
|
|
82
|
+
|
|
83
|
+
<div class="mt-auto pt-6">
|
|
84
|
+
{p.install && (
|
|
85
|
+
<div class="term mb-4 flex items-start gap-2.5 rounded-xl border border-line bg-ink-950/80 py-1.5 pr-1.5 pl-3.5 font-mono text-[0.8125rem] leading-5">
|
|
86
|
+
<span class="py-[0.3rem] text-view select-none" aria-hidden="true">$</span>
|
|
87
|
+
<code class="min-w-0 flex-1 py-[0.3rem] [overflow-wrap:anywhere] text-fg">
|
|
88
|
+
{breakable(p.install).map((part, i) => <>{i > 0 && <wbr />}<span class="whitespace-nowrap">{part}</span></>)}
|
|
89
|
+
</code>
|
|
90
|
+
<button type="button" class="copy" data-copy={p.install} aria-label={`Copy install command for ${p.name}`}>
|
|
91
|
+
<svg viewBox="0 0 16 16" class="ic-copy size-3.5" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="5" y="5" width="9" height="9" rx="2" /><path d="M11 5V3.5A1.5 1.5 0 0 0 9.5 2h-6A1.5 1.5 0 0 0 2 3.5v6A1.5 1.5 0 0 0 3.5 11H5" /></svg>
|
|
92
|
+
<svg viewBox="0 0 16 16" class="ic-ok size-3.5" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m3 8.5 3 3 7-7" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
93
|
+
<span data-copy-label>Copy</span>
|
|
94
|
+
</button>
|
|
95
|
+
<span class="sr-only" aria-live="polite" data-copy-status />
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
<ul class="flex flex-wrap gap-1.5" aria-label="Stack">
|
|
99
|
+
{p.stack.map((s) => (
|
|
100
|
+
<li class="rounded-md bg-fg/[0.04] px-2 py-0.5 font-mono text-[0.6875rem] text-fg-muted">{s}</li>
|
|
101
|
+
))}
|
|
102
|
+
</ul>
|
|
103
|
+
</div>
|
|
104
|
+
</article>
|
|
105
|
+
</li>
|
|
106
|
+
))
|
|
107
|
+
}
|
|
108
|
+
</ul>
|
|
109
|
+
</div>
|
|
110
|
+
</section>
|
|
111
|
+
|
|
112
|
+
<style>
|
|
113
|
+
.bento[data-filtered] .proj {
|
|
114
|
+
grid-column: auto;
|
|
115
|
+
}
|
|
116
|
+
.copy {
|
|
117
|
+
display: inline-flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: 0.4rem;
|
|
120
|
+
height: 1.9rem;
|
|
121
|
+
padding-inline: 0.6rem;
|
|
122
|
+
border-radius: 0.55rem;
|
|
123
|
+
font-size: 0.75rem;
|
|
124
|
+
color: var(--color-fg-muted);
|
|
125
|
+
border: 1px solid var(--color-line);
|
|
126
|
+
background: color-mix(in srgb, var(--color-fg) 3%, transparent);
|
|
127
|
+
transition: all 160ms ease;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
flex-shrink: 0;
|
|
130
|
+
}
|
|
131
|
+
.copy:hover {
|
|
132
|
+
color: var(--color-fg);
|
|
133
|
+
border-color: var(--color-line-strong);
|
|
134
|
+
}
|
|
135
|
+
.copy .ic-ok,
|
|
136
|
+
.copy[data-copied='true'] .ic-copy {
|
|
137
|
+
display: none;
|
|
138
|
+
}
|
|
139
|
+
.copy[data-copied='true'] {
|
|
140
|
+
color: var(--color-view);
|
|
141
|
+
border-color: rgb(45 212 191 / 0.4);
|
|
142
|
+
background: rgb(45 212 191 / 0.08);
|
|
143
|
+
}
|
|
144
|
+
.copy[data-copied='true'] .ic-ok {
|
|
145
|
+
display: block;
|
|
146
|
+
}
|
|
147
|
+
.proj[hidden] {
|
|
148
|
+
display: none;
|
|
149
|
+
}
|
|
150
|
+
:global(.js-motion) .proj.enter {
|
|
151
|
+
animation: var(--animate-pop-in);
|
|
152
|
+
}
|
|
153
|
+
</style>
|
|
154
|
+
|
|
155
|
+
<script>
|
|
156
|
+
import { onPageLoad } from '../lib/lifecycle';
|
|
157
|
+
onPageLoad(() => {
|
|
158
|
+
const bar = document.querySelector<HTMLElement>('[data-filters]');
|
|
159
|
+
const grid = document.querySelector<HTMLElement>('[data-bento]');
|
|
160
|
+
const status = document.querySelector<HTMLElement>('[data-filter-status]');
|
|
161
|
+
if (bar && grid) {
|
|
162
|
+
bar.hidden = false;
|
|
163
|
+
const cards = [...grid.querySelectorAll<HTMLElement>('.proj')];
|
|
164
|
+
bar.addEventListener('click', (e) => {
|
|
165
|
+
const btn = (e.target as Element).closest<HTMLButtonElement>('[data-filter]');
|
|
166
|
+
if (!btn) return;
|
|
167
|
+
const id = btn.dataset['filter'];
|
|
168
|
+
bar.querySelectorAll('[data-filter]').forEach((b) => b.setAttribute('aria-pressed', String(b === btn)));
|
|
169
|
+
grid.toggleAttribute('data-filtered', id !== 'all');
|
|
170
|
+
let shown = 0;
|
|
171
|
+
for (const card of cards) {
|
|
172
|
+
const match = id === 'all' || card.dataset['category'] === id;
|
|
173
|
+
card.hidden = !match;
|
|
174
|
+
card.classList.remove('enter');
|
|
175
|
+
if (match) {
|
|
176
|
+
shown++;
|
|
177
|
+
void card.offsetWidth;
|
|
178
|
+
card.classList.add('enter', 'is-visible');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (status) status.textContent = `Showing ${shown} project${shown === 1 ? '' : 's'}`;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
</script>
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
// The live feed of merged public pull requests: rendered from the build-time snapshot, refreshed in the browser.
|
|
6
|
+
import type { CastView } from '../profile/CastView';
|
|
7
|
+
import { relativeTime } from '../lib/reltime';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
cast: CastView;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
const { class: className = '' } = Astro.props;
|
|
14
|
+
const { cast } = Astro.props;
|
|
15
|
+
const { config, github, snapshotDate } = cast;
|
|
16
|
+
// The section shows the newest eight; the terminal's `stream tail` gets the full snapshot.
|
|
17
|
+
const feed = github.mergedPullRequests.latest.slice(0, 8);
|
|
18
|
+
const builtAt = github.fetchedAt ? Date.parse(github.fetchedAt) : Date.now();
|
|
19
|
+
const orgs = config.github.orgs.join(', ');
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<div class:list={['panel overflow-hidden', className]} data-reveal data-feed>
|
|
23
|
+
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-line px-5 py-3.5">
|
|
24
|
+
<h3 class="flex items-center gap-2.5 font-mono text-sm">
|
|
25
|
+
<span class="size-1.5 animate-pulse-dot rounded-full bg-view" aria-hidden="true"></span>
|
|
26
|
+
<span class="text-fg-faint">tail -f</span> <span class="text-fg">Tailing the stream</span>
|
|
27
|
+
</h3>
|
|
28
|
+
<p data-pr-status class="font-mono text-xs text-fg-faint">as of <time datetime={github.fetchedAt}>{snapshotDate()}</time></p>
|
|
29
|
+
</div>
|
|
30
|
+
<template data-pr-template>
|
|
31
|
+
<li class="feed-row">
|
|
32
|
+
<a rel={cast.host.rel} class="group grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 px-5 py-3.5 transition-colors hover:bg-fg/[0.025] sm:grid-cols-[4.5rem_1fr_auto]">
|
|
33
|
+
<span data-pr-head class="font-mono text-xs leading-6 text-fg-faint tabular-nums"></span>
|
|
34
|
+
<span class="min-w-0">
|
|
35
|
+
<span class="flex flex-wrap items-center gap-x-2.5 gap-y-1 font-mono text-xs">
|
|
36
|
+
<span class="text-event">PullRequestMerged</span>
|
|
37
|
+
<span data-pr-repo class="rounded-md border border-command/25 bg-command/8 px-1.5 py-px text-command-soft"></span>
|
|
38
|
+
<span data-pr-number class="text-fg-faint"></span>
|
|
39
|
+
</span>
|
|
40
|
+
<span data-pr-title class="mt-1 block text-[0.9375rem] leading-snug text-fg group-hover:underline"></span>
|
|
41
|
+
</span>
|
|
42
|
+
<time data-rel class="col-start-2 font-mono text-xs text-fg-faint sm:col-start-3 sm:leading-6"></time>
|
|
43
|
+
</a>
|
|
44
|
+
</li>
|
|
45
|
+
</template>
|
|
46
|
+
{
|
|
47
|
+
feed.length ? (
|
|
48
|
+
<ol data-pr-feed class="divide-y divide-line" aria-label={`Latest merged pull requests in ${orgs}`}>
|
|
49
|
+
{feed.map((pr, i) => (
|
|
50
|
+
<li class="feed-row" style={`--i:${i}`}>
|
|
51
|
+
<a href={pr.url} rel={cast.host.rel} class="group grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 px-5 py-3.5 transition-colors hover:bg-fg/[0.025] sm:grid-cols-[4.5rem_1fr_auto]">
|
|
52
|
+
<span class="font-mono text-xs leading-6 text-fg-faint tabular-nums">{i === 0 ? 'HEAD' : `HEAD~${i}`}</span>
|
|
53
|
+
<span class="min-w-0">
|
|
54
|
+
<span class="flex flex-wrap items-center gap-x-2.5 gap-y-1 font-mono text-xs">
|
|
55
|
+
<span class="text-event">PullRequestMerged</span>
|
|
56
|
+
<span class="rounded-md border border-command/25 bg-command/8 px-1.5 py-px text-command-soft">{pr.repository}</span>
|
|
57
|
+
<span class="text-fg-faint">#{pr.number}</span>
|
|
58
|
+
</span>
|
|
59
|
+
<span class="mt-1 block text-[0.9375rem] leading-snug text-fg group-hover:underline">{pr.title}</span>
|
|
60
|
+
</span>
|
|
61
|
+
<time datetime={pr.mergedAt} data-rel class="col-start-2 font-mono text-xs text-fg-faint sm:col-start-3 sm:leading-6">{relativeTime(pr.mergedAt, builtAt)}</time>
|
|
62
|
+
</a>
|
|
63
|
+
</li>
|
|
64
|
+
))}
|
|
65
|
+
</ol>
|
|
66
|
+
) : (
|
|
67
|
+
<ol data-pr-feed class="divide-y divide-line" aria-label={`Latest merged pull requests in ${orgs}`}>
|
|
68
|
+
<li class="px-5 py-8 font-mono text-sm text-fg-faint">EventNotFound: the snapshot has no merged pull requests yet.</li>
|
|
69
|
+
</ol>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
<p class="border-t border-line px-5 py-3 font-mono text-[0.6875rem] text-fg-faint">
|
|
73
|
+
Public repositories only, newest first. Refreshed hourly and on page load.
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<style>
|
|
78
|
+
:global(.js-motion) [data-feed].is-visible :global(.feed-row) {
|
|
79
|
+
animation: feed-in 700ms var(--ease-out-expo) both;
|
|
80
|
+
animation-delay: calc(var(--i) * 45ms + 200ms);
|
|
81
|
+
}
|
|
82
|
+
@keyframes feed-in {
|
|
83
|
+
from {
|
|
84
|
+
opacity: 0;
|
|
85
|
+
transform: translateY(-6px);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { CastHost } from '../profile/CastHost';
|
|
6
|
+
import Inline from './Inline.astro';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
index: string;
|
|
10
|
+
kind?: 'event' | 'command' | 'view';
|
|
11
|
+
label: string;
|
|
12
|
+
/** May use *emphasis* for the italic serif accent. */
|
|
13
|
+
title: string;
|
|
14
|
+
id?: string;
|
|
15
|
+
/** Where the profile is rendered, for links in the title. */
|
|
16
|
+
host?: CastHost;
|
|
17
|
+
}
|
|
18
|
+
const { index, kind = 'event', label, title, id, host } = Astro.props;
|
|
19
|
+
const noteClass = kind === 'command' ? 'note note-command' : kind === 'view' ? 'note note-view' : 'note';
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<header class="mb-12 md:mb-16" data-reveal>
|
|
23
|
+
<p class="mb-5 flex items-center gap-3 font-mono text-xs text-fg-faint">
|
|
24
|
+
{index && <span class="text-fg-muted">{index}</span>}
|
|
25
|
+
{index && <span class="h-px w-8 bg-line-strong" aria-hidden="true"></span>}
|
|
26
|
+
<span class={noteClass}>{label}</span>
|
|
27
|
+
</p>
|
|
28
|
+
<h2 id={id} class="section-title max-w-[22ch]"><Inline text={title} host={host} /></h2>
|
|
29
|
+
<slot />
|
|
30
|
+
</header>
|
|
@@ -0,0 +1,44 @@
|
|
|
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 SectionHeader from './SectionHeader.astro';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
cast: CastView;
|
|
10
|
+
index: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Sits directly under the talks section, so it needs less top padding there. */
|
|
14
|
+
compact?: boolean;
|
|
15
|
+
}
|
|
16
|
+
const { index, title = 'The *toolbox.*', label = 'Read model', compact = false } = Astro.props;
|
|
17
|
+
const { cast } = Astro.props;
|
|
18
|
+
const { skills } = cast;
|
|
19
|
+
const groups = Object.entries(skills);
|
|
20
|
+
const accents = ['bg-event', 'bg-command', 'bg-view', 'bg-accent'];
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<section id="skills" class:list={['relative pb-24 md:pb-36', compact ? '' : 'pt-24 md:pt-32']} aria-labelledby="skills-title">
|
|
24
|
+
<div class="container-x">
|
|
25
|
+
<SectionHeader host={cast.host} index={index} kind="view" label={label} title={title} id="skills-title" />
|
|
26
|
+
<div class="grid grid-cols-1 gap-px overflow-hidden rounded-[1.25rem] border border-line bg-line sm:grid-cols-2 lg:grid-cols-3">
|
|
27
|
+
{
|
|
28
|
+
groups.map(([group, list], i) => (
|
|
29
|
+
<div class="bg-ink-950 p-6 md:p-7" data-reveal style={`--reveal-delay:${(i % 3) * 60}ms`}>
|
|
30
|
+
<h3 class="mb-4 flex items-center gap-2.5 font-mono text-xs tracking-wider text-fg-muted uppercase">
|
|
31
|
+
<span class={`size-2 rounded-[2px] ${accents[i % accents.length]}`} aria-hidden="true" />
|
|
32
|
+
{group}
|
|
33
|
+
</h3>
|
|
34
|
+
<ul class="flex flex-wrap gap-1.5">
|
|
35
|
+
{list.map((s) => (
|
|
36
|
+
<li class="tag">{s}</li>
|
|
37
|
+
))}
|
|
38
|
+
</ul>
|
|
39
|
+
</div>
|
|
40
|
+
))
|
|
41
|
+
}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</section>
|