@shiinasaku/github-card 2.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 サクシイナ
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,354 @@
1
+ # GitHub Profile Card
2
+
3
+ <div align="center">
4
+
5
+ <br/>
6
+
7
+ <picture>
8
+ <source media="(prefers-color-scheme: dark)" srcset="https://card.shiina.xyz/card/ShiinaSaku?theme=github_dark" />
9
+ <img src="https://card.shiina.xyz/card/ShiinaSaku" alt="GitHub Profile Card" width="500" />
10
+ </picture>
11
+
12
+ <br/><br/>
13
+
14
+ # github-card
15
+
16
+ **Beautiful GitHub stats cards for your README, site, or npm project.**
17
+
18
+ [![npm](https://img.shields.io/npm/v/github-card?style=flat&colorA=0b1220&colorB=58a6ff)](https://www.npmjs.com/package/github-card)
19
+ [![Built with Bun](https://img.shields.io/badge/Bun-000?style=flat&logo=bun&logoColor=ffd700)](https://bun.sh)
20
+ [![MIT](https://img.shields.io/badge/License-MIT-green?style=flat)](LICENSE)
21
+ [![Tests](https://img.shields.io/badge/Tests-21%20passed-34d399?style=flat)](#testing)
22
+
23
+ [Live Demo](https://card.shiina.xyz/card/ShiinaSaku) &#183;
24
+ [All Themes](https://card.shiina.xyz/ShiinaSaku/themes) &#183;
25
+ [API Docs](https://card.shiina.xyz/openapi) &#183;
26
+ [npm Package](#programmatic-usage-npm)
27
+
28
+ </div>
29
+
30
+ ---
31
+
32
+ ## Embed in 10 seconds
33
+
34
+ ```markdown
35
+ ![GitHub Stats](https://card.shiina.xyz/card/YOUR_USERNAME)
36
+ ```
37
+
38
+ Replace `YOUR_USERNAME`. Done.
39
+
40
+ ### Dark mode aware
41
+
42
+ ```html
43
+ <picture>
44
+ <source
45
+ media="(prefers-color-scheme: dark)"
46
+ srcset="https://card.shiina.xyz/card/YOUR_USERNAME?theme=github_dark"
47
+ />
48
+ <img src="https://card.shiina.xyz/card/YOUR_USERNAME" alt="GitHub Stats" />
49
+ </picture>
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Themes
55
+
56
+ 20 built-in themes. Pass `?theme=name` to use one.
57
+
58
+ <table>
59
+ <tr>
60
+ <td align="center"><img src="https://card.shiina.xyz/card/ShiinaSaku?theme=default" width="400" alt="default" /><br/><code>default</code></td>
61
+ <td align="center"><img src="https://card.shiina.xyz/card/ShiinaSaku?theme=github_dark" width="400" alt="github_dark" /><br/><code>github_dark</code></td>
62
+ </tr>
63
+ <tr>
64
+ <td align="center"><img src="https://card.shiina.xyz/card/ShiinaSaku?theme=tokyonight" width="400" alt="tokyonight" /><br/><code>tokyonight</code></td>
65
+ <td align="center"><img src="https://card.shiina.xyz/card/ShiinaSaku?theme=dracula" width="400" alt="dracula" /><br/><code>dracula</code></td>
66
+ </tr>
67
+ <tr>
68
+ <td align="center"><img src="https://card.shiina.xyz/card/ShiinaSaku?theme=nord" width="400" alt="nord" /><br/><code>nord</code></td>
69
+ <td align="center"><img src="https://card.shiina.xyz/card/ShiinaSaku?theme=radical" width="400" alt="radical" /><br/><code>radical</code></td>
70
+ </tr>
71
+ <tr>
72
+ <td align="center"><img src="https://card.shiina.xyz/card/ShiinaSaku?theme=synthwave" width="400" alt="synthwave" /><br/><code>synthwave</code></td>
73
+ <td align="center"><img src="https://card.shiina.xyz/card/ShiinaSaku?theme=rose" width="400" alt="rose" /><br/><code>rose</code></td>
74
+ </tr>
75
+ </table>
76
+
77
+ <details>
78
+ <summary><strong>All 20 themes</strong></summary>
79
+ <br/>
80
+
81
+ `default` `dark` `radical` `merko` `gruvbox` `tokyonight` `onedark` `cobalt` `synthwave` `highcontrast` `dracula` `monokai` `nord` `github_dark` `pearl` `slate` `forest` `rose` `sand`
82
+
83
+ Preview every theme with your own data:
84
+
85
+ ```
86
+ https://card.shiina.xyz/YOUR_USERNAME/themes
87
+ ```
88
+
89
+ </details>
90
+
91
+ ---
92
+
93
+ ## Parameters
94
+
95
+ All parameters are optional query strings on `/card/:username`.
96
+
97
+ ### Appearance
98
+
99
+ | Parameter | Default | Description |
100
+ | :------------ | :-------- | :----------------------------------- |
101
+ | `theme` | `default` | One of the 20 built-in themes |
102
+ | `compact` | `false` | Hides bio, pronouns, language labels |
103
+ | `hide_border` | `false` | Removes the card border |
104
+
105
+ ### Custom Colors
106
+
107
+ Pass hex values **without** `#`.
108
+
109
+ | Parameter | Description |
110
+ | :------------- | :----------------- |
111
+ | `bg_color` | Background |
112
+ | `title_color` | Name text |
113
+ | `text_color` | Body text & labels |
114
+ | `icon_color` | Stat icons |
115
+ | `border_color` | Card border |
116
+
117
+ ### Data Filtering
118
+
119
+ | Parameter | Default | Description |
120
+ | :----------- | :--------- | :---------------------------------------------------------------------- |
121
+ | `lang_count` | `5` | Top languages shown (1--10) |
122
+ | `hide` | -- | Comma-separated: `stars,commits,issues,repos,prs` |
123
+ | `scope` | `personal` | `personal` / `org` / `all` -- see [scope details](#what-the-card-shows) |
124
+ | `orgs` | -- | Comma-separated org logins to include (filters contributed-to repos) |
125
+
126
+ ### Examples
127
+
128
+ ```bash
129
+ # Compact with 3 languages
130
+ https://card.shiina.xyz/card/ShiinaSaku?compact=true&lang_count=3
131
+
132
+ # GitHub dark colors
133
+ https://card.shiina.xyz/card/ShiinaSaku?bg_color=0d1117&text_color=c9d1d9&title_color=58a6ff
134
+
135
+ # Organization stats
136
+ https://card.shiina.xyz/card/ShiinaSaku?scope=all&orgs=oven-sh,elysiajs
137
+
138
+ # Hide specific stats
139
+ https://card.shiina.xyz/card/ShiinaSaku?hide=issues,prs,stars
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Programmatic Usage (npm)
145
+
146
+ Install the package to render cards in your own server, CLI tool, or script:
147
+
148
+ ```bash
149
+ bun add github-card # or npm install github-card
150
+ ```
151
+
152
+ ### Render SVG from data
153
+
154
+ ```typescript
155
+ import { renderCard, themes } from "github-card";
156
+
157
+ const svg = renderCard(
158
+ {
159
+ login: "octocat",
160
+ name: "The Octocat",
161
+ avatarUrl: "https://github.com/octocat.png",
162
+ bio: "Open source enthusiast",
163
+ pronouns: "they/them",
164
+ twitter: "github",
165
+ },
166
+ { stars: 3200, commits: 1450, issues: 89, repos: 42, prs: 210 },
167
+ [
168
+ { name: "TypeScript", size: 120000, color: "#3178c6" },
169
+ { name: "Go", size: 80000, color: "#00ADD8" },
170
+ { name: "Rust", size: 40000, color: "#dea584" },
171
+ ],
172
+ { theme: "tokyonight", compact: false },
173
+ );
174
+
175
+ // svg is a self-contained SVG string with embedded font
176
+ ```
177
+
178
+ ### Available exports
179
+
180
+ ```typescript
181
+ import {
182
+ // Core renderer
183
+ renderCard,
184
+ type CardOpts,
185
+
186
+ // Types
187
+ type UserProfile,
188
+ type UserStats,
189
+ type LanguageStat,
190
+ type ProfileData,
191
+ type CardOptions,
192
+
193
+ // Theming
194
+ themes,
195
+ resolveColors,
196
+ type Theme,
197
+
198
+ // Utilities
199
+ kFormat, // 1500 -> "1.5k"
200
+ escapeXml, // "<>" -> "&lt;&gt;"
201
+ wrapText, // text wrapping with ellipsis
202
+ icon, // SVG stat icons (star, commit, pr, issue, repo, x)
203
+ icons, // raw SVG path data
204
+ getLangColor, // language -> hex color (650+ languages)
205
+ FONT_FACE, // base64 @font-face CSS rule
206
+ FONT_FAMILY, // font-family declaration
207
+ } from "github-card";
208
+ ```
209
+
210
+ ### Use as Elysia plugin
211
+
212
+ The default export is an Elysia app. Mount it into your own server:
213
+
214
+ ```typescript
215
+ import { Elysia } from "elysia";
216
+ import githubCard from "github-card/server";
217
+
218
+ new Elysia()
219
+ .use(githubCard) // adds /card/:username, /:username/themes, etc.
220
+ .get("/my-route", () => "hello")
221
+ .listen(3000);
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Self-Hosting
227
+
228
+ ### Vercel (recommended)
229
+
230
+ ```bash
231
+ git clone https://github.com/ShiinaSaku/Github-Card.git
232
+ cd Github-Card
233
+ vercel deploy
234
+ ```
235
+
236
+ Set environment variables:
237
+
238
+ ```
239
+ GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx # required
240
+ UPSTASH_REDIS_REST_URL=https://... # optional, L2 cache
241
+ UPSTASH_REDIS_REST_TOKEN=... # optional
242
+ ```
243
+
244
+ ### Local Development
245
+
246
+ ```bash
247
+ bun install
248
+ echo "GITHUB_TOKEN=ghp_xxx" > .env
249
+ bun dev
250
+ # http://localhost:3000/card/ShiinaSaku
251
+ ```
252
+
253
+ ---
254
+
255
+ ## API Reference
256
+
257
+ | Endpoint | Returns |
258
+ | :---------------------- | :------------------------------------------ |
259
+ | `GET /card/:username` | SVG stats card |
260
+ | `GET /:username/themes` | SVG grid of all themes |
261
+ | `GET /health` | JSON -- cache metrics, Redis status, uptime |
262
+ | `GET /openapi` | Interactive API documentation |
263
+
264
+ ### Response Headers
265
+
266
+ | Header | Value |
267
+ | :---------------------------- | :-------------------------------------------------------------- |
268
+ | `Cache-Control` | `public, max-age=0, s-maxage=1800, stale-while-revalidate=1800` |
269
+ | `ETag` | Enables 304 Not Modified |
270
+ | `Server-Timing` | Per-request timing telemetry |
271
+ | `Access-Control-Allow-Origin` | `*` |
272
+
273
+ ### Status Codes
274
+
275
+ | Code | Meaning |
276
+ | :--- | :------------------ |
277
+ | 200 | SVG card |
278
+ | 304 | Cached (ETag match) |
279
+ | 401 | Bad GitHub token |
280
+ | 404 | User not found |
281
+ | 422 | Invalid parameters |
282
+ | 429 | GitHub rate limited |
283
+ | 502 | GitHub API down |
284
+
285
+ ---
286
+
287
+ ## Architecture
288
+
289
+ ```
290
+ Request -> Elysia (CORS, OpenAPI, Server-Timing)
291
+ -> GitHub GraphQL API (parallel personal + org fetches)
292
+ -> L1 In-Memory Cache (30m fresh / 30m stale SWR)
293
+ -> L2 Upstash Redis (optional, 60m TTL)
294
+ -> SVG Renderer (embedded Roboto WOFF2 font)
295
+ -> ETag / 304 / Cache-Control
296
+ ```
297
+
298
+ | Layer | Tech |
299
+ | :-------- | :---------------------------------------- |
300
+ | Runtime | Bun |
301
+ | Framework | Elysia |
302
+ | Data | GitHub GraphQL API |
303
+ | Cache L1 | In-memory Map with SWR |
304
+ | Cache L2 | Upstash Redis (serverless) |
305
+ | Font | Roboto (subsetted WOFF2, base64 embedded) |
306
+ | Deploy | Vercel Functions |
307
+ | Tests | bun:test -- 21 tests |
308
+ | Lint | oxlint + oxfmt |
309
+
310
+ ---
311
+
312
+ ## Testing
313
+
314
+ ```bash
315
+ bun test # 21 tests
316
+ bun test --coverage # with coverage
317
+ bun run typecheck # tsc
318
+ bun run lint # oxlint
319
+ ```
320
+
321
+ ---
322
+
323
+ ## What the card shows
324
+
325
+ | Stat | Source |
326
+ | :-------- | :------------------------------------- |
327
+ | Stars | Total across owned repos (excl. forks) |
328
+ | Commits | Current calendar year |
329
+ | Issues | Open + closed |
330
+ | Repos | Owned, non-fork |
331
+ | PRs | Open + closed + merged |
332
+ | Languages | Top N by code size |
333
+
334
+ ### Scope
335
+
336
+ | Value | Stars / Repos / Languages from | Speed |
337
+ | :--------- | :--------------------------------------------------- | :--------------------------------- |
338
+ | `personal` | Your own repos only | Single query |
339
+ | `org` | Org repos you actually contributed to (commits, PRs) | Parallel: metadata + contributions |
340
+ | `all` | Your repos + org repos you contributed to | Parallel: personal + contributions |
341
+
342
+ Org stats use GitHub's `repositoriesContributedTo` API -- only repos you actually touched are counted, not every repo in the organization. Filter to specific orgs with `orgs=org1,org2`.
343
+
344
+ ---
345
+
346
+ ## License
347
+
348
+ [MIT](LICENSE)
349
+
350
+ ---
351
+
352
+ <div align="center">
353
+ <sub>Built with <a href="https://bun.sh">Bun</a> and <a href="https://elysiajs.com">Elysia</a>. Hosted on <a href="https://vercel.com">Vercel</a>.</sub>
354
+ </div>
Binary file
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@shiinasaku/github-card",
3
+ "version": "2.0.0",
4
+ "description": "Beautiful GitHub stats SVG cards. Embed in README, use as API, or render programmatically.",
5
+ "keywords": [
6
+ "badge",
7
+ "bun",
8
+ "elysia",
9
+ "github",
10
+ "github-card",
11
+ "github-stats",
12
+ "profile-card",
13
+ "readme",
14
+ "stats",
15
+ "svg"
16
+ ],
17
+ "homepage": "https://card.shiina.xyz",
18
+ "license": "MIT",
19
+ "author": {
20
+ "name": "saku shiina",
21
+ "email": "saku@shiina.xyz",
22
+ "url": "https://www.shiina.xyz"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/ShiinaSaku/Github-Card.git"
27
+ },
28
+ "files": [
29
+ "src",
30
+ "fonts",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "type": "module",
35
+ "main": "./src/lib.ts",
36
+ "module": "./src/lib.ts",
37
+ "types": "./src/lib.ts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./src/lib.ts",
41
+ "import": "./src/lib.ts"
42
+ },
43
+ "./server": {
44
+ "types": "./src/index.ts",
45
+ "import": "./src/index.ts"
46
+ }
47
+ },
48
+ "scripts": {
49
+ "dev": "bun run --watch src/index.ts",
50
+ "build": "bun build src/index.ts --target bun --outdir dist",
51
+ "fmt": "oxfmt",
52
+ "fmt:check": "oxfmt --check",
53
+ "lint": "oxlint",
54
+ "lint:fix": "oxlint --fix",
55
+ "typecheck": "tsc --noEmit --skipLibCheck",
56
+ "test": "bun test",
57
+ "test:coverage": "bun test --coverage"
58
+ },
59
+ "dependencies": {
60
+ "@elysiajs/cors": "^1.4.1",
61
+ "@elysiajs/openapi": "^1.4.14",
62
+ "@elysiajs/server-timing": "^1.4.0",
63
+ "@upstash/redis": "^1.36.2",
64
+ "elysia": "latest"
65
+ },
66
+ "devDependencies": {
67
+ "@types/bun": "latest",
68
+ "oxfmt": "0.33.0",
69
+ "oxlint": "1.48.0",
70
+ "typescript": "5.9.3"
71
+ }
72
+ }
package/src/card.ts ADDED
@@ -0,0 +1,137 @@
1
+ import { resolveColors, kFormat, escapeXml, icon, wrapText, FONT_FACE, FONT_FAMILY } from "./utils";
2
+ import type { UserProfile, UserStats, LanguageStat } from "./types";
3
+
4
+ export type CardOpts = {
5
+ theme?: string;
6
+ title_color?: string;
7
+ text_color?: string;
8
+ icon_color?: string;
9
+ bg_color?: string;
10
+ border_color?: string;
11
+ hide_border?: boolean;
12
+ compact?: boolean;
13
+ hide?: string[];
14
+ };
15
+
16
+ export function renderCard(
17
+ user: UserProfile,
18
+ stats: UserStats,
19
+ langs: LanguageStat[],
20
+ opts: CardOpts = {},
21
+ ): string {
22
+ const c = resolveColors(opts);
23
+ const hideBorder = opts.hide_border ?? false;
24
+ const compact = opts.compact ?? false;
25
+
26
+ const name = escapeXml(user.name || user.login);
27
+ const uname = escapeXml(user.login);
28
+ const bioRaw = !compact && user.bio ? escapeXml(user.bio) : "";
29
+ const bioLines = bioRaw ? wrapText(bioRaw, 42, 2) : [];
30
+ const pronouns = !compact && user.pronouns ? escapeXml(user.pronouns) : "";
31
+ const avatar = user.avatarUrl.replace(/&/g, "&amp;");
32
+ const twitter = !compact && user.twitter ? escapeXml(user.twitter) : "";
33
+ const hiddenStats = new Set((opts.hide || []).map((key) => key.toLowerCase()));
34
+ const svgTitle = `${name}'s GitHub Stats`;
35
+
36
+ const W = 500;
37
+ const H = 200;
38
+ const P = 22;
39
+ const avatarSize = 72;
40
+ const barWidth = W - P * 2;
41
+ const barY = H - 40;
42
+ const labelY = H - 16;
43
+ const infoX = P + avatarSize + 16;
44
+
45
+ const totalSize = langs.reduce((sum, l) => sum + l.size, 0) || 1;
46
+ let offset = 0;
47
+ const langRects = langs
48
+ .map((lang) => {
49
+ const w = (lang.size / totalSize) * barWidth;
50
+ const r = `<rect x="${P + offset}" y="${barY}" width="${w}" height="8" fill="${lang.color}"/>`;
51
+ offset += w;
52
+ return r;
53
+ })
54
+ .join("");
55
+
56
+ const labelSpacing = compact ? 0 : Math.floor(barWidth / Math.max(langs.length, 1));
57
+ const langLabels = compact
58
+ ? ""
59
+ : langs
60
+ .map((lang, i) => {
61
+ const pct = ((lang.size / totalSize) * 100).toFixed(0);
62
+ return `<circle cx="${P + i * labelSpacing + 5}" cy="${labelY}" r="4" fill="${lang.color}"/><text x="${P + i * labelSpacing + 13}" y="${labelY + 4}" class="lang">${lang.name} ${pct}%</text>`;
63
+ })
64
+ .join("");
65
+
66
+ const nameY = P + 22;
67
+ const usernameY = nameY + 18;
68
+ const bioY = usernameY + 14;
69
+ const twitterY = bioLines.length ? bioY + 28 : usernameY + 16;
70
+
71
+ const headerY = P + avatarSize + 12;
72
+ const statLabelY = 28;
73
+
74
+ const visibleStats = [
75
+ { key: "stars", iconName: "star" as const, label: "Stars", value: stats.stars },
76
+ { key: "commits", iconName: "commit" as const, label: "Commits", value: stats.commits },
77
+ { key: "issues", iconName: "issue" as const, label: "Issues", value: stats.issues },
78
+ { key: "repos", iconName: "repo" as const, label: "Repos", value: stats.repos },
79
+ { key: "prs", iconName: "pr" as const, label: "PRs", value: stats.prs },
80
+ ].filter((stat) => !hiddenStats.has(stat.key));
81
+
82
+ const statsMarkup = visibleStats
83
+ .map(
84
+ (stat, index) => `<g transform="translate(${index * 100},0)">
85
+ ${icon(stat.iconName, c.icon, 16)}
86
+ <text x="20" y="12" class="stat">${kFormat(stat.value)}</text>
87
+ <text x="0" y="${statLabelY}" class="stat-label">${stat.label}</text>
88
+ </g>`,
89
+ )
90
+ .join("");
91
+
92
+ return `<svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc" shape-rendering="auto" text-rendering="optimizeLegibility" image-rendering="optimizeQuality">
93
+ <title id="title">${svgTitle}</title>
94
+ <desc id="desc">GitHub stats card for ${name} including stars, commits, issues, repositories, pull requests, and top languages.</desc>
95
+ <defs>
96
+ <clipPath id="a"><circle cx="${P + avatarSize / 2}" cy="${P + avatarSize / 2}" r="${avatarSize / 2}"/></clipPath>
97
+ <clipPath id="b"><rect x="${P}" y="${barY}" width="${barWidth}" height="8" rx="4"/></clipPath>
98
+ <filter id="shadow" x="-10%" y="-10%" width="130%" height="140%">
99
+ <feGaussianBlur in="SourceAlpha" stdDeviation="2"/>
100
+ <feOffset dx="0" dy="1" result="offsetblur"/>
101
+ <feComponentTransfer>
102
+ <feFuncA type="linear" slope="0.15"/>
103
+ </feComponentTransfer>
104
+ <feMerge>
105
+ <feMergeNode/>
106
+ <feMergeNode in="SourceGraphic"/>
107
+ </feMerge>
108
+ </filter>
109
+ </defs>
110
+ <style>
111
+ ${FONT_FACE}
112
+ *{font-family:${FONT_FAMILY}}
113
+ .bg{fill:#${c.bg}}
114
+ .title{font-size:18px;font-weight:700;fill:#${c.title}}
115
+ .user{font-size:12px;fill:#${c.text};opacity:.7}
116
+ ${compact ? "" : `.bio{font-size:11px;fill:#${c.text};opacity:.65}.tw{font-size:11px;fill:#${c.text};opacity:.7}.lang{font-size:10px;fill:#${c.text}}`}
117
+ .stat{font-size:14px;font-weight:700;fill:#${c.text};text-anchor:start}
118
+ .stat-label{font-size:9px;font-weight:600;fill:#${c.text};opacity:.55;text-transform:uppercase;letter-spacing:.6px;text-anchor:start}
119
+ .sec{font-size:9px;font-weight:600;fill:#${c.text};opacity:.5;text-transform:uppercase;letter-spacing:.6px}
120
+ </style>
121
+ <rect class="bg" width="${W}" height="${H}" rx="10" stroke="${hideBorder ? "none" : `#${c.border}`}" stroke-width="1.5" filter="url(#shadow)"/>
122
+ <circle cx="${P + avatarSize / 2}" cy="${P + avatarSize / 2}" r="${avatarSize / 2 + 2}" fill="none" stroke="#${c.border}" stroke-width="1" opacity=".6"/>
123
+ <image href="${avatar}" x="${P}" y="${P}" width="${avatarSize}" height="${avatarSize}" clip-path="url(#a)" filter="url(#shadow)" preserveAspectRatio="xMidYMid slice"/>
124
+ <text x="${infoX}" y="${nameY}" class="title">${name}</text>
125
+ <text x="${infoX}" y="${usernameY}" class="user">@${uname}${pronouns ? ` · ${pronouns}` : ""}</text>
126
+ ${!compact && bioLines[0] ? `<text x="${infoX}" y="${bioY}" class="bio">${bioLines[0]}</text>` : ""}
127
+ ${!compact && bioLines[1] ? `<text x="${infoX}" y="${bioY + 12}" class="bio">${bioLines[1]}</text>` : ""}
128
+ ${!compact && twitter ? `<g transform="translate(${infoX},${twitterY - 9})">${icon("x", c.icon, 11)}<text x="14" y="9" class="tw">@${twitter}</text></g>` : ""}
129
+ <g transform="translate(${P},${headerY})">
130
+ ${statsMarkup}
131
+ </g>
132
+ <text x="${P}" y="${barY - 8}" class="sec">Top Languages</text>
133
+ <rect x="${P}" y="${barY}" width="${barWidth}" height="8" rx="4" fill="#${c.text}" opacity=".1"/>
134
+ <g clip-path="url(#b)">${langRects}</g>
135
+ ${langLabels}
136
+ </svg>`;
137
+ }