design-constraint-validator 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 +21 -0
- package/README.md +659 -0
- package/adapters/README.md +46 -0
- package/adapters/css.d.ts +44 -0
- package/adapters/css.d.ts.map +1 -0
- package/adapters/css.js +97 -0
- package/adapters/css.ts +116 -0
- package/adapters/js.d.ts +3 -0
- package/adapters/js.d.ts.map +1 -0
- package/adapters/js.js +15 -0
- package/adapters/js.ts +14 -0
- package/adapters/json.d.ts +18 -0
- package/adapters/json.d.ts.map +1 -0
- package/adapters/json.js +35 -0
- package/adapters/json.ts +45 -0
- package/cli/build-css.d.ts +2 -0
- package/cli/build-css.d.ts.map +1 -0
- package/cli/build-css.js +23 -0
- package/cli/build-css.ts +32 -0
- package/cli/commands/build.d.ts +5 -0
- package/cli/commands/build.d.ts.map +1 -0
- package/cli/commands/build.js +89 -0
- package/cli/commands/build.ts +65 -0
- package/cli/commands/graph.d.ts +3 -0
- package/cli/commands/graph.d.ts.map +1 -0
- package/cli/commands/graph.js +219 -0
- package/cli/commands/graph.ts +137 -0
- package/cli/commands/index.d.ts +8 -0
- package/cli/commands/index.d.ts.map +1 -0
- package/cli/commands/index.js +7 -0
- package/cli/commands/index.ts +7 -0
- package/cli/commands/patch-apply.d.ts +3 -0
- package/cli/commands/patch-apply.d.ts.map +1 -0
- package/cli/commands/patch-apply.js +75 -0
- package/cli/commands/patch-apply.ts +80 -0
- package/cli/commands/patch.d.ts +3 -0
- package/cli/commands/patch.d.ts.map +1 -0
- package/cli/commands/patch.js +21 -0
- package/cli/commands/patch.ts +22 -0
- package/cli/commands/set.d.ts +3 -0
- package/cli/commands/set.d.ts.map +1 -0
- package/cli/commands/set.js +286 -0
- package/cli/commands/set.ts +225 -0
- package/cli/commands/utils.d.ts +4 -0
- package/cli/commands/utils.d.ts.map +1 -0
- package/cli/commands/utils.js +51 -0
- package/cli/commands/utils.ts +50 -0
- package/cli/commands/validate.d.ts +3 -0
- package/cli/commands/validate.d.ts.map +1 -0
- package/cli/commands/validate.js +131 -0
- package/cli/commands/validate.ts +115 -0
- package/cli/commands/why.d.ts +3 -0
- package/cli/commands/why.d.ts.map +1 -0
- package/cli/commands/why.js +64 -0
- package/cli/commands/why.ts +46 -0
- package/cli/config-schema.d.ts +238 -0
- package/cli/config-schema.d.ts.map +1 -0
- package/cli/config-schema.js +21 -0
- package/cli/config-schema.ts +27 -0
- package/cli/config.d.ts +4 -0
- package/cli/config.d.ts.map +1 -0
- package/cli/config.js +37 -0
- package/cli/config.ts +35 -0
- package/cli/dcv.d.ts +3 -0
- package/cli/dcv.d.ts.map +1 -0
- package/cli/dcv.js +86 -0
- package/cli/dcv.ts +107 -0
- package/cli/engine-helpers.d.ts +8 -0
- package/cli/engine-helpers.d.ts.map +1 -0
- package/cli/engine-helpers.js +70 -0
- package/cli/engine-helpers.ts +61 -0
- package/cli/graph-poset.d.ts +9 -0
- package/cli/graph-poset.d.ts.map +1 -0
- package/cli/graph-poset.js +58 -0
- package/cli/graph-poset.ts +74 -0
- package/cli/index.d.ts +3 -0
- package/cli/index.d.ts.map +1 -0
- package/cli/index.js +2 -0
- package/cli/index.ts +2 -0
- package/cli/result.d.ts +17 -0
- package/cli/result.d.ts.map +1 -0
- package/cli/result.js +29 -0
- package/cli/result.ts +27 -0
- package/cli/run.d.ts +3 -0
- package/cli/run.d.ts.map +1 -0
- package/cli/run.js +47 -0
- package/cli/run.ts +54 -0
- package/cli/smoke-test.d.ts +2 -0
- package/cli/smoke-test.d.ts.map +1 -0
- package/cli/smoke-test.js +33 -0
- package/cli/smoke-test.ts +40 -0
- package/cli/types.d.ts +86 -0
- package/cli/types.d.ts.map +1 -0
- package/cli/types.js +1 -0
- package/cli/types.ts +78 -0
- package/core/breakpoints.d.ts +12 -0
- package/core/breakpoints.d.ts.map +1 -0
- package/core/breakpoints.js +48 -0
- package/core/breakpoints.ts +50 -0
- package/core/cli-format.d.ts +8 -0
- package/core/cli-format.d.ts.map +1 -0
- package/core/cli-format.js +29 -0
- package/core/cli-format.ts +31 -0
- package/core/color.d.ts +14 -0
- package/core/color.d.ts.map +1 -0
- package/core/color.js +136 -0
- package/core/color.ts +148 -0
- package/core/constraints/cross-axis.d.ts +33 -0
- package/core/constraints/cross-axis.d.ts.map +1 -0
- package/core/constraints/cross-axis.js +93 -0
- package/core/constraints/cross-axis.ts +114 -0
- package/core/constraints/monotonic-lightness.d.ts +5 -0
- package/core/constraints/monotonic-lightness.d.ts.map +1 -0
- package/core/constraints/monotonic-lightness.js +37 -0
- package/core/constraints/monotonic-lightness.ts +38 -0
- package/core/constraints/monotonic.d.ts +7 -0
- package/core/constraints/monotonic.d.ts.map +1 -0
- package/core/constraints/monotonic.js +65 -0
- package/core/constraints/monotonic.ts +74 -0
- package/core/constraints/threshold.d.ts +10 -0
- package/core/constraints/threshold.d.ts.map +1 -0
- package/core/constraints/threshold.js +36 -0
- package/core/constraints/threshold.ts +43 -0
- package/core/constraints/wcag.d.ts +11 -0
- package/core/constraints/wcag.d.ts.map +1 -0
- package/core/constraints/wcag.js +53 -0
- package/core/constraints/wcag.ts +70 -0
- package/core/cross-axis-config.d.ts +5 -0
- package/core/cross-axis-config.d.ts.map +1 -0
- package/core/cross-axis-config.js +144 -0
- package/core/cross-axis-config.ts +152 -0
- package/core/engine.d.ts +32 -0
- package/core/engine.d.ts.map +1 -0
- package/core/engine.js +46 -0
- package/core/engine.ts +65 -0
- package/core/flatten.d.ts +20 -0
- package/core/flatten.d.ts.map +1 -0
- package/core/flatten.js +80 -0
- package/core/flatten.ts +116 -0
- package/core/image-export.d.ts +10 -0
- package/core/image-export.d.ts.map +1 -0
- package/core/image-export.js +43 -0
- package/core/image-export.ts +48 -0
- package/core/index.d.ts +31 -0
- package/core/index.d.ts.map +1 -0
- package/core/index.js +54 -0
- package/core/index.ts +72 -0
- package/core/patch.d.ts +28 -0
- package/core/patch.d.ts.map +1 -0
- package/core/patch.js +110 -0
- package/core/patch.ts +134 -0
- package/core/poset.d.ts +41 -0
- package/core/poset.d.ts.map +1 -0
- package/core/poset.js +275 -0
- package/core/poset.ts +311 -0
- package/core/why.d.ts +17 -0
- package/core/why.d.ts.map +1 -0
- package/core/why.js +45 -0
- package/core/why.ts +63 -0
- package/dist/test-overrides-removal.json +4 -0
- package/dist/tmp.patch.json +35 -0
- package/package.json +90 -0
- package/themes/color.lg.order.json +15 -0
- package/themes/color.md.order.json +15 -0
- package/themes/color.order.json +15 -0
- package/themes/color.sm.order.json +15 -0
- package/themes/cross-axis.rules.json +36 -0
- package/themes/cross-axis.sm.rules.json +12 -0
- package/themes/layout.lg.order.json +18 -0
- package/themes/layout.md.order.json +18 -0
- package/themes/layout.order.json +18 -0
- package/themes/layout.sm.order.json +18 -0
- package/themes/spacing.order.json +14 -0
- package/themes/typography.lg.order.json +15 -0
- package/themes/typography.md.order.json +15 -0
- package/themes/typography.order.json +15 -0
- package/themes/typography.sm.order.json +15 -0
- package/tokens/overrides/base.json +22 -0
- package/tokens/overrides/lg.json +20 -0
- package/tokens/overrides/md.json +16 -0
- package/tokens/overrides/sm.json +16 -0
- package/tokens/overrides/viol.color.json +6 -0
- package/tokens/overrides/viol.typography.json +6 -0
- package/tokens/tokens.demo-violations.json +116 -0
- package/tokens/tokens.example.json +128 -0
- package/tokens/tokens.json +67 -0
- package/tokens/tokens.multi-violations.json +21 -0
- package/tokens/tokens.schema.d.ts +2298 -0
- package/tokens/tokens.schema.d.ts.map +1 -0
- package/tokens/tokens.schema.js +148 -0
- package/tokens/tokens.schema.ts +196 -0
- package/tokens/tokens.test.json +38 -0
- package/tokens/tokens.touch-violation.json +8 -0
- package/tokens/typography.classes.css +11 -0
- package/tokens/typography.css +20 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function MonotonicPlugin(orders, parse, ruleId = "monotonic") {
|
|
2
|
+
return {
|
|
3
|
+
id: ruleId,
|
|
4
|
+
evaluate(engine, candidates) {
|
|
5
|
+
const issues = [];
|
|
6
|
+
for (const [a, op, b] of orders) {
|
|
7
|
+
const va = parse(engine.get(a));
|
|
8
|
+
const vb = parse(engine.get(b));
|
|
9
|
+
if (va == null || vb == null)
|
|
10
|
+
continue; // skip unparseable
|
|
11
|
+
const ok = op === ">=" ? va >= vb : va <= vb;
|
|
12
|
+
if (!ok && (candidates.has(a) || candidates.has(b))) {
|
|
13
|
+
issues.push({
|
|
14
|
+
id: `${a}|${b}`,
|
|
15
|
+
rule: "monotonic",
|
|
16
|
+
level: "error",
|
|
17
|
+
message: `${a} ${op} ${b} violated: ${va} vs ${vb}`
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return issues;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// a minimal parser for "rem"/"px" numbers
|
|
26
|
+
export const parseSize = (v) => {
|
|
27
|
+
if (typeof v !== "string")
|
|
28
|
+
return null;
|
|
29
|
+
const m = v.trim().match(/^([0-9.]+)(rem|px)?$/i);
|
|
30
|
+
if (!m)
|
|
31
|
+
return null;
|
|
32
|
+
const num = parseFloat(m[1]);
|
|
33
|
+
const unit = (m[2] || "rem").toLowerCase();
|
|
34
|
+
return unit === "px" ? num : num * 16; // assume 1rem=16px for comparisons
|
|
35
|
+
};
|
|
36
|
+
// Parser for unitless numbers (like scale factors)
|
|
37
|
+
export const parseNumber = (v) => {
|
|
38
|
+
if (typeof v === "number")
|
|
39
|
+
return v;
|
|
40
|
+
if (typeof v === "string") {
|
|
41
|
+
const num = parseFloat(v.trim());
|
|
42
|
+
return isNaN(num) ? null : num;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
// Parser for color lightness (OKLCH L channel)
|
|
47
|
+
export const parseLightness = (v) => {
|
|
48
|
+
if (typeof v !== "string")
|
|
49
|
+
return null;
|
|
50
|
+
// Match oklch(L C H / A) format
|
|
51
|
+
const oklchMatch = v.trim().match(/oklch\s*\(\s*([0-9.]+)\s+/i);
|
|
52
|
+
if (oklchMatch) {
|
|
53
|
+
return parseFloat(oklchMatch[1]);
|
|
54
|
+
}
|
|
55
|
+
// For hex colors, rough approximation (you'd want a proper converter)
|
|
56
|
+
const hexMatch = v.trim().match(/^#([0-9a-f]{6})$/i);
|
|
57
|
+
if (hexMatch) {
|
|
58
|
+
const r = parseInt(hexMatch[1].slice(0, 2), 16) / 255;
|
|
59
|
+
const g = parseInt(hexMatch[1].slice(2, 4), 16) / 255;
|
|
60
|
+
const b = parseInt(hexMatch[1].slice(4, 6), 16) / 255;
|
|
61
|
+
// Simple luminance approximation
|
|
62
|
+
return 0.299 * r + 0.587 * g + 0.114 * b;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// core/constraints/monotonic.ts
|
|
2
|
+
import type { ConstraintPlugin } from "../engine.js";
|
|
3
|
+
import type { Order } from "../poset.js";
|
|
4
|
+
|
|
5
|
+
export function MonotonicPlugin(
|
|
6
|
+
orders: Order[],
|
|
7
|
+
parse: (v: unknown) => number | null,
|
|
8
|
+
ruleId = "monotonic"
|
|
9
|
+
): ConstraintPlugin {
|
|
10
|
+
return {
|
|
11
|
+
id: ruleId,
|
|
12
|
+
evaluate(engine, candidates) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
for (const [a, op, b] of orders) {
|
|
15
|
+
const va = parse(engine.get(a));
|
|
16
|
+
const vb = parse(engine.get(b));
|
|
17
|
+
if (va == null || vb == null) continue; // skip unparseable
|
|
18
|
+
const ok = op === ">=" ? va >= vb : va <= vb;
|
|
19
|
+
if (!ok && (candidates.has(a) || candidates.has(b))) {
|
|
20
|
+
issues.push({
|
|
21
|
+
id: `${a}|${b}`,
|
|
22
|
+
rule: "monotonic",
|
|
23
|
+
level: "error" as const,
|
|
24
|
+
message: `${a} ${op} ${b} violated: ${va} vs ${vb}`
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return issues;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// a minimal parser for "rem"/"px" numbers
|
|
34
|
+
export const parseSize = (v: unknown): number | null => {
|
|
35
|
+
if (typeof v !== "string") return null;
|
|
36
|
+
const m = v.trim().match(/^([0-9.]+)(rem|px)?$/i);
|
|
37
|
+
if (!m) return null;
|
|
38
|
+
const num = parseFloat(m[1]);
|
|
39
|
+
const unit = (m[2] || "rem").toLowerCase();
|
|
40
|
+
return unit === "px" ? num : num * 16; // assume 1rem=16px for comparisons
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Parser for unitless numbers (like scale factors)
|
|
44
|
+
export const parseNumber = (v: unknown): number | null => {
|
|
45
|
+
if (typeof v === "number") return v;
|
|
46
|
+
if (typeof v === "string") {
|
|
47
|
+
const num = parseFloat(v.trim());
|
|
48
|
+
return isNaN(num) ? null : num;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Parser for color lightness (OKLCH L channel)
|
|
54
|
+
export const parseLightness = (v: unknown): number | null => {
|
|
55
|
+
if (typeof v !== "string") return null;
|
|
56
|
+
|
|
57
|
+
// Match oklch(L C H / A) format
|
|
58
|
+
const oklchMatch = v.trim().match(/oklch\s*\(\s*([0-9.]+)\s+/i);
|
|
59
|
+
if (oklchMatch) {
|
|
60
|
+
return parseFloat(oklchMatch[1]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// For hex colors, rough approximation (you'd want a proper converter)
|
|
64
|
+
const hexMatch = v.trim().match(/^#([0-9a-f]{6})$/i);
|
|
65
|
+
if (hexMatch) {
|
|
66
|
+
const r = parseInt(hexMatch[1].slice(0, 2), 16) / 255;
|
|
67
|
+
const g = parseInt(hexMatch[1].slice(2, 4), 16) / 255;
|
|
68
|
+
const b = parseInt(hexMatch[1].slice(4, 6), 16) / 255;
|
|
69
|
+
// Simple luminance approximation
|
|
70
|
+
return 0.299 * r + 0.587 * g + 0.114 * b;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ConstraintPlugin } from "../engine.js";
|
|
2
|
+
export type ThresholdRule = {
|
|
3
|
+
id: string;
|
|
4
|
+
op: ">=" | "<=";
|
|
5
|
+
valuePx: number;
|
|
6
|
+
where?: string;
|
|
7
|
+
level?: "error" | "warn";
|
|
8
|
+
};
|
|
9
|
+
export declare function ThresholdPlugin(rules: ThresholdRule[], ruleId?: string): ConstraintPlugin;
|
|
10
|
+
//# sourceMappingURL=threshold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threshold.d.ts","sourceRoot":"","sources":["threshold.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1B,CAAC;AAWF,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,MAAM,SAAc,GAAG,gBAAgB,CAuB9F"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const parseSizePx = (v) => {
|
|
2
|
+
if (typeof v !== "string")
|
|
3
|
+
return null;
|
|
4
|
+
const m = v.trim().match(/^([0-9.]+)\s*(px|rem)?$/i);
|
|
5
|
+
if (!m)
|
|
6
|
+
return null;
|
|
7
|
+
const n = parseFloat(m[1]);
|
|
8
|
+
const unit = (m[2] || "px").toLowerCase();
|
|
9
|
+
return unit === "rem" ? n * 16 : n;
|
|
10
|
+
};
|
|
11
|
+
export function ThresholdPlugin(rules, ruleId = "threshold") {
|
|
12
|
+
return {
|
|
13
|
+
id: ruleId,
|
|
14
|
+
evaluate(engine, candidates) {
|
|
15
|
+
const issues = [];
|
|
16
|
+
for (const r of rules) {
|
|
17
|
+
if (!candidates.has(r.id))
|
|
18
|
+
continue; // incremental
|
|
19
|
+
const px = parseSizePx(engine.get(r.id));
|
|
20
|
+
if (px == null)
|
|
21
|
+
continue;
|
|
22
|
+
const ok = r.op === ">=" ? px >= r.valuePx : px <= r.valuePx;
|
|
23
|
+
if (!ok) {
|
|
24
|
+
issues.push({
|
|
25
|
+
id: r.id,
|
|
26
|
+
rule: ruleId,
|
|
27
|
+
level: r.level ?? "error",
|
|
28
|
+
where: r.where,
|
|
29
|
+
message: `${r.id} ${r.op} ${r.valuePx}px violated: ${px}px`
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return issues;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ConstraintPlugin } from "../engine.js";
|
|
2
|
+
|
|
3
|
+
export type ThresholdRule = {
|
|
4
|
+
id: string; // token id to check
|
|
5
|
+
op: ">=" | "<="; // comparison
|
|
6
|
+
valuePx: number; // threshold in px
|
|
7
|
+
where?: string; // optional context label
|
|
8
|
+
level?: "error" | "warn"; // default error
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const parseSizePx = (v: unknown): number | null => {
|
|
12
|
+
if (typeof v !== "string") return null;
|
|
13
|
+
const m = v.trim().match(/^([0-9.]+)\s*(px|rem)?$/i);
|
|
14
|
+
if (!m) return null;
|
|
15
|
+
const n = parseFloat(m[1]);
|
|
16
|
+
const unit = (m[2] || "px").toLowerCase();
|
|
17
|
+
return unit === "rem" ? n * 16 : n;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function ThresholdPlugin(rules: ThresholdRule[], ruleId = "threshold"): ConstraintPlugin {
|
|
21
|
+
return {
|
|
22
|
+
id: ruleId,
|
|
23
|
+
evaluate(engine, candidates) {
|
|
24
|
+
const issues = [];
|
|
25
|
+
for (const r of rules) {
|
|
26
|
+
if (!candidates.has(r.id)) continue; // incremental
|
|
27
|
+
const px = parseSizePx(engine.get(r.id));
|
|
28
|
+
if (px == null) continue;
|
|
29
|
+
const ok = r.op === ">=" ? px >= r.valuePx : px <= r.valuePx;
|
|
30
|
+
if (!ok) {
|
|
31
|
+
issues.push({
|
|
32
|
+
id: r.id,
|
|
33
|
+
rule: ruleId,
|
|
34
|
+
level: r.level ?? "error",
|
|
35
|
+
where: r.where,
|
|
36
|
+
message: `${r.id} ${r.op} ${r.valuePx}px violated: ${px}px`
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return issues;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConstraintPlugin } from "../engine.js";
|
|
2
|
+
import type { TokenId } from "../flatten.js";
|
|
3
|
+
export type ContrastPair = {
|
|
4
|
+
fg: TokenId;
|
|
5
|
+
bg: TokenId;
|
|
6
|
+
min: number;
|
|
7
|
+
where?: string;
|
|
8
|
+
backdrop?: TokenId | string;
|
|
9
|
+
};
|
|
10
|
+
export declare function WcagContrastPlugin(pairs: ContrastPair[]): ConstraintPlugin;
|
|
11
|
+
//# sourceMappingURL=wcag.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wcag.d.ts","sourceRoot":"","sources":["wcag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG7C,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,EAAE,EAAE,OAAO,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC7B,CAAC;AASF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,gBAAgB,CAiD1E"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { parseCssColor, compositeOver, relativeLuminance, contrastRatio } from "../color.js";
|
|
2
|
+
function resolveColor(engineGet, x) {
|
|
3
|
+
if (typeof x !== "string")
|
|
4
|
+
return undefined;
|
|
5
|
+
// If it's a CSS color literal, return as is; else treat as token id.
|
|
6
|
+
const isLiteral = /^#|^rgb|^hsl|^oklch|^oklab|^transparent/i.test(x);
|
|
7
|
+
return isLiteral ? x : String(engineGet(x) ?? "");
|
|
8
|
+
}
|
|
9
|
+
export function WcagContrastPlugin(pairs) {
|
|
10
|
+
return {
|
|
11
|
+
id: "wcag-contrast",
|
|
12
|
+
evaluate(engine, candidates) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
for (const p of pairs) {
|
|
15
|
+
if (!candidates.has(p.fg) && !candidates.has(p.bg))
|
|
16
|
+
continue;
|
|
17
|
+
const fgStr = String(engine.get(p.fg) ?? "");
|
|
18
|
+
const bgStr = String(engine.get(p.bg) ?? "");
|
|
19
|
+
const fg = parseCssColor(fgStr);
|
|
20
|
+
const bgRaw = parseCssColor(bgStr);
|
|
21
|
+
const backdropStr = resolveColor(engine.get.bind(engine), p.backdrop ?? "#ffffff");
|
|
22
|
+
const backdrop = parseCssColor(backdropStr || "#ffffff");
|
|
23
|
+
if (!fg || !bgRaw || !backdrop) {
|
|
24
|
+
issues.push({
|
|
25
|
+
id: `${p.fg}|${p.bg}`,
|
|
26
|
+
rule: "wcag-contrast",
|
|
27
|
+
level: "warn",
|
|
28
|
+
where: p.where,
|
|
29
|
+
message: `Unparseable color(s): fg="${fgStr}" bg="${bgStr}" backdrop="${backdropStr}"`
|
|
30
|
+
});
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Effective background (handle bg alpha)
|
|
34
|
+
const effBg = bgRaw.a < 1 ? compositeOver(bgRaw, backdrop) : bgRaw;
|
|
35
|
+
// Effective foreground over effective background
|
|
36
|
+
const effFg = fg.a < 1 ? compositeOver(fg, effBg) : fg;
|
|
37
|
+
const Lfg = relativeLuminance(effFg);
|
|
38
|
+
const Lbg = relativeLuminance(effBg);
|
|
39
|
+
const ratio = contrastRatio(Lfg, Lbg);
|
|
40
|
+
if (ratio < p.min) {
|
|
41
|
+
issues.push({
|
|
42
|
+
id: `${p.fg}|${p.bg}`,
|
|
43
|
+
rule: "wcag-contrast",
|
|
44
|
+
level: "error",
|
|
45
|
+
where: p.where,
|
|
46
|
+
message: `Contrast ${ratio.toFixed(2)}:1 < ${p.min}:1`
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return issues;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ConstraintPlugin } from "../engine.js";
|
|
2
|
+
import type { TokenId } from "../flatten.js";
|
|
3
|
+
import { parseCssColor, compositeOver, relativeLuminance, contrastRatio } from "../color.js";
|
|
4
|
+
|
|
5
|
+
export type ContrastPair = {
|
|
6
|
+
fg: TokenId;
|
|
7
|
+
bg: TokenId;
|
|
8
|
+
min: number; // e.g., 4.5
|
|
9
|
+
where?: string;
|
|
10
|
+
// Optional ultimate backdrop if bg is transparent; token id or literal color.
|
|
11
|
+
backdrop?: TokenId | string; // default: "#ffffff"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function resolveColor(engineGet: (id: TokenId)=>unknown, x: TokenId | string): string | undefined {
|
|
15
|
+
if (typeof x !== "string") return undefined;
|
|
16
|
+
// If it's a CSS color literal, return as is; else treat as token id.
|
|
17
|
+
const isLiteral = /^#|^rgb|^hsl|^oklch|^oklab|^transparent/i.test(x);
|
|
18
|
+
return isLiteral ? x : String(engineGet(x) ?? "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function WcagContrastPlugin(pairs: ContrastPair[]): ConstraintPlugin {
|
|
22
|
+
return {
|
|
23
|
+
id: "wcag-contrast",
|
|
24
|
+
evaluate(engine, candidates) {
|
|
25
|
+
const issues = [];
|
|
26
|
+
for (const p of pairs) {
|
|
27
|
+
if (!candidates.has(p.fg) && !candidates.has(p.bg)) continue;
|
|
28
|
+
|
|
29
|
+
const fgStr = String(engine.get(p.fg) ?? "");
|
|
30
|
+
const bgStr = String(engine.get(p.bg) ?? "");
|
|
31
|
+
const fg = parseCssColor(fgStr);
|
|
32
|
+
const bgRaw = parseCssColor(bgStr);
|
|
33
|
+
|
|
34
|
+
const backdropStr = resolveColor(engine.get.bind(engine), p.backdrop ?? "#ffffff");
|
|
35
|
+
const backdrop = parseCssColor(backdropStr || "#ffffff");
|
|
36
|
+
|
|
37
|
+
if (!fg || !bgRaw || !backdrop) {
|
|
38
|
+
issues.push({
|
|
39
|
+
id: `${p.fg}|${p.bg}`,
|
|
40
|
+
rule: "wcag-contrast",
|
|
41
|
+
level: "warn" as const,
|
|
42
|
+
where: p.where,
|
|
43
|
+
message: `Unparseable color(s): fg="${fgStr}" bg="${bgStr}" backdrop="${backdropStr}"`
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Effective background (handle bg alpha)
|
|
49
|
+
const effBg = bgRaw.a < 1 ? compositeOver(bgRaw, backdrop) : bgRaw;
|
|
50
|
+
// Effective foreground over effective background
|
|
51
|
+
const effFg = fg.a < 1 ? compositeOver(fg, effBg) : fg;
|
|
52
|
+
|
|
53
|
+
const Lfg = relativeLuminance(effFg);
|
|
54
|
+
const Lbg = relativeLuminance(effBg);
|
|
55
|
+
const ratio = contrastRatio(Lfg, Lbg);
|
|
56
|
+
|
|
57
|
+
if (ratio < p.min) {
|
|
58
|
+
issues.push({
|
|
59
|
+
id: `${p.fg}|${p.bg}`,
|
|
60
|
+
rule: "wcag-contrast",
|
|
61
|
+
level: "error" as const,
|
|
62
|
+
where: p.where,
|
|
63
|
+
message: `Contrast ${ratio.toFixed(2)}:1 < ${p.min}:1`
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return issues;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-axis-config.d.ts","sourceRoot":"","sources":["cross-axis-config.ts"],"names":[],"mappings":"AAQA,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,EAAE,CAAC,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,0CA2HnD"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { CrossAxisPlugin } from "./constraints/cross-axis.js";
|
|
3
|
+
export function loadCrossAxisPlugin(path, bp, opts) {
|
|
4
|
+
const debug = !!opts?.debug;
|
|
5
|
+
const known = opts?.knownIds ?? new Set();
|
|
6
|
+
const log = (...args) => { if (debug)
|
|
7
|
+
console.log("[cross-axis]", ...args); };
|
|
8
|
+
if (!fs.existsSync(path)) {
|
|
9
|
+
log(`no rules file at ${path} (bp=${bp ?? "global"})`);
|
|
10
|
+
return CrossAxisPlugin([], bp);
|
|
11
|
+
}
|
|
12
|
+
const raw = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
13
|
+
const rules = [];
|
|
14
|
+
const unknownIds = new Set();
|
|
15
|
+
const skipped = [];
|
|
16
|
+
// Fuzzy suggestion helpers (lightweight Levenshtein)
|
|
17
|
+
function levenshtein(a, b) {
|
|
18
|
+
const dp = Array(b.length + 1).fill(0).map((_, j) => j);
|
|
19
|
+
for (let i = 1; i <= a.length; i++) {
|
|
20
|
+
let prev = i - 1, cur = i;
|
|
21
|
+
for (let j = 1; j <= b.length; j++) {
|
|
22
|
+
const tmp = cur;
|
|
23
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
24
|
+
cur = Math.min(dp[j] + 1, cur + 1, prev + cost);
|
|
25
|
+
dp[j] = tmp;
|
|
26
|
+
prev = tmp;
|
|
27
|
+
}
|
|
28
|
+
dp[b.length] = cur;
|
|
29
|
+
}
|
|
30
|
+
return dp[b.length];
|
|
31
|
+
}
|
|
32
|
+
function suggest(id, k = 3) {
|
|
33
|
+
return [...known].map(c => ({ id: c, d: levenshtein(id, c) }))
|
|
34
|
+
.sort((a, b) => a.d - b.d)
|
|
35
|
+
.slice(0, k);
|
|
36
|
+
}
|
|
37
|
+
const needId = (id) => {
|
|
38
|
+
if (!id)
|
|
39
|
+
return false;
|
|
40
|
+
if (!known.has(id)) {
|
|
41
|
+
unknownIds.add(id);
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
45
|
+
for (const r of raw.rules || []) {
|
|
46
|
+
if (r.bp && bp && r.bp !== bp) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (r.bp && !bp) { // rule targets specific breakpoint; skip in global run
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
if (r.when && r.require) {
|
|
54
|
+
// Validate IDs
|
|
55
|
+
needId(r.when.id);
|
|
56
|
+
needId(r.require.id);
|
|
57
|
+
if (r.require.ref)
|
|
58
|
+
needId(r.require.ref);
|
|
59
|
+
rules.push({
|
|
60
|
+
id: r.id, level: r.level, where: r.where,
|
|
61
|
+
when: { id: r.when.id, test: makeOp(r.when.op, r.when.value) },
|
|
62
|
+
require: {
|
|
63
|
+
id: r.require.id,
|
|
64
|
+
test: (v, ctx) => {
|
|
65
|
+
const rhs = valueOrRef(ctx, r.require.ref, r.require.fallback);
|
|
66
|
+
return cmp(v, rhs, r.require.op);
|
|
67
|
+
},
|
|
68
|
+
msg: (v, ctx) => {
|
|
69
|
+
const rhs = valueOrRef(ctx, r.require.ref, r.require.fallback);
|
|
70
|
+
return `${r.require.id} ${prettyFail(r.require.op)} ${fmt(rhs)} (was ${fmt(v)})`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else if (r.compare) {
|
|
76
|
+
needId(r.compare.a);
|
|
77
|
+
needId(r.compare.b);
|
|
78
|
+
rules.push({
|
|
79
|
+
id: r.id, level: r.level, where: r.where,
|
|
80
|
+
when: { id: r.compare.a, test: () => true },
|
|
81
|
+
require: {
|
|
82
|
+
id: r.compare.a,
|
|
83
|
+
test: (_, ctx) => {
|
|
84
|
+
const a = ctx.getPx(r.compare.a) ?? NaN;
|
|
85
|
+
const b = ctx.getPx(r.compare.b) ?? NaN;
|
|
86
|
+
const delta = px(r.compare.delta ?? 0);
|
|
87
|
+
if (Number.isNaN(a) || Number.isNaN(b))
|
|
88
|
+
return true; // skip check if missing
|
|
89
|
+
return cmp(a, b + delta, r.compare.op);
|
|
90
|
+
},
|
|
91
|
+
msg: (_, ctx) => {
|
|
92
|
+
const a = ctx.getPx(r.compare.a);
|
|
93
|
+
const b = ctx.getPx(r.compare.b);
|
|
94
|
+
const delta = px(r.compare.delta ?? 0);
|
|
95
|
+
return `${r.compare.a} ${prettyFail(r.compare.op)} ${fmt((b ?? 0) + delta)} (was ${fmt(a ?? NaN)})`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
skipped.push({ id: r.id, reason: "neither when+require nor compare present" });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
skipped.push({ id: r.id, reason: `exception: ${e?.message ?? e}` });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
log(`loaded ${rules.length} rule(s) from ${path}${bp ? ` [bp=${bp}]` : ""}`);
|
|
109
|
+
if (unknownIds.size) {
|
|
110
|
+
log(`unknown ids referenced:`, [...unknownIds].join(", "));
|
|
111
|
+
for (const u of unknownIds) {
|
|
112
|
+
const s = suggest(u, 3);
|
|
113
|
+
if (s.length)
|
|
114
|
+
log(` did you mean: ${s.map(x => `${x.id} (d=${x.d})`).join(', ')}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (skipped.length) {
|
|
118
|
+
for (const s of skipped)
|
|
119
|
+
log(`skipped rule ${s.id ?? "(no id)"} — ${s.reason}`);
|
|
120
|
+
}
|
|
121
|
+
// Extra hint for common anchor pitfall
|
|
122
|
+
for (const r of raw.rules || []) {
|
|
123
|
+
if (r.require?.ref && !known.has(r.require.ref)) {
|
|
124
|
+
log(`anchor missing: ${r.require.ref} → will use fallback=${JSON.stringify(r.require.fallback)} when evaluating`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return CrossAxisPlugin(rules, bp);
|
|
128
|
+
}
|
|
129
|
+
// helpers
|
|
130
|
+
const px = (v) => typeof v === "number" ? v : parseFloat(String(v)) * (String(v).trim().endsWith("rem") ? 16 : 1);
|
|
131
|
+
const cmp = (a, b, op) => op === ">=" ? a >= b : op === ">" ? a > b : op === "<=" ? a <= b : op === "<" ? a < b : op === "==" ? a === b : a !== b;
|
|
132
|
+
const prettyFail = (op) => ({ ">=": "<", ">": "≤", "<=": ">", "<": "≥", "==": "≠", "!=": "=" }[op] || "≠");
|
|
133
|
+
const fmt = (v) => Number.isFinite(Number(v)) ? `${Number(v)}px` : String(v);
|
|
134
|
+
function valueOrRef(ctx, ref, fallback) {
|
|
135
|
+
if (ref) {
|
|
136
|
+
const v = ctx.getPx(ref);
|
|
137
|
+
if (v != null)
|
|
138
|
+
return v;
|
|
139
|
+
}
|
|
140
|
+
return typeof fallback === "number" ? fallback : px(fallback ?? 0);
|
|
141
|
+
}
|
|
142
|
+
function makeOp(op, rhs) {
|
|
143
|
+
return (v) => cmp(v, rhs, op);
|
|
144
|
+
}
|