fix: fix RGBA issue

This commit is contained in:
2026-06-26 21:38:04 +09:30
parent e0ee41c270
commit b4aca7277b
4 changed files with 116 additions and 22 deletions
+1 -20
View File
@@ -4,6 +4,7 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { mount, unmount } from "svelte";
import GradeAnalyticsPage from "./GradeAnalyticsPage.svelte";
import { buildContrastAccentPalette } from "./utils/accentColor";
import { extractSolidColor } from "@/seqta/ui/colors/parseCssColor";
type ThemeSettingKey =
| "selectedColor"
@@ -62,26 +63,6 @@ const ACCENT_CSS_VARS = [
"--colour-betterseqta-blue",
] as const;
/** Resolve a solid colour for charts (gradients → first stop). */
function extractSolidColor(value: string): string | null {
const trimmed = value.trim();
if (!trimmed || trimmed === "initial") return null;
if (
trimmed.startsWith("#") ||
trimmed.startsWith("rgb") ||
trimmed.startsWith("hsl")
) {
return trimmed;
}
if (trimmed.includes("gradient")) {
const match = trimmed.match(
/#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}|rgba?\([^)]+\)/i,
);
return match?.[0] ?? null;
}
return null;
}
const THEME_ACCENT_OVERRIDES: Record<string, string> = {
"bb0aaf40-55ef-40f7-bc64-93b67ef96c01": "#4ade80",
};
@@ -1,4 +1,5 @@
import Color from "color";
import { parseCssColor } from "@/seqta/ui/colors/parseCssColor";
export type ContrastAccentPalette = {
accent: string;
@@ -52,8 +53,8 @@ export function buildContrastAccentPalette(
accentRaw: string,
backgroundRaw: string,
): ContrastAccentPalette {
const accent = Color(accentRaw);
const background = Color(backgroundRaw);
const accent = parseCssColor(accentRaw);
const background = parseCssColor(backgroundRaw, "#ffffff");
const isDark = background.isDark();
const { h, s } = accent.hsl().object();
+47
View File
@@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import {
extractSolidColor,
normalizeCssColorString,
parseCssColor,
} from "./parseCssColor";
describe("normalizeCssColorString", () => {
it("lowercases uppercase RGBA/RGB function names", () => {
expect(normalizeCssColorString("RGBA(3, 29, 11, 0.58)")).toBe(
"rgba(3, 29, 11, 0.58)",
);
expect(normalizeCssColorString("RGB(10, 20, 30)")).toBe("rgb(10, 20, 30)");
});
});
describe("extractSolidColor", () => {
it("extracts solid uppercase RGBA values", () => {
expect(extractSolidColor("RGBA(3, 29, 11, 0.58)")).toBe(
"rgba(3, 29, 11, 0.58)",
);
});
it("extracts the first rgba stop from gradients with mixed casing", () => {
expect(
extractSolidColor(
"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)",
),
).toBe("rgba(201,61,0,1)");
});
});
describe("parseCssColor", () => {
it("parses uppercase RGBA without throwing", () => {
const parsed = parseCssColor("RGBA(3, 29, 11, 0.58)");
expect(parsed.alpha()).toBeCloseTo(0.58, 2);
expect(parsed.red()).toBe(3);
expect(parsed.green()).toBe(29);
expect(parsed.blue()).toBe(11);
});
it("falls back when the value is not a colour", () => {
expect(parseCssColor("not-a-color", "#007bff").hex().toLowerCase()).toBe(
"#007bff",
);
});
});
+65
View File
@@ -0,0 +1,65 @@
import Color from "color";
type ColorInstance = ReturnType<typeof Color>;
/**
* SEQTA themes and user gradients often use uppercase `RGBA()` / `RGB()`.
* The `color` package only accepts lowercase function names.
*/
export function normalizeCssColorString(value: string): string {
return value
.trim()
.replace(/\bRGBA?\(/gi, (match) => match.toLowerCase())
.replace(/\bHSLA?\(/gi, (match) => match.toLowerCase());
}
/** Pick a single solid colour from a CSS value (hex, rgb(a), hsl(a), or gradient). */
export function extractSolidColor(value: string): string | null {
const trimmed = normalizeCssColorString(value);
if (!trimmed || trimmed === "initial") return null;
if (
trimmed.startsWith("#") ||
/^rgba?\(/i.test(trimmed) ||
/^hsla?\(/i.test(trimmed)
) {
return trimmed;
}
if (trimmed.includes("gradient")) {
const match = trimmed.match(
/#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}|rgba?\([^)]+\)/gi,
);
return match?.[0] ? normalizeCssColorString(match[0]) : null;
}
return null;
}
/** Parse a CSS colour for the `color` library; never throws. */
export function parseCssColor(value: string, fallback = "#007bff"): ColorInstance {
const candidates = [
extractSolidColor(value),
normalizeCssColorString(value),
].filter((candidate): candidate is string => Boolean(candidate));
for (const candidate of candidates) {
try {
return Color(candidate);
} catch {
// try next strategy
}
const rgbaMatch = candidate.match(
/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)/i,
);
if (rgbaMatch) {
try {
const [, r, g, b, a] = rgbaMatch;
const rgb = Color.rgb(Number(r), Number(g), Number(b));
return a !== undefined ? rgb.alpha(Number(a)) : rgb;
} catch {
// fall through
}
}
}
return Color(fallback);
}