@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,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
3
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
|
+
|
|
5
|
+
import type { CastView } from '../profile/CastView';
|
|
6
|
+
import Inline from './Inline.astro';
|
|
7
|
+
import SectionHeader from './SectionHeader.astro';
|
|
8
|
+
import PullRequestFeed from './PullRequestFeed.astro';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
cast: CastView;
|
|
12
|
+
index: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
intro?: string;
|
|
16
|
+
}
|
|
17
|
+
const { index, title = 'Every contribution, *appended.*', label = 'Event store', intro } = Astro.props;
|
|
18
|
+
const { cast } = Astro.props;
|
|
19
|
+
const { cratis, github, snapshotDate } = cast;
|
|
20
|
+
import { foldCalendar, formatDay, formatMonthLong } from '../lib/contributions';
|
|
21
|
+
|
|
22
|
+
const fmt = new Intl.NumberFormat('en-US');
|
|
23
|
+
const cal = foldCalendar(github.contributions.weeks);
|
|
24
|
+
const total = github.contributions.total;
|
|
25
|
+
const pad = cal.days[0]?.weekday ?? 0;
|
|
26
|
+
|
|
27
|
+
// Month labels sit above the first week that contains the 1st of that month.
|
|
28
|
+
const monthLabels: { week: number; label: string }[] = [];
|
|
29
|
+
for (const d of cal.days) {
|
|
30
|
+
if (d.date.endsWith('-01') && d.week > 0) {
|
|
31
|
+
monthLabels.push({ week: d.week, label: new Date(`${d.date}T00:00:00Z`).toLocaleDateString('en-US', { month: 'short', timeZone: 'UTC' }) });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const facts = [
|
|
36
|
+
{ key: 'activeDays', value: `${cal.activeDays} / ${cal.days.length}` },
|
|
37
|
+
{ key: 'longestStreak', value: `${cal.longest} days` },
|
|
38
|
+
{ key: 'currentStreak', value: `${cal.current} days` },
|
|
39
|
+
{ key: 'busiestDay', value: `${fmt.format(cal.busiest.count)} · ${formatDay(cal.busiest.date)}` },
|
|
40
|
+
];
|
|
41
|
+
const summary = `Contribution heatmap: ${fmt.format(total)} contributions between ${formatDay(cal.first)} and ${formatDay(cal.last)}, on ${cal.activeDays} of ${cal.days.length} days. Longest streak ${cal.longest} days. Busiest day ${formatDay(cal.busiest.date)} with ${fmt.format(cal.busiest.count)}.`;
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
<section id="activity" class="relative py-24 md:py-36" aria-labelledby="activity-title">
|
|
45
|
+
<div class="container-x">
|
|
46
|
+
<SectionHeader host={cast.host} index={index} label={label} title={title} id="activity-title">
|
|
47
|
+
<p class="mt-6 max-w-[60ch] text-lg leading-relaxed text-fg-muted">
|
|
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.
|
|
49
|
+
Captured when the site was built.</>}
|
|
50
|
+
</p>
|
|
51
|
+
</SectionHeader>
|
|
52
|
+
|
|
53
|
+
{cal.days.length === 0 ? (
|
|
54
|
+
<div class="panel p-8 font-mono text-sm text-fg-faint" data-reveal>EventNotFound: no contribution calendar has been captured yet. It appears after the first build with a GitHub token.</div>
|
|
55
|
+
) : <div class="panel overflow-hidden" data-reveal data-heatmap>
|
|
56
|
+
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-line px-5 py-3.5 md:px-7">
|
|
57
|
+
<p class="font-mono text-sm">
|
|
58
|
+
<span class="text-fg-faint">EventStore:</span> <span class="text-event">github.com/{github.login}</span>
|
|
59
|
+
<span class="text-fg-faint">· partitioned by day</span>
|
|
60
|
+
</p>
|
|
61
|
+
<p class="font-mono text-xs text-fg-faint">snapshot taken <time datetime={github.fetchedAt}>{snapshotDate()}</time></p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="grid gap-8 p-5 md:p-7 xl:grid-cols-[1fr_15rem]">
|
|
65
|
+
<div class="min-w-0">
|
|
66
|
+
<p class="mb-6 flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
67
|
+
<span class="font-display text-[clamp(2.5rem,5vw,3.75rem)] leading-none font-[650] tracking-[-0.04em] text-event tabular-nums">{fmt.format(total)}</span>
|
|
68
|
+
<span class="text-fg-muted">events appended in the last year</span>
|
|
69
|
+
</p>
|
|
70
|
+
|
|
71
|
+
<div class="scroller -mx-1 overflow-x-auto px-1 pb-2" data-heatmap-scroll>
|
|
72
|
+
<div class="relative min-w-[40rem] pl-8" aria-hidden="true">
|
|
73
|
+
<div class="relative mb-1.5 h-4 font-mono text-[0.625rem] text-fg-faint">
|
|
74
|
+
{monthLabels.map((m) => <span class="absolute" style={`left: calc(${(m.week / cal.weekCount) * 100}%)`}>{m.label}</span>)}
|
|
75
|
+
</div>
|
|
76
|
+
<div class="absolute top-[1.375rem] left-0 grid h-[calc(100%-1.375rem)] grid-rows-7 font-mono text-[0.625rem] leading-none text-fg-faint">
|
|
77
|
+
{['', 'Mon', '', 'Wed', '', 'Fri', ''].map((d) => <span class="flex items-center">{d}</span>)}
|
|
78
|
+
</div>
|
|
79
|
+
<div class="hm" style={`--weeks:${cal.weekCount}`} data-heatmap-grid>
|
|
80
|
+
{Array.from({ length: pad }, () => <span class="pad"></span>)}
|
|
81
|
+
{cal.days.map((d, i) => <span class="c" data-l={d.level} data-d={d.date} data-n={d.count} style={`--w:${d.week}`} data-i={i}></span>)}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="mt-4 flex flex-wrap items-center justify-between gap-4 font-mono text-[0.6875rem] text-fg-faint">
|
|
87
|
+
<span
|
|
88
|
+
class="focus-hint rounded-md"
|
|
89
|
+
tabindex="0"
|
|
90
|
+
role="group"
|
|
91
|
+
aria-label={`${summary} Use the arrow keys to read individual days.`}
|
|
92
|
+
aria-describedby="activity-table-caption"
|
|
93
|
+
data-heatmap-keys
|
|
94
|
+
>
|
|
95
|
+
<span aria-hidden="true">Focus here and use <kbd class="kbd">←</kbd><kbd class="kbd">→</kbd><kbd class="kbd">↑</kbd><kbd class="kbd">↓</kbd> to read days</span>
|
|
96
|
+
</span>
|
|
97
|
+
<span class="flex items-center gap-1.5" aria-hidden="true">
|
|
98
|
+
less
|
|
99
|
+
{[0, 1, 2, 3, 4].map((l) => <span class="c legend" data-l={l}></span>)}
|
|
100
|
+
more
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
<p class="sr-only" aria-live="polite" data-heatmap-live></p>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<dl class="grid grid-cols-2 content-start gap-px overflow-hidden rounded-xl border border-line bg-line xl:grid-cols-1">
|
|
107
|
+
{
|
|
108
|
+
facts.map((f) => (
|
|
109
|
+
<div class="bg-ink-900 px-4 py-3">
|
|
110
|
+
<dt class="font-mono text-[0.6875rem] text-fg-faint">{f.key}</dt>
|
|
111
|
+
<dd class="mt-1 text-sm text-fg tabular-nums">{f.value}</dd>
|
|
112
|
+
</div>
|
|
113
|
+
))
|
|
114
|
+
}
|
|
115
|
+
</dl>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<table class="sr-only">
|
|
119
|
+
<caption id="activity-table-caption">Contributions per month</caption>
|
|
120
|
+
<thead><tr><th scope="col">Month</th><th scope="col">Contributions</th></tr></thead>
|
|
121
|
+
<tbody>
|
|
122
|
+
{cal.months.map((m) => <tr><th scope="row">{formatMonthLong(m.month)}</th><td>{fmt.format(m.total)}</td></tr>)}
|
|
123
|
+
</tbody>
|
|
124
|
+
</table>
|
|
125
|
+
</div>}
|
|
126
|
+
{!cratis && <PullRequestFeed cast={cast} class="mt-6" />}
|
|
127
|
+
</div>
|
|
128
|
+
<div class="hm-tip" role="presentation" data-heatmap-tip hidden></div>
|
|
129
|
+
</section>
|
|
130
|
+
|
|
131
|
+
<style>
|
|
132
|
+
.hm {
|
|
133
|
+
display: grid;
|
|
134
|
+
grid-template-rows: repeat(7, 1fr);
|
|
135
|
+
grid-template-columns: repeat(var(--weeks), 1fr);
|
|
136
|
+
grid-auto-flow: column;
|
|
137
|
+
gap: 3px;
|
|
138
|
+
}
|
|
139
|
+
.c {
|
|
140
|
+
aspect-ratio: 1;
|
|
141
|
+
border-radius: 2.5px;
|
|
142
|
+
background: var(--hm-0);
|
|
143
|
+
transition: background-color 500ms var(--ease-out-expo), box-shadow 200ms ease;
|
|
144
|
+
}
|
|
145
|
+
.c.legend {
|
|
146
|
+
width: 0.7rem;
|
|
147
|
+
}
|
|
148
|
+
.c[data-l='1'] { background: var(--hm-1); }
|
|
149
|
+
.c[data-l='2'] { background: var(--hm-2); }
|
|
150
|
+
.c[data-l='3'] { background: var(--hm-3); }
|
|
151
|
+
.c[data-l='4'] { background: var(--hm-4); box-shadow: 0 0 8px color-mix(in oklab, var(--color-event) 35%, transparent); }
|
|
152
|
+
.c[data-on] {
|
|
153
|
+
outline: 1.5px solid var(--color-fg);
|
|
154
|
+
outline-offset: 1px;
|
|
155
|
+
}
|
|
156
|
+
[data-heatmap] {
|
|
157
|
+
--hm-0: color-mix(in oklab, var(--color-fg) 6%, transparent);
|
|
158
|
+
--hm-1: color-mix(in oklab, var(--color-event) 28%, transparent);
|
|
159
|
+
--hm-2: color-mix(in oklab, var(--color-event) 50%, transparent);
|
|
160
|
+
--hm-3: color-mix(in oklab, var(--color-event) 75%, transparent);
|
|
161
|
+
--hm-4: var(--color-event);
|
|
162
|
+
}
|
|
163
|
+
/* Partitions fill in left to right on first view. */
|
|
164
|
+
:global(.js-motion) [data-heatmap]:not(.filled) .hm .c {
|
|
165
|
+
background: var(--hm-0);
|
|
166
|
+
box-shadow: none;
|
|
167
|
+
}
|
|
168
|
+
:global(.js-motion) [data-heatmap].filled .hm .c {
|
|
169
|
+
transition-delay: calc(var(--w) * 14ms);
|
|
170
|
+
}
|
|
171
|
+
.hm-tip {
|
|
172
|
+
position: fixed;
|
|
173
|
+
z-index: 70;
|
|
174
|
+
pointer-events: none;
|
|
175
|
+
transform: translate(-50%, calc(-100% - 10px));
|
|
176
|
+
padding: 0.35rem 0.6rem;
|
|
177
|
+
border-radius: 0.5rem;
|
|
178
|
+
border: 1px solid var(--color-line-strong);
|
|
179
|
+
background: var(--color-ink-800);
|
|
180
|
+
color: var(--color-fg);
|
|
181
|
+
font-family: var(--font-mono);
|
|
182
|
+
font-size: 0.6875rem;
|
|
183
|
+
white-space: nowrap;
|
|
184
|
+
box-shadow: 0 10px 30px -10px rgb(0 0 0 / 0.5);
|
|
185
|
+
}
|
|
186
|
+
.scroller {
|
|
187
|
+
scrollbar-width: thin;
|
|
188
|
+
scrollbar-color: var(--color-line-strong) transparent;
|
|
189
|
+
}
|
|
190
|
+
</style>
|
|
191
|
+
|
|
192
|
+
<script>
|
|
193
|
+
import { initHeatmap } from '../scripts/heatmap';
|
|
194
|
+
import { onPageLoad } from '../lib/lifecycle';
|
|
195
|
+
onPageLoad(initHeatmap);
|
|
196
|
+
</script>
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
}
|
|
15
|
+
const { cast } = Astro.props;
|
|
16
|
+
const { agentic: agenticData, config, github, postsFor, themes } = cast;
|
|
17
|
+
const agentic = agenticData!;
|
|
18
|
+
const { index, title = agentic.title, label = agentic.label } = Astro.props;
|
|
19
|
+
const fmt = new Intl.NumberFormat('en-US');
|
|
20
|
+
const aiTheme = themes.find((theme) => theme.id === config.writing.agenticTheme);
|
|
21
|
+
const aiPosts = postsFor(aiTheme?.id);
|
|
22
|
+
const columns = Math.max(1, Math.min(agentic.workflow.length, 6));
|
|
23
|
+
const kinds = ['command', 'event', 'view', 'event', 'command'] as const;
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<section id="agentic" class="relative overflow-hidden py-24 md:py-36" aria-labelledby="agentic-title">
|
|
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>
|
|
28
|
+
<div class="container-x relative">
|
|
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>
|
|
31
|
+
</SectionHeader>
|
|
32
|
+
|
|
33
|
+
<div class="panel overflow-hidden" data-reveal data-harness>
|
|
34
|
+
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-line px-5 py-3 md:px-7">
|
|
35
|
+
<p class="font-mono text-sm"><span class="text-fg-faint">Workflow:</span> <span class="text-view">{agentic.workflowLabel}</span></p>
|
|
36
|
+
<p class="font-mono text-xs text-fg-faint">
|
|
37
|
+
{github.contributions.total > 0 && <><span class="text-fg">{fmt.format(github.contributions.total)}</span> contributions</>}
|
|
38
|
+
{github.contributions.commits ? <> · <span class="text-fg">{fmt.format(github.contributions.commits)}</span> commits</> : null}{github.contributions.total > 0 && ' in the last year'}
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
{agentic.workflow.length > 0 && <ol class="harness relative grid gap-px bg-line" style={`--columns:${columns}`} aria-label="Agent workflow">
|
|
42
|
+
{
|
|
43
|
+
agentic.workflow.map((w, i) => (
|
|
44
|
+
<li class="stage relative bg-ink-900 p-5" style={`--i:${i}`}>
|
|
45
|
+
<span class="flex items-center justify-between font-mono text-[0.6875rem] text-fg-faint">
|
|
46
|
+
<span>{String(i + 1).padStart(2, '0')} · {w.who}</span>
|
|
47
|
+
</span>
|
|
48
|
+
<span class="mt-4 block font-display text-2xl font-[650] tracking-[-0.02em]">{w.step}</span>
|
|
49
|
+
<span class={`note mt-1 ${kinds[i] === 'command' ? 'note-command' : kinds[i] === 'view' ? 'note-view' : ''}`}>{w.event}</span>
|
|
50
|
+
<span class="mt-3 block text-sm leading-relaxed text-fg-muted"><Inline text={w.text} host={cast.host} /></span>
|
|
51
|
+
{w.loop && (
|
|
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">
|
|
53
|
+
↺ {w.loop}
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
</li>
|
|
57
|
+
))
|
|
58
|
+
}
|
|
59
|
+
<span class="pulse pointer-events-none absolute top-0 left-0 hidden h-px md:block" aria-hidden="true"></span>
|
|
60
|
+
</ol>}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<ul class="mt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
64
|
+
{
|
|
65
|
+
agentic.items.map((item, i) => {
|
|
66
|
+
const Tag = item.url ? 'a' : 'div';
|
|
67
|
+
return (
|
|
68
|
+
<li class:list={[item.list && 'md:col-span-2']} data-reveal style={`--reveal-delay:${(i % 3) * 60}ms`}>
|
|
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)">
|
|
70
|
+
<span class="note note-view">{item.kind}</span>
|
|
71
|
+
<span class="mt-4 font-display text-2xl font-[650] tracking-[-0.02em] transition-colors group-hover:text-view-soft">{item.name}</span>
|
|
72
|
+
<span class="mt-2 text-sm leading-relaxed text-fg-muted"><Inline text={item.text} host={cast.host} /></span>
|
|
73
|
+
{item.list && (
|
|
74
|
+
<span class="mt-5 block">
|
|
75
|
+
<span class="block font-mono text-[0.6875rem] tracking-[0.08em] text-fg-faint uppercase">{item.listLabel}</span>
|
|
76
|
+
<span class="mt-2 flex flex-wrap gap-1.5">
|
|
77
|
+
{item.list.map((x) => <span class="tag px-2 py-0.5 text-xs">{x}</span>)}
|
|
78
|
+
</span>
|
|
79
|
+
</span>
|
|
80
|
+
)}
|
|
81
|
+
</Tag>
|
|
82
|
+
</li>
|
|
83
|
+
);
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
{
|
|
87
|
+
aiPosts.length > 0 && (
|
|
88
|
+
<li class="md:col-span-2" data-reveal><div class="panel h-full p-5 md:px-7">
|
|
89
|
+
<p class="font-mono text-xs text-fg-faint">On the blog · {aiPosts.length} posts about {aiTheme?.label}</p>
|
|
90
|
+
<ul class="mt-3 grid gap-x-8 gap-y-2 md:grid-cols-2">
|
|
91
|
+
{aiPosts.map((p) => (
|
|
92
|
+
<li>
|
|
93
|
+
<a href={p.url} rel={cast.host.rel} class="group flex items-baseline gap-3 text-[0.9375rem]">
|
|
94
|
+
<span class="text-view" aria-hidden="true">→</span>
|
|
95
|
+
<span class="text-fg group-hover:underline">{p.headline}</span>
|
|
96
|
+
</a>
|
|
97
|
+
</li>
|
|
98
|
+
))}
|
|
99
|
+
</ul>
|
|
100
|
+
</div></li>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
</ul>
|
|
104
|
+
|
|
105
|
+
</div>
|
|
106
|
+
</section>
|
|
107
|
+
|
|
108
|
+
<style>
|
|
109
|
+
@media (min-width: 768px) {
|
|
110
|
+
.harness {
|
|
111
|
+
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
.pulse {
|
|
115
|
+
width: calc(100% / var(--columns, 5));
|
|
116
|
+
background: linear-gradient(90deg, transparent, var(--color-view), transparent);
|
|
117
|
+
opacity: 0;
|
|
118
|
+
}
|
|
119
|
+
:global(.js-motion) [data-harness].is-visible .pulse {
|
|
120
|
+
animation: harness 7.5s var(--ease-out-expo) infinite;
|
|
121
|
+
}
|
|
122
|
+
@keyframes harness {
|
|
123
|
+
0% { transform: translateX(0); opacity: 0; }
|
|
124
|
+
6% { opacity: 1; }
|
|
125
|
+
94% { opacity: 1; }
|
|
126
|
+
100% { transform: translateX(calc((var(--columns, 5) - 1) * 100%)); opacity: 0; }
|
|
127
|
+
}
|
|
128
|
+
.stage::before {
|
|
129
|
+
content: '';
|
|
130
|
+
position: absolute;
|
|
131
|
+
inset: 0;
|
|
132
|
+
background: color-mix(in oklab, var(--color-view) 6%, transparent);
|
|
133
|
+
opacity: 0;
|
|
134
|
+
pointer-events: none;
|
|
135
|
+
}
|
|
136
|
+
:global(.js-motion) [data-harness].is-visible .stage::before {
|
|
137
|
+
animation: stage-lit 7.5s ease-in-out infinite;
|
|
138
|
+
animation-delay: calc(var(--i) * 1.5s);
|
|
139
|
+
}
|
|
140
|
+
@keyframes stage-lit {
|
|
141
|
+
0%, 22%, 100% { opacity: 0; }
|
|
142
|
+
8% { opacity: 1; }
|
|
143
|
+
}
|
|
144
|
+
</style>
|