From 3e82a19c620e9e41d3c44e5d8c9717cd91cb4da9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:58:51 +0000 Subject: [PATCH 1/6] Initial plan From c46da47f341195edb519b4a2630b3c4b545ef5c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 01:03:15 +0000 Subject: [PATCH 2/6] Add CSS color validator utility and tests Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- frontend/util/color-validator.test.ts | 43 ++++++++++++++++++++ frontend/util/color-validator.ts | 57 +++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 frontend/util/color-validator.test.ts create mode 100644 frontend/util/color-validator.ts diff --git a/frontend/util/color-validator.test.ts b/frontend/util/color-validator.test.ts new file mode 100644 index 0000000000..9deca2f0b8 --- /dev/null +++ b/frontend/util/color-validator.test.ts @@ -0,0 +1,43 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { validateCssColor } from "./color-validator"; + +describe("validateCssColor", () => { + beforeEach(() => { + vi.stubGlobal("CSS", { + supports: (_property: string, value: string) => { + return [ + "red", + "#aabbcc", + "#aabbccdd", + "rgb(255, 0, 0)", + "rgba(255, 0, 0, 0.5)", + "hsl(120 100% 50%)", + "transparent", + "currentColor", + ].includes(value); + }, + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("returns type for supported CSS color formats", () => { + expect(validateCssColor("red")).toBe("keyword"); + expect(validateCssColor("#aabbcc")).toBe("hex"); + expect(validateCssColor("#aabbccdd")).toBe("hex8"); + expect(validateCssColor("rgb(255, 0, 0)")).toBe("rgb"); + expect(validateCssColor("rgba(255, 0, 0, 0.5)")).toBe("rgba"); + expect(validateCssColor("hsl(120 100% 50%)")).toBe("hsl"); + expect(validateCssColor("transparent")).toBe("transparent"); + expect(validateCssColor("currentColor")).toBe("currentcolor"); + }); + + it("throws for invalid CSS colors", () => { + expect(() => validateCssColor(":not-a-color:" as any)).toThrow("Invalid CSS color"); + expect(() => validateCssColor("#12")).toThrow("Invalid CSS color"); + expect(() => validateCssColor("rgb(255, 0)")).toThrow("Invalid CSS color"); + }); +}); diff --git a/frontend/util/color-validator.ts b/frontend/util/color-validator.ts new file mode 100644 index 0000000000..c3217e3bfe --- /dev/null +++ b/frontend/util/color-validator.ts @@ -0,0 +1,57 @@ +const HexColorRegex = /^#([\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i; +const FunctionalColorRegex = /^([a-z-]+)\(/i; +const NamedColorRegex = /^[a-z]+$/i; + +function isValidCssColor(color: string): boolean { + if (typeof CSS != "undefined" && typeof CSS.supports == "function") { + return CSS.supports("color", color); + } + if (typeof document == "undefined") { + return false; + } + const temp = document.createElement("div"); + temp.style.color = ""; + temp.style.color = color; + return temp.style.color != ""; +} + +function getCssColorType(color: string): string { + const normalizedColor = color.toLowerCase(); + if (HexColorRegex.test(normalizedColor)) { + if (normalizedColor.length === 4) { + return "hex3"; + } + if (normalizedColor.length === 5) { + return "hex4"; + } + if (normalizedColor.length === 9) { + return "hex8"; + } + return "hex"; + } + if (normalizedColor === "transparent") { + return "transparent"; + } + if (normalizedColor === "currentcolor") { + return "currentcolor"; + } + const functionMatch = normalizedColor.match(FunctionalColorRegex); + if (functionMatch) { + return functionMatch[1]; + } + if (NamedColorRegex.test(normalizedColor)) { + return "keyword"; + } + return "color"; +} + +export function validateCssColor(color: string): string { + if (typeof color != "string") { + throw new Error(`Invalid CSS color: ${String(color)}`); + } + const normalizedColor = color.trim(); + if (normalizedColor == "" || !isValidCssColor(normalizedColor)) { + throw new Error(`Invalid CSS color: ${color}`); + } + return getCssColorType(normalizedColor); +} From 7ed95d9b5ba2f788b8e8ebf7aabed7a09fda9cd0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 01:06:47 +0000 Subject: [PATCH 3/6] Refine color validator test typing Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- frontend/util/color-validator.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/util/color-validator.test.ts b/frontend/util/color-validator.test.ts index 9deca2f0b8..fccfbaca1b 100644 --- a/frontend/util/color-validator.test.ts +++ b/frontend/util/color-validator.test.ts @@ -36,7 +36,7 @@ describe("validateCssColor", () => { }); it("throws for invalid CSS colors", () => { - expect(() => validateCssColor(":not-a-color:" as any)).toThrow("Invalid CSS color"); + expect(() => validateCssColor(":not-a-color:")).toThrow("Invalid CSS color"); expect(() => validateCssColor("#12")).toThrow("Invalid CSS color"); expect(() => validateCssColor("rgb(255, 0)")).toThrow("Invalid CSS color"); }); From 9c46dcbfefb61147a9e24e887639c675206aa219 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 01:38:40 +0000 Subject: [PATCH 4/6] Remove DOM fallback from color validator Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- frontend/util/color-validator.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/frontend/util/color-validator.ts b/frontend/util/color-validator.ts index c3217e3bfe..cf76f7219d 100644 --- a/frontend/util/color-validator.ts +++ b/frontend/util/color-validator.ts @@ -3,16 +3,10 @@ const FunctionalColorRegex = /^([a-z-]+)\(/i; const NamedColorRegex = /^[a-z]+$/i; function isValidCssColor(color: string): boolean { - if (typeof CSS != "undefined" && typeof CSS.supports == "function") { - return CSS.supports("color", color); - } - if (typeof document == "undefined") { + if (typeof CSS == "undefined" || typeof CSS.supports != "function") { return false; } - const temp = document.createElement("div"); - temp.style.color = ""; - temp.style.color = color; - return temp.style.color != ""; + return CSS.supports("color", color); } function getCssColorType(color: string): string { From d9419109d3ccda3a552ccf0b9ed8f3e46bffa52d Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 27 Feb 2026 15:48:09 -0800 Subject: [PATCH 5/6] add header, update copyright --- Taskfile.yml | 2 +- frontend/util/color-validator.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Taskfile.yml b/Taskfile.yml index c9aa8bc6a2..f4055489d2 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,4 +1,4 @@ -# Copyright 2024, Command Line Inc. +# Copyright 2026, Command Line Inc. # SPDX-License-Identifier: Apache-2.0 version: "3" diff --git a/frontend/util/color-validator.ts b/frontend/util/color-validator.ts index cf76f7219d..510ee0fb62 100644 --- a/frontend/util/color-validator.ts +++ b/frontend/util/color-validator.ts @@ -1,3 +1,6 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + const HexColorRegex = /^#([\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i; const FunctionalColorRegex = /^([a-z-]+)\(/i; const NamedColorRegex = /^[a-z]+$/i; From e3a3c73f88abada78315c5737777207cb5415574 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 27 Feb 2026 15:48:45 -0800 Subject: [PATCH 6/6] fix nit --- frontend/util/color-validator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/util/color-validator.ts b/frontend/util/color-validator.ts index 510ee0fb62..6be4671e23 100644 --- a/frontend/util/color-validator.ts +++ b/frontend/util/color-validator.ts @@ -47,7 +47,7 @@ export function validateCssColor(color: string): string { throw new Error(`Invalid CSS color: ${String(color)}`); } const normalizedColor = color.trim(); - if (normalizedColor == "" || !isValidCssColor(normalizedColor)) { + if (normalizedColor === "" || !isValidCssColor(normalizedColor)) { throw new Error(`Invalid CSS color: ${color}`); } return getCssColorType(normalizedColor);