@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.
@@ -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.0",
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>as of {asOf}</span>)</>}
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, snapshotDate } = cast;
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">as of {snapshotDate()}</span>}
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>
@@ -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 mergedPrTotal = github.mergedPullRequests.total || (config.stats.find(stat => stat.live === 'mergedPullRequests')?.value ?? 0);
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,
@@ -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, mergedPrTotal, feedOrg, snapshotDate } = this.cast;
127
- this.print(c.ev(this.prompt), c.faint(` · stream position ${careerEvents.length} · ${(getLiveMerged()?.total ?? mergedPrTotal).toLocaleString('en-US')} PRs merged in ${feedOrg}`));
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, mergedPrTotal, snapshotDate, themes: blogThemes, posts: blogPosts, blogHost } = this.cast;
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.replace(/\*/g, '')));
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.replace(/\*/g, ''),
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}`} · ${(live?.total ?? mergedPrTotal).toLocaleString('en-US')} merged in total`));
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.replace(/\*/g, '')));
513
- this.print(c.faint(`${(getLiveMerged()?.total ?? mergedPrTotal).toLocaleString('en-US')} merged PRs · `), link(LINKS['cratis'] ?? 'https://cratis.io'));
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
  },