feat: release Colour Tools 0.1.0

This commit is contained in:
2026-08-31 21:21:35 +02:00
commit 0c1fc94b58
100 changed files with 17406 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
import fc from "fast-check";
import { describe, expect, it } from "vitest";
import {
conversionRows,
formatColour,
parseColour,
parseColourList,
toSrgbPreview,
tryParseColour,
} from "../../src/colour";
describe("colour parsing and conversion", () => {
it("converts the RapidTables reference colour", () => {
const colour = parseColour("#f8f5ee");
expect(colour.space).toBe("srgb");
expect(colour.coords).toEqual([248 / 255, 245 / 255, 238 / 255]);
expect(formatColour(colour, "hex")).toBe("#f8f5ee");
expect(formatColour(colour, "rgb")).toBe("rgb(248, 245, 238)");
expect(formatColour(colour, "hsl")).toBe("hsl(42 41.6667% 95.2941%)");
});
it("accepts CSS Color 4, named, bare hex and numeric RGB input", () => {
expect(formatColour("rebeccapurple", "hex")).toBe("#663399");
expect(formatColour("f8f5ee", "hex")).toBe("#f8f5ee");
expect(formatColour("0xff000080", "hex8")).toBe("#ff000080");
expect(formatColour("248, 245, 238", "hex")).toBe("#f8f5ee");
const p3 = parseColour("color(display-p3 1 0.2 0.1 / 75%)");
expect(p3.space).toBe("p3");
expect(p3.alpha).toBeCloseTo(0.75);
});
it("parses custom HSV/HSB with angles and alpha", () => {
expect(formatColour("hsv(120 50% 60% / 25%)", "rgba")).toBe(
"rgba(77, 153, 77, 0.25)",
);
expect(formatColour("hsb(.333333turn, .5, .6)", "hex")).toBe("#4d994d");
expect(parseColour("hsv(-30deg 100% 100%)").coords[0]).toBe(330);
});
it("parses custom CMYK and converts it deterministically", () => {
expect(formatColour("cmyk(0% 100% 100% 0% / .5)", "hex8")).toBe(
"#ff000080",
);
expect(formatColour("cmyk(100%, 0%, 100%, 25%)", "rgb")).toBe(
"rgb(0, 191, 0)",
);
expect(formatColour("#000", "cmyk")).toBe("cmyk(0% 0% 0% 100%)");
});
it("returns structured failures without throwing", () => {
expect(tryParseColour("")).toMatchObject({
ok: false,
error: { code: "empty" },
});
expect(tryParseColour("hsv(20 200% 20%)")).toMatchObject({
ok: false,
error: { code: "out-of-range" },
});
expect(tryParseColour("definitely-not-a-colour").ok).toBe(false);
});
it("splits lists only at top-level delimiters", () => {
const colours = parseColourList("rgb(1, 2, 3), hsl(20 30% 40%);\n#fff");
expect(colours).toHaveLength(3);
expect(formatColour(colours[0]!, "rgb")).toBe("rgb(1, 2, 3)");
expect(parseColourList('["#f00", "#0f0"]')).toHaveLength(2);
});
it("offers copy-ready conversion rows and a safe preview", () => {
const rows = conversionRows("#f8f5ee", ["hex", "rgb", "oklch"]);
expect(rows.map((row) => row.label)).toEqual(["HEX", "RGB", "OKLCH"]);
expect(rows[0]?.copyValue).toBe("#f8f5ee");
const preview = toSrgbPreview("color(display-p3 1 0 0)");
expect(preview.wasMapped).toBe(true);
expect(preview.css).toMatch(/^rgb\(/);
expect(preview.rgb.every((channel) => channel >= 0 && channel <= 1)).toBe(
true,
);
});
it("round-trips arbitrary 8-bit RGB colours through HEX", () => {
fc.assert(
fc.property(
fc.integer({ min: 0, max: 255 }),
fc.integer({ min: 0, max: 255 }),
fc.integer({ min: 0, max: 255 }),
(red, green, blue) => {
const value = {
space: "srgb",
coords: [red / 255, green / 255, blue / 255] as [
number,
number,
number,
],
alpha: 1,
};
expect(parseColour(formatColour(value, "hex")).coords).toEqual(
value.coords,
);
},
),
{ numRuns: 200 },
);
});
});