mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-20 01:56:37 +03:00
Port: Fix flat type hierarchy false positives (#702)
Upstream sha 84728e9ce4.
The rule now reads rendered semantic roles and the dominant size per role
instead of the raw set of font sizes on the page, and it fires only when
every adjacent role step is under 1.25x.
- crates/core checks::rules gains TYPE_HIERARCHY_SELECTOR / MIN_ROLES /
MIN_STEP_RATIO, typeHierarchyRole, dominantTypeRoleSize and
checkFlatTypeHierarchySamples, the shared half of checks.mjs.
- crates/core browser::page_checks gets checkFlatTypeHierarchyFromDoc over
the Dom trait, with the overlay skip selector checkTypography passes.
- crates/html page.rs gets the same walk over StaticDocument.
- crates/detect drops the source-only analyzer: flat-type-hierarchy leaves
REGEX_ANALYZERS, the text-content analyzers shift to index 1, and
analyzer_rule_id loses its first row.
- crates/html cascade defaults gain contentVisibility, and crates/foundation
registry carries the reworded description.
Goldens re-recorded (the binary now matches origin/main's JS engine on every
one of these fixtures, verified by scanning the shared corpus with both):
glow, icon-tile-stack, layout, modern-color-borders, motion,
named-color-borders, numbered-section-markers, oklch-neon-text,
typography-should-flag, json and text.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0375e219f1
commit
66482a9808
@@ -7,8 +7,8 @@
|
||||
|
||||
#![allow(unused_imports)]
|
||||
use super::dom::{
|
||||
class_attr, closest_or_none, direct_text, has_direct_text_longer_than, pf0, style_px,
|
||||
tag_lower, Dom, ElId, ElStyle, Rect,
|
||||
ancestors_inclusive, class_attr, closest_or_none, direct_text, has_direct_text_longer_than, pf0,
|
||||
style_px, tag_lower, Dom, ElId, ElStyle, Rect,
|
||||
};
|
||||
use super::element_checks::{class_selector, effective_opacity_dom, is_rendered_for_browser_rule};
|
||||
use super::{BrowserFinding, ElFinding};
|
||||
@@ -16,7 +16,10 @@ use crate::checks::measures::{
|
||||
cream_from_class_list, is_cream_color, is_opaque_decorated_box,
|
||||
is_screen_reader_only_text_style, SrOnlyMetrics, StyleMap,
|
||||
};
|
||||
use crate::checks::rules::{is_card_like_from_props, RuleHit};
|
||||
use crate::checks::rules::{
|
||||
check_flat_type_hierarchy_samples, is_card_like_from_props, type_hierarchy_role, RuleHit,
|
||||
TypeSample, TYPE_HIERARCHY_SELECTOR,
|
||||
};
|
||||
use crate::color::parse_any_color;
|
||||
use crate::constants::{is_brand_font_on_own_domain, CSS_GENERIC_FONTS, OVERUSED_FONTS, SAFE_TAGS};
|
||||
use crate::js::{self, math_max, math_min, math_round, number_to_string, parse_float, to_fixed};
|
||||
@@ -162,42 +165,72 @@ pub fn check_typography(dom: &dyn Dom) -> Vec<BrowserFinding> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut sizes: Vec<f64> = Vec::new();
|
||||
for el in dom
|
||||
.query_all(None, "h1,h2,h3,h4,h5,h6,p,span,a,li,td,th,label,button,div")
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let fs = parse_float(&dom.style(el, "fontSize"));
|
||||
if fs > 0.0 && fs < 200.0 {
|
||||
let v = math_round(fs * 10.0) / 10.0;
|
||||
if !sizes.iter().any(|s| *s == v) {
|
||||
sizes.push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
if sizes.len() >= 3 {
|
||||
let mut sorted = sizes.clone();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let ratio = sorted[sorted.len() - 1] / sorted[0];
|
||||
if ratio < 2.0 {
|
||||
findings.push(BrowserFinding::new(
|
||||
"flat-type-hierarchy",
|
||||
format!(
|
||||
"Sizes: {} (ratio {}:1)",
|
||||
sorted
|
||||
.iter()
|
||||
.map(|s| format!("{}px", number_to_string(*s)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
to_fixed(ratio, 1)
|
||||
),
|
||||
));
|
||||
}
|
||||
for hit in check_flat_type_hierarchy_from_dom(dom, Some(TYPE_HIERARCHY_SKIP_SELECTOR)) {
|
||||
findings.push(BrowserFinding::new(&hit.id, hit.snippet));
|
||||
}
|
||||
|
||||
findings
|
||||
}
|
||||
|
||||
/// The overlay chrome `checkTypography` hands `checkFlatTypeHierarchyFromDoc`
|
||||
/// as its `skipElement` selector.
|
||||
pub const TYPE_HIERARCHY_SKIP_SELECTOR: &str =
|
||||
".impeccable-overlay, .impeccable-label, .impeccable-banner, .impeccable-tooltip, [id^=\"impeccable-live-\"]";
|
||||
|
||||
/// JS: checks.mjs#isRenderedTypeElement over a live DOM.
|
||||
fn is_rendered_type_element(dom: &dyn Dom, el: ElId) -> bool {
|
||||
for current in ancestors_inclusive(dom, el) {
|
||||
if dom.hidden_prop(current) || dom.attr(current, "hidden").is_some() {
|
||||
return false;
|
||||
}
|
||||
let display = js::to_lower_case(&dom.style(current, "display"));
|
||||
let visibility = js::to_lower_case(&dom.style(current, "visibility"));
|
||||
let content_visibility = js::to_lower_case(&dom.style(current, "contentVisibility"));
|
||||
if display == "none"
|
||||
|| visibility == "hidden"
|
||||
|| visibility == "collapse"
|
||||
|| content_visibility == "hidden"
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let opacity = parse_float(&dom.style(current, "opacity"));
|
||||
if opacity.is_finite() && opacity <= 0.01 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#checkFlatTypeHierarchyFromDoc over a live DOM.
|
||||
pub fn check_flat_type_hierarchy_from_dom(
|
||||
dom: &dyn Dom,
|
||||
skip_selector: Option<&str>,
|
||||
) -> Vec<RuleHit> {
|
||||
let mut samples: Vec<TypeSample> = Vec::new();
|
||||
for el in dom
|
||||
.query_all(None, TYPE_HIERARCHY_SELECTOR)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if let Some(sel) = skip_selector {
|
||||
if closest_or_none(dom, el, sel).is_some() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if js::trim(&dom.text_content(el)).is_empty() || !is_rendered_type_element(dom, el) {
|
||||
continue;
|
||||
}
|
||||
let font_size = parse_float(&dom.style(el, "fontSize"));
|
||||
if !font_size.is_finite() || font_size < 8.0 || font_size >= 200.0 {
|
||||
continue;
|
||||
}
|
||||
samples.push(TypeSample {
|
||||
role: type_hierarchy_role(&tag_lower(dom, el)),
|
||||
size: font_size,
|
||||
});
|
||||
}
|
||||
check_flat_type_hierarchy_samples(&samples)
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#isCardLikeDOM(el)
|
||||
pub fn is_card_like_dom(dom: &dyn Dom, el: ElId) -> bool {
|
||||
let tag = tag_lower(dom, el);
|
||||
@@ -1404,11 +1437,54 @@ mod tests {
|
||||
d.set_style(s, "fontFamily", "Georgia");
|
||||
d.set_style(s, "fontSize", "24px");
|
||||
let f = check_typography(&d);
|
||||
assert_eq!(f.len(), 2, "{f:?}");
|
||||
// One `body` role is under TYPE_HIERARCHY_MIN_ROLES, so the flat-type
|
||||
// rule abstains and only the font finding stands (#702).
|
||||
assert_eq!(f.len(), 1, "{f:?}");
|
||||
assert_eq!(f[0].type_, "overused-font");
|
||||
assert_eq!(f[0].detail, "Primary font: inter (95% of text)");
|
||||
assert_eq!(f[1].type_, "flat-type-hierarchy");
|
||||
assert_eq!(f[1].detail, "Sizes: 16px, 18px, 24px (ratio 1.5:1)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_type_hierarchy_roles() {
|
||||
let mut d = FakeDom::new();
|
||||
let (_h, body) = d.with_page();
|
||||
for (tag, size) in [("p", "16px"), ("p", "16px"), ("h2", "17px"), ("h1", "18px")] {
|
||||
let el = d.add(Some(body), tag);
|
||||
d.add_text(el, "text");
|
||||
d.set_style(el, "fontSize", size);
|
||||
}
|
||||
let f = check_typography(&d);
|
||||
assert_eq!(f.len(), 1, "{f:?}");
|
||||
assert_eq!(f[0].type_, "flat-type-hierarchy");
|
||||
assert_eq!(
|
||||
f[0].detail,
|
||||
"Role sizes: body 16px, h2 17px, h1 18px (largest adjacent step 1.06:1; target 1.25:1)"
|
||||
);
|
||||
|
||||
// A clear step at any adjacent pair clears the rule.
|
||||
let mut d = FakeDom::new();
|
||||
let (_h, body) = d.with_page();
|
||||
for (tag, size) in [("p", "16px"), ("h2", "24px"), ("h1", "40px")] {
|
||||
let el = d.add(Some(body), tag);
|
||||
d.add_text(el, "text");
|
||||
d.set_style(el, "fontSize", size);
|
||||
}
|
||||
assert!(check_typography(&d).is_empty());
|
||||
|
||||
// A hidden ancestor takes its text out of the sample.
|
||||
let mut d = FakeDom::new();
|
||||
let (_h, body) = d.with_page();
|
||||
for (tag, size) in [("p", "16px"), ("p", "16px"), ("h2", "17px")] {
|
||||
let el = d.add(Some(body), tag);
|
||||
d.add_text(el, "text");
|
||||
d.set_style(el, "fontSize", size);
|
||||
}
|
||||
let wrap = d.add(Some(body), "div");
|
||||
d.set_style(wrap, "display", "none");
|
||||
let h1 = d.add(Some(wrap), "h1");
|
||||
d.add_text(h1, "text");
|
||||
d.set_style(h1, "fontSize", "18px");
|
||||
assert!(check_typography(&d).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -794,6 +794,113 @@ pub fn check_glow(opts: &GlowOpts) -> Vec<RuleHit> {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Section 6 shared: flat type hierarchy ──────────────────────────────────
|
||||
|
||||
/// JS: checks.mjs#TYPE_HIERARCHY_SELECTOR
|
||||
pub const TYPE_HIERARCHY_SELECTOR: &str = "h1,h2,h3,h4,h5,h6,p,li,td,th,dd,blockquote,figcaption";
|
||||
/// JS: checks.mjs#TYPE_HIERARCHY_MIN_ROLES
|
||||
pub const TYPE_HIERARCHY_MIN_ROLES: usize = 3;
|
||||
/// JS: checks.mjs#TYPE_HIERARCHY_MIN_STEP_RATIO
|
||||
pub const TYPE_HIERARCHY_MIN_STEP_RATIO: f64 = 1.25;
|
||||
|
||||
/// One `{ role, size }` entry the JS pushes into `samples`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TypeSample {
|
||||
pub role: String,
|
||||
pub size: f64,
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#typeHierarchyRole
|
||||
pub fn type_hierarchy_role(tag: &str) -> String {
|
||||
let tag = js::to_lower_case(tag);
|
||||
let b = tag.as_bytes();
|
||||
if b.len() == 2 && b[0] == b'h' && (b'1'..=b'6').contains(&b[1]) {
|
||||
tag
|
||||
} else {
|
||||
"body".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#dominantTypeRoleSize
|
||||
fn dominant_type_role_size(samples: &[f64]) -> Option<f64> {
|
||||
// `new Map()` keeps insertion order; the JS sorts by count desc then size asc.
|
||||
let mut counts: Vec<(f64, f64)> = Vec::new();
|
||||
for size in samples {
|
||||
match counts
|
||||
.iter_mut()
|
||||
.find(|(k, _)| crate::js_ext_b::same_value_zero(*k, *size))
|
||||
{
|
||||
Some(slot) => slot.1 += 1.0,
|
||||
None => counts.push((*size, 1.0)),
|
||||
}
|
||||
}
|
||||
let mut ranked = counts;
|
||||
ranked.sort_by(|a, b| {
|
||||
b.1.partial_cmp(&a.1)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then(a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
|
||||
});
|
||||
if ranked.len() > 1 && ranked[0].1 == ranked[1].1 {
|
||||
return None;
|
||||
}
|
||||
ranked.first().map(|(size, _)| *size)
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#checkFlatTypeHierarchySamples
|
||||
pub fn check_flat_type_hierarchy_samples(samples: &[TypeSample]) -> Vec<RuleHit> {
|
||||
// `new Map()` keyed by role, in first-seen order.
|
||||
let mut by_role: Vec<(String, Vec<f64>)> = Vec::new();
|
||||
for sample in samples {
|
||||
let size = math_round(sample.size * 10.0) / 10.0;
|
||||
if sample.role.is_empty() || !size.is_finite() || size < 8.0 || size >= 200.0 {
|
||||
continue;
|
||||
}
|
||||
match by_role.iter_mut().find(|(r, _)| *r == sample.role) {
|
||||
Some(slot) => slot.1.push(size),
|
||||
None => by_role.push((sample.role.clone(), vec![size])),
|
||||
}
|
||||
}
|
||||
|
||||
let mut roles: Vec<(String, f64)> = by_role
|
||||
.into_iter()
|
||||
.filter_map(|(role, sizes)| dominant_type_role_size(&sizes).map(|size| (role, size)))
|
||||
.collect();
|
||||
|
||||
if roles.len() < TYPE_HIERARCHY_MIN_ROLES {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
roles.sort_by(|a, b| {
|
||||
a.1.partial_cmp(&b.1)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
// JS-PARITY: checks.mjs sorts ties with `a.role.localeCompare(b.role)`.
|
||||
// Every role is `body` or `h1`..`h6`, lowercase ASCII, where the ICU
|
||||
// root collation and byte order agree.
|
||||
.then_with(|| a.0.cmp(&b.0))
|
||||
});
|
||||
let mut largest_step = 1.0f64;
|
||||
for i in 1..roles.len() {
|
||||
largest_step = math_max(largest_step, roles[i].1 / roles[i - 1].1);
|
||||
}
|
||||
if largest_step >= TYPE_HIERARCHY_MIN_STEP_RATIO {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let role_sizes: Vec<String> = roles
|
||||
.iter()
|
||||
.map(|(role, size)| format!("{} {}px", role, number_to_string(*size)))
|
||||
.collect();
|
||||
vec![RuleHit::new(
|
||||
"flat-type-hierarchy",
|
||||
format!(
|
||||
"Role sizes: {} (largest adjacent step {}:1; target {}:1)",
|
||||
role_sizes.join(", "),
|
||||
to_fixed(largest_step, 2),
|
||||
number_to_string(TYPE_HIERARCHY_MIN_STEP_RATIO)
|
||||
),
|
||||
)]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1367,8 +1367,11 @@ pub fn run_text_content_analyzers(
|
||||
return vec![];
|
||||
}
|
||||
let mut findings = Vec::new();
|
||||
// JS: the 3 text-content analyzers sit at indices 1-3 of REGEX_ANALYZERS.
|
||||
// flat-type-hierarchy left this source-only path in #702 because it needs
|
||||
// rendered role and usage evidence.
|
||||
for (i, rule_id) in TEXT_CONTENT_ANALYZER_IDS.iter().enumerate() {
|
||||
let analyzer = REGEX_ANALYZERS[2 + i];
|
||||
let analyzer = REGEX_ANALYZERS[1 + i];
|
||||
let meta = ProfileMeta {
|
||||
engine: "regex",
|
||||
phase: "text-content",
|
||||
|
||||
@@ -9,7 +9,7 @@ use impeccable_core::constants::{EM_DASH_CHARS_PER_DASH, EM_DASH_FLOOR, OVERUSED
|
||||
use impeccable_core::findings::{finding, Finding};
|
||||
use impeccable_core::fonts::extract_google_font_families;
|
||||
use impeccable_core::js::{
|
||||
self, ci, math_round, number_to_string, parse_float, string_to_number, to_fixed,
|
||||
self, ci, math_round, number_to_string, parse_float, string_to_number,
|
||||
};
|
||||
use impeccable_core::js_ext_a::{advance_utf16, slice_utf16_start, utf16_index, utf16_length};
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -787,33 +787,6 @@ fn utf16_slice(s: &str, start: usize, end: usize) -> String {
|
||||
|
||||
pub type Analyzer = fn(&str, &str) -> Vec<Finding>;
|
||||
|
||||
re!(
|
||||
FONT_SIZE_RE,
|
||||
format!(
|
||||
"{}{WS}*:{WS}*([0-9.]+)({px}|{rem}|{em}){B}",
|
||||
ci("font-size"),
|
||||
px = ci("px"),
|
||||
rem = ci("rem"),
|
||||
em = ci("em")
|
||||
)
|
||||
);
|
||||
re!(
|
||||
CLAMP_SIZE_RE,
|
||||
format!(
|
||||
"{fs}{WS}*:{WS}*{clamp}\\({WS}*([0-9.]+)({px}|{rem}|{em}){WS}*,{WS}*[^,]+,{WS}*([0-9.]+)({px}|{rem}|{em}){WS}*\\)",
|
||||
fs = ci("font-size"),
|
||||
clamp = ci("clamp"),
|
||||
px = ci("px"),
|
||||
rem = ci("rem"),
|
||||
em = ci("em")
|
||||
)
|
||||
);
|
||||
re!(FONT_SIZE_WORD_RE, ci("font-size"));
|
||||
re!(
|
||||
TEXT_SIZE_CLASS_RE,
|
||||
format!("{B}{}(?:xs|sm|base|lg|xl|[0-9])", ci("text-"))
|
||||
);
|
||||
|
||||
fn same_value_zero(a: f64, b: f64) -> bool {
|
||||
(a.is_nan() && b.is_nan()) || a == b
|
||||
}
|
||||
@@ -824,85 +797,6 @@ fn set_add(set: &mut Vec<f64>, v: f64) {
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_flat_type_hierarchy(content: &str, file_path: &str) -> Vec<Finding> {
|
||||
let mut sizes: Vec<f64> = Vec::new();
|
||||
let rem = 16.0;
|
||||
for m in FONT_SIZE_RE.captures_iter(content) {
|
||||
let px = if &m[2] == "px" {
|
||||
num(&m[1])
|
||||
} else {
|
||||
num(&m[1]) * rem
|
||||
};
|
||||
if px > 0.0 && px < 200.0 {
|
||||
set_add(&mut sizes, math_round(px * 10.0) / 10.0);
|
||||
}
|
||||
}
|
||||
for m in CLAMP_SIZE_RE.captures_iter(content) {
|
||||
let a = if &m[2] == "px" {
|
||||
num(&m[1])
|
||||
} else {
|
||||
num(&m[1]) * rem
|
||||
};
|
||||
set_add(&mut sizes, math_round(a * 10.0) / 10.0);
|
||||
let b = if &m[4] == "px" {
|
||||
num(&m[3])
|
||||
} else {
|
||||
num(&m[3]) * rem
|
||||
};
|
||||
set_add(&mut sizes, math_round(b * 10.0) / 10.0);
|
||||
}
|
||||
const TW: &[(&str, f64)] = &[
|
||||
("text-xs", 12.0),
|
||||
("text-sm", 14.0),
|
||||
("text-base", 16.0),
|
||||
("text-lg", 18.0),
|
||||
("text-xl", 20.0),
|
||||
("text-2xl", 24.0),
|
||||
("text-3xl", 30.0),
|
||||
("text-4xl", 36.0),
|
||||
("text-5xl", 48.0),
|
||||
("text-6xl", 60.0),
|
||||
("text-7xl", 72.0),
|
||||
("text-8xl", 96.0),
|
||||
("text-9xl", 128.0),
|
||||
];
|
||||
for (cls, px) in TW {
|
||||
let re = Regex::new(&format!("{B}{cls}{B}")).unwrap();
|
||||
if re.is_match(content) {
|
||||
set_add(&mut sizes, *px);
|
||||
}
|
||||
}
|
||||
if sizes.len() < 3 {
|
||||
return vec![];
|
||||
}
|
||||
let mut sorted = sizes.clone();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let ratio = sorted[sorted.len() - 1] / sorted[0];
|
||||
if ratio >= 2.0 {
|
||||
return vec![];
|
||||
}
|
||||
let mut line = 1;
|
||||
for (i, l) in content.split('\n').enumerate() {
|
||||
if FONT_SIZE_WORD_RE.is_match(l) || TEXT_SIZE_CLASS_RE.is_match(l) {
|
||||
line = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let list: Vec<String> = sorted
|
||||
.iter()
|
||||
.map(|s| format!("{}px", number_to_string(*s)))
|
||||
.collect();
|
||||
vec![finding(
|
||||
"flat-type-hierarchy",
|
||||
file_path,
|
||||
&format!(
|
||||
"Sizes: {} (ratio {}:1)",
|
||||
list.join(", "),
|
||||
to_fixed(ratio, 1)
|
||||
),
|
||||
line as f64,
|
||||
)]
|
||||
}
|
||||
|
||||
re!(
|
||||
SPACING_PX_RE,
|
||||
@@ -1220,7 +1114,6 @@ fn analyze_marquee(content: &str, file_path: &str) -> Vec<Finding> {
|
||||
|
||||
/// JS `REGEX_ANALYZERS` in order.
|
||||
pub const REGEX_ANALYZERS: &[Analyzer] = &[
|
||||
analyze_flat_type_hierarchy,
|
||||
analyze_monotonous_spacing,
|
||||
analyze_em_dash_overuse,
|
||||
analyze_marketing_buzzword,
|
||||
@@ -1240,7 +1133,6 @@ pub const TEXT_CONTENT_ANALYZER_IDS: &[&str] = &[
|
||||
/// The ruleIds the JS assigns to analyzers by index (`analyzerIds[i] || analyzer-${i+1}`).
|
||||
pub fn analyzer_rule_id(i: usize) -> String {
|
||||
const IDS: &[&str] = &[
|
||||
"flat-type-hierarchy",
|
||||
"monotonous-spacing",
|
||||
"em-dash-overuse",
|
||||
"marketing-buzzword",
|
||||
|
||||
@@ -73,7 +73,7 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Flat type hierarchy",
|
||||
description: "Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).",
|
||||
description: "Dominant heading and body roles are separated by less than 1.25× at every step, leaving the size hierarchy flat. Add at least one stronger size step.",
|
||||
skill_section: Some("Typography"),
|
||||
skill_guideline: Some("flat type hierarchy"),
|
||||
},
|
||||
|
||||
@@ -109,6 +109,7 @@ pub const STATIC_DEFAULT_STYLE: &[(&str, &str)] = &[
|
||||
("marginLeft", "0px"),
|
||||
("position", "static"),
|
||||
("visibility", "visible"),
|
||||
("contentVisibility", "visible"),
|
||||
("opacity", "1"),
|
||||
("top", "auto"),
|
||||
("right", "auto"),
|
||||
|
||||
+58
-33
@@ -8,12 +8,15 @@ use crate::background::{read_own_background_color, resolve_border_radius_px, sv}
|
||||
use crate::dom::{StaticDocument, StaticElement};
|
||||
use crate::quality::{has_nonblank_direct_text, pf0};
|
||||
use impeccable_core::checks::measures::{cream_from_class_list, is_cream_color};
|
||||
use impeccable_core::checks::rules::{is_card_like_from_props, RuleHit};
|
||||
use impeccable_core::checks::rules::{
|
||||
check_flat_type_hierarchy_samples, is_card_like_from_props, type_hierarchy_role, RuleHit,
|
||||
TypeSample, TYPE_HIERARCHY_SELECTOR,
|
||||
};
|
||||
use impeccable_core::checks::text_rules::{
|
||||
is_repeated_text_container, REPEATED_TEXT_CONTAINER_TAGS, REPEATED_TEXT_SKIP_SELECTOR,
|
||||
};
|
||||
use impeccable_core::constants::{CSS_GENERIC_FONTS, OVERUSED_FONTS, SAFE_TAGS};
|
||||
use impeccable_core::js::{self, math_round, number_to_string, parse_float, to_fixed};
|
||||
use impeccable_core::js::{self, number_to_string, parse_float};
|
||||
use impeccable_core::js_ext_b::{slice_utf16_prefix, utf16_len};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
@@ -60,40 +63,62 @@ pub fn check_static_page_typography(doc: &StaticDocument) -> Vec<RuleHit> {
|
||||
format!("Primary font: {}", font),
|
||||
));
|
||||
}
|
||||
let mut sizes: Vec<f64> = Vec::new();
|
||||
for el in
|
||||
doc.query_selector_all("h1, h2, h3, h4, h5, h6, p, span, a, li, td, th, label, button, div")
|
||||
{
|
||||
let font_size = parse_float(sv(el.style(), "fontSize"));
|
||||
if font_size >= 8.0 && font_size < 200.0 {
|
||||
let v = math_round(font_size * 10.0) / 10.0;
|
||||
if !sizes.contains(&v) {
|
||||
sizes.push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
if sizes.len() >= 3 {
|
||||
let mut sorted = sizes.clone();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let ratio = sorted[sorted.len() - 1] / sorted[0];
|
||||
if ratio < 2.0 {
|
||||
let list: Vec<String> = sorted
|
||||
.iter()
|
||||
.map(|s| format!("{}px", number_to_string(*s)))
|
||||
.collect();
|
||||
findings.push(RuleHit::new(
|
||||
"flat-type-hierarchy",
|
||||
format!(
|
||||
"Sizes: {} (ratio {}:1)",
|
||||
list.join(", "),
|
||||
to_fixed(ratio, 1)
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
findings.extend(check_flat_type_hierarchy_from_doc(doc));
|
||||
findings
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#isRenderedTypeElement over the static cascade.
|
||||
///
|
||||
/// JS-PARITY: jsdom's `el.hidden` reflects the `hidden` attribute, which the
|
||||
/// attribute test already covers. `contentVisibility` only ever reads its
|
||||
/// `STATIC_DEFAULT_STYLE` default here: css-cascade.mjs#STATIC_PROP_MAP has no
|
||||
/// `content-visibility` entry, so a declared `content-visibility: hidden`
|
||||
/// never reaches the static computed style.
|
||||
fn is_rendered_type_element(el: &StaticElement<'_>) -> bool {
|
||||
let mut current = Some(el.clone());
|
||||
while let Some(node) = current {
|
||||
if node.get_attribute("hidden").is_some() {
|
||||
return false;
|
||||
}
|
||||
let style = node.style();
|
||||
let display = js::to_lower_case(sv(style, "display"));
|
||||
let visibility = js::to_lower_case(sv(style, "visibility"));
|
||||
let content_visibility = js::to_lower_case(sv(style, "contentVisibility"));
|
||||
if display == "none"
|
||||
|| visibility == "hidden"
|
||||
|| visibility == "collapse"
|
||||
|| content_visibility == "hidden"
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let opacity = parse_float(sv(style, "opacity"));
|
||||
if opacity.is_finite() && opacity <= 0.01 {
|
||||
return false;
|
||||
}
|
||||
current = node.parent_element();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#checkFlatTypeHierarchyFromDoc over the static document.
|
||||
pub fn check_flat_type_hierarchy_from_doc(doc: &StaticDocument) -> Vec<RuleHit> {
|
||||
let mut samples: Vec<TypeSample> = Vec::new();
|
||||
for el in doc.query_selector_all(TYPE_HIERARCHY_SELECTOR) {
|
||||
if js::trim(&el.text_content()).is_empty() || !is_rendered_type_element(&el) {
|
||||
continue;
|
||||
}
|
||||
let font_size = parse_float(sv(el.style(), "fontSize"));
|
||||
if !font_size.is_finite() || font_size < 8.0 || font_size >= 200.0 {
|
||||
continue;
|
||||
}
|
||||
samples.push(TypeSample {
|
||||
role: type_hierarchy_role(&el.tag_lower()),
|
||||
size: font_size,
|
||||
});
|
||||
}
|
||||
check_flat_type_hierarchy_samples(&samples)
|
||||
}
|
||||
|
||||
// ─── Nested cards ───────────────────────────────────────────────────────────
|
||||
|
||||
static SHADOW_CLASS_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"80x80px icon tile above h3 \\\"Lightning Fast\\\"\"\n },\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"64x64px icon tile above h3 \\\"Secure Storage\\\"\"\n },\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"72x72px icon tile above h3 \\\"Easy Setup\\\"\"\n },\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"96x96px icon tile above h3 \\\"Powerful Analytics\\\"\"\n },\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"48x48px icon tile above h3 \\\"Emoji Inline Icon\\\"\"\n },\n {\n \"antipattern\": \"flat-type-hierarchy\",\n \"name\": \"Flat type hierarchy\",\n \"description\": \"Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"Sizes: 14px, 16px, 18px, 24px (ratio 1.7:1)\"\n },\n {\n \"antipattern\": \"ai-color-palette\",\n \"name\": \"AI color palette\",\n \"description\": \"Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"Purple/violet accent colors detected\"\n },\n {\n \"antipattern\": \"marketing-buzzword\",\n \"name\": \"Marketing buzzword\",\n \"description\": \"Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"1 buzzword phrase: \\\"ure Storage Enterprise-grade security fo\\\"\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"80x80px icon tile above h3 \\\"Lightning Fast\\\"\"\n },\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"64x64px icon tile above h3 \\\"Secure Storage\\\"\"\n },\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"72x72px icon tile above h3 \\\"Easy Setup\\\"\"\n },\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"96x96px icon tile above h3 \\\"Powerful Analytics\\\"\"\n },\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"48x48px icon tile above h3 \\\"Emoji Inline Icon\\\"\"\n },\n {\n \"antipattern\": \"ai-color-palette\",\n \"name\": \"AI color palette\",\n \"description\": \"Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"Purple/violet accent colors detected\"\n },\n {\n \"antipattern\": \"marketing-buzzword\",\n \"name\": \"Marketing buzzword\",\n \"description\": \"Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"1 buzzword phrase: \\\"ure Storage Enterprise-grade security fo\\\"\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"flat-type-hierarchy\",\n \"name\": \"Flat type hierarchy\",\n \"description\": \"Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Sizes: 11px, 14px, 16px (ratio 1.5:1)\"\n },\n {\n \"antipattern\": \"nested-cards\",\n \"name\": \"Nested cards\",\n \"description\": \"Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Card inside card (div)\"\n },\n {\n \"antipattern\": \"nested-cards\",\n \"name\": \"Nested cards\",\n \"description\": \"Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Card inside card (div)\"\n },\n {\n \"antipattern\": \"nested-cards\",\n \"name\": \"Nested cards\",\n \"description\": \"Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Card inside card (div)\"\n },\n {\n \"antipattern\": \"nested-cards\",\n \"name\": \"Nested cards\",\n \"description\": \"Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Card inside card (div)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"nested-cards\",\n \"name\": \"Nested cards\",\n \"description\": \"Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Card inside card (div)\"\n },\n {\n \"antipattern\": \"nested-cards\",\n \"name\": \"Nested cards\",\n \"description\": \"Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Card inside card (div)\"\n },\n {\n \"antipattern\": \"nested-cards\",\n \"name\": \"Nested cards\",\n \"description\": \"Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Card inside card (div)\"\n },\n {\n \"antipattern\": \"nested-cards\",\n \"name\": \"Nested cards\",\n \"description\": \"Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/layout.html\",\n \"line\": 0,\n \"snippet\": \"Card inside card (div)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 4px + border-radius: 8px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 5px + border-radius: 4px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"border-top: 4px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 3px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 6px + border-radius: 4px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 7px\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"4.4:1 (need 4.5:1) — text #64748b on #f6f6f6\"\n },\n {\n \"antipattern\": \"flat-type-hierarchy\",\n \"name\": \"Flat type hierarchy\",\n \"description\": \"Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"Sizes: 13px, 14px, 16px (ratio 1.2:1)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 4px + border-radius: 8px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 5px + border-radius: 4px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"border-top: 4px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 3px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 6px + border-radius: 4px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 7px\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"4.4:1 (need 4.5:1) — text #64748b on #f6f6f6\"\n },\n {\n \"antipattern\": \"flat-type-hierarchy\",\n \"name\": \"Flat type hierarchy\",\n \"description\": \"Dominant heading and body roles are separated by less than 1.25× at every step, leaving the size hierarchy flat. Add at least one stronger size step.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/named-color-borders.html\",\n \"line\": 0,\n \"snippet\": \"Role sizes: body 13px, h2 14px, h3 14px (largest adjacent step 1.08:1; target 1.25:1)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"stdout": "[]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"flat-type-hierarchy\",\n \"name\": \"Flat type hierarchy\",\n \"description\": \"Dominant heading and body roles are separated by less than 1.25× at every step, leaving the size hierarchy flat. Add at least one stronger size step.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/numbered-section-markers.html\",\n \"line\": 0,\n \"snippet\": \"Role sizes: body 16px, h1 16px, h2 16px (largest adjacent step 1.00:1; target 1.25:1)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/oklch-neon-text.html\",\n \"line\": 0,\n \"snippet\": \"1.3:1 (need 4.5:1) — text #00f4f6 on #f5f5f5\"\n },\n {\n \"antipattern\": \"flat-type-hierarchy\",\n \"name\": \"Flat type hierarchy\",\n \"description\": \"Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/oklch-neon-text.html\",\n \"line\": 0,\n \"snippet\": \"Sizes: 13px, 16px, 18px (ratio 1.4:1)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/oklch-neon-text.html\",\n \"line\": 0,\n \"snippet\": \"1.3:1 (need 4.5:1) — text #00f4f6 on #f5f5f5\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"overused-font\",\n \"name\": \"Overused font\",\n \"description\": \"Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/typography-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"Primary font: inter\"\n },\n {\n \"antipattern\": \"flat-type-hierarchy\",\n \"name\": \"Flat type hierarchy\",\n \"description\": \"Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/typography-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"Sizes: 13px, 14px, 15px, 16px, 18px (ratio 1.4:1)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"overused-font\",\n \"name\": \"Overused font\",\n \"description\": \"Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/typography-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"Primary font: inter\"\n },\n {\n \"antipattern\": \"flat-type-hierarchy\",\n \"name\": \"Flat type hierarchy\",\n \"description\": \"Dominant heading and body roles are separated by less than 1.25× at every step, leaving the size hierarchy flat. Add at least one stronger size step.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/typography-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"Role sizes: body 14px, h3 15px, h2 16px, h1 18px (largest adjacent step 1.13:1; target 1.25:1)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\n [icon-tile-stack] 80x80px icon tile above h3 \"Lightning Fast\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [icon-tile-stack] 64x64px icon tile above h3 \"Secure Storage\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [icon-tile-stack] 72x72px icon tile above h3 \"Easy Setup\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [icon-tile-stack] 96x96px icon tile above h3 \"Powerful Analytics\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [icon-tile-stack] 48x48px icon tile above h3 \"Emoji Inline Icon\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [flat-type-hierarchy] Sizes: 14px, 16px, 18px, 24px (ratio 1.7:1)\n → Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\n [ai-color-palette] Purple/violet accent colors detected\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n [marketing-buzzword] 1 buzzword phrase: \"ure Storage Enterprise-grade security fo\"\n → Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.\n\n8 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\n [icon-tile-stack] 80x80px icon tile above h3 \"Lightning Fast\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [icon-tile-stack] 64x64px icon tile above h3 \"Secure Storage\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [icon-tile-stack] 72x72px icon tile above h3 \"Easy Setup\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [icon-tile-stack] 96x96px icon tile above h3 \"Powerful Analytics\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [icon-tile-stack] 48x48px icon tile above h3 \"Emoji Inline Icon\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [ai-color-palette] Purple/violet accent colors detected\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n [marketing-buzzword] 1 buzzword phrase: \"ure Storage Enterprise-grade security fo\"\n → Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.\n\n7 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/layout.html\n [flat-type-hierarchy] Sizes: 11px, 14px, 16px (ratio 1.5:1)\n → Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\n [nested-cards] Card inside card (div)\n → Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\n [nested-cards] Card inside card (div)\n → Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\n [nested-cards] Card inside card (div)\n → Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\n [nested-cards] Card inside card (div)\n → Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\n\n5 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/layout.html\n [nested-cards] Card inside card (div)\n → Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\n [nested-cards] Card inside card (div)\n → Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\n [nested-cards] Card inside card (div)\n → Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\n [nested-cards] Card inside card (div)\n → Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.\n\n4 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/modern-color-borders.html\n [side-tab] border-left: 3px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 6px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 6px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 6px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 3px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 6px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 5px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-right: 4px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 6px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [low-contrast] 4.49:1 (need 4.5:1) — text #64748b on #fef7f2\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [flat-type-hierarchy] Sizes: 13px, 14px, 16px (ratio 1.2:1)\n → Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\n\n14 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/modern-color-borders.html\n [side-tab] border-left: 3px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 6px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 6px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 6px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 3px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 6px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 5px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-right: 4px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 4px + border-radius: 6px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [low-contrast] 4.49:1 (need 4.5:1) — text #64748b on #fef7f2\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [flat-type-hierarchy] Role sizes: body 13px, h2 14px, h3 14px (largest adjacent step 1.08:1; target 1.25:1)\n → Dominant heading and body roles are separated by less than 1.25× at every step, leaving the size hierarchy flat. Add at least one stronger size step.\n\n14 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/motion.html\n [bounce-easing] animation: bounce-keyframe\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n [bounce-easing] cubic-bezier(0.68, -0.55, 0.265, 1.55)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n [layout-transition] transition: width\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: height\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: padding\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: margin\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: max-height\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: width, height\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: width\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: width\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [flat-type-hierarchy] Sizes: 11px, 12px, 13px, 14px, 16px (ratio 1.5:1)\n → Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\n\n11 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/motion.html\n [bounce-easing] animation: bounce-keyframe\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n [bounce-easing] cubic-bezier(0.68, -0.55, 0.265, 1.55)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n [layout-transition] transition: width\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: height\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: padding\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: margin\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: max-height\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: width, height\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: width\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [layout-transition] transition: width\n → Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.\n [flat-type-hierarchy] Role sizes: h3 11px, body 12px, h4 13px, h2 14px (largest adjacent step 1.09:1; target 1.25:1)\n → Dominant heading and body roles are separated by less than 1.25× at every step, leaving the size hierarchy flat. Add at least one stronger size step.\n\n11 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/named-color-borders.html\n [side-tab] border-left: 4px + border-radius: 8px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 5px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-top: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 3px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 6px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 7px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [low-contrast] 4.4:1 (need 4.5:1) — text #64748b on #f6f6f6\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [flat-type-hierarchy] Sizes: 13px, 14px, 16px (ratio 1.2:1)\n → Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\n\n8 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/named-color-borders.html\n [side-tab] border-left: 4px + border-radius: 8px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 5px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-top: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 3px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 6px + border-radius: 4px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 7px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [low-contrast] 4.4:1 (need 4.5:1) — text #64748b on #f6f6f6\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [flat-type-hierarchy] Role sizes: body 13px, h2 14px, h3 14px (largest adjacent step 1.08:1; target 1.25:1)\n → Dominant heading and body roles are separated by less than 1.25× at every step, leaving the size hierarchy flat. Add at least one stronger size step.\n\n8 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/numbered-section-markers.html\n [flat-type-hierarchy] Role sizes: body 16px, h1 16px, h2 16px (largest adjacent step 1.00:1; target 1.25:1)\n → Dominant heading and body roles are separated by less than 1.25× at every step, leaving the size hierarchy flat. Add at least one stronger size step.\n\n1 anti-pattern found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/oklch-neon-text.html\n [low-contrast] 1.3:1 (need 4.5:1) — text #00f4f6 on #f5f5f5\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [flat-type-hierarchy] Sizes: 13px, 16px, 18px (ratio 1.4:1)\n → Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\n\n2 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/oklch-neon-text.html\n [low-contrast] 1.3:1 (need 4.5:1) — text #00f4f6 on #f5f5f5\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n\n1 anti-pattern found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/typography-should-flag.html\n [overused-font] Primary font: inter\n → Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.\n [flat-type-hierarchy] Sizes: 13px, 14px, 15px, 16px, 18px (ratio 1.4:1)\n → Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\n\n2 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/typography-should-flag.html\n [overused-font] Primary font: inter\n → Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.\n [flat-type-hierarchy] Role sizes: body 14px, h3 15px, h2 16px, h1 18px (largest adjacent step 1.13:1; target 1.25:1)\n → Dominant heading and body roles are separated by less than 1.25× at every step, leaving the size hierarchy flat. Add at least one stronger size step.\n\n2 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
Reference in New Issue
Block a user