mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 22:26:38 +03:00
Fix: a DESIGN.md token is not the generic AI palette
`ai-color-palette` catches the palette nobody picked: the violet and the cyan a model reaches for when there is no design system. The browser sweep was reading only the hue, so a site whose own documented tokens land in those bands tripped it on every element wearing one. On impeccable-site's "Paper and instruments" pull request, where the DESIGN.md palette is a verdigris `oklch(70% 0.12 188)` on an `oklch(24% 0 0)` instrument face, that was 218 findings across 54 pages, all of them "Cyan neon text on dark background" against a token the author had written down. `check_element_ai_palette_dom` now takes the scan's design system and skips any gradient stop or text color the DESIGN.md declares, matched with the same `browser_colors_close` tolerance the `design-system-color` rule uses, so a color that rule calls declared is declared here too. A scan with no DESIGN.md, or one whose DESIGN.md has no palette, is unchanged: there is nothing to consult and every color stays in scope. Measured on that site (54 pages, headless Chrome, the in-page bundle): with the design system, ai-color-palette drops 218 -> 8; without one it stays at 218. The 8 survivors are the real thing, the purple gradients the /slop and /docs before-and-after exhibits ship on purpose. No design-system-* count moves. Assisted-by: Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LQBUunp8QttxZqihybNmtL
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
cb56ed6c19
commit
bb1ca3c7df
@@ -1353,7 +1353,11 @@ pub fn collect_browser_findings(dom: &dyn Dom, config: &BrowserConfig) -> Collec
|
||||
findings.extend(hits(ec::check_element_colors_dom(dom, el)));
|
||||
findings.extend(hits(ec::check_element_motion_dom(dom, el)));
|
||||
findings.extend(hits(ec::check_element_glow_dom(dom, el)));
|
||||
findings.extend(hits(ec::check_element_ai_palette_dom(dom, el)));
|
||||
findings.extend(hits(ec::check_element_ai_palette_dom(
|
||||
dom,
|
||||
el,
|
||||
design_system.as_ref(),
|
||||
)));
|
||||
findings.extend(hits(ec::check_element_radial_spotlight_dom(dom, el)));
|
||||
findings.extend(hits(ec::check_element_icon_tile_dom(dom, el)));
|
||||
findings.extend(hits(ec::check_element_italic_serif_dom(dom, el)));
|
||||
@@ -1791,6 +1795,64 @@ mod tests {
|
||||
assert!(types(&out).contains(&"design-system-font".to_string()));
|
||||
}
|
||||
|
||||
/// End to end through the collector: a page whose colors are all its own
|
||||
/// documented oklch tokens must not report `ai-color-palette`, while a
|
||||
/// color the DESIGN.md never declared still reports both rules.
|
||||
#[test]
|
||||
fn ai_palette_respects_the_design_system_palette() {
|
||||
// oklch(24% 0 0) instrument face carrying oklch(70% 0.12 188) verdigris.
|
||||
let make_dom = |text_color: &str| {
|
||||
let mut d = FakeDom::new();
|
||||
let (_h, body) = d.with_page();
|
||||
let panel = d.add(Some(body), "div");
|
||||
d.set_styles(panel, &[("backgroundColor", "rgb(58, 58, 58)")]);
|
||||
d.el_mut(panel).check_visibility = Some(true);
|
||||
let label = d.add(Some(panel), "span");
|
||||
d.add_text(label, "Live");
|
||||
d.set_styles(
|
||||
label,
|
||||
&[
|
||||
("color", text_color),
|
||||
("backgroundColor", "rgba(0, 0, 0, 0)"),
|
||||
("fontFamily", "Inter, sans-serif"),
|
||||
],
|
||||
);
|
||||
d.el_mut(label).check_visibility = Some(true);
|
||||
d
|
||||
};
|
||||
let types = |out: &CollectResult| -> Vec<String> {
|
||||
out.groups
|
||||
.iter()
|
||||
.flat_map(|g| g.findings.iter().map(|f| f.type_.clone()))
|
||||
.collect()
|
||||
};
|
||||
let design_system = json!({
|
||||
"present": true,
|
||||
"hasFonts": true, "allowedFonts": ["Inter"],
|
||||
"hasColors": true,
|
||||
"allowedColors": [{ "r": 15, "g": 182, "b": 172 }, { "r": 58, "g": 58, "b": 58 }]
|
||||
});
|
||||
let with_ds = BrowserConfig {
|
||||
design_system: Some(design_system),
|
||||
..Default::default()
|
||||
};
|
||||
let without_ds = BrowserConfig::default();
|
||||
|
||||
// No DESIGN.md: the teal is an unexplained neon and the rule fires.
|
||||
let out = collect_browser_findings(&make_dom("rgb(15, 182, 172)"), &without_ds);
|
||||
assert!(types(&out).contains(&"ai-color-palette".to_string()));
|
||||
|
||||
// Declared token: neither the palette rule nor the drift rule fires.
|
||||
let out = collect_browser_findings(&make_dom("rgb(15, 182, 172)"), &with_ds);
|
||||
assert!(!types(&out).contains(&"ai-color-palette".to_string()), "{:?}", types(&out));
|
||||
assert!(!types(&out).contains(&"design-system-color".to_string()), "{:?}", types(&out));
|
||||
|
||||
// An undeclared neon on the same page still reports both.
|
||||
let out = collect_browser_findings(&make_dom("rgb(0, 229, 255)"), &with_ds);
|
||||
assert!(types(&out).contains(&"ai-color-palette".to_string()), "{:?}", types(&out));
|
||||
assert!(types(&out).contains(&"design-system-color".to_string()), "{:?}", types(&out));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_values_parse_and_normalize_like_the_js() {
|
||||
// JS `.filter(e => e && typeof e === 'object' && e.rule && e.value)`:
|
||||
|
||||
@@ -11,6 +11,7 @@ use super::dom::{
|
||||
class_attr, class_attr_or_prop, closest_or_none, direct_text, has_direct_text_longer_than,
|
||||
matches_or_false, pf0, safe_id, style_px, tag_lower, Dom, ElId, ElStyle, Rect,
|
||||
};
|
||||
use super::driver::{browser_colors_close, DesignSystemConfig};
|
||||
use super::BrowserFinding;
|
||||
use crate::checks::measures::{
|
||||
self, border_colors_from_style, border_widths_from_style, check_gpt_thin_border_wide_shadow,
|
||||
@@ -716,11 +717,42 @@ pub fn check_element_glow_dom(dom: &dyn Dom, el: ElId) -> Vec<RuleHit> {
|
||||
})
|
||||
}
|
||||
|
||||
/// True when the scan was given a DESIGN.md and that file declares this
|
||||
/// color as one of the project's own.
|
||||
///
|
||||
/// `ai-color-palette` is a rule about the *unchosen* palette: the purple and
|
||||
/// the cyan a model reaches for when nobody picked one. A color the author
|
||||
/// wrote down in DESIGN.md was picked, so it is not that default whatever
|
||||
/// its hue, and a site whose whole palette is its own documented tokens must
|
||||
/// not trip the rule on every element that wears one. With no design system
|
||||
/// there is nothing to consult and every color stays in scope, which is the
|
||||
/// behavior every scan without a DESIGN.md keeps.
|
||||
///
|
||||
/// The tolerance is `browser_colors_close`, the same one the
|
||||
/// `design-system-color` rule matches computed colors with, so a token the
|
||||
/// design-system rule calls declared is declared here too.
|
||||
fn is_declared_design_color(ds: Option<&DesignSystemConfig>, c: &Rgba) -> bool {
|
||||
let Some(ds) = ds else { return false };
|
||||
if !ds.has_colors {
|
||||
return false;
|
||||
}
|
||||
ds.allowed_colors
|
||||
.iter()
|
||||
.any(|allowed| browser_colors_close(c, allowed))
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#checkElementAIPaletteDOM(el)
|
||||
pub fn check_element_ai_palette_dom(dom: &dyn Dom, el: ElId) -> Vec<RuleHit> {
|
||||
pub fn check_element_ai_palette_dom(
|
||||
dom: &dyn Dom,
|
||||
el: ElId,
|
||||
design_system: Option<&DesignSystemConfig>,
|
||||
) -> Vec<RuleHit> {
|
||||
let mut findings = Vec::new();
|
||||
let bg_image = dom.style(el, "backgroundImage");
|
||||
for c in parse_gradient_colors(Some(&bg_image)) {
|
||||
if is_declared_design_color(design_system, &c) {
|
||||
continue;
|
||||
}
|
||||
if has_chroma(Some(&c), Some(50.0)) {
|
||||
let hue = get_hue(Some(&c));
|
||||
if hue >= 260.0 && hue <= 310.0 {
|
||||
@@ -739,7 +771,8 @@ pub fn check_element_ai_palette_dom(dom: &dyn Dom, el: ElId) -> Vec<RuleHit> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let text_color = parse_rgb_or_any(&dom.style(el, "color"));
|
||||
let text_color = parse_rgb_or_any(&dom.style(el, "color"))
|
||||
.filter(|c| !is_declared_design_color(design_system, c));
|
||||
if let Some(tc) = text_color {
|
||||
if has_chroma(Some(&tc), Some(80.0)) {
|
||||
let hue = get_hue(Some(&tc));
|
||||
@@ -1534,7 +1567,72 @@ mod tests {
|
||||
"linear-gradient(rgb(168, 85, 247), rgb(59, 130, 246))",
|
||||
);
|
||||
d.set_style(hero, "color", "rgb(0, 0, 0)");
|
||||
let hits = check_element_ai_palette_dom(&d, hero);
|
||||
let hits = check_element_ai_palette_dom(&d, hero, None);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].snippet, "Purple/violet gradient background");
|
||||
}
|
||||
|
||||
/// A DESIGN.md palette built out of the project's own oklch tokens is not
|
||||
/// the generic assistant default, however cyan or violet the tokens are.
|
||||
/// The verdigris-on-instrument pair here is the shape that fired 80 times
|
||||
/// on one site whose whole palette is documented.
|
||||
fn design_system_with(colors: &[(f64, f64, f64)]) -> DesignSystemConfig {
|
||||
DesignSystemConfig {
|
||||
has_colors: true,
|
||||
allowed_colors: colors
|
||||
.iter()
|
||||
.map(|&(r, g, b)| Rgba { r, g, b, a: None })
|
||||
.collect(),
|
||||
..DesignSystemConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_palette_skips_colors_the_design_system_declares() {
|
||||
let (mut d, body) = page();
|
||||
// oklch(24% 0 0) instrument face, oklch(70% 0.12 188) verdigris text.
|
||||
let panel = d.add(Some(body), "div");
|
||||
d.set_style(panel, "backgroundColor", "rgb(58, 58, 58)");
|
||||
let label = d.add(Some(panel), "span");
|
||||
d.set_style(label, "color", "rgb(15, 182, 172)");
|
||||
|
||||
// With no DESIGN.md the rule still fires: nothing says the teal was chosen.
|
||||
let hits = check_element_ai_palette_dom(&d, label, None);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].snippet, "Cyan neon text on dark background");
|
||||
|
||||
// Declared in DESIGN.md, so it is the project's palette, not the default.
|
||||
let ds = design_system_with(&[(15.0, 182.0, 172.0)]);
|
||||
assert!(check_element_ai_palette_dom(&d, label, Some(&ds)).is_empty());
|
||||
|
||||
// A design system that declares some other color leaves the rule alone.
|
||||
let other = design_system_with(&[(200.0, 40.0, 30.0)]);
|
||||
assert_eq!(check_element_ai_palette_dom(&d, label, Some(&other)).len(), 1);
|
||||
|
||||
// `hasColors: false` is a DESIGN.md with no palette section: no allowlist
|
||||
// to consult, so the rule keeps its unconstrained behavior.
|
||||
let empty = DesignSystemConfig::default();
|
||||
assert_eq!(check_element_ai_palette_dom(&d, label, Some(&empty)).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_palette_gradient_skips_declared_stops_but_not_undeclared_ones() {
|
||||
let (mut d, body) = page();
|
||||
let hero = d.add(Some(body), "section");
|
||||
d.set_style(
|
||||
hero,
|
||||
"backgroundImage",
|
||||
"linear-gradient(rgb(168, 85, 247), rgb(59, 130, 246))",
|
||||
);
|
||||
d.set_style(hero, "color", "rgb(0, 0, 0)");
|
||||
|
||||
// The violet stop is a declared token, so this gradient is the project's.
|
||||
let ds = design_system_with(&[(168.0, 85.0, 247.0), (59.0, 130.0, 246.0)]);
|
||||
assert!(check_element_ai_palette_dom(&d, hero, Some(&ds)).is_empty());
|
||||
|
||||
// Declaring only the blue stop leaves the violet one in scope.
|
||||
let partial = design_system_with(&[(59.0, 130.0, 246.0)]);
|
||||
let hits = check_element_ai_palette_dom(&d, hero, Some(&partial));
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].snippet, "Purple/violet gradient background");
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user