hacklab 0.3.1 → 0.4.2

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.
@@ -1,36 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "satori/jsx/jsx-runtime";
2
2
  import { execSync } from 'node:child_process';
3
- import { mkdir, writeFile } from 'node:fs/promises';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import { homedir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { Resvg } from '@resvg/resvg-js';
7
7
  import satori from 'satori';
8
- const BELT_COLORS = {
9
- white: '#E8E8E8',
10
- orange: '#FF8C00',
11
- red: '#DC2626',
12
- blue: '#2563EB',
13
- cyan: '#06B6D4',
14
- yellow: '#EAB308',
15
- lime: '#84CC16',
16
- green: '#22C55E',
17
- pink: '#EC4899',
18
- purple: '#A855F7',
19
- black: '#FFFFFF',
20
- };
21
- const TITLE_KANJI = {
22
- gaijin: '\u5916\u4eba',
23
- deshi: '\u5F1F\u5B50',
24
- ronin: '\u6D6A\u4EBA',
25
- shinobi: '\u5FCD',
26
- samurai: '\u4F8D',
27
- senpai: '\u5148\u8F29',
28
- tatsujin: '\u9054\u4EBA',
29
- kensei: '\u5263\u8056',
30
- oni: '\u9B3C',
31
- ryujin: '\u9F8D\u795E',
32
- sensei: '\u5E2B\u7BC4',
33
- };
8
+ // ── Design tokens (DESIGN.md) ──────────────────────────────────────────────
9
+ // Phosphor Mint is the one signal colour: corner brackets, the belt progress
10
+ // bar, the ASCII proficiency bars, and the lit heatmap cells. Everything else
11
+ // is neutral graphite + grey. Numbers + the nametag are Orbitron; every other
12
+ // glyph is JetBrains Mono.
13
+ const ACCENT = '#82F5C6';
14
+ const ACCENT_TRACK = 'rgba(130,245,198,0.1)'; // unfilled bar / belt track
15
+ const PAGE_BG = '#0B0D0C';
16
+ const CARD_BG = '#101513';
17
+ const BORDER = '#1D221F';
18
+ const WHITE = '#FFFFFF';
19
+ const LABEL = '#B4B4B4';
20
+ const LABEL_DIM = '#9A9A9A';
21
+ const FOOTER_FG = '#8A8A8A';
22
+ // Activity heatmap: 26 weeks × 7 days, matching the web profile (column = week,
23
+ // row = day-of-week), so the CLI card and the site read identically.
24
+ const HEAT_COLS = 26;
25
+ const HEAT_ROWS = 7;
26
+ const HEAT_CELL = 14;
27
+ const HEAT_GAP = 3;
34
28
  function formatTokens(n) {
35
29
  if (n >= 1_000_000_000)
36
30
  return `${(n / 1_000_000_000).toFixed(1)}B`;
@@ -40,304 +34,347 @@ function formatTokens(n) {
40
34
  return `${(n / 1_000).toFixed(0)}K`;
41
35
  return String(n);
42
36
  }
43
- function Heatmap({ data, accent, }) {
44
- // Build 12-week grid (84 days)
45
- const now = new Date();
46
- const cells = [];
47
- for (let i = 83; i >= 0; i--) {
48
- const d = new Date(now);
49
- d.setUTCDate(d.getUTCDate() - i);
50
- const dateStr = d.toISOString().split('T')[0];
51
- const entry = data.find((e) => e.date === dateStr);
52
- cells.push({ date: dateStr, tokens: entry?.tokens ?? 0 });
37
+ // Fold a raw model id into a short display label, keeping the version:
38
+ // "claude-opus-4-1-20250805" "OPUS 4.1"
39
+ // "claude-3-5-sonnet-latest" "SONNET 3.5"
40
+ // "gpt-5.3-codex" → "GPT-5.3-CODEX"
41
+ export function shortModelName(raw) {
42
+ const s = raw.trim().toLowerCase();
43
+ if (!s)
44
+ return '';
45
+ // Drop a trailing release date (…-20250805) so it doesn't crowd the label.
46
+ const t = s.replace(/[-_]?\d{8}$/, '');
47
+ const tier = t.includes('opus')
48
+ ? 'opus'
49
+ : t.includes('sonnet')
50
+ ? 'sonnet'
51
+ : t.includes('haiku')
52
+ ? 'haiku'
53
+ : null;
54
+ if (tier) {
55
+ const ver = t
56
+ .split(/[-_.\s]+/)
57
+ .filter((p) => /^\d+$/.test(p))
58
+ .slice(0, 2)
59
+ .join('.');
60
+ return (ver ? `${tier} ${ver}` : tier).toUpperCase();
61
+ }
62
+ return t.toUpperCase().slice(0, 16);
63
+ }
64
+ // ── Heatmap ─────────────────────────────────────────────────────────────────
65
+ // Opacity per intensity tier (0 = empty), matching the web's 18/38/62/100% mix.
66
+ const TIER_OPACITY = [0, 0.18, 0.38, 0.62, 1];
67
+ // Build the HEAT_ROWS × HEAT_COLS grid from real daily usage. Mirrors the web
68
+ // profile's buildRecentWeeks: columns are the latest 26 weeks ending on the
69
+ // Saturday on/after today, rows are Sun→Sat, intensity bucketed vs the window
70
+ // max. matrix[day][week] holds the tier for that date.
71
+ function buildMatrix(daily) {
72
+ const perDay = new Map();
73
+ for (const e of daily) {
74
+ perDay.set(e.date, (perDay.get(e.date) ?? 0) + e.tokens);
53
75
  }
54
- const maxTokens = Math.max(...cells.map((c) => c.tokens), 1);
55
- const cellSize = 10;
56
- const gap = 2;
57
- // Organize into weeks (columns of 7)
58
- const weeks = [];
59
- for (let i = 0; i < cells.length; i += 7) {
60
- weeks.push(cells.slice(i, i + 7));
76
+ const today = new Date();
77
+ const gridEnd = new Date(today);
78
+ gridEnd.setUTCDate(today.getUTCDate() + (6 - today.getUTCDay()));
79
+ const gridStart = new Date(gridEnd);
80
+ gridStart.setUTCDate(gridEnd.getUTCDate() - HEAT_COLS * 7 + 1);
81
+ const values = [];
82
+ let max = 0;
83
+ for (let d = 0; d < HEAT_ROWS; d++) {
84
+ const row = [];
85
+ for (let w = 0; w < HEAT_COLS; w++) {
86
+ const date = new Date(gridStart);
87
+ date.setUTCDate(gridStart.getUTCDate() + w * 7 + d);
88
+ const v = perDay.get(date.toISOString().slice(0, 10)) ?? 0;
89
+ row.push(v);
90
+ if (v > max)
91
+ max = v;
92
+ }
93
+ values.push(row);
61
94
  }
62
- return (_jsx("div", { style: { display: 'flex', gap }, children: weeks.map((week) => (
63
- // biome-ignore lint/correctness/useJsxKeyInIterable: satori JSX types don't support key
64
- _jsx("div", { style: {
65
- display: 'flex',
66
- flexDirection: 'column',
67
- gap,
68
- }, children: week.map((day) => {
69
- const ratio = day.tokens / maxTokens;
70
- let opacity = 0;
71
- if (ratio > 0)
72
- opacity = 0.2;
73
- if (ratio > 0.25)
74
- opacity = 0.4;
75
- if (ratio > 0.5)
76
- opacity = 0.7;
77
- if (ratio > 0.75)
78
- opacity = 1.0;
79
- return (
80
- // biome-ignore lint/correctness/useJsxKeyInIterable: satori JSX types don't support key
81
- _jsx("div", { style: {
95
+ return values.map((row) => row.map((v) => {
96
+ if (v <= 0 || max === 0)
97
+ return 0;
98
+ const r = v / max;
99
+ return r > 0.75 ? 4 : r > 0.5 ? 3 : r > 0.25 ? 2 : 1;
100
+ }));
101
+ }
102
+ function Heatmap({ matrix }) {
103
+ return (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: HEAT_GAP }, children: matrix.map((row) => (
104
+ // biome-ignore lint/correctness/useJsxKeyInIterable: satori has no keys
105
+ _jsx("div", { style: { display: 'flex', gap: HEAT_GAP }, children: row.map((tier) => tier === 0 ? (
106
+ // Empty slot: a tiny centred dot, not a full cell.
107
+ // biome-ignore lint/correctness/useJsxKeyInIterable: satori has no keys
108
+ _jsx("div", { style: {
109
+ display: 'flex',
110
+ width: HEAT_CELL,
111
+ height: HEAT_CELL,
112
+ alignItems: 'center',
113
+ justifyContent: 'center',
114
+ }, children: _jsx("div", { style: {
82
115
  display: 'flex',
83
- width: cellSize,
84
- height: cellSize,
85
- backgroundColor: day.tokens === 0 ? '#1a1a1a' : accent,
86
- opacity: day.tokens === 0 ? 1 : opacity,
87
- borderRadius: 2,
88
- } }));
89
- }) }))) }));
116
+ width: 3,
117
+ height: 3,
118
+ backgroundColor: 'rgba(130,245,198,0.35)',
119
+ } }) })) : (
120
+ // biome-ignore lint/correctness/useJsxKeyInIterable: satori has no keys
121
+ _jsx("div", { style: {
122
+ display: 'flex',
123
+ width: HEAT_CELL,
124
+ height: HEAT_CELL,
125
+ backgroundColor: `rgba(130,245,198,${TIER_OPACITY[tier]})`,
126
+ } }))) }))) }));
90
127
  }
91
- function ShareCard({ data }) {
92
- const accent = BELT_COLORS[data.beltColor] ?? '#06B6D4';
93
- const kanji = TITLE_KANJI[data.title] ?? '';
94
- const total = data.tokensTotal;
95
- const darkText = data.beltColor === 'yellow' ||
96
- data.beltColor === 'lime' ||
97
- data.beltColor === 'white'
98
- ? '#000'
99
- : '#FFF';
100
- const tools = [
101
- {
102
- label: 'Claude Code',
103
- short: 'CC',
104
- tokens: data.toolBreakdown.claudeCode,
105
- },
106
- { label: 'Codex', short: 'CDX', tokens: data.toolBreakdown.codex },
107
- { label: 'Cursor', short: 'CUR', tokens: data.toolBreakdown.cursor },
108
- ].filter((t) => t.tokens > 0);
109
- const toolMax = Math.max(...tools.map((t) => t.tokens), 1);
110
- const modelMax = data.models.length > 0 ? data.models[0]?.tokens : 1;
128
+ // ── Proficiency bar: solid mint fill over a 10%-mint track. ──────────────────
129
+ function Bar({ pct }) {
130
+ const fill = Math.max(0, Math.min(100, pct));
131
+ return (_jsx("div", { style: {
132
+ display: 'flex',
133
+ width: 64,
134
+ height: 6,
135
+ backgroundColor: ACCENT_TRACK,
136
+ }, children: _jsx("div", { style: {
137
+ display: 'flex',
138
+ height: 6,
139
+ width: `${fill}%`,
140
+ backgroundColor: ACCENT,
141
+ } }) }));
142
+ }
143
+ function StatCard({ value, label }) {
111
144
  return (_jsxs("div", { style: {
112
- width: '100%',
113
- height: '100%',
114
145
  display: 'flex',
115
146
  flexDirection: 'column',
116
- backgroundColor: '#000000',
117
- fontFamily: 'monospace',
118
- padding: 48,
119
- color: '#FFFFFF',
120
- }, children: [_jsxs("div", { style: {
147
+ flex: 1,
148
+ justifyContent: 'center',
149
+ backgroundColor: CARD_BG,
150
+ border: `1px solid ${BORDER}`,
151
+ paddingLeft: 16,
152
+ gap: 20,
153
+ }, children: [_jsx("div", { style: {
121
154
  display: 'flex',
122
- justifyContent: 'space-between',
123
- alignItems: 'center',
124
- marginBottom: 24,
125
- }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 16 }, children: [_jsxs("div", { style: {
126
- display: 'flex',
127
- backgroundColor: accent,
128
- color: darkText,
129
- padding: '6px 14px',
130
- fontSize: 18,
131
- fontWeight: 700,
132
- }, children: [kanji, " ", data.title, " lv.", data.level] }), _jsxs("div", { style: {
133
- display: 'flex',
134
- fontSize: 22,
135
- color: '#A3A3A3',
136
- }, children: ["@", data.handle] })] }), _jsxs("div", { style: { display: 'flex', fontSize: 20, color: accent }, children: ["#", data.rank] })] }), _jsxs("div", { style: {
155
+ fontFamily: 'Orbitron',
156
+ fontWeight: 700,
157
+ fontSize: 33,
158
+ lineHeight: 1,
159
+ letterSpacing: 0.5,
160
+ color: WHITE,
161
+ }, children: value }), _jsx("div", { style: {
137
162
  display: 'flex',
163
+ fontSize: 16,
164
+ letterSpacing: 2,
165
+ color: LABEL,
166
+ }, children: label })] }));
167
+ }
168
+ function PillStat({ value, label }) {
169
+ return (_jsxs("div", { style: {
170
+ display: 'flex',
171
+ flex: 1,
172
+ alignItems: 'center',
173
+ justifyContent: 'space-between',
174
+ backgroundColor: CARD_BG,
175
+ border: `1px solid ${BORDER}`,
176
+ paddingLeft: 17,
177
+ paddingRight: 17,
178
+ }, children: [_jsx("div", { style: {
179
+ display: 'flex',
180
+ fontFamily: 'Orbitron',
181
+ fontWeight: 700,
182
+ fontSize: 20,
183
+ letterSpacing: 0.5,
184
+ color: WHITE,
185
+ }, children: value }), _jsx("div", { style: {
186
+ display: 'flex',
187
+ fontSize: 16,
188
+ letterSpacing: 2,
189
+ color: LABEL,
190
+ }, children: label })] }));
191
+ }
192
+ function CornerBracket({ pos }) {
193
+ const arm = 11;
194
+ const thick = 2;
195
+ const inset = 0;
196
+ const vert = pos[0] === 't' ? 'top' : 'bottom';
197
+ const horiz = pos[1] === 'l' ? 'left' : 'right';
198
+ return (_jsx("div", { style: {
199
+ display: 'flex',
200
+ position: 'absolute',
201
+ width: arm,
202
+ height: arm,
203
+ [vert]: inset,
204
+ [horiz]: inset,
205
+ [`border${vert === 'top' ? 'Top' : 'Bottom'}`]: `${thick}px solid ${ACCENT}`,
206
+ [`border${horiz === 'left' ? 'Left' : 'Right'}`]: `${thick}px solid ${ACCENT}`,
207
+ } }));
208
+ }
209
+ function ShareCard({ data, matrix, avatar, }) {
210
+ const models = data.models.slice(0, 4);
211
+ const modelMax = models.length > 0 ? (models[0]?.tokens ?? 1) : 1;
212
+ const cost = Math.round(data.estimatedCost).toLocaleString('en-US');
213
+ const progress = Math.max(0, Math.min(100, data.progressPercent));
214
+ const initial = (data.handle[0] ?? '?').toUpperCase();
215
+ return (_jsxs("div", { style: {
216
+ position: 'relative',
217
+ width: 800,
218
+ height: 500,
219
+ display: 'flex',
220
+ flexDirection: 'column',
221
+ backgroundColor: PAGE_BG,
222
+ padding: 16,
223
+ fontFamily: 'JetBrains Mono',
224
+ color: WHITE,
225
+ }, children: [_jsx(CornerBracket, { pos: 'tl' }), _jsx(CornerBracket, { pos: 'tr' }), _jsx(CornerBracket, { pos: 'bl' }), _jsx(CornerBracket, { pos: 'br' }), _jsxs("div", { style: {
226
+ display: 'flex',
227
+ height: 70,
228
+ alignItems: 'center',
138
229
  justifyContent: 'space-between',
139
- alignItems: 'flex-start',
140
- marginBottom: 28,
141
- }, children: [_jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsx("div", { style: {
230
+ backgroundColor: CARD_BG,
231
+ border: `1px solid ${BORDER}`,
232
+ paddingLeft: 12,
233
+ paddingRight: 16,
234
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 18 }, children: [_jsx("div", { style: {
142
235
  display: 'flex',
143
- fontSize: 64,
144
- fontWeight: 700,
145
- color: accent,
146
- lineHeight: 1,
147
- }, children: formatTokens(total) }), _jsx("div", { style: {
236
+ width: 46,
237
+ height: 46,
238
+ border: `1px solid ${BORDER}`,
239
+ backgroundColor: '#0C100E',
240
+ alignItems: 'center',
241
+ justifyContent: 'center',
242
+ overflow: 'hidden',
243
+ }, children: avatar ? (
244
+ // biome-ignore lint/performance/noImgElement: satori rasterises to SVG, not a DOM img
245
+ _jsx("img", { src: avatar, width: 46, height: 46, alt: '' })) : (_jsx("div", { style: {
246
+ display: 'flex',
247
+ fontFamily: 'Orbitron',
248
+ fontWeight: 700,
249
+ fontSize: 22,
250
+ color: ACCENT,
251
+ }, children: initial })) }), _jsxs("div", { style: {
148
252
  display: 'flex',
253
+ fontFamily: 'Orbitron',
254
+ fontWeight: 700,
149
255
  fontSize: 20,
150
- color: '#525252',
151
- marginTop: 4,
152
- }, children: "tokens burned" })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsx("div", { style: {
256
+ letterSpacing: 0.5,
257
+ color: WHITE,
258
+ }, children: ["@", data.handle.toUpperCase()] })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 14 }, children: [_jsxs("div", { style: {
153
259
  display: 'flex',
154
- fontSize: 11,
155
- color: '#525252',
156
- }, children: "ACTIVITY" }), _jsx(Heatmap, { data: data.dailyActivity, accent: accent })] })] }), _jsxs("div", { style: {
157
- display: 'flex',
158
- gap: 32,
159
- marginBottom: 24,
160
- }, children: [_jsxs("div", { style: {
161
- display: 'flex',
162
- flexDirection: 'column',
163
- flex: 1,
164
- gap: 6,
165
- }, children: [_jsx("div", { style: {
260
+ fontFamily: 'Orbitron',
261
+ fontWeight: 700,
262
+ fontSize: 17,
263
+ letterSpacing: 1,
264
+ color: WHITE,
265
+ }, children: ["L", data.level] }), _jsx("div", { style: {
166
266
  display: 'flex',
167
- fontSize: 11,
168
- color: '#525252',
169
- marginBottom: 4,
170
- }, children: "PROVIDERS" }), tools.map((tool) => {
171
- const pct = Math.round((tool.tokens / toolMax) * 100);
172
- return (
173
- // biome-ignore lint/correctness/useJsxKeyInIterable: satori JSX types don't support key
174
- _jsxs("div", { style: {
267
+ width: 104,
268
+ height: 8,
269
+ backgroundColor: ACCENT_TRACK,
270
+ }, children: _jsx("div", { style: {
175
271
  display: 'flex',
176
- alignItems: 'center',
177
- gap: 8,
178
- }, children: [_jsx("div", { style: {
179
- display: 'flex',
180
- fontSize: 13,
181
- color: '#525252',
182
- width: 32,
183
- }, children: tool.short }), _jsx("div", { style: {
184
- display: 'flex',
185
- flex: 1,
186
- height: 16,
187
- backgroundColor: '#171717',
188
- }, children: _jsx("div", { style: {
189
- display: 'flex',
190
- width: `${pct}%`,
191
- height: '100%',
192
- backgroundColor: accent,
193
- } }) }), _jsx("div", { style: {
194
- display: 'flex',
195
- fontSize: 13,
196
- color: '#A3A3A3',
197
- width: 60,
198
- justifyContent: 'flex-end',
199
- }, children: formatTokens(tool.tokens) })] }));
200
- })] }), _jsxs("div", { style: {
272
+ width: `${progress}%`,
273
+ height: 8,
274
+ backgroundColor: ACCENT,
275
+ } }) })] })] }), _jsxs("div", { style: { display: 'flex', height: 114, gap: 26, marginTop: 18 }, children: [_jsx(StatCard, { value: formatTokens(data.tokensTotal), label: 'TOKENS BURNED' }), _jsx(StatCard, { value: `${data.streak}D`, label: 'STREAK' }), _jsx(StatCard, { value: `$${cost}`, label: 'EST. COST' })] }), _jsxs("div", { style: { display: 'flex', height: 142, gap: 18, marginTop: 18 }, children: [_jsx("div", { style: {
201
276
  display: 'flex',
277
+ width: 284,
202
278
  flexDirection: 'column',
203
- flex: 1,
204
- gap: 6,
205
- }, children: [_jsx("div", { style: {
206
- display: 'flex',
207
- fontSize: 11,
208
- color: '#525252',
209
- marginBottom: 4,
210
- }, children: "MODELS" }), data.models.slice(0, 4).map((model) => {
211
- const pct = Math.round((model.tokens / modelMax) * 100);
212
- const shortName = model.name.length > 18 ? model.name.slice(0, 18) : model.name;
213
- return (
214
- // biome-ignore lint/correctness/useJsxKeyInIterable: satori JSX types don't support key
215
- _jsxs("div", { style: {
279
+ justifyContent: 'center',
280
+ backgroundColor: CARD_BG,
281
+ border: `1px solid ${BORDER}`,
282
+ paddingLeft: 17,
283
+ paddingRight: 17,
284
+ gap: 14,
285
+ }, children: models.map((m) => (
286
+ // biome-ignore lint/correctness/useJsxKeyInIterable: satori has no keys
287
+ _jsxs("div", { style: {
288
+ display: 'flex',
289
+ alignItems: 'center',
290
+ justifyContent: 'space-between',
291
+ }, children: [_jsx("div", { style: {
216
292
  display: 'flex',
217
- alignItems: 'center',
218
- gap: 8,
219
- }, children: [_jsx("div", { style: {
220
- display: 'flex',
221
- fontSize: 11,
222
- color: '#525252',
223
- width: 110,
224
- }, children: shortName }), _jsx("div", { style: {
225
- display: 'flex',
226
- flex: 1,
227
- height: 16,
228
- backgroundColor: '#171717',
229
- }, children: _jsx("div", { style: {
230
- display: 'flex',
231
- width: `${pct}%`,
232
- height: '100%',
233
- backgroundColor: accent,
234
- opacity: 0.7,
235
- } }) }), _jsx("div", { style: {
236
- display: 'flex',
237
- fontSize: 11,
238
- color: '#A3A3A3',
239
- width: 50,
240
- justifyContent: 'flex-end',
241
- }, children: formatTokens(model.tokens) })] }));
242
- })] })] }), _jsxs("div", { style: { display: 'flex', gap: 16, marginBottom: 24 }, children: [_jsxs("div", { style: {
293
+ fontSize: 15,
294
+ letterSpacing: 0.4,
295
+ color: LABEL_DIM,
296
+ }, children: shortModelName(m.name) }), _jsx(Bar, { pct: (m.tokens / modelMax) * 100 })] }))) }), _jsx("div", { style: {
243
297
  display: 'flex',
244
- flexDirection: 'column',
245
- flex: 1,
246
- backgroundColor: '#0a0a0a',
247
- border: '1px solid #262626',
248
- padding: 16,
249
- alignItems: 'center',
250
- }, children: [_jsx("div", { style: {
251
- display: 'flex',
252
- fontSize: 11,
253
- color: '#525252',
254
- }, children: "STREAK" }), _jsxs("div", { style: {
255
- display: 'flex',
256
- fontSize: 28,
257
- fontWeight: 700,
258
- color: accent,
259
- }, children: [data.streak, "d"] })] }), _jsxs("div", { style: {
260
- display: 'flex',
261
- flexDirection: 'column',
262
- flex: 1,
263
- backgroundColor: '#0a0a0a',
264
- border: '1px solid #262626',
265
- padding: 16,
266
- alignItems: 'center',
267
- }, children: [_jsx("div", { style: {
268
- display: 'flex',
269
- fontSize: 11,
270
- color: '#525252',
271
- }, children: "BEST STREAK" }), _jsxs("div", { style: {
272
- display: 'flex',
273
- fontSize: 28,
274
- fontWeight: 700,
275
- color: accent,
276
- }, children: [data.longestStreak, "d"] })] }), _jsxs("div", { style: {
277
- display: 'flex',
278
- flexDirection: 'column',
279
- flex: 1,
280
- backgroundColor: '#0a0a0a',
281
- border: '1px solid #262626',
282
- padding: 16,
283
- alignItems: 'center',
284
- }, children: [_jsx("div", { style: {
285
- display: 'flex',
286
- fontSize: 11,
287
- color: '#525252',
288
- }, children: "EST. COST" }), _jsxs("div", { style: {
289
- display: 'flex',
290
- fontSize: 28,
291
- fontWeight: 700,
292
- color: accent,
293
- }, children: ["$", Math.round(data.estimatedCost)] })] }), _jsxs("div", { style: {
294
- display: 'flex',
295
- flexDirection: 'column',
296
298
  flex: 1,
297
- backgroundColor: '#0a0a0a',
298
- border: '1px solid #262626',
299
- padding: 16,
300
299
  alignItems: 'center',
301
- }, children: [_jsx("div", { style: {
302
- display: 'flex',
303
- fontSize: 11,
304
- color: '#525252',
305
- }, children: "RANK" }), _jsxs("div", { style: {
306
- display: 'flex',
307
- fontSize: 28,
308
- fontWeight: 700,
309
- color: accent,
310
- }, children: ["#", data.rank] })] })] }), _jsxs("div", { style: {
300
+ justifyContent: 'center',
301
+ backgroundColor: CARD_BG,
302
+ border: `1px solid ${BORDER}`,
303
+ }, children: _jsx(Heatmap, { matrix: matrix }) })] }), _jsxs("div", { style: { display: 'flex', height: 42, gap: 17, marginTop: 18 }, children: [_jsx(PillStat, { value: `${data.longestStreak}D`, label: 'BEST STREAK' }), _jsx(PillStat, { value: `#${data.rank}`, label: 'RANK' }), _jsx(PillStat, { value: String(data.followers ?? 0), label: 'FOLLOWERS' })] }), _jsxs("div", { style: {
311
304
  display: 'flex',
312
305
  justifyContent: 'space-between',
313
- alignItems: 'center',
314
306
  marginTop: 'auto',
315
- borderTop: '1px solid #262626',
316
- paddingTop: 12,
317
- }, children: [_jsxs("div", { style: { display: 'flex', fontSize: 14, color: '#525252' }, children: ["hacklab.so/", data.handle] }), _jsx("div", { style: { display: 'flex', fontSize: 14, color: '#525252' }, children: "#SweatySunday" })] })] }));
307
+ paddingLeft: 2,
308
+ paddingRight: 2,
309
+ }, children: [_jsxs("div", { style: { display: 'flex', fontSize: 15, color: FOOTER_FG }, children: ["hacklab.so/", data.handle] }), _jsx("div", { style: { display: 'flex', fontSize: 15, color: FOOTER_FG }, children: "#sweatysunday" })] })] }));
310
+ }
311
+ // ── Font loading (disk-cached so repeat renders are offline + fast) ──────────
312
+ const FONT_SOURCES = {
313
+ 'orbitron-700.woff': 'https://cdn.jsdelivr.net/fontsource/fonts/orbitron@latest/latin-700-normal.woff',
314
+ 'jetbrains-400.woff': 'https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono@latest/latin-400-normal.woff',
315
+ 'jetbrains-500.woff': 'https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono@latest/latin-500-normal.woff',
316
+ };
317
+ async function loadFont(name) {
318
+ const dir = join(homedir(), '.hacklab', 'fonts');
319
+ const file = join(dir, name);
320
+ try {
321
+ const cached = await readFile(file);
322
+ return cached.buffer.slice(cached.byteOffset, cached.byteOffset + cached.byteLength);
323
+ }
324
+ catch {
325
+ const url = FONT_SOURCES[name];
326
+ if (!url)
327
+ throw new Error(`unknown font: ${name}`);
328
+ const buf = await fetch(url).then((r) => r.arrayBuffer());
329
+ await mkdir(dir, { recursive: true });
330
+ await writeFile(file, Buffer.from(buf));
331
+ return buf;
332
+ }
333
+ }
334
+ async function loadAvatar(url) {
335
+ if (!url)
336
+ return null;
337
+ try {
338
+ const res = await fetch(url);
339
+ if (!res.ok)
340
+ return null;
341
+ const type = res.headers.get('content-type') ?? 'image/png';
342
+ const buf = Buffer.from(await res.arrayBuffer());
343
+ return `data:${type};base64,${buf.toString('base64')}`;
344
+ }
345
+ catch {
346
+ return null;
347
+ }
318
348
  }
319
349
  export async function generateShareCard(data) {
320
- const fontUrl = 'https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono@latest/latin-400-normal.woff';
321
- const fontBoldUrl = 'https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono@latest/latin-700-normal.woff';
322
- const [fontData, fontBoldData] = await Promise.all([
323
- fetch(fontUrl).then((r) => r.arrayBuffer()),
324
- fetch(fontBoldUrl).then((r) => r.arrayBuffer()),
350
+ const [orbitron700, jetbrains400, jetbrains500, avatar] = await Promise.all([
351
+ loadFont('orbitron-700.woff'),
352
+ loadFont('jetbrains-400.woff'),
353
+ loadFont('jetbrains-500.woff'),
354
+ loadAvatar(data.avatarUrl),
325
355
  ]);
326
- const svg = await satori(_jsx(ShareCard, { data: data }), {
327
- width: 900,
328
- height: 600,
356
+ const matrix = buildMatrix(data.dailyActivity);
357
+ const svg = await satori(_jsx(ShareCard, { data: data, matrix: matrix, avatar: avatar }), {
358
+ width: 800,
359
+ height: 500,
329
360
  fonts: [
330
- { name: 'monospace', data: fontData, weight: 400, style: 'normal' },
361
+ { name: 'Orbitron', data: orbitron700, weight: 700, style: 'normal' },
362
+ {
363
+ name: 'JetBrains Mono',
364
+ data: jetbrains400,
365
+ weight: 400,
366
+ style: 'normal',
367
+ },
331
368
  {
332
- name: 'monospace',
333
- data: fontBoldData,
334
- weight: 700,
369
+ name: 'JetBrains Mono',
370
+ data: jetbrains500,
371
+ weight: 500,
335
372
  style: 'normal',
336
373
  },
337
374
  ],
338
375
  });
339
376
  const resvg = new Resvg(svg, {
340
- fitTo: { mode: 'width', value: 1800 },
377
+ fitTo: { mode: 'width', value: 1600 },
341
378
  });
342
379
  const png = Buffer.from(resvg.render().asPng());
343
380
  const dir = join(homedir(), '.hacklab');