contrast-gate 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chandra Pratap
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # contrast-gate
2
+
3
+ Audit your entire design-token file for WCAG contrast compliance in one command, and fail CI when something doesn't meet the bar — instead of manually checking colors one pair at a time.
4
+
5
+ Zero runtime dependencies.
6
+
7
+ ## Why this exists
8
+
9
+ Contrast-ratio calculators already exist as libraries you import into code. What's missing is a standalone tool that checks your **whole set of design tokens at once** and plugs straight into CI — so a color that fails accessibility never gets shipped, the same way a failing test or lint error wouldn't.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install --save-dev contrast-gate
15
+ ```
16
+
17
+ Or run it without installing:
18
+
19
+ ```bash
20
+ npx contrast-gate wcag.config.json
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ Create a config file listing the foreground/background pairs your product actually uses:
26
+
27
+ ```json
28
+ {
29
+ "pairs": [
30
+ { "name": "Body text", "fg": "#302a22", "bg": "#faf6ee" },
31
+ { "name": "Primary button", "fg": "#ffffff", "bg": "#667355" },
32
+ { "name": "Muted caption", "fg": "#c8c0ae", "bg": "#faf6ee" }
33
+ ]
34
+ }
35
+ ```
36
+
37
+ Run it:
38
+
39
+ ```bash
40
+ contrast-gate wcag.config.json
41
+ ```
42
+
43
+ ```
44
+ Pair Ratio AA Normal AA Large AAA Normal AAA Large
45
+ -----------------------------------------------------------------------
46
+ Body text 13.16:1 PASS PASS PASS PASS
47
+ Primary button 5.06:1 PASS PASS FAIL PASS
48
+ Muted caption 1.68:1 FAIL FAIL FAIL FAIL
49
+
50
+ 1 of 3 pair(s) fail WCAG AA (normal text):
51
+ - Muted caption: 1.68:1 (#c8c0ae on #faf6ee) — needs at least 4.5:1
52
+ ```
53
+
54
+ Exit code is `1` if anything fails WCAG AA — which means it works as a CI gate with zero extra scripting.
55
+
56
+ ### Options
57
+
58
+ | Flag | Effect |
59
+ |---|---|
60
+ | `--json` | Print machine-readable JSON instead of a table |
61
+ | `--aaa` | Require WCAG AAA (7:1 normal text) instead of AA (4.5:1) |
62
+ | `-h, --help` | Show usage |
63
+
64
+ ### Exit codes
65
+
66
+ | Code | Meaning |
67
+ |---|---|
68
+ | `0` | Every pair passes the selected level |
69
+ | `1` | At least one pair fails |
70
+ | `2` | Config file error (missing, malformed, or invalid) |
71
+
72
+ ## CI integration
73
+
74
+ GitHub Actions example — fail the build if any color pair drops below WCAG AA:
75
+
76
+ ```yaml
77
+ name: Accessibility
78
+
79
+ on: [push, pull_request]
80
+
81
+ jobs:
82
+ contrast:
83
+ runs-on: ubuntu-latest
84
+ steps:
85
+ - uses: actions/checkout@v4
86
+ - uses: actions/setup-node@v4
87
+ with:
88
+ node-version: 20
89
+ - run: npx contrast-gate wcag.config.json
90
+ ```
91
+
92
+ ## Using it as a library
93
+
94
+ The contrast-calculation core is exported directly, if you want to check colors from your own code rather than a config file:
95
+
96
+ ```ts
97
+ import { contrastRatioHex, checkCompliance } from "contrast-gate";
98
+
99
+ const ratio = contrastRatioHex("#302a22", "#faf6ee"); // 13.16
100
+ const result = checkCompliance(ratio);
101
+ // { ratio: 13.16, passesAA: { normal: true, large: true }, passesAAA: { normal: true, large: true } }
102
+ ```
103
+
104
+ ## How the math works
105
+
106
+ Implements the WCAG 2.1 relative luminance and contrast ratio formulas exactly as specified:
107
+ - [Relative luminance](https://www.w3.org/TR/WCAG21/#dfn-relative-luminance)
108
+ - [Contrast ratio](https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio)
109
+
110
+ Verified against known reference values (black-on-white computes to exactly 21:1, the maximum possible ratio; `#767676` on white computes to ~4.54:1, a commonly cited borderline-AA gray).
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/src/cli.js";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { loadConfig, ConfigError } from "./config.js";
4
+ import { evaluatePairs } from "./core.js";
5
+ import { renderTable, renderJson } from "./report.js";
6
+ const HELP = `contrast-gate — audit a design-token file for WCAG contrast compliance
7
+
8
+ Usage:
9
+ contrast-gate <config-file> [options]
10
+
11
+ Options:
12
+ --json Output machine-readable JSON instead of a table
13
+ --aaa Require WCAG AAA (7:1 normal text) instead of AA (4.5:1) to pass
14
+ -h, --help Show this help message
15
+
16
+ Example:
17
+ contrast-gate wcag.config.json
18
+ contrast-gate wcag.config.json --json > report.json
19
+
20
+ Config file format:
21
+ {
22
+ "pairs": [
23
+ { "name": "Primary button", "fg": "#ffffff", "bg": "#667355" },
24
+ { "name": "Body text", "fg": "#302a22", "bg": "#faf6ee" }
25
+ ]
26
+ }
27
+
28
+ Exit codes:
29
+ 0 every pair passes the selected level
30
+ 1 at least one pair fails the selected level
31
+ 2 config file error (missing, malformed, or invalid)
32
+ `;
33
+ async function main() {
34
+ const { values, positionals } = parseArgs({
35
+ args: process.argv.slice(2),
36
+ options: {
37
+ json: { type: "boolean", default: false },
38
+ aaa: { type: "boolean", default: false },
39
+ help: { type: "boolean", short: "h", default: false },
40
+ },
41
+ allowPositionals: true,
42
+ });
43
+ if (values.help || positionals.length === 0) {
44
+ console.log(HELP);
45
+ process.exit(values.help ? 0 : 2);
46
+ }
47
+ const configPath = positionals[0];
48
+ let config;
49
+ try {
50
+ config = await loadConfig(configPath);
51
+ }
52
+ catch (err) {
53
+ if (err instanceof ConfigError) {
54
+ console.error(`contrast-gate: ${err.message}`);
55
+ process.exit(2);
56
+ }
57
+ throw err;
58
+ }
59
+ const results = evaluatePairs(config.pairs);
60
+ const level = values.aaa ? "passesAAA" : "passesAA";
61
+ const allPass = results.every((r) => r[level].normal);
62
+ if (values.json) {
63
+ renderJson(results);
64
+ }
65
+ else {
66
+ renderTable(results);
67
+ }
68
+ process.exit(allPass ? 0 : 1);
69
+ }
70
+ main().catch((err) => {
71
+ console.error("contrast-gate: unexpected error —", err);
72
+ process.exit(2);
73
+ });
@@ -0,0 +1,8 @@
1
+ import type { ColorPair } from "./core.js";
2
+ export interface Config {
3
+ pairs: ColorPair[];
4
+ }
5
+ export declare class ConfigError extends Error {
6
+ }
7
+ /** Loads and validates a contrast-gate config file. Throws ConfigError on any malformed input. */
8
+ export declare function loadConfig(path: string): Promise<Config>;
@@ -0,0 +1,38 @@
1
+ import { readFile } from "node:fs/promises";
2
+ export class ConfigError extends Error {
3
+ }
4
+ /** Loads and validates a contrast-gate config file. Throws ConfigError on any malformed input. */
5
+ export async function loadConfig(path) {
6
+ let raw;
7
+ try {
8
+ raw = await readFile(path, "utf-8");
9
+ }
10
+ catch (err) {
11
+ throw new ConfigError(`Could not read config file at "${path}": ${err.message}`);
12
+ }
13
+ let parsed;
14
+ try {
15
+ parsed = JSON.parse(raw);
16
+ }
17
+ catch (err) {
18
+ throw new ConfigError(`Config file at "${path}" is not valid JSON: ${err.message}`);
19
+ }
20
+ if (typeof parsed !== "object" || parsed === null || !("pairs" in parsed)) {
21
+ throw new ConfigError(`Config file must be an object with a "pairs" array.`);
22
+ }
23
+ const pairsRaw = parsed.pairs;
24
+ if (!Array.isArray(pairsRaw) || pairsRaw.length === 0) {
25
+ throw new ConfigError(`Config "pairs" must be a non-empty array.`);
26
+ }
27
+ const pairs = pairsRaw.map((entry, index) => {
28
+ if (typeof entry !== "object" ||
29
+ entry === null ||
30
+ typeof entry.name !== "string" ||
31
+ typeof entry.fg !== "string" ||
32
+ typeof entry.bg !== "string") {
33
+ throw new ConfigError(`Config "pairs[${index}]" must have string "name", "fg", and "bg" fields.`);
34
+ }
35
+ return entry;
36
+ });
37
+ return { pairs };
38
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * WCAG 2.1 contrast calculation — relative luminance + contrast ratio,
3
+ * per https://www.w3.org/TR/WCAG21/#dfn-relative-luminance and
4
+ * https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio
5
+ */
6
+ export interface RGB {
7
+ r: number;
8
+ g: number;
9
+ b: number;
10
+ }
11
+ export type ThresholdLevel = "AA" | "AAA";
12
+ export type TextSize = "normal" | "large";
13
+ /** Parses a hex color string (#fff, #ffffff, with or without leading #) into RGB. */
14
+ export declare function parseHexColor(hex: string): RGB;
15
+ /** WCAG relative luminance of an RGB color, in the range [0, 1]. */
16
+ export declare function relativeLuminance({ r, g, b }: RGB): number;
17
+ /** WCAG contrast ratio between two colors, in the range [1, 21]. */
18
+ export declare function contrastRatio(a: RGB, b: RGB): number;
19
+ /** Contrast ratio directly from two hex color strings. */
20
+ export declare function contrastRatioHex(fgHex: string, bgHex: string): number;
21
+ export interface ComplianceResult {
22
+ ratio: number;
23
+ passesAA: {
24
+ normal: boolean;
25
+ large: boolean;
26
+ };
27
+ passesAAA: {
28
+ normal: boolean;
29
+ large: boolean;
30
+ };
31
+ }
32
+ /** Full pass/fail breakdown for a contrast ratio against all four WCAG thresholds. */
33
+ export declare function checkCompliance(ratio: number): ComplianceResult;
34
+ export interface ColorPair {
35
+ name: string;
36
+ fg: string;
37
+ bg: string;
38
+ }
39
+ export interface PairResult extends ComplianceResult {
40
+ name: string;
41
+ fg: string;
42
+ bg: string;
43
+ }
44
+ /** Evaluates a named list of foreground/background pairs in one pass. */
45
+ export declare function evaluatePairs(pairs: ColorPair[]): PairResult[];
@@ -0,0 +1,71 @@
1
+ /**
2
+ * WCAG 2.1 contrast calculation — relative luminance + contrast ratio,
3
+ * per https://www.w3.org/TR/WCAG21/#dfn-relative-luminance and
4
+ * https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio
5
+ */
6
+ const THRESHOLDS = {
7
+ AA: { normal: 4.5, large: 3 },
8
+ AAA: { normal: 7, large: 4.5 },
9
+ };
10
+ /** Parses a hex color string (#fff, #ffffff, with or without leading #) into RGB. */
11
+ export function parseHexColor(hex) {
12
+ const cleaned = hex.trim().replace(/^#/, "");
13
+ const expanded = cleaned.length === 3
14
+ ? cleaned
15
+ .split("")
16
+ .map((c) => c + c)
17
+ .join("")
18
+ : cleaned;
19
+ if (!/^[0-9a-fA-F]{6}$/.test(expanded)) {
20
+ throw new Error(`Invalid hex color: "${hex}"`);
21
+ }
22
+ return {
23
+ r: parseInt(expanded.slice(0, 2), 16),
24
+ g: parseInt(expanded.slice(2, 4), 16),
25
+ b: parseInt(expanded.slice(4, 6), 16),
26
+ };
27
+ }
28
+ function channelToLinear(channel) {
29
+ const c = channel / 255;
30
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
31
+ }
32
+ /** WCAG relative luminance of an RGB color, in the range [0, 1]. */
33
+ export function relativeLuminance({ r, g, b }) {
34
+ const rLin = channelToLinear(r);
35
+ const gLin = channelToLinear(g);
36
+ const bLin = channelToLinear(b);
37
+ return 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin;
38
+ }
39
+ /** WCAG contrast ratio between two colors, in the range [1, 21]. */
40
+ export function contrastRatio(a, b) {
41
+ const l1 = relativeLuminance(a);
42
+ const l2 = relativeLuminance(b);
43
+ const lighter = Math.max(l1, l2);
44
+ const darker = Math.min(l1, l2);
45
+ return (lighter + 0.05) / (darker + 0.05);
46
+ }
47
+ /** Contrast ratio directly from two hex color strings. */
48
+ export function contrastRatioHex(fgHex, bgHex) {
49
+ return contrastRatio(parseHexColor(fgHex), parseHexColor(bgHex));
50
+ }
51
+ /** Full pass/fail breakdown for a contrast ratio against all four WCAG thresholds. */
52
+ export function checkCompliance(ratio) {
53
+ return {
54
+ ratio,
55
+ passesAA: {
56
+ normal: ratio >= THRESHOLDS.AA.normal,
57
+ large: ratio >= THRESHOLDS.AA.large,
58
+ },
59
+ passesAAA: {
60
+ normal: ratio >= THRESHOLDS.AAA.normal,
61
+ large: ratio >= THRESHOLDS.AAA.large,
62
+ },
63
+ };
64
+ }
65
+ /** Evaluates a named list of foreground/background pairs in one pass. */
66
+ export function evaluatePairs(pairs) {
67
+ return pairs.map((pair) => {
68
+ const ratio = contrastRatioHex(pair.fg, pair.bg);
69
+ return { ...pair, ...checkCompliance(ratio) };
70
+ });
71
+ }
@@ -0,0 +1,5 @@
1
+ import type { PairResult } from "./core.js";
2
+ /** Renders a human-readable colored table to stdout. */
3
+ export declare function renderTable(results: PairResult[]): void;
4
+ /** Renders results as machine-readable JSON to stdout. */
5
+ export declare function renderJson(results: PairResult[]): void;
@@ -0,0 +1,36 @@
1
+ const RESET = "\x1b[0m";
2
+ const GREEN = "\x1b[32m";
3
+ const RED = "\x1b[31m";
4
+ const DIM = "\x1b[2m";
5
+ const BOLD = "\x1b[1m";
6
+ function pad(text, width) {
7
+ return text.length >= width ? text : text + " ".repeat(width - text.length);
8
+ }
9
+ function badge(pass) {
10
+ return pass ? `${GREEN}PASS${RESET}` : `${RED}FAIL${RESET}`;
11
+ }
12
+ /** Renders a human-readable colored table to stdout. */
13
+ export function renderTable(results) {
14
+ const nameWidth = Math.max(4, ...results.map((r) => r.name.length));
15
+ console.log(`${BOLD}${pad("Pair", nameWidth)} Ratio AA Normal AA Large AAA Normal AAA Large${RESET}`);
16
+ console.log(DIM + "-".repeat(nameWidth + 58) + RESET);
17
+ for (const r of results) {
18
+ const ratioStr = pad(`${r.ratio.toFixed(2)}:1`, 8);
19
+ console.log(`${pad(r.name, nameWidth)} ${ratioStr} ${badge(r.passesAA.normal)} ${badge(r.passesAA.large)} ${badge(r.passesAAA.normal)} ${badge(r.passesAAA.large)}`);
20
+ }
21
+ const failing = results.filter((r) => !r.passesAA.normal);
22
+ console.log("");
23
+ if (failing.length === 0) {
24
+ console.log(`${GREEN}All ${results.length} pair(s) pass WCAG AA (normal text).${RESET}`);
25
+ }
26
+ else {
27
+ console.log(`${RED}${failing.length} of ${results.length} pair(s) fail WCAG AA (normal text):${RESET}`);
28
+ for (const f of failing) {
29
+ console.log(` - ${f.name}: ${f.ratio.toFixed(2)}:1 (${f.fg} on ${f.bg}) — needs at least 4.5:1`);
30
+ }
31
+ }
32
+ }
33
+ /** Renders results as machine-readable JSON to stdout. */
34
+ export function renderJson(results) {
35
+ console.log(JSON.stringify({ results, allPassAA: results.every((r) => r.passesAA.normal) }, null, 2));
36
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { writeFile, unlink } from "node:fs/promises";
4
+ import { loadConfig, ConfigError } from "../src/config.js";
5
+ const TMP_PATH = "./test/.tmp-config.json";
6
+ test("loadConfig: loads a valid config file", async () => {
7
+ await writeFile(TMP_PATH, JSON.stringify({
8
+ pairs: [{ name: "Body text", fg: "#000000", bg: "#ffffff" }],
9
+ }));
10
+ const config = await loadConfig(TMP_PATH);
11
+ assert.equal(config.pairs.length, 1);
12
+ assert.equal(config.pairs[0].name, "Body text");
13
+ await unlink(TMP_PATH);
14
+ });
15
+ test("loadConfig: throws ConfigError for a missing file", async () => {
16
+ await assert.rejects(() => loadConfig("./does-not-exist.json"), ConfigError);
17
+ });
18
+ test("loadConfig: throws ConfigError for invalid JSON", async () => {
19
+ await writeFile(TMP_PATH, "{ not valid json");
20
+ await assert.rejects(() => loadConfig(TMP_PATH), ConfigError);
21
+ await unlink(TMP_PATH);
22
+ });
23
+ test("loadConfig: throws ConfigError when pairs is missing", async () => {
24
+ await writeFile(TMP_PATH, JSON.stringify({}));
25
+ await assert.rejects(() => loadConfig(TMP_PATH), ConfigError);
26
+ await unlink(TMP_PATH);
27
+ });
28
+ test("loadConfig: throws ConfigError when pairs is empty", async () => {
29
+ await writeFile(TMP_PATH, JSON.stringify({ pairs: [] }));
30
+ await assert.rejects(() => loadConfig(TMP_PATH), ConfigError);
31
+ await unlink(TMP_PATH);
32
+ });
33
+ test("loadConfig: throws ConfigError when a pair entry is malformed", async () => {
34
+ await writeFile(TMP_PATH, JSON.stringify({ pairs: [{ name: "Missing colors" }] }));
35
+ await assert.rejects(() => loadConfig(TMP_PATH), ConfigError);
36
+ await unlink(TMP_PATH);
37
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseHexColor, relativeLuminance, contrastRatio, contrastRatioHex, checkCompliance, evaluatePairs, } from "../src/core.js";
4
+ test("parseHexColor handles 6-digit and 3-digit hex, with or without #", () => {
5
+ assert.deepEqual(parseHexColor("#ffffff"), { r: 255, g: 255, b: 255 });
6
+ assert.deepEqual(parseHexColor("ffffff"), { r: 255, g: 255, b: 255 });
7
+ assert.deepEqual(parseHexColor("#fff"), { r: 255, g: 255, b: 255 });
8
+ assert.deepEqual(parseHexColor("#000"), { r: 0, g: 0, b: 0 });
9
+ });
10
+ test("parseHexColor rejects invalid input", () => {
11
+ assert.throws(() => parseHexColor("not-a-color"));
12
+ assert.throws(() => parseHexColor("#12345"));
13
+ });
14
+ test("relativeLuminance: white is 1, black is 0", () => {
15
+ assert.equal(relativeLuminance({ r: 255, g: 255, b: 255 }), 1);
16
+ assert.equal(relativeLuminance({ r: 0, g: 0, b: 0 }), 0);
17
+ });
18
+ test("contrastRatio: black on white is the maximum possible, 21:1", () => {
19
+ const ratio = contrastRatio({ r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 });
20
+ assert.equal(ratio, 21);
21
+ });
22
+ test("contrastRatio: identical colors is the minimum possible, 1:1", () => {
23
+ const white = { r: 255, g: 255, b: 255 };
24
+ assert.equal(contrastRatio(white, white), 1);
25
+ });
26
+ test("contrastRatio is symmetric (order of arguments doesn't matter)", () => {
27
+ const black = { r: 0, g: 0, b: 0 };
28
+ const white = { r: 255, g: 255, b: 255 };
29
+ assert.equal(contrastRatio(black, white), contrastRatio(white, black));
30
+ });
31
+ test("contrastRatioHex: #767676 on white is ~4.54:1 (the well-known borderline AA gray)", () => {
32
+ const ratio = contrastRatioHex("#767676", "#ffffff");
33
+ assert.ok(Math.abs(ratio - 4.54) < 0.01, `expected ~4.54, got ${ratio}`);
34
+ });
35
+ test("checkCompliance: 4.54:1 passes AA normal text and AAA large text, but not AAA normal text", () => {
36
+ const result = checkCompliance(4.542224959605253);
37
+ assert.equal(result.passesAA.normal, true);
38
+ assert.equal(result.passesAA.large, true);
39
+ assert.equal(result.passesAAA.normal, false, "4.54:1 is below the 7:1 AAA normal threshold");
40
+ assert.equal(result.passesAAA.large, true, "4.54:1 is above the 4.5:1 AAA large threshold");
41
+ });
42
+ test("checkCompliance: #777777 on white (~4.48:1) fails AA normal text", () => {
43
+ const ratio = contrastRatioHex("#777777", "#ffffff");
44
+ const result = checkCompliance(ratio);
45
+ assert.equal(result.passesAA.normal, false, "4.48:1 should fail the 4.5:1 AA normal threshold");
46
+ assert.equal(result.passesAA.large, true, "4.48:1 should still pass the 3:1 AA large threshold");
47
+ });
48
+ test("checkCompliance: black on white (21:1) passes every threshold", () => {
49
+ const result = checkCompliance(21);
50
+ assert.equal(result.passesAA.normal, true);
51
+ assert.equal(result.passesAA.large, true);
52
+ assert.equal(result.passesAAA.normal, true);
53
+ assert.equal(result.passesAAA.large, true);
54
+ });
55
+ test("evaluatePairs: evaluates a named list end to end", () => {
56
+ const results = evaluatePairs([
57
+ { name: "Body text", fg: "#000000", bg: "#ffffff" },
58
+ { name: "Borderline gray", fg: "#767676", bg: "#ffffff" },
59
+ { name: "Failing gray", fg: "#777777", bg: "#ffffff" },
60
+ ]);
61
+ assert.equal(results.length, 3);
62
+ assert.equal(results[0].name, "Body text");
63
+ assert.equal(results[0].passesAAA.normal, true);
64
+ assert.equal(results[1].passesAA.normal, true);
65
+ assert.equal(results[2].passesAA.normal, false);
66
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "contrast-gate",
3
+ "version": "1.0.0",
4
+ "description": "Audit an entire design-token file for WCAG contrast compliance in one command, and gate CI on it.",
5
+ "type": "module",
6
+ "bin": {
7
+ "contrast-gate": "bin/contrast-gate.js"
8
+ },
9
+ "main": "./dist/src/core.js",
10
+ "types": "./dist/src/core.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "bin"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "pretest": "tsc",
18
+ "test": "node --test dist/test/*.test.js",
19
+ "prepublishOnly": "npm run build && npm test"
20
+ },
21
+ "keywords": [
22
+ "wcag",
23
+ "accessibility",
24
+ "a11y",
25
+ "contrast",
26
+ "design-tokens",
27
+ "design-system",
28
+ "cli",
29
+ "ci"
30
+ ],
31
+ "author": "Chandra Pratap",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/cptechstack/contrast-gate.git"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.3.0"
39
+ },
40
+ "devDependencies": {
41
+ "typescript": "^5.6.3",
42
+ "@types/node": "^22.9.0"
43
+ }
44
+ }