From 39b93b57c8c8170df7b2ba028fa4c159dea81a72 Mon Sep 17 00:00:00 2001 From: martin-mfg <2026226+martin-mfg@users.noreply.github.com> Date: Sun, 5 Oct 2025 22:03:25 +0200 Subject: [PATCH] feat: wakatime card width customization (#4458) * add card_width to wakatime card * better handling of card_width in wakatime card * move default card_width for wakatime card, add to readme * review * review --------- Co-authored-by: Alexandr --- api/wakatime.js | 2 ++ readme.md | 1 + src/cards/types.d.ts | 1 + src/cards/wakatime.js | 61 +++++++++++++++++++++++++++++++------------ 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/api/wakatime.js b/api/wakatime.js index 69f0ce7d..4b9ce1f8 100644 --- a/api/wakatime.js +++ b/api/wakatime.js @@ -16,6 +16,7 @@ export default async (req, res) => { title_color, icon_color, hide_border, + card_width, line_height, text_color, bg_color, @@ -90,6 +91,7 @@ export default async (req, res) => { custom_title, hide_title: parseBoolean(hide_title), hide_border: parseBoolean(hide_border), + card_width: parseInt(card_width, 10), hide: parseArray(hide), line_height, title_color, diff --git a/readme.md b/readme.md index d1348777..dab957dd 100644 --- a/readme.md +++ b/readme.md @@ -648,6 +648,7 @@ You can customize the appearance and behavior of the WakaTime stats card using t | --- | --- | --- | --- | | `hide` | Hides the languages specified from the card. | string (comma-separated values) | `null` | | `hide_title` | Hides the title of your card. | boolean | `false` | +| `card_width` | Sets the card's width manually. | number | `495` | | `line_height` | Sets the line height between text. | integer | `25` | | `hide_progress` | Hides the progress bar and percentage. | boolean | `false` | | `custom_title` | Sets a custom title for the card. | string | `WakaTime Stats` | diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index 94f36adc..c7d8c60f 100644 --- a/src/cards/types.d.ts +++ b/src/cards/types.d.ts @@ -51,6 +51,7 @@ export type TopLangOptions = CommonOptions & { export type WakaTimeOptions = CommonOptions & { hide_title: boolean; hide: string[]; + card_width: number; line_height: string; hide_progress: boolean; custom_title: string; diff --git a/src/cards/wakatime.js b/src/cards/wakatime.js index 65e1d54d..64ded83d 100644 --- a/src/cards/wakatime.js +++ b/src/cards/wakatime.js @@ -1,4 +1,5 @@ // @ts-check + import { Card } from "../common/Card.js"; import { createProgressNode } from "../common/createProgressNode.js"; import { I18n } from "../common/I18n.js"; @@ -21,6 +22,10 @@ import { createRequire } from "module"; const require = createRequire(import.meta.url); const languageColors = require("../common/languageColors.json"); // now works +const DEFAULT_CARD_WIDTH = 495; +const MIN_CARD_WIDTH = 250; +const COMPACT_LAYOUT_MIN_WIDTH = 400; + /** * Creates the no coding activity SVG node. * @@ -84,22 +89,21 @@ const createCompactLangNode = ({ lang, x, y, display_format }) => { * @param {WakaTimeLang[]} args.langs The language objects. * @param {number} args.y The y position of the language node. * @param {"time" | "percent"} args.display_format The display format of the language node. + * @param {number} args.card_width Width in px of the card. * @returns {string[]} The language text node items. */ -const createLanguageTextNode = ({ langs, y, display_format }) => { +const createLanguageTextNode = ({ langs, y, display_format, card_width }) => { + const LEFT_X = 25; + const RIGHT_X_BASE = 230; + const rightOffset = (card_width - DEFAULT_CARD_WIDTH) / 2; + const RIGHT_X = RIGHT_X_BASE + rightOffset; + return langs.map((lang, index) => { - if (index % 2 === 0) { - return createCompactLangNode({ - lang, - x: 25, - y: 12.5 * index + y, - display_format, - }); - } + const isLeft = index % 2 === 0; return createCompactLangNode({ lang, - x: 230, - y: 12.5 + 12.5 * index, + x: isLeft ? LEFT_X : RIGHT_X, + y: isLeft ? 12.5 * index + y : 12.5 + 12.5 * index, display_format, }); }); @@ -117,6 +121,7 @@ const createLanguageTextNode = ({ langs, y, display_format }) => { * @param {boolean=} args.hideProgress Whether to hide the progress bar. * @param {string} args.progressBarColor The color of the progress bar. * @param {string} args.progressBarBackgroundColor The color of the progress bar background. + * @param {number} args.progressBarWidth The width of the progress bar. * @returns {string} The text SVG node. */ const createTextNode = ({ @@ -128,6 +133,7 @@ const createTextNode = ({ hideProgress, progressBarColor, progressBarBackgroundColor, + progressBarWidth, }) => { const staggerDelay = (index + 3) * 150; @@ -138,7 +144,7 @@ const createTextNode = ({ y: 4, progress: percent, color: progressBarColor, - width: 220, + width: progressBarWidth, // @ts-ignore name: label, progressBarBackgroundColor, @@ -150,7 +156,7 @@ const createTextNode = ({ ${label}: ${value} ${cardProgress} @@ -206,6 +212,24 @@ const getStyles = ({ `; }; +/** + * Normalize incoming width (string or number) and clamp to minimum. + * + * @param {Object} args The function arguments. + * @param {WakaTimeOptions["layout"] | undefined} args.layout The incoming layout value. + * @param {number|undefined} args.value The incoming width value. + * @returns {number} The normalized width value. + */ +const normalizeCardWidth = ({ value, layout }) => { + if (value === undefined || value === null || isNaN(value)) { + return DEFAULT_CARD_WIDTH; + } + return Math.max( + layout === "compact" ? COMPACT_LAYOUT_MIN_WIDTH : MIN_CARD_WIDTH, + value, + ); +}; + /** * @typedef {import('../fetchers/types').WakaTimeData} WakaTimeData * @typedef {import('./types').WakaTimeOptions} WakaTimeOptions @@ -223,6 +247,7 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { const { hide_title = false, hide_border = false, + card_width, hide, line_height = 25, title_color, @@ -241,6 +266,8 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { disable_animations, } = options; + const normalizedWidth = normalizeCardWidth({ value: card_width, layout }); + const shouldHideLangs = Array.isArray(hide) && hide.length > 0; if (shouldHideLangs) { const languagesToHide = new Set(hide.map((lang) => lowercaseTrim(lang))); @@ -289,11 +316,9 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { let finalLayout = ""; - let width = 440; - // RENDER COMPACT LAYOUT if (layout === "compact") { - width = width + 50; + let width = normalizedWidth - 5; height = 90 + Math.round(filteredLanguages.length / 2) * 25; // progressOffset holds the previous language's width and used to offset the next language @@ -333,6 +358,7 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { y: 25, langs: filteredLanguages, display_format, + card_width: normalizedWidth, }).join("") : noCodingActivityNode({ // @ts-ignore @@ -360,6 +386,7 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { // @ts-ignore progressBarBackgroundColor: textColor, hideProgress: hide_progress, + progressBarWidth: normalizedWidth - 275, }); }) : [ @@ -392,7 +419,7 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { const card = new Card({ customTitle: custom_title, defaultTitle: titleText, - width: 495, + width: normalizedWidth, height, border_radius, colors: {