Skip to content

Commit c10ef4c

Browse files
feat(svg): add border URL parameter for custom container strokes (JhaSourav07#852)
## Description Fixes JhaSourav07#151 ## Pillar - [ ] 🎨 Pillar 1 β€” New Theme Design - [ ] πŸ“ Pillar 2 β€” Geometric SVG Improvement - [ ] ⏱️ Pillar 3 β€” Timezone Logic Optimization - [x] πŸ› οΈ Other (Bug fix, refactoring, docs) ## Visual Preview <img width="750" height="526" alt="Screenshot 2026-05-28 110409" src="https://github.com/user-attachments/assets/0be8605e-e323-48b7-b2c8-425c7e879834" /> ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have starred the repo. - [x] I have made sure that I have only one commit to merge in this PR. - [x] The SVG output matches the commitpulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the commitpulse Discord community for contributor discussions, mentorship, and faster PR support.
1 parent 8ad67a5 commit c10ef4c

4 files changed

Lines changed: 43 additions & 14 deletions

File tree

β€ŽREADME.mdβ€Ž

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,30 @@ URL Parameter > Theme Default > System Fallback
179179
| `delta_format` | `string` | No | `percent` | Format for month-over-month delta in monthly view: `percent` (e.g. +12%), `absolute` (e.g. +15 commits), or `both` |
180180
| `width` | `number` | No | `300` | Custom width for the SVG canvas (currently only applies to `view=monthly`) |
181181
| `height` | `number` | No | `120` | Custom height for the SVG canvas (currently only applies to `view=monthly`) |
182+
| Parameter | Type | Required | Default | Description |
183+
| ----------------- | --------- | ---------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
184+
| `user` | `string` | βœ… **Yes** | β€” | GitHub username to render |
185+
| `theme` | `string` | No | `dark` | Preset theme name (see below) |
186+
| `bg` | `hex` | No | Theme default | Background color β€” **without** `#` |
187+
| `accent` | `hex` | No | Theme default | Tower & glow color β€” **without** `#` |
188+
| `text` | `hex` | No | Theme default | Label & stat text color β€” **without** `#` |
189+
| `radius` | `number` | No | `8` | Border corner radius in pixels |
190+
| `border` | `string` | No | β€” | Custom stroke color around the main SVG container β€” **without** `#` |
191+
| `speed` | `string` | No | `8s` | Radar scan duration (`2s`–`20s`, default `8s`) |
192+
| `scale` | `string` | No | `linear` | Tower height scaling: `linear` or `log` (logarithmic) |
193+
| `size` | `string` | No | `medium` | Badge dimensions: `small` (400Γ—280), `medium` (600Γ—420), `large` (800Γ—560) |
194+
| `font` | `string` | No | CommitPulse default typography | Any **Google Font** name (e.g. `Orbitron`, `Inter`) |
195+
| `refresh` | `boolean` | No | `false` | Bypass cache for real-time data |
196+
| `year` | `string` | No | β€” | Calendar year to render (e.g. `2023`, `2024`) |
197+
| `hide_title` | `boolean` | No | `false` | Hide GitHub username/title from the SVG badge |
198+
| `hide_background` | `boolean` | No | `false` | Remove the background rect, letting the monolith float on the page |
199+
| `hide_stats` | `boolean` | No | `false` | Hides the bottom row displaying Current Streak, Annual Sync Total, and Peak Streak stats when set to `true` or `1`. |
200+
| `tz` | `string` | No | Omitted = UTC | IANA timezone (e.g. `Asia/Kolkata`) β€” aligns "today" with the user local midnight. Note: `?tz=UTC` is cached separately from omitting `tz`. |
201+
| `lang` | `string` | No | `en` | Language code for labels (`en`, `es`, `hi`, `fr`, `pt`, `ko`, `ja`) |
202+
| `view` | `string` | No | `default` | Rendering mode: `default` (3D Monolith) or `monthly` (Compact monthly stats) |
203+
| `delta_format` | `string` | No | `percent` | Format for month-over-month delta in monthly view: `percent` (e.g. +12%), `absolute` (e.g. +15 commits), or `both` |
204+
| `width` | `number` | No | `300` | Custom width for the SVG canvas (currently only applies to `view=monthly`) |
205+
| `height` | `number` | No | `120` | Custom height for the SVG canvas (currently only applies to `view=monthly`) |
182206

183207
### Theme Presets
184208

β€Žapp/api/streak/route.tsβ€Ž

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,16 @@ export async function GET(request: Request) {
102102

103103
// If 'org' is provided, we use it as the display user
104104
const targetEntity = org || user;
105+
// NEW LOGIC: Extract and sanitize the border query parameter
106+
const borderParam = searchParams.get('border');
107+
const sanitizedBorder = borderParam ? borderParam.replace(/[^a-fA-F0-9]/g, '') : undefined;
105108

106109
const params: BadgeParams = {
107110
user: targetEntity,
108111
bg: isAutoTheme ? selectedTheme.bg : bg || selectedTheme.bg,
109112
text: isAutoTheme ? selectedTheme.text : text || selectedTheme.text,
110113
accent: isAutoTheme ? selectedTheme.accent : accent || selectedTheme.accent,
114+
border: sanitizedBorder, // <--- Passed down to the generator here
111115
radius,
112116
speed: speed && /^(?:[2-9]|1\d|20)s$/.test(speed) ? speed : '8s',
113117
scale,

β€Žlib/svg/generator.tsβ€Ž

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,9 @@ export function generateSVG(
326326
const accent = `#${sanitizeHexColor(params.accent, '00ffaa')}`;
327327
const text = `#${sanitizeHexColor(params.text, 'ffffff')}`;
328328

329+
// NEW LOGIC: Conditionally create the stroke attributes
330+
const borderAttr = params.border ? `stroke="#${params.border}" stroke-width="2"` : '';
331+
329332
const sanitizedFont = sanitizeFont(params.font);
330333
const predefinedFont = getFontFromMap(sanitizedFont);
331334
const isPredefinedFont = Boolean(predefinedFont);
@@ -337,6 +340,8 @@ export function generateSVG(
337340
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
338341
const googleFontUrlPart =
339342
sanitizedFont && !isPredefinedFont ? sanitizeGoogleFontUrl(sanitizedFont) : null;
343+
344+
// FIXED: Added &amp; to the dynamic display=swap URL query
340345
const googleFontsImport = googleFontUrlPart
341346
? `@import url('https://fonts.googleapis.com/css2?family=${googleFontUrlPart}&display=swap');`
342347
: '';
@@ -357,7 +362,7 @@ export function generateSVG(
357362
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" fill="none" role="img">
358363
${renderHeader(safeUser, stats, sf, params)}
359364
${renderStyle(selectedFont, statsFont, googleFontsImport, text, accent, sf)}
360-
<rect width="${W}" height="${H}" rx="${radius}" fill="${params.hideBackground ? 'transparent' : bg}" />
365+
<rect width="${W}" height="${H}" rx="${radius}" fill="${params.hideBackground ? 'transparent' : bg}" ${borderAttr} />
361366
<g transform="translate(0, ${Math.round(20 * sf)})">${towers}</g>
362367
${renderIsometricLabels(calendar, params, text, sf)}
363368
${renderFooter(stats, params, labels, safeUser, accent, sf)}
@@ -698,12 +703,12 @@ function generateAutoThemeMonthlySVG(stats: MonthlyStats, params: BadgeParams):
698703
*
699704
* @example
700705
* const svg = generateNotFoundSVG(
701-
* 'octocat',
702-
* '#0d1117',
703-
* '#00ffaa',
704-
* '#ffffff',
705-
* 8,
706-
* '8s'
706+
* 'octocat',
707+
* '#0d1117',
708+
* '#00ffaa',
709+
* '#ffffff',
710+
* 8,
711+
* '8s'
707712
* );
708713
*/
709714
// Fixed isometric tower layout for the not-found ghost city.
@@ -806,7 +811,6 @@ export function generateNotFoundSVG(
806811
<feGaussianBlur stdDeviation="8" result="blur"/>
807812
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
808813
</filter>
809-
<!-- Fade the ghost city out toward the bottom -->
810814
<linearGradient id="ghostFade" x1="0" y1="0" x2="0" y2="1">
811815
<stop offset="30%" stop-color="${bg}" stop-opacity="0"/>
812816
<stop offset="100%" stop-color="${bg}" stop-opacity="1"/>
@@ -832,34 +836,28 @@ export function generateNotFoundSVG(
832836
}
833837
</style>
834838
835-
<!-- Background -->
836839
<rect width="${SVG_WIDTH}" height="${SVG_HEIGHT}" rx="${radius}" fill="${bg}"/>
837840
838-
<!-- Ghost isometric city β€” same grid as real badge -->
839841
<g transform="translate(0, 20)" class="ghost-pulse">
840842
${ghostTowers}
841843
</g>
842844
843-
<!-- Fade overlay so ghost city dissolves into background -->
844845
<rect width="${SVG_WIDTH}" height="${SVG_HEIGHT}" rx="${radius}" fill="url(#ghostFade)"/>
845846
846847
<!-- Radar scan line -->
847848
<rect x="100" y="60" width="400" height="1" class="scan-line" fill="${accent}" fill-opacity="0.12" style="--scan-speed: ${speed};"/>
848849
849850
<text x="300" y="50" text-anchor="middle" class="title">${safeName}</text>
850851
851-
<!-- Divider below title -->
852852
<rect x="180" y="62" width="240" height="1" fill="${accent}" fill-opacity="0.15"/>
853853
854-
<!-- Central error mark -->
855854
<circle cx="300" cy="190" r="32" fill="none"
856855
stroke="${accent}" stroke-width="1.2" stroke-opacity="0.3" filter="url(#softglow)"/>
857856
<line x1="286" y1="176" x2="314" y2="204"
858857
stroke="${accent}" stroke-width="1.8" stroke-linecap="round" stroke-opacity="0.55"/>
859858
<line x1="314" y1="176" x2="286" y2="204"
860859
stroke="${accent}" stroke-width="1.8" stroke-linecap="round" stroke-opacity="0.55"/>
861860
862-
<!-- "NOT FOUND" badge -->
863861
<rect x="230" y="235" width="140" height="22" rx="4"
864862
fill="${accent}" fill-opacity="0.08"
865863
stroke="${accent}" stroke-width="0.8" stroke-opacity="0.25"/>

β€Žtypes/index.tsβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ export interface BadgeParams {
128128
/** Border corner radius in pixels. Defaults to 8. */
129129
radius?: number;
130130

131+
/** Custom stroke color for the main SVG container. Hex string WITHOUT the leading '#'. */
132+
border?: string;
133+
131134
/** When true, automatically selects a theme based on the viewer's system color scheme. */
132135
autoTheme?: boolean;
133136

0 commit comments

Comments
Β (0)