claude-rpc 0.3.11 → 0.6.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 +96 -136
- package/config.example.json +2 -65
- package/package.json +3 -2
- package/src/badge.js +38 -10
- package/src/card.js +345 -0
- package/src/cli.js +239 -12
- package/src/config.js +89 -0
- package/src/daemon.js +133 -23
- package/src/doctor.js +376 -0
- package/src/git.js +2 -2
- package/src/hook.js +1 -1
- package/src/install.js +51 -5
- package/src/pricing.js +29 -6
- package/src/privacy.js +231 -0
- package/src/scanner.js +62 -7
- package/src/server/api.js +175 -0
- package/src/server/index.js +98 -0
- package/src/{server.js → server/page.js} +58 -327
- package/src/server/routes.js +63 -0
- package/src/server/sse.js +32 -0
- package/src/tui.js +6 -7
- package/src/ui.js +89 -0
- package/src/version.js +26 -0
package/src/card.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// Poster-style SVG card — designed to be screenshotted and shared.
|
|
2
|
+
//
|
|
3
|
+
// Renders an 880×540 magazine-style summary for a given range:
|
|
4
|
+
// year on claude / month on claude / week on claude / all-time
|
|
5
|
+
//
|
|
6
|
+
// Stats: total hours, prompts, tokens, lines added, cost, top language,
|
|
7
|
+
// top file, longest streak, top weekday, peak hour. Visual style matches
|
|
8
|
+
// the indie cream-paper landing page — same palette, same monospace +
|
|
9
|
+
// display-font split.
|
|
10
|
+
//
|
|
11
|
+
// Output is SVG only (no PNG dep). Modern Discord / GitHub render the SVG
|
|
12
|
+
// inline; for hard-copy sharing, screenshot or convert via any web tool.
|
|
13
|
+
|
|
14
|
+
import { dayKey } from './scanner.js';
|
|
15
|
+
import { fmtCost } from './pricing.js';
|
|
16
|
+
import { rangeToDays, rangeLabel, pickWindow } from './badge.js';
|
|
17
|
+
import { VERSION } from './version.js';
|
|
18
|
+
|
|
19
|
+
const W = 880;
|
|
20
|
+
const H = 540;
|
|
21
|
+
|
|
22
|
+
const PALETTE = {
|
|
23
|
+
paper: '#f4ede0',
|
|
24
|
+
paper2: '#ebe2d2',
|
|
25
|
+
paper3: '#e1d6c0',
|
|
26
|
+
paper4: '#d6c8ac',
|
|
27
|
+
ink: '#1a1611',
|
|
28
|
+
inkSoft:'#2d2520',
|
|
29
|
+
inkMute:'#5c5147',
|
|
30
|
+
inkFaint:'#8a7c6d',
|
|
31
|
+
rust: '#c2491e',
|
|
32
|
+
rust3: '#f08a4a',
|
|
33
|
+
tape: '#f2d76e',
|
|
34
|
+
grass: '#4a9462',
|
|
35
|
+
blurple:'#5865f2',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const WEEKDAY = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
39
|
+
|
|
40
|
+
function escapeXml(s) {
|
|
41
|
+
return String(s == null ? '' : s)
|
|
42
|
+
.replace(/&/g, '&')
|
|
43
|
+
.replace(/</g, '<')
|
|
44
|
+
.replace(/>/g, '>')
|
|
45
|
+
.replace(/"/g, '"');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fmtNum(n) {
|
|
49
|
+
if (!n) return '0';
|
|
50
|
+
if (n < 1000) return String(Math.round(n));
|
|
51
|
+
if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`;
|
|
52
|
+
if (n < 1_000_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
|
|
53
|
+
return `${(n / 1_000_000_000).toFixed(2)}B`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fmtHours(ms) {
|
|
57
|
+
if (!ms || ms < 0) return '0h';
|
|
58
|
+
const h = ms / 3_600_000;
|
|
59
|
+
if (h < 1) return `${Math.round(h * 60)}m`;
|
|
60
|
+
if (h < 10) return `${h.toFixed(1)}h`;
|
|
61
|
+
return `${Math.round(h)}h`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function rangeTitle(range) {
|
|
65
|
+
const rl = rangeLabel(range);
|
|
66
|
+
if (rl === 'all-time') return 'on claude';
|
|
67
|
+
return `${rl} on claude`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Roll up a windowed metric across the active range.
|
|
71
|
+
function rollup(aggregate, range) {
|
|
72
|
+
const window = pickWindow(aggregate?.byDay, range);
|
|
73
|
+
const out = {
|
|
74
|
+
activeMs: 0,
|
|
75
|
+
userMessages: 0,
|
|
76
|
+
toolCalls: 0,
|
|
77
|
+
tokens: 0,
|
|
78
|
+
linesAdded: 0,
|
|
79
|
+
linesRemoved: 0,
|
|
80
|
+
cost: 0,
|
|
81
|
+
sessions: 0,
|
|
82
|
+
days: 0,
|
|
83
|
+
daysActive: 0,
|
|
84
|
+
};
|
|
85
|
+
for (const [, d] of window) {
|
|
86
|
+
out.days += 1;
|
|
87
|
+
if ((d.activeMs || 0) > 0) out.daysActive += 1;
|
|
88
|
+
out.activeMs += d.activeMs || 0;
|
|
89
|
+
out.userMessages += d.userMessages || 0;
|
|
90
|
+
out.toolCalls += d.toolCalls || 0;
|
|
91
|
+
out.tokens += (d.inputTokens || 0) + (d.outputTokens || 0)
|
|
92
|
+
+ (d.cacheReadTokens || 0) + (d.cacheWriteTokens || 0);
|
|
93
|
+
out.linesAdded += d.linesAdded || 0;
|
|
94
|
+
out.linesRemoved += d.linesRemoved || 0;
|
|
95
|
+
out.cost += d.cost || 0;
|
|
96
|
+
out.sessions += d.sessions || 0;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Best weekday by active time, computed from byWeekday on the aggregate.
|
|
102
|
+
function topWeekday(aggregate) {
|
|
103
|
+
const wd = aggregate?.byWeekday || {};
|
|
104
|
+
let best = null;
|
|
105
|
+
for (const [k, v] of Object.entries(wd)) {
|
|
106
|
+
if (!best || (v.activeMs || 0) > (best.ms || 0)) best = { day: Number(k), ms: v.activeMs || 0 };
|
|
107
|
+
}
|
|
108
|
+
return best;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Top language by edits.
|
|
112
|
+
function topLanguage(aggregate) {
|
|
113
|
+
const langs = aggregate?.languages || {};
|
|
114
|
+
let best = null;
|
|
115
|
+
for (const [name, v] of Object.entries(langs)) {
|
|
116
|
+
if (!best || (v.edits || 0) > (best.edits || 0)) best = { name, edits: v.edits || 0 };
|
|
117
|
+
}
|
|
118
|
+
return best;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Top edited file (basename only — full path would overflow).
|
|
122
|
+
function topFile(aggregate) {
|
|
123
|
+
const list = aggregate?.topEditedFiles || [];
|
|
124
|
+
if (!list.length) return null;
|
|
125
|
+
const top = list[0];
|
|
126
|
+
const p = String(top.path || '').replace(/\\/g, '/');
|
|
127
|
+
const name = p.split('/').pop() || p;
|
|
128
|
+
return { name, count: top.count };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Compute peak hour-of-day (24h clock).
|
|
132
|
+
function peakHourLabel(aggregate) {
|
|
133
|
+
const ph = aggregate?.peakHour;
|
|
134
|
+
if (!ph || ph.hour == null) return null;
|
|
135
|
+
const h = Number(ph.hour);
|
|
136
|
+
return `${String(h).padStart(2, '0')}:00`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── SVG building blocks ─────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
function paperDefs() {
|
|
142
|
+
return `
|
|
143
|
+
<defs>
|
|
144
|
+
<pattern id="dotgrid" width="22" height="22" patternUnits="userSpaceOnUse">
|
|
145
|
+
<circle cx="1" cy="1" r="1" fill="${PALETTE.ink}" opacity="0.07"/>
|
|
146
|
+
</pattern>
|
|
147
|
+
<filter id="noise" x="0" y="0" width="100%" height="100%">
|
|
148
|
+
<feTurbulence type="fractalNoise" baseFrequency="0.95" numOctaves="2" stitchTiles="stitch"/>
|
|
149
|
+
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
|
150
|
+
</filter>
|
|
151
|
+
</defs>`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function background() {
|
|
155
|
+
return `
|
|
156
|
+
<rect width="${W}" height="${H}" fill="${PALETTE.paper}"/>
|
|
157
|
+
<rect width="${W}" height="${H}" fill="url(#dotgrid)"/>
|
|
158
|
+
<rect width="${W}" height="${H}" filter="url(#noise)" opacity="0.6"/>`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function tapeSticker(x, y, text, { rotate = -2, bg = PALETTE.tape, fg = PALETTE.ink } = {}) {
|
|
162
|
+
const pad = 12;
|
|
163
|
+
const fontSize = 14;
|
|
164
|
+
const w = text.length * 8.4 + pad * 2;
|
|
165
|
+
const h = fontSize + 12;
|
|
166
|
+
return `
|
|
167
|
+
<g transform="translate(${x} ${y}) rotate(${rotate})">
|
|
168
|
+
<rect x="0" y="0" width="${w}" height="${h}" fill="${bg}" stroke="${PALETTE.ink}" stroke-width="1.5"/>
|
|
169
|
+
<rect x="2" y="2" width="${w}" height="${h}" fill="none" stroke="${PALETTE.ink}" stroke-width="1.5" opacity="0.18"/>
|
|
170
|
+
<text x="${w / 2}" y="${h / 2 + 5}"
|
|
171
|
+
font-family="JetBrains Mono, ui-monospace, monospace"
|
|
172
|
+
font-size="${fontSize}" font-weight="700"
|
|
173
|
+
letter-spacing="1.5"
|
|
174
|
+
text-anchor="middle" fill="${fg}">${escapeXml(text.toUpperCase())}</text>
|
|
175
|
+
</g>`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function statBox(x, y, w, h, { label, value, sub = '', accent = PALETTE.ink, tilt = 0 } = {}) {
|
|
179
|
+
// Drop-shadow style: a second rect offset by (2,2) under the main one.
|
|
180
|
+
return `
|
|
181
|
+
<g transform="translate(${x} ${y}) rotate(${tilt})">
|
|
182
|
+
<rect x="2" y="3" width="${w}" height="${h}" fill="${PALETTE.ink}"/>
|
|
183
|
+
<rect x="0" y="0" width="${w}" height="${h}" fill="${PALETTE.paper}" stroke="${PALETTE.ink}" stroke-width="1.5"/>
|
|
184
|
+
<text x="18" y="28"
|
|
185
|
+
font-family="JetBrains Mono, ui-monospace, monospace"
|
|
186
|
+
font-size="11" font-weight="700"
|
|
187
|
+
letter-spacing="2"
|
|
188
|
+
fill="${PALETTE.inkMute}">${escapeXml(label.toUpperCase())}</text>
|
|
189
|
+
<text x="18" y="${h - 24}"
|
|
190
|
+
font-family="Space Grotesk, Inter, system-ui, sans-serif"
|
|
191
|
+
font-size="34" font-weight="800"
|
|
192
|
+
fill="${accent}">${escapeXml(value)}</text>
|
|
193
|
+
${sub ? `<text x="18" y="${h - 8}"
|
|
194
|
+
font-family="JetBrains Mono, ui-monospace, monospace"
|
|
195
|
+
font-size="11"
|
|
196
|
+
fill="${PALETTE.inkMute}">${escapeXml(sub)}</text>` : ''}
|
|
197
|
+
</g>`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Mini bar chart of daily active hours across the windowed range.
|
|
201
|
+
// Used as a strip across the bottom of the card.
|
|
202
|
+
function activityStrip(x, y, w, h, byDay, range) {
|
|
203
|
+
const window = pickWindow(byDay, range).reverse(); // oldest → newest
|
|
204
|
+
if (!window.length) return '';
|
|
205
|
+
const max = Math.max(1, ...window.map(([, d]) => d.activeMs || 0));
|
|
206
|
+
const N = window.length;
|
|
207
|
+
const colW = w / N;
|
|
208
|
+
const bw = Math.max(1, colW - 1.5);
|
|
209
|
+
let bars = '';
|
|
210
|
+
for (let i = 0; i < N; i++) {
|
|
211
|
+
const [, d] = window[i];
|
|
212
|
+
const ratio = (d.activeMs || 0) / max;
|
|
213
|
+
const bh = Math.max(0, ratio * h);
|
|
214
|
+
bars += `<rect x="${x + i * colW}" y="${y + (h - bh)}" width="${bw}" height="${bh}" fill="${PALETTE.rust}" opacity="${0.45 + ratio * 0.45}"/>`;
|
|
215
|
+
}
|
|
216
|
+
return `<g>${bars}</g>`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Heatmap-like dot row of the last N days.
|
|
220
|
+
function dotRow(x, y, byDay, range) {
|
|
221
|
+
const window = pickWindow(byDay, range).reverse();
|
|
222
|
+
const sz = 9;
|
|
223
|
+
const gap = 2;
|
|
224
|
+
let svg = '';
|
|
225
|
+
for (let i = 0; i < window.length; i++) {
|
|
226
|
+
const [, d] = window[i];
|
|
227
|
+
const ms = d.activeMs || 0;
|
|
228
|
+
const intensity = ms === 0 ? 0 : Math.min(1, ms / (4 * 3_600_000)); // 4h = full
|
|
229
|
+
const fill = intensity === 0
|
|
230
|
+
? PALETTE.paper3
|
|
231
|
+
: intensity < 0.34 ? '#f6dccb'
|
|
232
|
+
: intensity < 0.67 ? PALETTE.rust3
|
|
233
|
+
: PALETTE.rust;
|
|
234
|
+
svg += `<rect x="${x + i * (sz + gap)}" y="${y}" width="${sz}" height="${sz}" fill="${fill}" stroke="${PALETTE.ink}" stroke-width="0.5"/>`;
|
|
235
|
+
}
|
|
236
|
+
return svg;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── public entry point ─────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
export function renderCard(aggregate, { range = 'year', generatedAt = new Date() } = {}) {
|
|
242
|
+
const r = rollup(aggregate, range);
|
|
243
|
+
const lang = topLanguage(aggregate);
|
|
244
|
+
const file = topFile(aggregate);
|
|
245
|
+
const wd = topWeekday(aggregate);
|
|
246
|
+
const peak = peakHourLabel(aggregate);
|
|
247
|
+
const streak = aggregate?.streak || 0;
|
|
248
|
+
const longestStreak = aggregate?.longestStreak || 0;
|
|
249
|
+
const linesNet = r.linesAdded - r.linesRemoved;
|
|
250
|
+
const allTimeHours = ((aggregate?.activeMs || 0) / 3_600_000).toFixed(1);
|
|
251
|
+
|
|
252
|
+
const subtitle = `${escapeXml(r.daysActive)} active days / ${escapeXml(rangeLabel(range))} ending ${escapeXml(generatedAt.toISOString().slice(0, 10))}`;
|
|
253
|
+
|
|
254
|
+
// Layout grid
|
|
255
|
+
// Title block 80 → W-80
|
|
256
|
+
// Hero hours card (60, 130) 380×170
|
|
257
|
+
// Right-column stats (470, 130) → 4 mini boxes
|
|
258
|
+
// Activity strip 60, 332, W-120, 50
|
|
259
|
+
// Footer credits 60, 480
|
|
260
|
+
|
|
261
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">
|
|
262
|
+
${paperDefs()}
|
|
263
|
+
${background()}
|
|
264
|
+
|
|
265
|
+
<!-- ── title ── -->
|
|
266
|
+
<g transform="translate(60 60)">
|
|
267
|
+
<text x="0" y="0"
|
|
268
|
+
font-family="Space Grotesk, Inter, system-ui, sans-serif"
|
|
269
|
+
font-size="48" font-weight="800"
|
|
270
|
+
letter-spacing="-1.5"
|
|
271
|
+
fill="${PALETTE.ink}">${escapeXml(rangeTitle(range))}</text>
|
|
272
|
+
<text x="0" y="26"
|
|
273
|
+
font-family="JetBrains Mono, ui-monospace, monospace"
|
|
274
|
+
font-size="13"
|
|
275
|
+
fill="${PALETTE.inkMute}">${subtitle}</text>
|
|
276
|
+
</g>
|
|
277
|
+
${tapeSticker(W - 220, 40, `claude-rpc · v${VERSION}`, { rotate: 3 })}
|
|
278
|
+
|
|
279
|
+
<!-- ── hero hours card ── -->
|
|
280
|
+
<g transform="translate(60 130)">
|
|
281
|
+
<rect x="3" y="4" width="380" height="170" fill="${PALETTE.ink}"/>
|
|
282
|
+
<rect x="0" y="0" width="380" height="170" fill="${PALETTE.paper}" stroke="${PALETTE.ink}" stroke-width="2"/>
|
|
283
|
+
<text x="22" y="36"
|
|
284
|
+
font-family="JetBrains Mono, ui-monospace, monospace"
|
|
285
|
+
font-size="12" font-weight="700"
|
|
286
|
+
letter-spacing="2.5"
|
|
287
|
+
fill="${PALETTE.inkMute}">TIME WITH CLAUDE</text>
|
|
288
|
+
<text x="22" y="118"
|
|
289
|
+
font-family="Space Grotesk, Inter, system-ui, sans-serif"
|
|
290
|
+
font-size="82" font-weight="800"
|
|
291
|
+
letter-spacing="-3"
|
|
292
|
+
fill="${PALETTE.rust}">${escapeXml(fmtHours(r.activeMs))}</text>
|
|
293
|
+
<text x="22" y="148"
|
|
294
|
+
font-family="JetBrains Mono, ui-monospace, monospace"
|
|
295
|
+
font-size="13"
|
|
296
|
+
fill="${PALETTE.ink}">${escapeXml(r.daysActive)} days · ${escapeXml(fmtNum(r.sessions || 0))} sessions · streak ${escapeXml(streak)} (best ${escapeXml(longestStreak)})</text>
|
|
297
|
+
</g>
|
|
298
|
+
|
|
299
|
+
<!-- ── right-column stats (2x2 grid) ── -->
|
|
300
|
+
${statBox(470, 130, 170, 82, { label: 'prompts', value: fmtNum(r.userMessages), accent: PALETTE.ink, tilt: -0.8 })}
|
|
301
|
+
${statBox(650, 130, 170, 82, { label: 'tokens', value: fmtNum(r.tokens), accent: PALETTE.ink, tilt: 0.6 })}
|
|
302
|
+
${statBox(470, 218, 170, 82, { label: 'lines', value: `${linesNet >= 0 ? '+' : '−'}${fmtNum(Math.abs(linesNet))}`, sub: `${fmtNum(r.linesAdded)} added`, accent: PALETTE.grass, tilt: 0.6 })}
|
|
303
|
+
${statBox(650, 218, 170, 82, { label: 'cost', value: fmtCost(r.cost), sub: `≈ ${fmtCost(r.cost / Math.max(1, r.daysActive))}/day`, accent: PALETTE.blurple, tilt: -0.5 })}
|
|
304
|
+
|
|
305
|
+
<!-- ── activity strip ── -->
|
|
306
|
+
<g>
|
|
307
|
+
<text x="60" y="328"
|
|
308
|
+
font-family="JetBrains Mono, ui-monospace, monospace"
|
|
309
|
+
font-size="11" font-weight="700"
|
|
310
|
+
letter-spacing="2.5"
|
|
311
|
+
fill="${PALETTE.inkMute}">DAILY ACTIVITY</text>
|
|
312
|
+
<rect x="60" y="335" width="${W - 120}" height="62" fill="${PALETTE.paper2}" stroke="${PALETTE.ink}" stroke-width="1.5"/>
|
|
313
|
+
${activityStrip(64, 337, W - 128, 58, aggregate?.byDay, range)}
|
|
314
|
+
</g>
|
|
315
|
+
|
|
316
|
+
<!-- ── footer stats row ── -->
|
|
317
|
+
<g transform="translate(60 420)">
|
|
318
|
+
<text x="0" y="0"
|
|
319
|
+
font-family="JetBrains Mono, ui-monospace, monospace"
|
|
320
|
+
font-size="11" font-weight="700"
|
|
321
|
+
letter-spacing="2.5"
|
|
322
|
+
fill="${PALETTE.inkMute}">HIGHLIGHTS</text>
|
|
323
|
+
|
|
324
|
+
<g transform="translate(0 16)">
|
|
325
|
+
<text font-family="JetBrains Mono, ui-monospace, monospace" font-size="13" fill="${PALETTE.ink}">
|
|
326
|
+
<tspan x="0" dy="14" fill="${PALETTE.inkMute}">top language</tspan><tspan dx="10" font-weight="700">${escapeXml(lang ? lang.name : '—')}</tspan><tspan dx="6" fill="${PALETTE.inkFaint}">${escapeXml(lang ? `(${fmtNum(lang.edits)} edits)` : '')}</tspan>
|
|
327
|
+
<tspan x="0" dy="20" fill="${PALETTE.inkMute}">hotspot file</tspan><tspan dx="14" font-weight="700">${escapeXml(file ? file.name : '—')}</tspan><tspan dx="6" fill="${PALETTE.inkFaint}">${escapeXml(file ? `(× ${fmtNum(file.count)})` : '')}</tspan>
|
|
328
|
+
<tspan x="0" dy="20" fill="${PALETTE.inkMute}">peak day</tspan><tspan dx="36" font-weight="700">${escapeXml(wd && wd.day != null ? WEEKDAY[wd.day] : '—')}</tspan><tspan dx="6" fill="${PALETTE.inkFaint}">${escapeXml(wd ? `(${fmtHours(wd.ms)})` : '')}</tspan>
|
|
329
|
+
<tspan x="0" dy="20" fill="${PALETTE.inkMute}">peak hour</tspan><tspan dx="30" font-weight="700">${escapeXml(peak || '—')}</tspan>
|
|
330
|
+
</text>
|
|
331
|
+
</g>
|
|
332
|
+
</g>
|
|
333
|
+
|
|
334
|
+
<!-- ── credits ── -->
|
|
335
|
+
<g transform="translate(${W - 60} ${H - 24})" text-anchor="end">
|
|
336
|
+
<text font-family="JetBrains Mono, ui-monospace, monospace" font-size="10"
|
|
337
|
+
fill="${PALETTE.inkFaint}">${escapeXml(allTimeHours)}h all-time · github.com/rar-file/claude-rpc</text>
|
|
338
|
+
</g>
|
|
339
|
+
</svg>`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Top-level convenience matching badge.js shape.
|
|
343
|
+
export function cardSvg({ aggregate, range = 'year' }) {
|
|
344
|
+
return renderCard(aggregate, { range });
|
|
345
|
+
}
|