2026-08-01 14:13:44 -04:00
|
|
|
import { find, findByStoreName } from "@vendetta/metro";
|
|
|
|
|
import { semanticColors } from "@vendetta/ui";
|
|
|
|
|
|
2026-08-01 10:34:47 -04:00
|
|
|
export const HEX_REGEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
|
|
|
|
2026-08-01 14:13:44 -04:00
|
|
|
const ThemeStore = findByStoreName("ThemeStore");
|
|
|
|
|
|
|
|
|
|
// @vendetta/ui exposes the semanticColors token map but not a resolver function for it - every
|
|
|
|
|
// plugin that needs one has to find it the same way, so it lives here instead of being
|
|
|
|
|
// reimplemented per plugin.
|
|
|
|
|
const rawResolveSemanticColor: (theme: any, semanticColor: any) => string | undefined =
|
|
|
|
|
find((m: any) => m?.default?.internal?.resolveSemanticColor)?.default?.internal?.resolveSemanticColor ??
|
|
|
|
|
find((m: any) => m?.meta?.resolveSemanticColor)?.meta?.resolveSemanticColor ??
|
|
|
|
|
(() => undefined);
|
|
|
|
|
|
|
|
|
|
/** Resolves a semanticColors[TOKEN_NAME] descriptor to a real color for the current theme. */
|
|
|
|
|
export function resolveSemanticColor(token: any, theme: any = ThemeStore?.theme): string | undefined {
|
|
|
|
|
if (!token) return undefined;
|
|
|
|
|
try {
|
|
|
|
|
return rawResolveSemanticColor(theme, token);
|
|
|
|
|
} catch {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Same as resolveSemanticColor, but tries a list of token names in order and falls back to a
|
|
|
|
|
* hardcoded hex color if every one of them is missing or fails to resolve - Discord renames these
|
|
|
|
|
* tokens across versions often enough that a single hardcoded name isn't reliable on its own.
|
|
|
|
|
*/
|
|
|
|
|
export function resolveSemanticColorSafe(tokenNames: string[], fallbackHex: string, theme: any = ThemeStore?.theme): string {
|
|
|
|
|
for (const name of tokenNames) {
|
|
|
|
|
const token = (semanticColors as Record<string, any> | undefined)?.[name];
|
|
|
|
|
if (!token) continue;
|
|
|
|
|
const resolved = resolveSemanticColor(token, theme);
|
|
|
|
|
if (resolved) return resolved;
|
|
|
|
|
}
|
|
|
|
|
return fallbackHex;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 10:34:47 -04:00
|
|
|
export function isValidHex(value: string | undefined | null): value is string {
|
|
|
|
|
return !!value && HEX_REGEX.test(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Expands short form #abc to #aabbcc; leaves 6-digit hex untouched. */
|
|
|
|
|
export function normalizeHex(hex: string): string {
|
|
|
|
|
if (hex.length === 4) {
|
|
|
|
|
return "#" + hex.slice(1).split("").map((c) => c + c).join("");
|
|
|
|
|
}
|
|
|
|
|
return hex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function interpolateColor(color1: string, color2: string, percentage: number): string {
|
|
|
|
|
const hexToRgb = (hex: string) => (normalizeHex(hex).match(/\w\w/g) ?? ["00", "00", "00"]).map((x) => parseInt(x, 16));
|
|
|
|
|
const rgbToHex = (rgb: number[]) => "#" + rgb.map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
|
|
|
|
|
|
|
|
const rgb1 = hexToRgb(color1);
|
|
|
|
|
const rgb2 = hexToRgb(color2);
|
|
|
|
|
const interpolated = rgb1.map((c1, i) => Math.round(c1 + (rgb2[i] - c1) * percentage));
|
|
|
|
|
|
|
|
|
|
return rgbToHex(interpolated);
|
|
|
|
|
}
|