From ed7a8c3ff884b5a3651c14adf3e4a4e28aca988e Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Thu, 19 Mar 2026 21:45:14 -0700 Subject: [PATCH] fix: improve combined card layout - Swap streak positions: longest streak (gold trophy) on left, current streak (orange fire) on right - Full-width language progress bar in the title area - Language items aligned with stats rows (same 25px spacing, 14px font) - Proper ring/icon sizing matching the standalone streak card - Remove all divider lines for cleaner look - Add gold trophy ring + icon for longest streak - Reduce card width to 650px - Add preview script for local iteration Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/preview-combined.js | 41 ++++++++++ src/cards/combined.js | 154 +++++++++++++++++++++--------------- 2 files changed, 130 insertions(+), 65 deletions(-) create mode 100644 scripts/preview-combined.js diff --git a/scripts/preview-combined.js b/scripts/preview-combined.js new file mode 100644 index 0000000..704e2ab --- /dev/null +++ b/scripts/preview-combined.js @@ -0,0 +1,41 @@ +import { renderCombinedCard } from "../src/cards/combined.js"; +import { writeFileSync } from "fs"; + +const mockData = { + overview: { + name: "Jason Ernst", + totalStars: 1878, + totalForks: 243, + totalCommits: 25963, + linesChanged: 9604194, + repoViews: 5487, + contributedTo: 167, + }, + streak: { + currentStreak: 11, + currentStreakStart: "2026-03-09", + currentStreakEnd: "2026-03-19", + longestStreak: 39, + longestStreakStart: "2024-07-22", + longestStreakEnd: "2024-08-29", + }, + langs: { + Kotlin: { name: "Kotlin", color: "#A97BFF", size: 5296 }, + C: { name: "C", color: "#555555", size: 898 }, + Shell: { name: "Shell", color: "#89e051", size: 704 }, + TypeScript: { name: "TypeScript", color: "#3178c6", size: 663 }, + Java: { name: "Java", color: "#b07219", size: 500 }, + JavaScript: { name: "JavaScript", color: "#f1e05a", size: 452 }, + Python: { name: "Python", color: "#3572A5", size: 347 }, + Go: { name: "Go", color: "#00ADD8", size: 300 }, + }, +}; + +const svg = renderCombinedCard(mockData, { + theme: "dark", + langs_count: 8, + disable_animations: true, +}); + +writeFileSync("preview-combined.svg", svg); +console.log("Written to preview-combined.svg"); diff --git a/src/cards/combined.js b/src/cards/combined.js index c3acc6c..bf79392 100644 --- a/src/cards/combined.js +++ b/src/cards/combined.js @@ -5,7 +5,7 @@ import { getCardColors } from "../common/color.js"; import { icons } from "../common/icons.js"; import { clampValue } from "../common/ops.js"; -const CARD_DEFAULT_WIDTH = 800; +const CARD_DEFAULT_WIDTH = 550; const STREAK_COLOR = "#FB8C00"; /** @@ -92,9 +92,8 @@ const renderCombinedCard = (data, options = {}) => { const width = card_width && !isNaN(card_width) ? card_width : CARD_DEFAULT_WIDTH; - const leftColWidth = width * 0.5; - const rightColX = leftColWidth + 10; - const rightColWidth = width - rightColX - 25; + const leftColWidth = width * 0.68; + const rightColX = leftColWidth + 15; // Colors. const { titleColor, iconColor, textColor, bgColor, borderColor } = @@ -148,8 +147,8 @@ const renderCombinedCard = (data, options = {}) => { const statsHeight = statsData.length * lineHeight; // Streak section. - const streakSectionY = statsHeight + 10; - const streakHeight = 100; + const streakSectionY = statsHeight; + const streakHeight = 148; // Languages. const langEntries = Object.values(langs); @@ -175,7 +174,7 @@ const renderCombinedCard = (data, options = {}) => { ${stat.icon} ${stat.label} - ${stat.value} + ${stat.value} `; }) @@ -194,99 +193,112 @@ const renderCombinedCard = (data, options = {}) => { ) : ""; - const streakCenterLeft = leftColWidth * 0.3; - const streakCenterRight = leftColWidth * 0.7; - const streakMid = leftColWidth * 0.5; + const streakAreaWidth = leftColWidth - 30; + const streakCenterLeft = streakAreaWidth * 0.25; + const streakCenterRight = streakAreaWidth * 0.7; const streakSection = ` - - + + - - - + + + + + + + + + + + + + + + ${streak.longestStreak} + + Longest Streak + + ${longestDates} - + + + + + + + - - - + - ${streak.currentStreak} + ${streak.currentStreak} - Current Streak - ${currentDates} - - - - - - ${streak.longestStreak} - - Longest Streak - - ${longestDates} + ${currentDates} `; // Build languages SVG. - const progressBarWidth = rightColWidth; - // Create stacked progress bar using SVG offsets. - let progressOffset = 0; - const progressSegments = topLangs - .map((lang) => { - const percent = (lang.size / totalSize) * progressBarWidth; - const segment = ``; - progressOffset += percent; - return segment; - }) - .join(""); - const langItems = topLangs .map((lang, i) => { const percent = ((lang.size / totalSize) * 100).toFixed(2); - const y = i * 20; + const y = i * 25; const staggerDelay = (i + 3) * 150; return ` - - ${lang.name} ${percent}% + + ${lang.name} ${percent}% `; }) .join(""); - const languagesSection = ` - - - - - - ${progressSegments} + // Progress bar rendered at absolute position in the title area, spanning full width. + const fullBarWidth = width - 50; + let fullBarOffset = 0; + const fullBarSegments = topLangs + .map((lang) => { + const segWidth = (lang.size / totalSize) * fullBarWidth; + const segment = ``; + fullBarOffset += segWidth; + return segment; + }) + .join(""); + + const progressBarSection = ` + + + + ${fullBarSegments} - - - ${langItems} - `; - // Vertical divider between columns. - const colDivider = ``; + const languagesSection = ` + + ${langItems} + + `; + + const colDivider = ""; // CSS. const cssStyles = ` @@ -306,7 +318,7 @@ const renderCombinedCard = (data, options = {}) => { display: block; } .lang-name { - font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; + font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor}; } .streak-number { @@ -321,6 +333,10 @@ const renderCombinedCard = (data, options = {}) => { font: 700 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${streakColor}; } + .streak-label-longest { + font: 700 12px 'Segoe UI', Ubuntu, Sans-Serif; + fill: #FFD700; + } .streak-dates { font: 400 10px 'Segoe UI', Ubuntu, Sans-Serif; fill: #9E9E9E; @@ -357,10 +373,10 @@ const renderCombinedCard = (data, options = {}) => { desc: `Stars: ${overview.totalStars}, Forks: ${overview.totalForks}, Contributions: ${overview.totalCommits}, Current streak: ${streak.currentStreak} days, Longest streak: ${streak.longestStreak} days`, }); - return card.render(` + const cardSvg = card.render(` - + ${statsItems} ${streakSection} @@ -370,6 +386,14 @@ const renderCombinedCard = (data, options = {}) => { ${languagesSection} `); + + // Inject progress bar into the title area by inserting before the final . + const lastSvgClose = cardSvg.lastIndexOf(""); + return ( + cardSvg.slice(0, lastSvgClose) + + progressBarSection + + cardSvg.slice(lastSvgClose) + ); }; export { renderCombinedCard };