diff --git a/api/top-langs.js b/api/top-langs.js index 382ee4205a87e..922e850816c9a 100644 --- a/api/top-langs.js +++ b/api/top-langs.js @@ -33,6 +33,8 @@ export default async (req, res) => { border_color, disable_animations, hide_progress, + progress_bar_border_color, + progress_bar_border_thickness, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -104,6 +106,11 @@ export default async (req, res) => { locale: locale ? locale.toLowerCase() : null, disable_animations: parseBoolean(disable_animations), hide_progress: parseBoolean(hide_progress), + progress_bar_border_color, + progress_bar_border_thickness: parseInt( + progress_bar_border_thickness || "1", + 10, + ), }), ); } catch (err) { diff --git a/api/wakatime.js b/api/wakatime.js index de263e0644c43..233e6374db307 100644 --- a/api/wakatime.js +++ b/api/wakatime.js @@ -22,6 +22,8 @@ export default async (req, res) => { cache_seconds, hide_title, hide_progress, + progress_bar_border_color, + progress_bar_border_thickness, custom_title, locale, layout, @@ -80,6 +82,11 @@ export default async (req, res) => { bg_color, theme, hide_progress, + progress_bar_border_color, + progress_bar_border_thickness: parseInt( + progress_bar_border_thickness || "1", + 10, + ), border_radius, border_color, locale: locale ? locale.toLowerCase() : null, diff --git a/readme.md b/readme.md index 363b008b2f8c2..ba52abaa38469 100644 --- a/readme.md +++ b/readme.md @@ -413,6 +413,8 @@ If we don't support your language, please consider contributing! You can find mo | `custom_title` | Sets a custom title for the card. | string | `Most Used Languages` | | `disable_animations` | Disables all animations in the card. | boolean | `false` | | `hide_progress` | Uses the compact layout option, hides percentages, and removes the bars. | boolean | `false` | +| `progress_bar_border_color` | Sets the border color for the progress bar. | string (hex color or none for no border) | `none` | +| `progress_bar_border_thickness` | Sets the border thickness for the progress bar. Does not apply when `progress_bar_border_color` is `none`. | number | `1` | | `size_weight` | Configures language stats algorithm (see [Language stats algorithm](#language-stats-algorithm)). | integer | `1` | | `count_weight` | Configures language stats algorithm (see [Language stats algorithm](#language-stats-algorithm)). | integer | `0` | @@ -429,6 +431,8 @@ If we don't support your language, please consider contributing! You can find mo | `hide_title` | Hides the title of your card. | boolean | `false` | | `line_height` | Sets the line height between text. | integer | `25` | | `hide_progress` | Hides the progress bar and percentage. | boolean | `false` | +| `progress_bar_border_color` | Sets the border color for the progress bar. | string (hex color or none for no border) | `none` | +| `progress_bar_border_thickness` | Sets the border thickness for the progress bar. Does not apply when `progress_bar_border_color` is `none`. | number | `1` | | `custom_title` | Sets a custom title for the card. | string | `WakaTime Stats` | | `layout` | Switches between two available layouts `default` & `compact`. | enum | `default` | | `langs_count` | Limits the number of languages on the card, defaults to all reported languages. | integer | `null` | diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js index 9385f4a7ebed3..360f29b3a2299 100644 --- a/src/cards/top-languages-card.js +++ b/src/cards/top-languages-card.js @@ -205,9 +205,19 @@ const trimTopLanguages = (topLangs, langs_count, hide) => { * @param {string} props.name Name of the programming language. * @param {number} props.progress Usage of the programming language in percentage. * @param {number} props.index Index of the programming language. + * @param {string} props.progressBarBorderColor Border color of the progress bar. + * @param {number} props.progressBarBorderThickness Border thickness of the progress bar. * @returns {string} Programming language SVG node. */ -const createProgressTextNode = ({ width, color, name, progress, index }) => { +const createProgressTextNode = ({ + width, + color, + name, + progress, + index, + progressBarBorderColor, + progressBarBorderThickness, +}) => { const staggerDelay = (index + 3) * 150; const paddingRight = 95; const progressTextX = width - paddingRight + 10; @@ -224,6 +234,8 @@ const createProgressTextNode = ({ width, color, name, progress, index }) => { width: progressWidth, progress, progressBarBackgroundColor: "#ddd", + progressBarBorderColor, + progressBarBorderThickness, delay: staggerDelay + 300, })} @@ -322,9 +334,17 @@ const createDonutLanguagesNode = ({ langs, totalSize }) => { * @param {Lang[]} langs Array of programming languages. * @param {number} width Card width. * @param {number} totalLanguageSize Total size of all languages. + * @param {string} progressBarBorderColor Border color of progress bar. + * @param {number} progressBarBorderThickness Border thickness of progress bar. * @returns {string} Normal layout card SVG object. */ -const renderNormalLayout = (langs, width, totalLanguageSize) => { +const renderNormalLayout = ( + langs, + width, + totalLanguageSize, + progressBarBorderColor, + progressBarBorderThickness, +) => { return flexLayout({ items: langs.map((lang, index) => { return createProgressTextNode({ @@ -335,6 +355,8 @@ const renderNormalLayout = (langs, width, totalLanguageSize) => { ((lang.size / totalLanguageSize) * 100).toFixed(2), ), index, + progressBarBorderColor, + progressBarBorderThickness, }); }), gap: 40, @@ -348,10 +370,19 @@ const renderNormalLayout = (langs, width, totalLanguageSize) => { * @param {Lang[]} langs Array of programming languages. * @param {number} width Card width. * @param {number} totalLanguageSize Total size of all languages. + * @param {number} progressBarBorderThickness Border thickness of the progress bar + * @param {string} progressBarBorderColor Border color of the progress bar * @param {boolean=} hideProgress Whether to hide progress bar. * @returns {string} Compact layout card SVG object. */ -const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => { +const renderCompactLayout = ( + langs, + width, + totalLanguageSize, + progressBarBorderThickness, + progressBarBorderColor, + hideProgress, +) => { const paddingRight = 50; const offsetWidth = width - paddingRight; // progressOffset holds the previous language's width and used to offset the next language @@ -386,10 +417,20 @@ const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => { hideProgress ? "" : ` - + ${compactProgressBar} + ` } @@ -730,6 +771,8 @@ const renderTopLanguages = (topLangs, options = {}) => { bg_color, hide, hide_progress, + progress_bar_border_color, + progress_bar_border_thickness = 1, theme, layout, custom_title, @@ -766,6 +809,7 @@ const renderTopLanguages = (topLangs, options = {}) => { text_color, bg_color, border_color, + progress_bar_border_color, theme, }); @@ -791,6 +835,8 @@ const renderTopLanguages = (topLangs, options = {}) => { langs, width, totalLanguageSize, + progress_bar_border_thickness, + colors.progressBarBorderColor, hide_progress, ); } else if (layout === "donut") { @@ -798,7 +844,13 @@ const renderTopLanguages = (topLangs, options = {}) => { width = width + 50; // padding finalLayout = renderDonutLayout(langs, width, totalLanguageSize); } else { - finalLayout = renderNormalLayout(langs, width, totalLanguageSize); + finalLayout = renderNormalLayout( + langs, + width, + totalLanguageSize, + colors.progressBarBorderColor, + progress_bar_border_thickness, + ); } const card = new Card({ diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index 9a21be4a0160a..c2b9485e574b2 100644 --- a/src/cards/types.d.ts +++ b/src/cards/types.d.ts @@ -13,6 +13,12 @@ export type CommonOptions = { hide_border: boolean; }; +export type ProgressOptions = { + hide_progress: boolean; + progress_bar_border_color: string; + progress_bar_border_thickness: number; +}; + export type StatCardOptions = CommonOptions & { hide: string[]; show_icons: boolean; @@ -35,28 +41,29 @@ export type RepoCardOptions = CommonOptions & { description_lines_count: number; }; -export type TopLangOptions = CommonOptions & { - hide_title: boolean; - card_width: number; - hide: string[]; - layout: "compact" | "normal" | "donut" | "donut-vertical" | "pie"; - custom_title: string; - langs_count: number; - disable_animations: boolean; - hide_progress: boolean; -}; +export type TopLangOptions = CommonOptions & + ProgressOptions & { + hide_title: boolean; + card_width: number; + hide: string[]; + layout: "compact" | "normal" | "donut" | "donut-vertical" | "pie"; + custom_title: string; + langs_count: number; + disable_animations: boolean; + }; -export type WakaTimeOptions = CommonOptions & { - hide_title: boolean; - hide: string[]; - line_height: string; - hide_progress: boolean; - custom_title: string; - layout: "compact" | "normal"; - langs_count: number; - display_format: "time" | "percent"; - disable_animations: boolean; -}; +export type WakaTimeOptions = CommonOptions & + ProgressOptions & { + hide_title: boolean; + hide: string[]; + line_height: string; + hide_progress: boolean; + custom_title: string; + layout: "compact" | "normal"; + langs_count: number; + display_format: "time" | "percent"; + disable_animations: boolean; + }; export type GistCardOptions = CommonOptions & { show_owner: boolean; diff --git a/src/cards/wakatime-card.js b/src/cards/wakatime-card.js index 65e1d54d779ea..149055d40dc57 100644 --- a/src/cards/wakatime-card.js +++ b/src/cards/wakatime-card.js @@ -117,6 +117,8 @@ 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 {string} args.progressBarBorderColor The color of the progress bar border. + * @param {number} args.progressBarBorderThickness The thickness of the progress bar border. * @returns {string} The text SVG node. */ const createTextNode = ({ @@ -128,6 +130,8 @@ const createTextNode = ({ hideProgress, progressBarColor, progressBarBackgroundColor, + progressBarBorderColor, + progressBarBorderThickness, }) => { const staggerDelay = (index + 3) * 150; @@ -142,6 +146,8 @@ const createTextNode = ({ // @ts-ignore name: label, progressBarBackgroundColor, + progressBarBorderColor, + progressBarBorderThickness, delay: staggerDelay + 300, }); @@ -231,6 +237,8 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { bg_color, theme = "default", hide_progress, + progress_bar_border_color, + progress_bar_border_thickness = 1, custom_title, locale, layout, @@ -264,15 +272,22 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { const langsCount = clampValue(langs_count, 1, langs_count); // returns theme based colors with proper overrides and defaults - const { titleColor, textColor, iconColor, bgColor, borderColor } = - getCardColors({ - title_color, - icon_color, - text_color, - bg_color, - border_color, - theme, - }); + const { + titleColor, + textColor, + iconColor, + bgColor, + borderColor, + progressBarBorderColor, + } = getCardColors({ + title_color, + icon_color, + text_color, + bg_color, + border_color, + progress_bar_border_color, + theme, + }); const filteredLanguages = languages .filter((language) => language.hours || language.minutes) @@ -322,6 +337,23 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { }) .join(""); + const hasProgressBarBorder = progressBarBorderColor !== "none"; + const progressBarBorder = hasProgressBarBorder + ? ` + + ` + : ""; + finalLayout = ` @@ -343,7 +375,7 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { : i18n.t("wakatimecard.nocodedetails") : i18n.t("wakatimecard.notpublic"), }) - } + }${progressBarBorder} `; } else { finalLayout = flexLayout({ @@ -359,6 +391,8 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { progressBarColor: titleColor, // @ts-ignore progressBarBackgroundColor: textColor, + progressBarBorderColor, + progressBarBorderThickness: progress_bar_border_thickness, hideProgress: hide_progress, }); }) diff --git a/src/common/createProgressNode.js b/src/common/createProgressNode.js index 2d7303a5a78ef..d5d25867404df 100644 --- a/src/common/createProgressNode.js +++ b/src/common/createProgressNode.js @@ -12,6 +12,8 @@ import { clampValue } from "./utils.js"; * @param {string} createProgressNodeParams.color Progress color. * @param {number} createProgressNodeParams.progress Progress value. * @param {string} createProgressNodeParams.progressBarBackgroundColor Progress bar bg color. + * @param {string} createProgressNodeParams.progressBarBorderColor Progress bar border color. + * @param {number} createProgressNodeParams.progressBarBorderThickness Progress bar border thickness. * @param {number} createProgressNodeParams.delay Delay before animation starts. * @returns {string} Progress node. */ @@ -22,6 +24,8 @@ const createProgressNode = ({ color, progress, progressBarBackgroundColor, + progressBarBorderColor, + progressBarBorderThickness, delay, }) => { const progressPercentage = clampValue(progress, 2, 100); @@ -38,6 +42,18 @@ const createProgressNode = ({ style="animation-delay: ${delay}ms;" /> + + `; }; diff --git a/src/common/utils.js b/src/common/utils.js index 48ea051783b7f..979d31ea3f6c9 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -247,6 +247,7 @@ const request = (data, headers) => { * bgColor: string | string[]; * borderColor: string; * ringColor: string; + * progressBarBorderColor: string; * }} CardColors */ @@ -260,6 +261,7 @@ const request = (data, headers) => { * @param {string=} args.bg_color Card background color. * @param {string=} args.border_color Card border color. * @param {string=} args.ring_color Card ring color. + * @param {string=} args.progress_bar_border_color Progress bar border color. * @param {string=} args.theme Card theme. * @param {string=} args.fallbackTheme Fallback theme. * @returns {CardColors} Card colors. @@ -271,6 +273,7 @@ const getCardColors = ({ bg_color, border_color, ring_color, + progress_bar_border_color, theme, fallbackTheme = "default", }) => { @@ -278,6 +281,9 @@ const getCardColors = ({ const selectedTheme = themes[theme] || defaultTheme; const defaultBorderColor = selectedTheme.border_color || defaultTheme.border_color; + const defaultProgressBarBorderColor = + selectedTheme.progress_bar_border_color || + defaultTheme.progress_bar_border_color; // get the color provided by the user else the theme color // finally if both colors are invalid fallback to default theme @@ -310,19 +316,33 @@ const getCardColors = ({ "#" + defaultBorderColor, ); + const progressBarBorderColor = fallbackColor( + progress_bar_border_color || defaultProgressBarBorderColor, + "none", + ); + if ( typeof titleColor !== "string" || typeof textColor !== "string" || typeof ringColor !== "string" || typeof iconColor !== "string" || - typeof borderColor !== "string" + typeof borderColor !== "string" || + typeof progressBarBorderColor !== "string" ) { throw new Error( "Unexpected behavior, all colors except background should be string.", ); } - return { titleColor, iconColor, textColor, bgColor, borderColor, ringColor }; + return { + titleColor, + iconColor, + textColor, + bgColor, + borderColor, + ringColor, + progressBarBorderColor, + }; }; // Script parameters. diff --git a/tests/__snapshots__/renderWakatimeCard.test.js.snap b/tests/__snapshots__/renderWakatimeCard.test.js.snap index f38ac26ef07f7..cc78f98678cb3 100644 --- a/tests/__snapshots__/renderWakatimeCard.test.js.snap +++ b/tests/__snapshots__/renderWakatimeCard.test.js.snap @@ -299,6 +299,352 @@ exports[`Test Render WakaTime Card should render correctly with compact layout w + + + + + " +`; + +exports[`Test Render WakaTime Card when progress_bar_border_color is set should render correctly 1`] = ` +" + + + + + + + + + + + + + WakaTime Stats (last 7 days) + + + + + + + + + + Other: + 19 mins + + + + + + + + + + + + + + TypeScript: + 1 min + + + + + + + + + + + + + + + + + " +`; + +exports[`Test Render WakaTime Card when progress_bar_border_color is set should render correctly with compact layout 1`] = ` +" + + + + + + + + + + + + + WakaTime Stats (last 7 days) + + + + + + + + + + + + + + + + + + + + + Other - 19 mins + + + + + + + TypeScript - 1 min + + + + + + diff --git a/tests/renderWakatimeCard.test.js b/tests/renderWakatimeCard.test.js index 7c3710758c5ec..fa53be362f411 100644 --- a/tests/renderWakatimeCard.test.js +++ b/tests/renderWakatimeCard.test.js @@ -26,6 +26,25 @@ describe("Test Render WakaTime Card", () => { expect(card).toMatchSnapshot(); }); + describe("when progress_bar_border_color is set", () => { + it("should render correctly", () => { + const card = renderWakatimeCard(wakaTimeData.data, { + progress_bar_border_color: "fff", + }); + + expect(card).toMatchSnapshot(); + }); + + it("should render correctly with compact layout", () => { + const card = renderWakatimeCard(wakaTimeData.data, { + progress_bar_border_color: "fff", + layout: "compact", + }); + + expect(card).toMatchSnapshot(); + }); + }); + it("should hide languages when hide is passed", () => { document.body.innerHTML = renderWakatimeCard(wakaTimeData.data, { hide: ["YAML", "Other"], diff --git a/tests/utils.test.js b/tests/utils.test.js index 5bd43955485c3..8f722b9b993dc 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -71,6 +71,7 @@ describe("Test utils.js", () => { icon_color: "00f", bg_color: "fff", border_color: "fff", + progress_bar_border_color: "fff", theme: "dark", }); expect(colors).toStrictEqual({ @@ -80,6 +81,7 @@ describe("Test utils.js", () => { ringColor: "#0000ff", bgColor: "#fff", borderColor: "#fff", + progressBarBorderColor: "#fff", }); }); @@ -90,6 +92,7 @@ describe("Test utils.js", () => { icon_color: "00f", bg_color: "fff", border_color: "invalidColor", + progress_bar_border_color: "invalidColor", theme: "dark", }); expect(colors).toStrictEqual({ @@ -99,6 +102,7 @@ describe("Test utils.js", () => { ringColor: "#2f80ed", bgColor: "#fff", borderColor: "#e4e2e2", + progressBarBorderColor: "none", }); }); @@ -113,6 +117,7 @@ describe("Test utils.js", () => { iconColor: "#79ff97", bgColor: "#151515", borderColor: "#e4e2e2", + progressBarBorderColor: "none", }); }); @@ -123,6 +128,7 @@ describe("Test utils.js", () => { icon_color: "00f", bg_color: "fff", border_color: "fff", + progress_bar_border_color: "fff", theme: "dark", }); expect(colors).toStrictEqual({ @@ -132,6 +138,28 @@ describe("Test utils.js", () => { ringColor: "#f00", bgColor: "#fff", borderColor: "#fff", + progressBarBorderColor: "#fff", + }); + }); + + it("getCardColors: should return progress bar border color equal to none if not progress bar border color is defined", () => { + let colors = getCardColors({ + title_color: "f00", + text_color: "0f0", + ring_color: "0000ff", + icon_color: "00f", + bg_color: "fff", + border_color: "fff", + theme: "dark", + }); + expect(colors).toStrictEqual({ + titleColor: "#f00", + textColor: "#0f0", + iconColor: "#00f", + ringColor: "#0000ff", + bgColor: "#fff", + borderColor: "#fff", + progressBarBorderColor: "none", }); }); });