Problem
values.colors.unique is keyed on the color string, so #3a7bd5, #3b7cd6 and rgb(58, 123, 213) are reported as three unique colors even though they're perceptually identical. There's currently no signal for how many perceptually distinct colors a stylesheet actually uses.
For design-system auditing that distinction is the whole story: "47 unique colors" is noise if 40 are accidental near-duplicates. "47 declared → 9 perceptually distinct (5.2× sprawl)" is actionable.
(Related to #522 on wide-gamut detection — adjacent and complementary: that's syntactic, this is perceptual.)
Proposal
An additive perceptual block under values.colors:
Implementation: CIELAB ΔE76 single-linkage clustering, inlined with zero new dependencies (~120 LOC) — colors converted sRGB→Lab and grouped under a ΔE76 ≈ 2.5 just-noticeable-difference threshold. v1 scopes to hex + rgb()/rgba(); hsl()/lab()/oklch()/named/system are counted as unanalyzable (honest about coverage) and are easy follow-ups once the core lands. Clustering is O(n²) in the number of unique analyzable colors (low hundreds in practice), run once per analyze().
Questions before I open a PR
- Would you prefer this wired into the
analyze() output (changes the colors snapshot), or exposed as a standalone export to keep the output surface stable? Happy either way.
- ΔE76 (simple, zero-dep) vs ΔE2000 (more perceptually uniform, heavier)? I'd start with ΔE76 at a JND threshold for dedup-grade clustering, but will follow your call.
I have a working implementation + vitest tests ready to PR once you point me at the shape you'd prefer. Thanks for the project! 🙏
Problem
values.colors.uniqueis keyed on the color string, so#3a7bd5,#3b7cd6andrgb(58, 123, 213)are reported as three unique colors even though they're perceptually identical. There's currently no signal for how many perceptually distinct colors a stylesheet actually uses.For design-system auditing that distinction is the whole story: "47 unique colors" is noise if 40 are accidental near-duplicates. "47 declared → 9 perceptually distinct (5.2× sprawl)" is actionable.
(Related to #522 on wide-gamut detection — adjacent and complementary: that's syntactic, this is perceptual.)
Proposal
An additive
perceptualblock undervalues.colors:Implementation: CIELAB ΔE76 single-linkage clustering, inlined with zero new dependencies (~120 LOC) — colors converted sRGB→Lab and grouped under a ΔE76 ≈ 2.5 just-noticeable-difference threshold. v1 scopes to hex +
rgb()/rgba();hsl()/lab()/oklch()/named/system are counted asunanalyzable(honest about coverage) and are easy follow-ups once the core lands. Clustering is O(n²) in the number of unique analyzable colors (low hundreds in practice), run once peranalyze().Questions before I open a PR
analyze()output (changes the colors snapshot), or exposed as a standalone export to keep the output surface stable? Happy either way.I have a working implementation + vitest tests ready to PR once you point me at the shape you'd prefer. Thanks for the project! 🙏