Skip to content

Commit 5e6c08b

Browse files
authored
fix(generator): replace hardcoded SVG dimensions with width="100%" for responsive mobile scaling (JhaSourav07#1707)
## What does this PR do? Fixes the SVG badge overflowing and appearing blurry on mobile GitHub profile READMEs by removing hardcoded `width` and `height` pixel attributes from all SVG root elements and replacing them with `width="100%"`. The `viewBox` attribute already encodes the intrinsic 600×420 dimensions and handles aspect ratio — the fixed pixel attributes were actively preventing browsers from scaling the badge to fit narrow containers. ## Pillar - [x] Other: Bug fix / responsive design ## Fixes Closes JhaSourav07#1703 ## Changes | File | Change | |------|--------| | `lib/svg/generator.ts` | Removed `width="${W}" height="${H}"` from `generateSVG` | | `lib/svg/generator.ts` | Removed `width="${W}" height="${H}"` from `generateAutoThemeSVG` | | `lib/svg/generator.ts` | Removed `width/height` from `generateNotFoundSVG` | | `lib/svg/generator.ts` | Removed `width/height` from `generateRateLimitSVG` | | `lib/svg/generator.test.ts` | Updated size tests to assert `width="100%"` + correct `viewBox` | | `lib/svg/generator.additional.test.ts` | Updated versus size tests similarly | ## Before vs After **Before** (broken on mobile): ```svg <svg width="600" height="420" viewBox="0 0 600 420" ...> ``` **After** (responsive): ```svg <svg width="100%" viewBox="0 0 600 420" ...> ``` ## Why this is safe - `viewBox` already exists on all SVG variants — no dimensions are lost - Desktop GitHub renders SVGs at natural size when `width="100%"` and the container is wider than 600px - The CONTRIBUTING.md restriction ("do not increase beyond 600×420") applies to making the badge bigger — not to responsive behavior - All existing tests updated and passing ✅ ## Checklist - [x] npm run test passes locally - [x] npm run lint passes locally - [x] npm run format passes locally - [x] My commits follow Conventional Commits format - [x] No new dependencies added
2 parents 6a0d470 + fa58e90 commit 5e6c08b

3 files changed

Lines changed: 50 additions & 48 deletions

File tree

lib/svg/generator.additional.test.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,10 +285,32 @@ describe('[Issue] generateVersusSVG — zero existing test coverage', () => {
285285
expect(svg).toContain('stroke-dasharray="4 4"');
286286
});
287287

288-
it('total width is double the single card width for medium size', () => {
288+
it('total width viewBox is double the single card width for medium size', () => {
289289
const svg = generateVersusSVG(stats1, stats2, versusParams, calendar1, calendar2);
290-
// Medium size: SVG_WIDTH=600, versus = 600*2=1200
291-
expect(svg).toContain('width="1200"');
290+
expect(svg).toContain('viewBox="0 0 1200 420"');
291+
expect(svg).toContain('width="100%"');
292+
});
293+
294+
it('renders correct viewBox for small size in versus SVG', () => {
295+
const svg = generateVersusSVG(
296+
stats1,
297+
stats2,
298+
{ ...versusParams, size: 'small' },
299+
calendar1,
300+
calendar2
301+
);
302+
expect(svg).toContain('viewBox="0 0 800 280"');
303+
});
304+
305+
it('renders correct viewBox for large size in versus SVG', () => {
306+
const svg = generateVersusSVG(
307+
stats1,
308+
stats2,
309+
{ ...versusParams, size: 'large' },
310+
calendar1,
311+
calendar2
312+
);
313+
expect(svg).toContain('viewBox="0 0 1600 560"');
292314
});
293315

294316
it('includes total contribution counts from both users in <desc>', () => {

lib/svg/generator.test.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -768,37 +768,35 @@ describe('generateSVG', () => {
768768
});
769769

770770
describe('SVG dimensions per size', () => {
771-
it('renders width="600" and height="420" for medium size (default)', () => {
771+
it('renders responsive width="100%" for medium size (default)', () => {
772772
const svg = generateSVG(
773773
mockStats,
774774
{ user: 'avi', size: 'medium' } as unknown as BadgeParams,
775775
mockCalendar
776776
);
777-
778-
expect(svg).toContain('width="600"');
779-
expect(svg).toContain('height="420"');
777+
expect(svg).toContain('width="100%"');
778+
// viewBox should still carry the correct pixel dimensions
779+
expect(svg).toContain('viewBox="0 0 600 420"');
780780
});
781781

782-
it('renders width="400" and height="280" for small size', () => {
782+
it('renders responsive width="100%" for small size with correct viewBox', () => {
783783
const svg = generateSVG(
784784
mockStats,
785785
{ user: 'avi', size: 'small' } as unknown as BadgeParams,
786786
mockCalendar
787787
);
788-
789-
expect(svg).toContain('width="400"');
790-
expect(svg).toContain('height="280"');
788+
expect(svg).toContain('width="100%"');
789+
expect(svg).toContain('viewBox="0 0 400 280"');
791790
});
792791

793-
it('renders width="800" and height="560" for large size', () => {
792+
it('renders responsive width="100%" for large size with correct viewBox', () => {
794793
const svg = generateSVG(
795794
mockStats,
796795
{ user: 'avi', size: 'large' } as unknown as BadgeParams,
797796
mockCalendar
798797
);
799-
800-
expect(svg).toContain('width="800"');
801-
expect(svg).toContain('height="560"');
798+
expect(svg).toContain('width="100%"');
799+
expect(svg).toContain('viewBox="0 0 800 560"');
802800
});
803801
});
804802

lib/svg/generator.ts

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ export function generateSVG(
522522
const mainAccentHex = mainAccent.startsWith('#') ? mainAccent : `#${mainAccent}`;
523523

524524
return `
525-
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" fill="none" role="img">
525+
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 ${W} ${H}" fill="none" role="img">
526526
${renderHeader(safeUser, stats, sf, params)}
527527
${renderStyle(selectedFont, statsFont, googleFontsImport, text, mainAccentHex, sf, bg)}
528528
<rect width="${W}" height="${H}" rx="${radius}" fill="${params.hideBackground ? 'transparent' : bg}" ${borderAttr} />
@@ -572,8 +572,7 @@ function generateAutoThemeSVG(
572572
return `
573573
<svg
574574
xmlns="http://www.w3.org/2000/svg"
575-
width="${W}"
576-
height="${H}"
575+
width="100%"
577576
viewBox="0 0 ${W} ${H}"
578577
fill="none"
579578
role="img"
@@ -931,13 +930,13 @@ export function generateWrappedSVG(
931930
932931
.title-user { font-family: ${selectedFont || '"Syncopate", sans-serif'}; font-weight: 700; font-size: 13px; letter-spacing: 2.5px; opacity: 0.85; }
933932
.title-wrapped { font-family: ${selectedFont || '"Syncopate", sans-serif'}; font-weight: 700; font-size: 13px; letter-spacing: 2.5px; }
934-
933+
935934
.total-commits { font-family: ${statsFont}; font-size: 46px; font-weight: 700; }
936935
.total-label { font-family: "Roboto", sans-serif; font-size: 9.5px; font-weight: 700; letter-spacing: 1.5px; opacity: 0.5; }
937-
936+
938937
.grid-label { font-family: "Roboto", sans-serif; font-size: 9px; font-weight: 700; letter-spacing: 1.5px; opacity: 0.5; }
939938
.grid-val { font-family: ${statsFont}; font-size: 14.5px; font-weight: 600; }
940-
939+
941940
.scan-line {
942941
animation: scan-sweep var(--scan-speed, 8s) linear infinite;
943942
transform-box: fill-box;
@@ -954,12 +953,10 @@ export function generateWrappedSVG(
954953
955954
<rect width="420" height="260" rx="${radius}" ${rectFill} ${borderStroke} />
956955
957-
<!-- Background holographic mini-monolith -->
958956
<g opacity="0.8">
959957
${bgTowersMarkup}
960958
</g>
961959
962-
<!-- Horizontal holographic scan-line -->
963960
<rect
964961
x="15"
965962
y="15"
@@ -971,48 +968,37 @@ export function generateWrappedSVG(
971968
style="--scan-speed: ${params.speed || '8s'};"
972969
/>
973970
974-
<!-- Header -->
975971
<g transform="translate(25, 45)">
976972
<text x="0" y="0" class="title-user" ${textClass}>${safeUser.toUpperCase()}'S GRIND</text>
977973
<text x="370" y="0" text-anchor="end" class="title-wrapped" ${accentClass}>${year} WRAPPED</text>
978974
<line x1="0" y1="12" x2="370" y2="12" stroke="${params.autoTheme ? 'var(--cp-accent)' : accent}" stroke-opacity="0.15" stroke-width="1" />
979975
</g>
980976
981-
<!-- Left Stats Block (Total contributions) -->
982977
<g transform="translate(25, 120)">
983978
<text x="0" y="15" class="total-commits" ${accentClass} filter="url(#glow)">${stats.totalContributions}</text>
984979
<text x="2" y="38" class="total-label" ${textClass}>TOTAL CONTRIBUTIONS</text>
985980
</g>
986981
987-
<!-- Vertical Divider -->
988982
<line x1="185" y1="80" x2="185" y2="230" stroke="${params.autoTheme ? 'var(--cp-accent)' : accent}" stroke-opacity="0.12" stroke-width="1" stroke-dasharray="3 3" />
989983
990-
<!-- Right Section: 2x2 Spaced Grid -->
991-
<!-- Row 1: Top Language (Col 1) and Weekend Grind (Col 2) -->
992984
<g transform="translate(210, 80)">
993-
<!-- Top Language -->
994985
<g transform="translate(0, 20)">
995986
<text x="0" y="0" class="grid-label" ${textClass}>TOP LANGUAGE</text>
996987
<text x="0" y="20" class="grid-val" ${accentClass}>${stats.topLanguage || 'Unknown'}</text>
997988
</g>
998989
999-
<!-- Weekend Grind Progress Arc -->
1000990
<g transform="translate(130, 20)">
1001991
<text x="0" y="0" class="grid-label" ${textClass}>WEEKEND GRIND</text>
1002992
<g transform="translate(25, 24)">
1003-
<!-- Background Track -->
1004993
<circle cx="0" cy="0" r="14" stroke="${params.autoTheme ? 'var(--cp-text)' : text}" stroke-opacity="0.1" stroke-width="2.5" fill="none" />
1005-
<!-- Progress Bar -->
1006994
<circle cx="0" cy="0" r="14" stroke="${params.autoTheme ? 'var(--cp-accent)' : accent}" stroke-width="3" fill="none"
1007995
stroke-dasharray="${circ.toFixed(2)}" stroke-dashoffset="${strokeDashoffset.toFixed(2)}"
1008996
stroke-linecap="round" transform="rotate(-90)" />
1009-
<!-- Text inside progress circle -->
1010997
<text x="0" y="3.5" text-anchor="middle" font-family="${statsFont}" font-size="9" font-weight="700" ${textClass}>${clampedRatio}%</text>
1011998
</g>
1012999
</g>
10131000
</g>
10141001
1015-
<!-- Row 2: Peak Day -->
10161002
<g transform="translate(210, 150)">
10171003
<text x="0" y="0" class="grid-label" ${textClass}>PEAK DAY</text>
10181004
<text x="0" y="20" class="grid-val" ${textClass}>
@@ -1021,7 +1007,6 @@ export function generateWrappedSVG(
10211007
</text>
10221008
</g>
10231009
1024-
<!-- Row 3: Busiest Month -->
10251010
<g transform="translate(210, 205)">
10261011
<text x="0" y="0" class="grid-label" ${textClass}>BUSIEST MONTH</text>
10271012
<text x="0" y="20" class="grid-val" ${textClass}>
@@ -1104,7 +1089,7 @@ function generateAutoThemeMonthlySVG(stats: MonthlyStats, params: BadgeParams):
11041089
.cp-text-fill { fill: var(--cp-text); color: var(--cp-text); }
11051090
.cp-accent-fill { fill: var(--cp-accent); color: var(--cp-accent); }
11061091
.cp-delta-fill { fill: ${stats.deltaAbsolute >= 0 ? 'var(--cp-accent)' : 'var(--cp-negative)'}; }
1107-
1092+
11081093
.title { font-family: ${selectedFont || '"Syncopate", sans-serif'}; fill: var(--cp-text); font-size: 14px; letter-spacing: 2px; font-weight: 400; opacity: 0.8; }
11091094
.stats { font-family: ${statsFont}; fill: var(--cp-accent); font-size: 36px; font-weight: 600; letter-spacing: 0; }
11101095
.label { font-family: "Roboto", sans-serif; fill: var(--cp-text); font-size: 10px; font-weight: 400; letter-spacing: 1px; opacity: 0.7; }
@@ -1210,8 +1195,7 @@ export function generateNotFoundSVG(
12101195

12111196
return `<svg
12121197
xmlns="http://www.w3.org/2000/svg"
1213-
width="${SVG_WIDTH}"
1214-
height="${SVG_HEIGHT}"
1198+
width="100%"
12151199
viewBox="0 0 ${SVG_WIDTH} ${SVG_HEIGHT}"
12161200
fill="none"
12171201
role="img"
@@ -1360,13 +1344,13 @@ export function generateVersusSVG(
13601344
const unit = params.mode === 'loc' ? 'lines of code' : 'total contributions';
13611345

13621346
return `
1363-
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" fill="none" role="img">
1347+
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 ${W} ${H}" fill="none" role="img">
13641348
<title>CommitPulse Versus Stats: ${safeUser1} vs ${safeUser2}</title>
13651349
<desc>${safeUser1} has ${stats1.totalContributions} ${unit}. ${safeUser2} has ${stats2.totalContributions} ${unit}.</desc>
13661350
${renderDefs(sf, params)}
13671351
${renderStyle(selectedFont, statsFont, googleFontsImport, text, accent, sf, bg)}
13681352
<rect width="${W}" height="${H}" rx="${radius}" fill="${params.hideBackground ? 'transparent' : bg}" />
1369-
1353+
13701354
<g transform="translate(0, 0)">
13711355
<g transform="translate(0, ${Math.round(20 * sf)})">${towers1}</g>
13721356
${renderIsometricLabels(calendar1, params, text, sf)}
@@ -1380,7 +1364,7 @@ export function generateVersusSVG(
13801364
</g>
13811365
13821366
<line x1="${singleW}" y1="${s(40)}" x2="${singleW}" y2="${H - s(40)}" stroke="${text}" stroke-opacity="0.2" stroke-width="2" stroke-dasharray="4 4" />
1383-
1367+
13841368
<g transform="translate(${singleW}, ${H / 2})">
13851369
<circle cx="0" cy="0" r="${s(24)}" fill="${bg}" stroke="${accent}" stroke-width="2" />
13861370
<text x="0" y="${s(6)}" text-anchor="middle" font-family="${statsFont}" fill="${accent}" font-size="${s(16)}" font-weight="bold">VS</text>
@@ -1497,11 +1481,11 @@ function generateAutoThemeVersusSVG(
14971481
const unit = params.mode === 'loc' ? 'lines of code' : 'total contributions';
14981482

14991483
return `
1500-
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" fill="none" role="img">
1484+
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 ${W} ${H}" fill="none" role="img">
15011485
<title>CommitPulse Versus Stats: ${safeUser1} vs ${safeUser2}</title>
15021486
<desc>${safeUser1} has ${stats1.totalContributions} ${unit}. ${safeUser2} has ${stats2.totalContributions} ${unit}.</desc>
15031487
${renderDefs(sf, params)}
1504-
1488+
15051489
<style>
15061490
@import url('https://fonts.googleapis.com/css2?family=Fira+Code&amp;family=JetBrains+Mono&amp;family=Roboto&amp;family=Syncopate:wght@400;700&amp;family=Space+Grotesk:wght@400;500;600;700&amp;display=swap');
15071491
:root { --cp-bg: #${light.bg}; --cp-text: #${light.text}; --cp-accent: #${light.accent}; --cp-label-fill: ${lightLabelFill}; --cp-label-opacity: ${lightLabelOpacity}; }
@@ -1534,7 +1518,7 @@ function generateAutoThemeVersusSVG(
15341518
</style>
15351519
15361520
<rect width="${W}" height="${H}" rx="${radius}" class="${params.hideBackground ? '' : 'cp-bg-fill'}" fill="${params.hideBackground ? 'transparent' : ''}" />
1537-
1521+
15381522
<g transform="translate(0, 0)">
15391523
<g transform="translate(0, ${Math.round(20 * sf)})">${towers1}</g>
15401524
${renderIsometricLabels(calendar1, params, '', sf)}
@@ -1548,7 +1532,7 @@ function generateAutoThemeVersusSVG(
15481532
</g>
15491533
15501534
<line x1="${singleW}" y1="${s(40)}" x2="${singleW}" y2="${H - s(40)}" stroke="var(--cp-text)" stroke-opacity="0.2" stroke-width="2" stroke-dasharray="4 4" />
1551-
1535+
15521536
<g transform="translate(${singleW}, ${H / 2})">
15531537
<circle cx="0" cy="0" r="${s(24)}" class="cp-bg-fill" stroke="var(--cp-accent)" stroke-width="2" />
15541538
<text x="0" y="${s(6)}" text-anchor="middle" font-family="${statsFont}" class="cp-accent-fill" font-size="${s(16)}" font-weight="bold">VS</text>
@@ -1614,8 +1598,7 @@ export function generateRateLimitSVG(
16141598

16151599
return `<svg
16161600
xmlns="http://www.w3.org/2000/svg"
1617-
width="${SVG_WIDTH}"
1618-
height="${SVG_HEIGHT}"
1601+
width="100%"
16191602
viewBox="0 0 ${SVG_WIDTH} ${SVG_HEIGHT}"
16201603
fill="none"
16211604
role="img"
@@ -1662,7 +1645,6 @@ export function generateRateLimitSVG(
16621645
16631646
<rect x="180" y="62" width="240" height="1" fill="${accent}" fill-opacity="0.15"/>
16641647
1665-
<!-- Warning mark -->
16661648
<circle cx="300" cy="190" r="32" fill="none"
16671649
stroke="${accent}" stroke-width="1.2" stroke-opacity="0.3" filter="url(#softglow)"/>
16681650
<path d="M300 172 V200 M300 210 V210.1"

0 commit comments

Comments
 (0)