|
| 1 | +import { themes } from "@stats-organization/github-readme-stats-core"; |
| 2 | + |
| 3 | +// We use the same background colors here which GitHub uses. |
| 4 | +export const LIGHT_CARD_BG = "#ffffff"; |
| 5 | +const DARK_CARD_BG = "#0d1117"; |
| 6 | + |
| 7 | +// Themes that look good in either mode (their text/icon colors read on both a |
| 8 | +// light and a dark backdrop), so their backdrop follows the app theme. Note we |
| 9 | +// only list themes that are genuinely mode-agnostic here: other transparent |
| 10 | +// themes (e.g. shadow_*) have dark text and must stay on a light backdrop. |
| 11 | +const ADAPTIVE_THEMES = ["transparent", "ambient_gradient"]; |
| 12 | + |
| 13 | +/** Expand a shorthand `#abc` hex to `#aabbcc`. */ |
| 14 | +function expandHex(hex: string): string { |
| 15 | + if (hex.length === 3) { |
| 16 | + return hex |
| 17 | + .split("") |
| 18 | + .map((char) => char + char) |
| 19 | + .join(""); |
| 20 | + } |
| 21 | + return hex; |
| 22 | +} |
| 23 | + |
| 24 | +/** Whether a hex color (3-, 6- or 8-digit) is dark by perceived luminance. */ |
| 25 | +function isDarkHex(hex: string): boolean { |
| 26 | + const normalized = expandHex(hex); |
| 27 | + if (normalized.length < 6) { |
| 28 | + return false; |
| 29 | + } |
| 30 | + const r = parseInt(normalized.slice(0, 2), 16); |
| 31 | + const g = parseInt(normalized.slice(2, 4), 16); |
| 32 | + const b = parseInt(normalized.slice(4, 6), 16); |
| 33 | + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; |
| 34 | + return luminance < 0.5; |
| 35 | +} |
| 36 | + |
| 37 | +/** Whether a theme's `bg_color` (hex or `angle,c1,c2,…` gradient) is dark. */ |
| 38 | +function isDarkBg(bgColor: string): boolean { |
| 39 | + const parts = bgColor.split(","); |
| 40 | + // For gradients use the first color stop (index 0 is the angle). |
| 41 | + return isDarkHex((parts.length > 1 ? parts[1] : parts[0]) ?? ""); |
| 42 | +} |
| 43 | + |
| 44 | +/** A theme whose backdrop should follow the app mode rather than its own bg. */ |
| 45 | +function isAdaptiveTheme(themeName: string): boolean { |
| 46 | + return ADAPTIVE_THEMES.includes(themeName); |
| 47 | +} |
| 48 | + |
| 49 | +/** |
| 50 | + * Whether the backdrop a card actually gets is dark, for the given app mode. |
| 51 | + * Adaptive themes follow the app mode; the rest follow their own background. |
| 52 | + */ |
| 53 | +function isCardBackdropDark(themeName: string, appIsDark: boolean): boolean { |
| 54 | + const theme = themes[themeName as keyof typeof themes]; |
| 55 | + if (isAdaptiveTheme(themeName)) { |
| 56 | + return appIsDark; |
| 57 | + } |
| 58 | + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition |
| 59 | + if (!theme) { |
| 60 | + return appIsDark; |
| 61 | + } |
| 62 | + return isDarkBg(theme.bg_color); |
| 63 | +} |
| 64 | + |
| 65 | +/** |
| 66 | + * Sort rank for the theme grid: light themes first (0), adaptive themes in the |
| 67 | + * middle (1), dark themes last (2). Adaptive cards sit between the groups so |
| 68 | + * they blend with whichever side matches their backdrop in the current mode. |
| 69 | + */ |
| 70 | +export function getThemeSortRank(themeName: string): number { |
| 71 | + if (isAdaptiveTheme(themeName)) { |
| 72 | + return 1; |
| 73 | + } |
| 74 | + const theme = themes[themeName as keyof typeof themes]; |
| 75 | + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition |
| 76 | + if (!theme) { |
| 77 | + return 0; |
| 78 | + } |
| 79 | + return isDarkBg(theme.bg_color) ? 2 : 0; |
| 80 | +} |
| 81 | + |
| 82 | +/** The backdrop color a card rendered with the given theme should sit on. */ |
| 83 | +export function getCardThemeBackdrop( |
| 84 | + themeName: string, |
| 85 | + appIsDark: boolean, |
| 86 | +): string { |
| 87 | + return isCardBackdropDark(themeName, appIsDark) |
| 88 | + ? DARK_CARD_BG |
| 89 | + : LIGHT_CARD_BG; |
| 90 | +} |
0 commit comments