@cratis/cast 0.2.0 → 0.2.1
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/badges/badgeSvg.ts +10 -1
- package/machine/profileFromCastJson.ts +1 -0
- package/package.json +1 -1
- package/site/components/Activity.astro +1 -2
- package/site/components/Cratis.astro +2 -2
- package/site/components/Stats.astro +3 -3
- package/site/lib/github.ts +9 -1
- package/site/profile/clientCast.ts +3 -0
- package/site/scripts/terminal.ts +16 -8
package/badges/badgeSvg.ts
CHANGED
|
@@ -94,6 +94,15 @@ export type BadgeCardOptions = {
|
|
|
94
94
|
since?: string;
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
98
|
+
|
|
99
|
+
/** `2024-06` as `Jun 2024`, matching the directory's cards; anything else is shown as given. */
|
|
100
|
+
const monthYear = (since: string): string => {
|
|
101
|
+
const match = /^(\d{4})-(\d{2})$/.exec(since);
|
|
102
|
+
const month = match ? months[Number(match[2]) - 1] : undefined;
|
|
103
|
+
return match && month ? `${month} ${match[1]}` : since;
|
|
104
|
+
};
|
|
105
|
+
|
|
97
106
|
/**
|
|
98
107
|
* The large badge card (480 × 168): a dark card with a tier-colored edge, the Cratis mark, the tier in
|
|
99
108
|
* the italic serif and the verification address. Meant for READMEs, slides and profile pages.
|
|
@@ -106,7 +115,7 @@ export function badgeCardSvg(tier: Tier | undefined, options: BadgeCardOptions):
|
|
|
106
115
|
const handle = options.handle.toLowerCase();
|
|
107
116
|
const address = `${castDirectoryUrl.replace(/^https:\/\//, '')}/${handle}`;
|
|
108
117
|
const who = options.name ? `${options.name} · @${options.handle}` : `@${options.handle}`;
|
|
109
|
-
const sinceText = options.since ? ` · since ${options.since}` : '';
|
|
118
|
+
const sinceText = options.since ? ` · since ${monthYear(options.since)}` : '';
|
|
110
119
|
const title = tier ? `${who}: Cratis Cast ${style.label}` : `${who}: not verified in the Cratis Cast`;
|
|
111
120
|
|
|
112
121
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(title)}">
|
|
@@ -184,6 +184,7 @@ function finish(data: z.output<typeof castJsonSchema>, document: Record<string,
|
|
|
184
184
|
};
|
|
185
185
|
|
|
186
186
|
// Rendered without the captures when only they cannot be: the directory then uses its own GitHub data.
|
|
187
|
+
// A refused file keeps nothing, captures included: rejected member data never feeds the GitHub-only page.
|
|
187
188
|
const problem = renderProblem(profile);
|
|
188
189
|
if (!problem) return { dropped, profile };
|
|
189
190
|
const { github: _github, writing: _writing, ...withoutCaptures } = profile;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/cast",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Cast: the Cratis profile-site engine. One cast.yaml becomes a verified, event-sourced personal site with a printable CV, live GitHub numbers and a Cast badge.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cratis",
|
|
@@ -55,8 +55,7 @@ const summary = `Contribution heatmap: ${fmt.format(total)} contributions betwee
|
|
|
55
55
|
) : <div class="panel overflow-hidden" data-reveal data-heatmap>
|
|
56
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
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>
|
|
58
|
+
<span class="text-fg-faint">EventStore:</span> <span class="text-event">github.com/{github.login}</span> <span class="text-fg-faint">· partitioned by day</span>
|
|
60
59
|
</p>
|
|
61
60
|
<p class="font-mono text-xs text-fg-faint">snapshot taken <time datetime={github.fetchedAt}>{snapshotDate()}</time></p>
|
|
62
61
|
</div>
|
|
@@ -14,7 +14,7 @@ interface Props {
|
|
|
14
14
|
label?: string;
|
|
15
15
|
}
|
|
16
16
|
const { cast } = Astro.props;
|
|
17
|
-
const { cratis: cratisData, cratisRepos, mergedPrFloor, mergedPrTotal, snapshotDate, starsFor } = cast;
|
|
17
|
+
const { cratis: cratisData, cratisRepos, mergedPrFloor, mergedPrTotal, mergedPrStatus, snapshotDate, starsFor } = cast;
|
|
18
18
|
const cratis = cratisData!;
|
|
19
19
|
const { index, title = cratis.title, label = cratis.label } = Astro.props;
|
|
20
20
|
|
|
@@ -56,7 +56,7 @@ const badge = (r: CratisRepo) => (r.founded ? 'b-founded' : r.rank === 1 ? 'b-le
|
|
|
56
56
|
<div class="flex flex-col bg-ink-900 p-5 md:p-6">
|
|
57
57
|
<dt class="order-2 mt-2 text-sm leading-snug text-fg-muted">
|
|
58
58
|
{h.label}
|
|
59
|
-
{h.live && <> (<span data-merged-prs="exact">{fmt.format(mergedPrTotal)}</span>, <span data-pr-status>
|
|
59
|
+
{h.live && <> (<span data-merged-prs="exact">{fmt.format(mergedPrTotal)}</span>, <span data-pr-status>{mergedPrStatus}</span>)</>}
|
|
60
60
|
</dt>
|
|
61
61
|
<dd data-merged-prs={h.live ? 'floor' : undefined} class={`order-1 font-display text-[clamp(2.25rem,4.4vw,3.5rem)] leading-none font-[750] tracking-[-0.045em] tabular-nums ${h.accent}`}>{h.value}</dd>
|
|
62
62
|
</div>
|
|
@@ -8,7 +8,7 @@ interface Props {
|
|
|
8
8
|
cast: CastView;
|
|
9
9
|
}
|
|
10
10
|
const { cast } = Astro.props;
|
|
11
|
-
const { stats: siteStats, cratis, github, cratisRepos, mergedPrFloor,
|
|
11
|
+
const { stats: siteStats, cratis, github, cratisRepos, mergedPrFloor, mergedPrStatus } = cast;
|
|
12
12
|
|
|
13
13
|
type Stat = (typeof siteStats)[number];
|
|
14
14
|
const rankOne = cratisRepos.filter((r) => r.rank === 1).length;
|
|
@@ -37,9 +37,9 @@ const accents = ['text-event', 'text-command', 'text-view', 'text-fg'];
|
|
|
37
37
|
style={`--reveal-delay:${i * 80}ms`}
|
|
38
38
|
>
|
|
39
39
|
<dt class="order-2 max-w-[18ch] text-sm leading-snug text-fg-muted">{s.label}
|
|
40
|
-
{s.live === 'mergedPullRequests' && <span data-pr-status class="mt-1 block font-mono text-[0.6875rem] text-fg-faint">
|
|
40
|
+
{s.live === 'mergedPullRequests' && <span data-pr-status class="mt-1 block font-mono text-[0.6875rem] text-fg-faint">{mergedPrStatus}</span>}
|
|
41
41
|
</dt>
|
|
42
|
-
<dd class={`order-1 font-display text-[clamp(2.5rem,5vw,4rem)] leading-none font-[650] tracking-[-0.04em] tabular-nums ${accents[i % accents.length]}`}>
|
|
42
|
+
<dd class={`order-1 font-display whitespace-nowrap text-[clamp(2.5rem,5vw,4rem)] leading-none font-[650] tracking-[-0.04em] tabular-nums ${accents[i % accents.length]}`}>
|
|
43
43
|
{s.prefix}<span data-merged-prs={s.live === 'mergedPullRequests' ? 'number' : undefined} data-count={plain ? undefined : s.value}>{plain ? s.value : fmt.format(s.value)}</span>
|
|
44
44
|
{s.suffix && <span class="text-fg-faint">{s.suffix}</span>}
|
|
45
45
|
</dd>
|
package/site/lib/github.ts
CHANGED
|
@@ -32,6 +32,10 @@ export type GitHubView = {
|
|
|
32
32
|
github: GitHubSnapshot;
|
|
33
33
|
/** Merged pull requests across the configured organizations, falling back to a stat written in cast.yaml. */
|
|
34
34
|
mergedPrTotal: number;
|
|
35
|
+
/** Where {@link mergedPrTotal} comes from: 'as of' the capture date, or 'self-reported' for the cast.yaml fallback. */
|
|
36
|
+
mergedPrStatus: string;
|
|
37
|
+
/** True when {@link mergedPrTotal} is the cast.yaml fallback: never captured, so never shown as a snapshot or as live. */
|
|
38
|
+
mergedPrSelfReported: boolean;
|
|
35
39
|
/** Floors to hundreds for an honest "3,100+" style figure. */
|
|
36
40
|
mergedPrFloor: number;
|
|
37
41
|
/** Per-repository figures prefer the snapshot; values written in cast.yaml are fallbacks only. */
|
|
@@ -48,7 +52,9 @@ export type GitHubView = {
|
|
|
48
52
|
|
|
49
53
|
/** Builds the GitHub view for one profile. */
|
|
50
54
|
export function gitHubView(config: CastConfig, github: GitHubSnapshot): GitHubView {
|
|
51
|
-
const
|
|
55
|
+
const captured = github.mergedPullRequests.total;
|
|
56
|
+
// The cast.yaml figure was never captured: it carries no capture date, so it is labelled as the owner's own.
|
|
57
|
+
const mergedPrTotal = captured || (config.stats.find(stat => stat.live === 'mergedPullRequests')?.value ?? 0);
|
|
52
58
|
const cratisRepos = (config.cratis?.repos ?? []).map(repo => {
|
|
53
59
|
const counts = repo.url ? github.repositories[`Cratis/${repo.name}`] : undefined;
|
|
54
60
|
if (!counts) return { ...repo, role: repo.role ?? roleFor(repo, repo.rank) };
|
|
@@ -62,6 +68,8 @@ export function gitHubView(config: CastConfig, github: GitHubSnapshot): GitHubVi
|
|
|
62
68
|
return {
|
|
63
69
|
github,
|
|
64
70
|
mergedPrTotal,
|
|
71
|
+
mergedPrStatus: captured ? `as of ${formatSnapshotDate(github.fetchedAt)}` : 'self-reported',
|
|
72
|
+
mergedPrSelfReported: !captured && mergedPrTotal > 0,
|
|
65
73
|
mergedPrFloor: mergedPrTotal >= 100 ? Math.floor(mergedPrTotal / 100) * 100 : mergedPrTotal,
|
|
66
74
|
cratisRepos,
|
|
67
75
|
liveProjects,
|
|
@@ -34,6 +34,8 @@ export type ClientCast = {
|
|
|
34
34
|
projects: { name: string; url: string; pitch: string; version?: string; category: string; features: string[]; stack: string[]; install?: string }[];
|
|
35
35
|
pullRequests: PullRequest[];
|
|
36
36
|
mergedPrTotal: number;
|
|
37
|
+
/** The total is the cast.yaml fallback, not a capture. */
|
|
38
|
+
mergedPrSelfReported: boolean;
|
|
37
39
|
snapshotDate: string;
|
|
38
40
|
posts: { headline: string; url: string; part?: number; minutes?: number; themes: string[] }[];
|
|
39
41
|
themes: WritingTheme[];
|
|
@@ -85,6 +87,7 @@ export function clientCast(cast: CastView): ClientCast {
|
|
|
85
87
|
})),
|
|
86
88
|
pullRequests: cast.github.mergedPullRequests.latest,
|
|
87
89
|
mergedPrTotal: cast.mergedPrTotal,
|
|
90
|
+
mergedPrSelfReported: cast.mergedPrSelfReported,
|
|
88
91
|
snapshotDate: cast.snapshotDate(),
|
|
89
92
|
posts: cast.posts.map(post => ({
|
|
90
93
|
headline: post.headline,
|
package/site/scripts/terminal.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { getLiveMerged } from './live';
|
|
|
8
8
|
import { relativeTime } from '../lib/reltime';
|
|
9
9
|
import { copyText } from '../lib/clipboard';
|
|
10
10
|
import { formatMonth } from '../lib/career';
|
|
11
|
+
import { plainText } from '../lib/inline';
|
|
11
12
|
import { go, goSection, openExternal, profilePath, startReplay } from '../lib/nav';
|
|
12
13
|
import { currentTheme, setTheme } from '../lib/theme';
|
|
13
14
|
|
|
@@ -123,13 +124,20 @@ class Terminal {
|
|
|
123
124
|
|
|
124
125
|
private greet(): void {
|
|
125
126
|
this.greeted = true;
|
|
126
|
-
const { careerEvents,
|
|
127
|
-
this.print(c.ev(this.prompt), c.faint(` · stream position ${careerEvents.length} · ${
|
|
127
|
+
const { careerEvents, feedOrg, snapshotDate } = this.cast;
|
|
128
|
+
this.print(c.ev(this.prompt), c.faint(` · stream position ${careerEvents.length} · ${this.mergedTotal()} PRs merged in ${feedOrg}`));
|
|
128
129
|
this.print(c.faint(`GitHub snapshot taken ${snapshotDate}. Everything here is real data.`));
|
|
129
130
|
this.print('Type ', c.fg('help'), ' to see what you can do, or tap a suggestion below.');
|
|
130
131
|
this.blank();
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
/** The merged pull request total: live once the browser has it, else the snapshot, marked when self-reported. */
|
|
135
|
+
private mergedTotal(): string {
|
|
136
|
+
const live = getLiveMerged();
|
|
137
|
+
const total = (live?.total ?? this.cast.mergedPrTotal).toLocaleString('en-US');
|
|
138
|
+
return !live && this.cast.mergedPrSelfReported ? `${total} (self-reported)` : total;
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
// --- lifecycle ----------------------------------------------------------------
|
|
134
142
|
open(): void {
|
|
135
143
|
linkRel = this.cast.rel;
|
|
@@ -284,7 +292,7 @@ class Terminal {
|
|
|
284
292
|
|
|
285
293
|
// --- commands -------------------------------------------------------------------
|
|
286
294
|
private buildCommands(): Record<string, Command> {
|
|
287
|
-
const { person, now, careerEvents, skills, cratis, talks, profileLinks, firstName, feedOrg, projects,
|
|
295
|
+
const { person, now, careerEvents, skills, cratis, talks, profileLinks, firstName, feedOrg, projects, snapshotDate, themes: blogThemes, posts: blogPosts, blogHost } = this.cast;
|
|
288
296
|
const SECTIONS = ALL_SECTIONS.filter(([id]) => id === 'top' || this.cast.sections.includes(id));
|
|
289
297
|
const LINKS: Record<string, string> = {
|
|
290
298
|
...Object.fromEntries(profileLinks.map((profile) => [profile.key.toLowerCase(), profile.url])),
|
|
@@ -311,7 +319,7 @@ class Terminal {
|
|
|
311
319
|
run: () => {
|
|
312
320
|
this.print(c.fg(person.name), c.faint(` (${person.handle})`));
|
|
313
321
|
this.print(person.title, ...(person.location ? [c.faint(' · '), person.location] : []));
|
|
314
|
-
this.print(c.ev(person.tagline
|
|
322
|
+
this.print(c.ev(plainText(person.tagline)));
|
|
315
323
|
},
|
|
316
324
|
},
|
|
317
325
|
now: {
|
|
@@ -320,7 +328,7 @@ class Terminal {
|
|
|
320
328
|
this.print(c.faint('Projection: '), c.view(`${firstName}.Now`), c.faint(` · projected from ${careerEvents.length} events`));
|
|
321
329
|
const rows: [string, string][] = Object.entries(now).map(([key, value]) => [
|
|
322
330
|
key,
|
|
323
|
-
Array.isArray(value) ? value.join(', ') : /^\d{4}-\d{2}$/.test(value) ? formatMonth(value) : value
|
|
331
|
+
Array.isArray(value) ? value.join(', ') : /^\d{4}-\d{2}$/.test(value) ? formatMonth(value) : plainText(value),
|
|
324
332
|
]);
|
|
325
333
|
for (const [k, v] of rows) this.print(c.faint(pad(k, 12)), c.fg(v));
|
|
326
334
|
},
|
|
@@ -404,7 +412,7 @@ class Terminal {
|
|
|
404
412
|
const live = getLiveMerged();
|
|
405
413
|
const prs = live?.latest ?? this.cast.pullRequests;
|
|
406
414
|
const count = flag === '-n' ? Math.max(1, Math.min(prs.length, Number(n) || prs.length)) : prs.length;
|
|
407
|
-
this.print(c.faint(`tail ${feedOrg} · ${live ? 'live' : `snapshot taken ${snapshotDate}`} · ${
|
|
415
|
+
this.print(c.faint(`tail ${feedOrg} · ${live ? 'live' : `snapshot taken ${snapshotDate}`} · ${this.mergedTotal()} merged in total`));
|
|
408
416
|
prs.slice(0, count).forEach((pr, i) => {
|
|
409
417
|
this.print(
|
|
410
418
|
c.faint(pad(i === 0 ? 'HEAD' : `HEAD~${i}`, 8)),
|
|
@@ -509,8 +517,8 @@ class Terminal {
|
|
|
509
517
|
desc: 'what Cratis is',
|
|
510
518
|
run: () => {
|
|
511
519
|
if (!cratis) return this.print(c.faint('Cratis makes event sourcing practical. '), link('https://cratis.io'));
|
|
512
|
-
if (cratis.pitch) this.print(c.fg(cratis.pitch
|
|
513
|
-
this.print(c.faint(`${
|
|
520
|
+
if (cratis.pitch) this.print(c.fg(plainText(cratis.pitch)));
|
|
521
|
+
this.print(c.faint(`${this.mergedTotal()} merged PRs · `), link(LINKS['cratis'] ?? 'https://cratis.io'));
|
|
514
522
|
for (const r of cratis.repos) this.print(c.cmd(pad(r.name, 21)), c.faint(pad(r.role, 16)), r.url ? link(r.url, r.desc) : r.desc);
|
|
515
523
|
},
|
|
516
524
|
},
|