diff --git a/packages/core/src/cards/gist.ts b/packages/core/src/cards/gist.ts index 05434079ecddb..c8039b45625c6 100644 --- a/packages/core/src/cards/gist.ts +++ b/packages/core/src/cards/gist.ts @@ -54,7 +54,6 @@ const renderGistCard = ( } = options; const { lightColors, darkColors } = getLightDarkColors({ ...options, theme }); - const { textColor, iconColor } = lightColors; const desc = parseEmojis(description || "No description provided"); @@ -151,7 +150,7 @@ const renderGistCard = ( }); card.setCSS({ - light: ` + light: ({ textColor, iconColor }) => ` .description { font: 400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif;fill: ${textColor}; ${browser_rendering ? wrappedTextStyles(textColor) : ""} @@ -159,16 +158,14 @@ const renderGistCard = ( .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } .icon { fill: ${iconColor} } `, - dark: darkColors - ? ` + dark: ({ textColor, iconColor }) => ` .description { - fill: ${darkColors.textColor}; - ${browser_rendering ? wrappedTextStyles(darkColors.textColor) : ""} + fill: ${textColor}; + ${browser_rendering ? wrappedTextStyles(textColor) : ""} } - .gray { fill: ${darkColors.textColor} } - .icon { fill: ${darkColors.iconColor} } - ` - : null, + .gray { fill: ${textColor} } + .icon { fill: ${iconColor} } + `, }); card.setHideBorder(hide_border); diff --git a/packages/core/src/cards/repo.ts b/packages/core/src/cards/repo.ts index 4e1bc4e4f6c9f..09ed96a72abd9 100644 --- a/packages/core/src/cards/repo.ts +++ b/packages/core/src/cards/repo.ts @@ -328,17 +328,17 @@ const renderRepoCard = ( card.setHideBorder(hide_border); card.setHideTitle(false); card.setCSS({ - light: ` + light: ({ textColor, iconColor }) => ` .description { - font: 400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif;fill: ${lightColors.textColor}; - ${browser_rendering ? wrappedTextStyles(lightColors.textColor) : ""} + font: 400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif;fill: ${textColor}; + ${browser_rendering ? wrappedTextStyles(textColor) : ""} } - .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${lightColors.textColor} } + .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } .badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; } - .badge rect { opacity: 0.2; stroke: ${lightColors.textColor} } - .badge text { fill: ${lightColors.textColor} } + .badge rect { opacity: 0.2; stroke: ${textColor} } + .badge text { fill: ${textColor} } - .stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${lightColors.textColor} } + .stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } .stagger { opacity: 0; animation: fadeInAnimation 0.3s ease-in-out forwards; @@ -346,23 +346,21 @@ const renderRepoCard = ( .not_bold { font-weight: 400 } .bold { font-weight: 700 } .icon { - fill: ${lightColors.iconColor}; + fill: ${iconColor}; display: block; } `, - dark: darkColors - ? ` + dark: ({ textColor, iconColor }) => ` .description { - fill: ${darkColors.textColor}; - ${browser_rendering ? wrappedTextStyles(darkColors.textColor) : ""} + fill: ${textColor}; + ${browser_rendering ? wrappedTextStyles(textColor) : ""} } - .gray { fill: ${darkColors.textColor} } - .badge rect { stroke: ${darkColors.textColor} } - .badge text { fill: ${darkColors.textColor} } - .stat { fill: ${darkColors.textColor} } - .icon { fill: ${darkColors.iconColor}; } - ` - : null, + .gray { fill: ${textColor} } + .badge rect { stroke: ${textColor} } + .badge text { fill: ${textColor} } + .stat { fill: ${textColor} } + .icon { fill: ${iconColor}; } + `, }); return card.render(` diff --git a/packages/core/src/cards/stats.ts b/packages/core/src/cards/stats.ts index 0ebcf8461a435..eaa2af4b84efe 100644 --- a/packages/core/src/cards/stats.ts +++ b/packages/core/src/cards/stats.ts @@ -1,6 +1,7 @@ import { Card } from "../common/Card.js"; import { I18n } from "../common/I18n.js"; import { getLightDarkColors } from "../common/color.js"; +import type { CardColors } from "../common/color.js"; import { CustomError } from "../common/error.js"; import { icons, rankIcon } from "../common/icons.js"; import { buildSearchFilter, clampValue } from "../common/ops.js"; @@ -276,7 +277,6 @@ const renderStatsCard = ( const lheight = parseInt(String(line_height), 10); const { lightColors, darkColors } = getLightDarkColors(options); - const { iconColor, textColor, ringColor } = lightColors; const apostrophe = /s$/i.test(name.trim()) ? "" : "s"; const i18n = new I18n({ @@ -539,24 +539,14 @@ const renderStatsCard = ( card.setHideBorder(hide_border); card.setHideTitle(hide_title); - card.setCSS({ - light: getStyles({ - ringColor, - textColor, - iconColor, - show_icons, - progress, - }), - dark: darkColors - ? getStyles({ - ringColor: darkColors.ringColor, - textColor: darkColors.textColor, - iconColor: darkColors.iconColor, - show_icons, - progress, - }) - : null, - }); + const cardStyles = ({ + ringColor, + textColor, + iconColor, + }: CardColors): string => + getStyles({ ringColor, textColor, iconColor, show_icons, progress }); + + card.setCSS({ light: cardStyles, dark: cardStyles }); if (disable_animations) { card.disableAnimations(); diff --git a/packages/core/src/cards/top-languages.ts b/packages/core/src/cards/top-languages.ts index 10473252ced3c..c5c7607390776 100644 --- a/packages/core/src/cards/top-languages.ts +++ b/packages/core/src/cards/top-languages.ts @@ -1023,7 +1023,7 @@ const renderTopLanguages = ( card.setHideBorder(hide_border); card.setHideTitle(hide_title); card.setCSS({ - light: ` + light: ({ textColor, progBarBgColor }) => ` @keyframes slideInAnimation { from { width: 0; @@ -1041,7 +1041,7 @@ const renderTopLanguages = ( } } .stat { - font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${lightColors.textColor}; + font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor}; } @supports(-moz-appearance: auto) { /* Selector detects Firefox */ @@ -1050,7 +1050,7 @@ const renderTopLanguages = ( .bold { font-weight: 700 } .lang-name { font: 400 11px "Segoe UI", Ubuntu, Sans-Serif; - fill: ${lightColors.textColor}; + fill: ${textColor}; } .stagger { opacity: 0; @@ -1062,15 +1062,13 @@ const renderTopLanguages = ( .lang-progress{ animation: growWidthAnimation 0.6s ease-in-out forwards; } - .progress-background { fill: ${lightColors.progBarBgColor}; } + .progress-background { fill: ${progBarBgColor}; } + `, + dark: ({ textColor, progBarBgColor }) => ` + .stat { fill: ${textColor}; } + .lang-name { fill: ${textColor}; } + .progress-background { fill: ${progBarBgColor}; } `, - dark: darkColors - ? ` - .stat { fill: ${darkColors.textColor}; } - .lang-name { fill: ${darkColors.textColor}; } - .progress-background { fill: ${darkColors.progBarBgColor}; } - ` - : null, }); if (layout === "pie" || layout === "donut-vertical") { diff --git a/packages/core/src/cards/wakatime.ts b/packages/core/src/cards/wakatime.ts index 236f97533da49..c979032e00b22 100644 --- a/packages/core/src/cards/wakatime.ts +++ b/packages/core/src/cards/wakatime.ts @@ -331,7 +331,6 @@ const renderWakatimeCard = ( const langsCount = clampValue(langs_count, 1, langs_count); const { lightColors, darkColors } = getLightDarkColors(options); - const { titleColor, textColor } = lightColors; const filteredLanguages = languages .filter((language) => language.hours || language.minutes) @@ -453,7 +452,7 @@ const renderWakatimeCard = ( card.setHideBorder(hide_border); card.setHideTitle(hide_title); card.setCSS({ - light: ` + light: ({ titleColor, textColor }) => ` ${getStyles({ textColor })} @keyframes slideInAnimation { from { @@ -481,14 +480,12 @@ const renderWakatimeCard = ( } .progress-background { fill: ${textColor === titleColor ? "#fff0" /* transparent */ : textColor}; } `, - dark: darkColors - ? ` - ${getStyles({ textColor: darkColors.textColor })} - .lang-name { fill: ${darkColors.textColor} } - .lang-progress { fill: ${darkColors.titleColor}; } - .progress-background { fill: ${darkColors.textColor === darkColors.titleColor ? "#fff0" /* transparent */ : darkColors.textColor}; } - ` - : null, + dark: ({ titleColor, textColor }) => ` + ${getStyles({ textColor })} + .lang-name { fill: ${textColor} } + .lang-progress { fill: ${titleColor}; } + .progress-background { fill: ${textColor === titleColor ? "#fff0" /* transparent */ : textColor}; } + `, }); return card.render(` diff --git a/packages/core/src/common/Card.ts b/packages/core/src/common/Card.ts index 05dab887ea4e8..8d5f7ae0d2c2a 100644 --- a/packages/core/src/common/Card.ts +++ b/packages/core/src/common/Card.ts @@ -1,19 +1,12 @@ import { getCardColors, isPrefixedHexColor, isValidGradient } from "./color.js"; +import type { CardColors } from "./color.js"; import { encodeHTML } from "./html.js"; import { flexLayout } from "./render.js"; -interface CardColors { - /** Card title color. */ - titleColor: string; - /** Card text color. */ - textColor: string; - /** Card icon color. */ - iconColor: string; - /** Card background color. */ - bgColor: string | Array; - /** Card border color. */ - borderColor: string; -} +/** + * Builds the card CSS for one color scheme from that scheme's resolved colors. + */ +type CardCSSBuilder = (colors: CardColors) => string; class Card { width: number; @@ -111,15 +104,25 @@ class Card { /** * Sets the card CSS for light and dark mode. * - * The caller must ensure that the passed CSS strings are properly sanitized! + * Each builder receives the colors of its own color scheme, + * so a card never handles a missing dark palette itself: + * `dark` runs only when the card has dark colors, and always with non-null ones. + * + * @warning The caller must ensure that the returned CSS strings are properly sanitized! * * @param props The props object. - * @param props.light CSS applied unconditionally (light/default mode). - * @param props.dark CSS placed inside a `@media (prefers-color-scheme: dark)` block. Pass `null` when not needed. + * @param props.light Builds the CSS applied unconditionally (light/default mode). + * @param props.dark Builds the CSS placed inside a `@media (prefers-color-scheme: dark)` block. */ - setCSS({ light, dark }: { light: string; dark?: string | null }): void { - this.css = light; - this.darkCss = dark ?? ""; + setCSS({ + light, + dark, + }: { + light: CardCSSBuilder; + dark: CardCSSBuilder; + }): void { + this.css = light(this.colors.light); + this.darkCss = this.colors.dark ? dark(this.colors.dark) : ""; } /** diff --git a/packages/core/src/common/color.ts b/packages/core/src/common/color.ts index d991e6cecee97..2efb6749f263f 100644 --- a/packages/core/src/common/color.ts +++ b/packages/core/src/common/color.ts @@ -105,15 +105,22 @@ const fallbackColor = ( }; /** - * Object containing card colors. + * Resolved card colors for a single color scheme, as written into the SVG. */ interface CardColors { + /** Card title color. */ titleColor: string; + /** Card icon color. */ iconColor: string; + /** Card text color. */ textColor: string; + /** Card background color, or a gradient as `[angle, ...stops]`. */ bgColor: string | Array; + /** Card border color. */ borderColor: string; + /** Stats card rank ring color. */ ringColor: string; + /** Progress bar background color. */ progBarBgColor: string; } @@ -338,6 +345,7 @@ const findInvalidColorParam = (params: ColorParams): string | null => ), ); +export type { CardColors }; export type { ColorParams }; export { diff --git a/packages/core/tests/card.test.ts b/packages/core/tests/card.test.ts index 9f590f83396d1..e645902d00a4f 100644 --- a/packages/core/tests/card.test.ts +++ b/packages/core/tests/card.test.ts @@ -122,19 +122,16 @@ describe("Card", () => { }); it("should render with correct colors", () => { - const { titleColor, textColor, iconColor, bgColor, borderColor } = - getCardColors({ - title_color: "f00", - icon_color: "0f0", - text_color: "00f", - bg_color: "fff", - theme: "default", - }); - const card = new Card({ height: 200, colors: { - light: { titleColor, textColor, iconColor, bgColor, borderColor }, + light: getCardColors({ + title_color: "f00", + icon_color: "0f0", + text_color: "00f", + bg_color: "fff", + theme: "default", + }), dark: null, }, }); @@ -152,19 +149,16 @@ describe("Card", () => { }); it("should render gradient backgrounds", () => { - const { titleColor, textColor, iconColor, bgColor, borderColor } = - getCardColors({ - title_color: "f00", - icon_color: "0f0", - text_color: "00f", - bg_color: "90,fff,000,f00", - theme: "default", - }); - const card = new Card({ height: 200, colors: { - light: { titleColor, textColor, iconColor, bgColor, borderColor }, + light: getCardColors({ + title_color: "f00", + icon_color: "0f0", + text_color: "00f", + bg_color: "90,fff,000,f00", + theme: "default", + }), dark: null, }, });