mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 01:26:29 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f285b3d7f1 |
+28
-11
@@ -46,6 +46,24 @@ function __rectArray(r) {
|
||||
return [r.x, r.y, r.width, r.height, r.top, r.right, r.bottom, r.left];
|
||||
}
|
||||
|
||||
// The client rects of an element's non-blank direct text nodes, one per line
|
||||
// box, in document order. Both text-rect probes read the page through this:
|
||||
// the union one merges them, the line one hands them over as they are.
|
||||
function __textLineRects(el) {
|
||||
const node = __el(el);
|
||||
const rects = [];
|
||||
for (const child of node.childNodes) {
|
||||
if (child.nodeType !== 3 || !(child.textContent || '').trim()) continue;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(child);
|
||||
for (const rect of range.getClientRects()) {
|
||||
if (rect.width >= 1 && rect.height >= 1) rects.push(rect);
|
||||
}
|
||||
range.detach?.();
|
||||
}
|
||||
return rects;
|
||||
}
|
||||
|
||||
const __impeccableDom = {
|
||||
document_element() { return __intern(document.documentElement); },
|
||||
body() { return __intern(document.body); },
|
||||
@@ -181,17 +199,7 @@ const __impeccableDom = {
|
||||
// getDirectTextRect(el) from the JS driver: union of the client rects of
|
||||
// the element's non-blank direct text nodes.
|
||||
direct_text_rect(el) {
|
||||
const node = __el(el);
|
||||
const rects = [];
|
||||
for (const child of node.childNodes) {
|
||||
if (child.nodeType !== 3 || !(child.textContent || '').trim()) continue;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(child);
|
||||
for (const rect of range.getClientRects()) {
|
||||
if (rect.width >= 1 && rect.height >= 1) rects.push(rect);
|
||||
}
|
||||
range.detach?.();
|
||||
}
|
||||
const rects = __textLineRects(el);
|
||||
if (rects.length === 0) return [];
|
||||
const left = Math.min(...rects.map(r => r.left));
|
||||
const top = Math.min(...rects.map(r => r.top));
|
||||
@@ -199,4 +207,13 @@ const __impeccableDom = {
|
||||
const bottom = Math.max(...rects.map(r => r.bottom));
|
||||
return [left, top, right - left, bottom - top, top, right, bottom, left];
|
||||
},
|
||||
// The same rects, unmerged: getClientRects() returns one per line box, so
|
||||
// this is the element's text line by line, flattened into eights.
|
||||
direct_text_line_rects(el) {
|
||||
const out = [];
|
||||
for (const r of __textLineRects(el)) {
|
||||
out.push(r.left, r.top, r.width, r.height, r.top, r.right, r.bottom, r.left);
|
||||
}
|
||||
return out;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -224,6 +224,7 @@ pub fn serialize_design_system_for_browser(ds: Option<&DesignSystem>) -> Value {
|
||||
"hasRadii": ds.has_radii,
|
||||
"allowedRadii": radii,
|
||||
"hasPillRadius": ds.has_pill_radius,
|
||||
"declaredSelectors": ds.declared_selectors,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,10 @@ pub struct DesignSeen {
|
||||
/// `None` when `!raw?.present`.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DesignSystemConfig {
|
||||
/// Selectors the repository's design document names as its own, e.g.
|
||||
/// `.eyebrow` written into DESIGN.md. A rule that would charge one of
|
||||
/// these is reviewing the design system rather than the change (REN-406).
|
||||
pub declared_selectors: Vec<String>,
|
||||
pub has_fonts: bool,
|
||||
pub allowed_fonts: Vec<String>,
|
||||
pub has_colors: bool,
|
||||
@@ -217,8 +221,15 @@ pub fn browser_design_system_config(config: &BrowserConfig) -> Option<DesignSyst
|
||||
.map(js_number)
|
||||
.filter(|px| px.is_finite())
|
||||
.collect();
|
||||
let declared_selectors: Vec<String> = arr("declaredSelectors")
|
||||
.iter()
|
||||
.map(js_string_or_empty)
|
||||
.map(|s| crate::js::trim(&s).to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
let is_true = |k: &str| matches!(obj.get(k), Some(serde_json::Value::Bool(true)));
|
||||
Some(DesignSystemConfig {
|
||||
declared_selectors,
|
||||
has_fonts: is_true("hasFonts") && !allowed_fonts.is_empty(),
|
||||
allowed_fonts,
|
||||
has_colors: is_true("hasColors") && !allowed_colors.is_empty(),
|
||||
@@ -1318,6 +1329,11 @@ pub fn collect_browser_findings(dom: &dyn Dom, config: &BrowserConfig) -> Collec
|
||||
let rule_ok = |id: &str| disabled.is_empty() || !disabled.iter().any(|d| d == id);
|
||||
let design_system = browser_design_system_config(config);
|
||||
let mut design_seen = DesignSeen::default();
|
||||
// The AI palette is read over the whole page: neon ink on a near-black
|
||||
// ground waits here until a second tell hue turns up somewhere, so one
|
||||
// deliberate accent stays an accent (REN-405).
|
||||
let mut palette_tells: Vec<ec::TellHue> = Vec::new();
|
||||
let mut palette_ink: Vec<(ElId, BrowserFinding)> = Vec::new();
|
||||
let body = dom.body();
|
||||
let root = dom.document_element();
|
||||
// JS `document.body` may be null on a bare document; every
|
||||
@@ -1353,7 +1369,12 @@ 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)));
|
||||
let palette = ec::check_element_ai_palette_dom(dom, el);
|
||||
palette_tells.extend(palette.tells.iter().copied());
|
||||
if let Some(ink) = palette.ink {
|
||||
palette_ink.push((el, BrowserFinding::new(ink.id, ink.snippet)));
|
||||
}
|
||||
findings.extend(hits(palette.hits));
|
||||
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)));
|
||||
@@ -1390,6 +1411,17 @@ pub fn collect_browser_findings(dom: &dyn Dom, config: &BrowserConfig) -> Collec
|
||||
}
|
||||
}
|
||||
|
||||
// Two different tell hues on one page is the palette; one is an accent.
|
||||
if palette_tells.iter().any(|t| *t == ec::TellHue::Cyan)
|
||||
&& palette_tells.iter().any(|t| *t == ec::TellHue::Purple)
|
||||
{
|
||||
for (el, finding) in palette_ink {
|
||||
if rule_ok(&finding.type_) {
|
||||
add_browser_findings(dom, &mut groups, el, vec![finding]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let page_pass = |groups: &mut Vec<FindingGroup>, page_level: &mut Vec<BrowserFinding>, list: Vec<BrowserFinding>| {
|
||||
let list: Vec<BrowserFinding> = list.into_iter().filter(|f| rule_ok(&f.type_)).collect();
|
||||
if !list.is_empty() {
|
||||
@@ -1398,17 +1430,6 @@ pub fn collect_browser_findings(dom: &dyn Dom, config: &BrowserConfig) -> Collec
|
||||
}
|
||||
};
|
||||
|
||||
page_pass(
|
||||
&mut groups,
|
||||
&mut page_level,
|
||||
check_browser_design_system_sources(dom, design_system.as_ref(), &mut design_seen),
|
||||
);
|
||||
page_pass(&mut groups, &mut page_level, pc::check_typography(dom));
|
||||
page_pass(&mut groups, &mut page_level, hits(tc::check_kicker_above_heading_dom(dom)));
|
||||
page_pass(&mut groups, &mut page_level, hits(tc::check_numbered_section_labels_dom(dom)));
|
||||
page_pass(&mut groups, &mut page_level, hits(tc::check_repeated_container_text_dom(dom)));
|
||||
page_pass(&mut groups, &mut page_level, hits(tc::check_em_dash_overuse_dom(dom)));
|
||||
|
||||
let el_pass = |groups: &mut Vec<FindingGroup>, list: Vec<super::ElFinding>| {
|
||||
for f in list {
|
||||
if !rule_ok(&f.finding.type_) {
|
||||
@@ -1423,6 +1444,18 @@ pub fn collect_browser_findings(dom: &dyn Dom, config: &BrowserConfig) -> Collec
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
page_pass(
|
||||
&mut groups,
|
||||
&mut page_level,
|
||||
check_browser_design_system_sources(dom, design_system.as_ref(), &mut design_seen),
|
||||
);
|
||||
page_pass(&mut groups, &mut page_level, pc::check_typography(dom));
|
||||
el_pass(&mut groups, tc::check_kicker_above_heading_dom(dom, design_system.as_ref()));
|
||||
page_pass(&mut groups, &mut page_level, hits(tc::check_numbered_section_labels_dom(dom)));
|
||||
page_pass(&mut groups, &mut page_level, hits(tc::check_repeated_container_text_dom(dom)));
|
||||
page_pass(&mut groups, &mut page_level, hits(tc::check_em_dash_overuse_dom(dom)));
|
||||
|
||||
el_pass(&mut groups, pc::check_layout(dom));
|
||||
el_pass(&mut groups, pc::check_heading_rhythm_dom(dom));
|
||||
el_pass(&mut groups, pc::check_edge_flush_cards_dom(dom));
|
||||
@@ -1638,6 +1671,57 @@ mod tests {
|
||||
assert!(!is_likely_hashed_class("abcdefg"));
|
||||
}
|
||||
|
||||
/// REN-405. Northwind's Slate system: near-black ground, light ink, one
|
||||
/// teal accent. The accent lit 18 places on a page with nothing wrong with
|
||||
/// it. It stays quiet until the page shows the other half of the palette.
|
||||
#[test]
|
||||
fn one_accent_hue_on_dark_is_not_the_ai_palette() {
|
||||
let build = |gradient: bool| {
|
||||
let mut d = FakeDom::new();
|
||||
let (_html, body) = d.with_page();
|
||||
d.set_style(body, "backgroundColor", "rgb(15, 18, 17)");
|
||||
d.set_rect(body, 0.0, 0.0, 1440.0, 900.0);
|
||||
for i in 0..3 {
|
||||
let a = d.add(Some(body), "a");
|
||||
d.add_text(a, "Open the ledger");
|
||||
d.set_rect(a, 40.0, 40.0 + 30.0 * (i as f64), 160.0, 20.0);
|
||||
d.set_styles(a, &[("color", "rgb(47, 184, 166)")]);
|
||||
}
|
||||
if gradient {
|
||||
let hero = d.add(Some(body), "div");
|
||||
d.set_rect(hero, 0.0, 200.0, 1440.0, 320.0);
|
||||
d.set_style(
|
||||
hero,
|
||||
"backgroundImage",
|
||||
"linear-gradient(135deg, rgb(124, 58, 237) 0%, rgb(168, 85, 247) 100%)",
|
||||
);
|
||||
}
|
||||
d
|
||||
};
|
||||
let ids = |d: &FakeDom| {
|
||||
collect_browser_findings(d, &BrowserConfig::default())
|
||||
.groups
|
||||
.iter()
|
||||
.flat_map(|g| g.findings.iter())
|
||||
.filter(|f| f.type_ == "ai-color-palette")
|
||||
.map(|f| f.detail.clone())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
// One teal accent on near-black: an accent.
|
||||
assert_eq!(ids(&build(false)), Vec::<String>::new());
|
||||
// The same accent beside a purple gradient: the palette, and every
|
||||
// place it shows is named.
|
||||
assert_eq!(
|
||||
ids(&build(true)),
|
||||
vec![
|
||||
"Purple/violet gradient background".to_string(),
|
||||
"Cyan neon text on dark background".to_string(),
|
||||
"Cyan neon text on dark background".to_string(),
|
||||
"Cyan neon text on dark background".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_scan_empties_the_collect_pass() {
|
||||
// JS: index.mjs#skipScanActive() — an ignoreFiles-waived page answers
|
||||
|
||||
@@ -686,24 +686,67 @@ pub fn check_element_glow_dom(dom: &dyn Dom, el: ElId) -> Vec<RuleHit> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The two hues the AI palette is built out of. A page that uses one of them
|
||||
/// has an accent; a page that uses both has the palette.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TellHue {
|
||||
Cyan,
|
||||
Purple,
|
||||
}
|
||||
|
||||
impl TellHue {
|
||||
/// The band a colour falls in, `None` outside both.
|
||||
fn of(hue: f64) -> Option<TellHue> {
|
||||
if (160.0..=200.0).contains(&hue) {
|
||||
Some(TellHue::Cyan)
|
||||
} else if (260.0..=310.0).contains(&hue) {
|
||||
Some(TellHue::Purple)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
TellHue::Cyan => "Cyan",
|
||||
TellHue::Purple => "Purple/violet",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What one element contributes to the AI-palette reading.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AiPaletteReading {
|
||||
/// Charged where they are found: a saturated cyan or purple *gradient* is
|
||||
/// the pattern by itself, whatever else the page does.
|
||||
pub hits: Vec<RuleHit>,
|
||||
/// Neon ink on a near-black ground, held until a second tell hue shows up
|
||||
/// somewhere on the page (REN-405).
|
||||
pub ink: Option<RuleHit>,
|
||||
/// The tell hues this element showed, gradient and ink alike.
|
||||
pub tells: Vec<TellHue>,
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#checkElementAIPaletteDOM(el)
|
||||
pub fn check_element_ai_palette_dom(dom: &dyn Dom, el: ElId) -> Vec<RuleHit> {
|
||||
let mut findings = Vec::new();
|
||||
///
|
||||
/// One element's reading. The gradient half answers on its own; the ink half
|
||||
/// is held for the page pass, because a single saturated hue on a dark ground
|
||||
/// is how a great many ordinary systems draw their one accent — a teal
|
||||
/// `#2fb8a6` on near-black lit 18 places on the bench's base, and every one of
|
||||
/// them was the same deliberate accent (REN-405). Two different tell hues on
|
||||
/// one page is the palette the rule is named for.
|
||||
pub fn check_element_ai_palette_dom(dom: &dyn Dom, el: ElId) -> AiPaletteReading {
|
||||
let mut reading = AiPaletteReading::default();
|
||||
let bg_image = dom.style(el, "backgroundImage");
|
||||
for c in parse_gradient_colors(Some(&bg_image)) {
|
||||
if has_chroma(Some(&c), Some(50.0)) {
|
||||
let hue = get_hue(Some(&c));
|
||||
if hue >= 260.0 && hue <= 310.0 {
|
||||
findings.push(RuleHit::new(
|
||||
if let Some(tell) = TellHue::of(get_hue(Some(&c))) {
|
||||
reading.tells.push(tell);
|
||||
reading.hits.push(RuleHit::new(
|
||||
"ai-color-palette",
|
||||
"Purple/violet gradient background".to_string(),
|
||||
));
|
||||
break;
|
||||
}
|
||||
if hue >= 160.0 && hue <= 200.0 {
|
||||
findings.push(RuleHit::new(
|
||||
"ai-color-palette",
|
||||
"Cyan gradient background".to_string(),
|
||||
match tell {
|
||||
TellHue::Purple => "Purple/violet gradient background".to_string(),
|
||||
TellHue::Cyan => "Cyan gradient background".to_string(),
|
||||
},
|
||||
));
|
||||
break;
|
||||
}
|
||||
@@ -712,10 +755,7 @@ 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"));
|
||||
if let Some(tc) = text_color {
|
||||
if has_chroma(Some(&tc), Some(80.0)) {
|
||||
let hue = get_hue(Some(&tc));
|
||||
let is_ai_palette =
|
||||
(hue >= 160.0 && hue <= 200.0) || (hue >= 260.0 && hue <= 310.0);
|
||||
if is_ai_palette {
|
||||
if let Some(tell) = TellHue::of(get_hue(Some(&tc))) {
|
||||
let parent = dom.parent(el);
|
||||
let parent_bg_info = match parent {
|
||||
Some(p) => resolve_background_info(dom, p),
|
||||
@@ -730,21 +770,17 @@ pub fn check_element_ai_palette_dom(dom: &dyn Dom, el: ElId) -> Vec<RuleHit> {
|
||||
}
|
||||
if let Some(bg) = effective_bg {
|
||||
if relative_luminance(&bg) < 0.1 {
|
||||
let label = if hue >= 260.0 {
|
||||
"Purple/violet"
|
||||
} else {
|
||||
"Cyan"
|
||||
};
|
||||
findings.push(RuleHit::new(
|
||||
reading.tells.push(tell);
|
||||
reading.ink = Some(RuleHit::new(
|
||||
"ai-color-palette",
|
||||
format!("{label} neon text on dark background"),
|
||||
format!("{} neon text on dark background", tell.label()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
findings
|
||||
reading
|
||||
}
|
||||
|
||||
// ── radial spotlight ──────────────────────────────────────────────────────
|
||||
@@ -1449,9 +1485,11 @@ 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);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].snippet, "Purple/violet gradient background");
|
||||
let reading = check_element_ai_palette_dom(&d, hero);
|
||||
assert_eq!(reading.hits.len(), 1);
|
||||
assert_eq!(reading.hits[0].snippet, "Purple/violet gradient background");
|
||||
assert!(reading.ink.is_none());
|
||||
assert_eq!(reading.tells, vec![TellHue::Purple]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -92,7 +92,41 @@ pub fn has_meaningful_direct_text(dom: &dyn Dom, el: ElId) -> bool {
|
||||
has_direct_text_longer_than(dom, el, 4)
|
||||
}
|
||||
|
||||
/// The width of every line the element's own text rendered on.
|
||||
///
|
||||
/// `Range.getClientRects()` gives one rect per line box, so the browser probe
|
||||
/// hands the lines over as they are. A probe that can only merge them (a
|
||||
/// captured snapshot) hands over the union, and the union is divided by the
|
||||
/// line box to get its line count back — either way the caller reads lines
|
||||
/// and never a box.
|
||||
fn rendered_line_widths(dom: &dyn Dom, el: ElId, line_box: f64) -> Vec<f64> {
|
||||
let mut widths: Vec<f64> = Vec::new();
|
||||
for r in dom.direct_text_line_rects(el) {
|
||||
if r.width <= 0.0 || r.height <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
// Capped: a line box a stylesheet has shrunk to a fraction of the
|
||||
// glyphs would otherwise turn one paragraph into thousands of lines,
|
||||
// and no measure is read off a number that large anyway.
|
||||
let lines = if line_box > 0.0 {
|
||||
js::math_min(500.0, js::math_max(1.0, math_round(r.height / line_box)))
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
for _ in 0..(lines as usize) {
|
||||
widths.push(r.width);
|
||||
}
|
||||
}
|
||||
widths
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#textDescendantsFlushSides(el, rect) → [top, right, bottom, left]
|
||||
///
|
||||
/// The side is flush when the *text* lands on it, not when a text-bearing box
|
||||
/// does. A `<td>` fills its table edge to edge and insets its own text by the
|
||||
/// cell padding; reading the cell's border box called that flush and charged a
|
||||
/// framed table for having no inset, when the reader sees the padding the
|
||||
/// cells declare (REN-403).
|
||||
pub fn text_descendants_flush_sides(dom: &dyn Dom, el: ElId, rect: &Rect) -> [bool; 4] {
|
||||
let mut flush = [false; 4];
|
||||
const TEXT_EDGE_THRESHOLD: f64 = 4.0;
|
||||
@@ -102,7 +136,7 @@ pub fn text_descendants_flush_sides(dom: &dyn Dom, el: ElId, rect: &Rect) -> [bo
|
||||
if !TEXT_EDGE_TAGS.contains(&tag_name.as_str()) || !has_meaningful_direct_text(dom, node) {
|
||||
continue;
|
||||
}
|
||||
let nr = dom.rect(node);
|
||||
let nr = dom.direct_text_rect(node).unwrap_or_else(|| dom.rect(node));
|
||||
if nr.width <= 0.0 || nr.height <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
@@ -245,23 +279,50 @@ pub fn check_quality(dom: &dyn Dom, q: &QualityInput) -> Vec<RuleHit> {
|
||||
}
|
||||
|
||||
// --- Line length too long ---
|
||||
//
|
||||
// The measure is the line that rendered, not the box that could have held
|
||||
// it. `rect.width / (fontSize * 0.5)` is the box's capacity: a paragraph
|
||||
// sitting in a 1022px column whose text stops at 571px was charged with
|
||||
// 142 characters a line it never rendered (REN-402). What the reader sees
|
||||
// is `direct_text_line_rects`, one rect per line box, and the characters
|
||||
// divide between the lines in proportion to the ink each carries — one
|
||||
// element's text is one font at one size, so the average advance is the
|
||||
// same on every line of it.
|
||||
//
|
||||
// Charged when at least two rendered lines run past the maximum. The harm
|
||||
// this rule names is the eye losing its place tracking back to the start
|
||||
// of the next line, so it takes a column of long lines to do the damage;
|
||||
// one long line and a short tail is a sentence that wrapped once.
|
||||
if has_direct_text
|
||||
&& QUALITY_TEXT_TAGS.contains(&tag)
|
||||
&& rect.width > 0.0
|
||||
&& (text_len as f64) > line_max
|
||||
{
|
||||
let chars_per_line = rect.width / (font_size * 0.5);
|
||||
if chars_per_line > line_max + 5.0 {
|
||||
let line_box = match q.line_height_px {
|
||||
Some(px) if px > 0.0 => px,
|
||||
_ => font_size * 1.2,
|
||||
};
|
||||
let widths = rendered_line_widths(dom, el, line_box);
|
||||
let total: f64 = widths.iter().sum();
|
||||
if total > 0.0 {
|
||||
let over = line_max + 5.0;
|
||||
let chars = |w: f64| (text_len as f64) * w / total;
|
||||
let long = widths.iter().filter(|w| chars(**w) > over).count();
|
||||
if long >= 2 {
|
||||
let longest = widths.iter().copied().fold(0.0, js::math_max);
|
||||
findings.push(RuleHit::new(
|
||||
"line-length",
|
||||
format!(
|
||||
"~{} chars/line (aim for <{})",
|
||||
number_to_string(math_round(chars_per_line)),
|
||||
"~{} chars on {} of {} rendered lines (aim for <{})",
|
||||
number_to_string(math_round(chars(longest))),
|
||||
number_to_string(long as f64),
|
||||
number_to_string(widths.len() as f64),
|
||||
number_to_string(line_max)
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Cramped padding ---
|
||||
let is_inline_code = tag == "code" && closest_or_none(dom, el, "pre").is_none();
|
||||
@@ -274,31 +335,43 @@ pub fn check_quality(dom: &dyn Dom, q: &QualityInput) -> Vec<RuleHit> {
|
||||
];
|
||||
let border_count = borders.iter().filter(|w| **w > 0.0).count();
|
||||
let has_bg = has_visible_background_boundary(dom, el);
|
||||
if border_count >= 2 || has_bg {
|
||||
// The space the reader sees, not the space the stylesheet declares:
|
||||
// the inset between the rendered text and the inside of the border
|
||||
// box. A 44px control with `padding: 0 16px` whose label a flex box
|
||||
// centres has 12px of air above the label and was charged with "0px
|
||||
// vertical padding" (REN-403). Nothing to measure the text with is
|
||||
// nothing to charge on, and the text is measured only for the
|
||||
// elements that got this far: the probe builds a Range per call, and
|
||||
// a page has a great many boxes that are not bounded at all.
|
||||
if let Some(text_rect) = (border_count >= 2 || has_bg)
|
||||
.then(|| dom.direct_text_rect(el))
|
||||
.flatten()
|
||||
{
|
||||
let mut v_pads: Vec<f64> = Vec::new();
|
||||
let mut h_pads: Vec<f64> = Vec::new();
|
||||
if has_bg || borders[0] > 0.0 {
|
||||
v_pads.push(spx("paddingTop"));
|
||||
v_pads.push(text_rect.top - (rect.top + borders[0]));
|
||||
}
|
||||
if has_bg || borders[2] > 0.0 {
|
||||
v_pads.push(spx("paddingBottom"));
|
||||
v_pads.push((rect.bottom - borders[2]) - text_rect.bottom);
|
||||
}
|
||||
if has_bg || borders[3] > 0.0 {
|
||||
h_pads.push(spx("paddingLeft"));
|
||||
h_pads.push(text_rect.left - (rect.left + borders[3]));
|
||||
}
|
||||
if has_bg || borders[1] > 0.0 {
|
||||
h_pads.push(spx("paddingRight"));
|
||||
h_pads.push((rect.right - borders[1]) - text_rect.right);
|
||||
}
|
||||
let v_min = v_pads.iter().copied().fold(f64::INFINITY, js::math_min);
|
||||
let h_min = h_pads.iter().copied().fold(f64::INFINITY, js::math_min);
|
||||
let v_thresh = js::math_max(4.0, font_size * 0.3);
|
||||
let h_thresh = js::math_max(8.0, font_size * 0.5);
|
||||
let px = |v: f64| number_to_string(math_round(v * 10.0) / 10.0);
|
||||
if v_min < v_thresh {
|
||||
findings.push(RuleHit::new(
|
||||
"cramped-padding",
|
||||
format!(
|
||||
"{}px vertical padding (need ≥{}px for {}px text)",
|
||||
number_to_string(v_min),
|
||||
"{}px of space above and below the text (need ≥{}px for {}px text)",
|
||||
px(v_min),
|
||||
to_fixed(v_thresh, 1),
|
||||
number_to_string(font_size)
|
||||
),
|
||||
@@ -307,8 +380,8 @@ pub fn check_quality(dom: &dyn Dom, q: &QualityInput) -> Vec<RuleHit> {
|
||||
findings.push(RuleHit::new(
|
||||
"cramped-padding",
|
||||
format!(
|
||||
"{}px horizontal padding (need ≥{}px for {}px text)",
|
||||
number_to_string(h_min),
|
||||
"{}px of space beside the text (need ≥{}px for {}px text)",
|
||||
px(h_min),
|
||||
to_fixed(h_thresh, 1),
|
||||
number_to_string(font_size)
|
||||
),
|
||||
@@ -380,6 +453,13 @@ pub fn check_quality(dom: &dyn Dom, q: &QualityInput) -> Vec<RuleHit> {
|
||||
];
|
||||
const PAD_THRESHOLD: f64 = 2.0;
|
||||
const CHILD_INSULATE_THRESHOLD: f64 = 4.0;
|
||||
// Content that runs past the box is clipped, not snug. A
|
||||
// table with a min-width inside an `overflow: hidden` frame
|
||||
// has its far column cut off, which is a defect
|
||||
// `clipped-overflow-container` is named for; calling it "no
|
||||
// inset" points at the wrong thing (REN-403).
|
||||
const OVERFLOW_TOLERANCE: f64 = 1.0;
|
||||
let mut children_overflow = [false; 4];
|
||||
let mut children_insulate = [false; 4];
|
||||
for &child in &children {
|
||||
let child_pad = [
|
||||
@@ -396,6 +476,18 @@ pub fn check_quality(dom: &dyn Dom, q: &QualityInput) -> Vec<RuleHit> {
|
||||
];
|
||||
let cr = dom.rect(child);
|
||||
if cr.width > 0.0 && cr.height > 0.0 {
|
||||
if rect.top - cr.top > OVERFLOW_TOLERANCE {
|
||||
children_overflow[0] = true;
|
||||
}
|
||||
if cr.right - rect.right > OVERFLOW_TOLERANCE {
|
||||
children_overflow[1] = true;
|
||||
}
|
||||
if cr.bottom - rect.bottom > OVERFLOW_TOLERANCE {
|
||||
children_overflow[2] = true;
|
||||
}
|
||||
if rect.left - cr.left > OVERFLOW_TOLERANCE {
|
||||
children_overflow[3] = true;
|
||||
}
|
||||
if cr.top - rect.top >= CHILD_INSULATE_THRESHOLD {
|
||||
children_insulate[0] = true;
|
||||
}
|
||||
@@ -428,7 +520,12 @@ pub fn check_quality(dom: &dyn Dom, q: &QualityInput) -> Vec<RuleHit> {
|
||||
for s in 0..4 {
|
||||
let bg_bounds_side = bg_visible && !(full_bleed_bg_band && (s == 1 || s == 3));
|
||||
let side_bounded = border_visible[s] || outline_visible || bg_bounds_side;
|
||||
if side_bounded && pad[s] <= PAD_THRESHOLD && !children_insulate[s] && text_flush[s] {
|
||||
if side_bounded
|
||||
&& pad[s] <= PAD_THRESHOLD
|
||||
&& !children_insulate[s]
|
||||
&& !children_overflow[s]
|
||||
&& text_flush[s]
|
||||
{
|
||||
flush_sides.push(side_names[s]);
|
||||
}
|
||||
}
|
||||
@@ -766,22 +863,68 @@ mod tests {
|
||||
fn line_length_and_viewport_edge() {
|
||||
let mut d = FakeDom::new();
|
||||
let (_h, body) = d.with_page();
|
||||
let long = "x".repeat(120);
|
||||
let long = "x".repeat(240);
|
||||
let p = text_el(&mut d, body, "p", &long, "16px");
|
||||
d.set_rect(p, 0.0, 100.0, 1200.0, 40.0);
|
||||
d.set_rect(p, 0.0, 100.0, 1200.0, 72.0);
|
||||
// Three rendered lines: two full ones and a tail.
|
||||
d.set_text_lines(p, &[(0.0, 100.0, 1180.0, 19.0), (0.0, 124.0, 1180.0, 19.0), (0.0, 148.0, 400.0, 19.0)]);
|
||||
let hits = check_element_quality_dom(&d, p, &BrowserConfig::default());
|
||||
let ids: Vec<&str> = hits.iter().map(|h| h.id.as_str()).collect();
|
||||
assert!(ids.contains(&"line-length"), "{ids:?}");
|
||||
assert_eq!(hits[0].snippet, "~150 chars/line (aim for <80)");
|
||||
assert_eq!(hits[0].snippet, "~103 chars on 2 of 3 rendered lines (aim for <80)");
|
||||
assert!(ids.contains(&"body-text-viewport-edge"));
|
||||
let edge = hits.iter().find(|h| h.id == "body-text-viewport-edge").unwrap();
|
||||
assert_eq!(edge.snippet, "<p> with 120-char body bleeds to viewport edge (left 0px)");
|
||||
assert_eq!(edge.snippet, "<p> with 240-char body bleeds to viewport edge (left 0px)");
|
||||
// narrower, inset paragraph: neither fires
|
||||
d.set_rect(p, 40.0, 100.0, 600.0, 40.0);
|
||||
d.set_rect(p, 40.0, 100.0, 600.0, 72.0);
|
||||
d.set_text_lines(p, &[(40.0, 100.0, 580.0, 19.0), (40.0, 124.0, 580.0, 19.0), (40.0, 148.0, 580.0, 19.0)]);
|
||||
let hits = check_element_quality_dom(&d, p, &BrowserConfig::default());
|
||||
assert!(hits.is_empty(), "{hits:?}");
|
||||
}
|
||||
|
||||
/// REN-402. Halfday's pricing copy: a 158-character paragraph in a 1022px
|
||||
/// card body, rendering 145 characters on its first line and 13 on its
|
||||
/// second. The old measurement charged the box (`1022 / (15 * 0.5)` = 136
|
||||
/// "chars/line") on every paragraph that shape, including the ones whose
|
||||
/// text stops well short of the box.
|
||||
#[test]
|
||||
fn line_length_reads_the_rendered_line_not_the_box() {
|
||||
let mut d = FakeDom::new();
|
||||
let (_h, body) = d.with_page();
|
||||
let copy = "People are counted on the first of the month. Someone invited on the 3rd is free until the 1st, and someone removed mid-month is credited on the next invoice.";
|
||||
assert_eq!(copy.chars().count(), 158);
|
||||
let p = text_el(&mut d, body, "p", copy, "15px");
|
||||
d.set_style(p, "lineHeight", "24px");
|
||||
d.set_rect(p, 200.0, 971.0, 1022.0, 48.0);
|
||||
// One long line and a 13-character tail: the eye tracks back once.
|
||||
d.set_text_lines(p, &[(200.0, 974.0, 995.4, 18.0), (200.0, 998.0, 85.5, 18.0)]);
|
||||
assert_eq!(check_element_quality_dom(&d, p, &BrowserConfig::default()), vec![]);
|
||||
|
||||
// The same box, text that stops at 571px: 89 characters on one line.
|
||||
let meta = text_el(&mut d, body, "p", &"y".repeat(89), "14px");
|
||||
d.set_style(meta, "lineHeight", "21.7px");
|
||||
d.set_rect(meta, 200.0, 122.0, 992.0, 21.7);
|
||||
d.set_text_lines(meta, &[(200.0, 124.2, 571.4, 17.0)]);
|
||||
assert_eq!(check_element_quality_dom(&d, meta, &BrowserConfig::default()), vec![]);
|
||||
|
||||
// A column of long lines is the defect the rule is named for.
|
||||
let wall = text_el(&mut d, body, "p", &"z".repeat(500), "15px");
|
||||
d.set_style(wall, "lineHeight", "24px");
|
||||
d.set_rect(wall, 200.0, 100.0, 1022.0, 96.0);
|
||||
d.set_text_lines(
|
||||
wall,
|
||||
&[
|
||||
(200.0, 100.0, 1000.0, 18.0),
|
||||
(200.0, 124.0, 1000.0, 18.0),
|
||||
(200.0, 148.0, 1000.0, 18.0),
|
||||
(200.0, 172.0, 600.0, 18.0),
|
||||
],
|
||||
);
|
||||
let hits = check_element_quality_dom(&d, wall, &BrowserConfig::default());
|
||||
assert_eq!(hits.len(), 1, "{hits:?}");
|
||||
assert_eq!(hits[0].snippet, "~139 chars on 3 of 4 rendered lines (aim for <80)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cramped_padding_vertical() {
|
||||
let mut d = FakeDom::new();
|
||||
@@ -803,9 +946,54 @@ mod tests {
|
||||
("paddingRight", "12px"),
|
||||
],
|
||||
);
|
||||
// The text lands 2px under the top edge, which is what the reader sees
|
||||
// and what the declared padding happens to say here.
|
||||
d.set_text_lines(p, &[(52.0, 102.0, 276.0, 19.0)]);
|
||||
let hits = check_element_quality_dom(&d, p, &BrowserConfig::default());
|
||||
assert_eq!(hits.len(), 1, "{hits:?}");
|
||||
assert_eq!(hits[0].snippet, "2px vertical padding (need ≥4.8px for 16px text)");
|
||||
assert_eq!(
|
||||
hits[0].snippet,
|
||||
"2px of space above and below the text (need ≥4.8px for 16px text)"
|
||||
);
|
||||
}
|
||||
|
||||
/// REN-403. Halfday's plan button: 44px tall because the design system
|
||||
/// says every control is, `padding: 0 16px`, and the label optically
|
||||
/// centred by a flex box. The declared vertical padding is zero and the
|
||||
/// space above the label is 12px, which is what the reader sees.
|
||||
#[test]
|
||||
fn cramped_padding_measures_the_space_around_the_text() {
|
||||
let mut d = FakeDom::new();
|
||||
let (_h, body) = d.with_page();
|
||||
d.set_style(body, "backgroundColor", "rgb(255, 255, 255)");
|
||||
let btn = text_el(&mut d, body, "a", "Talk to us about Studio", "15px");
|
||||
d.set_rect(btn, 0.0, 778.7, 296.7, 44.0);
|
||||
d.set_styles(
|
||||
btn,
|
||||
&[
|
||||
("backgroundColor", "rgb(255, 255, 255)"),
|
||||
("borderTopWidth", "1px"),
|
||||
("borderRightWidth", "1px"),
|
||||
("borderBottomWidth", "1px"),
|
||||
("borderLeftWidth", "1px"),
|
||||
("paddingTop", "0px"),
|
||||
("paddingBottom", "0px"),
|
||||
("paddingLeft", "16px"),
|
||||
("paddingRight", "16px"),
|
||||
("display", "flex"),
|
||||
],
|
||||
);
|
||||
d.set_text_lines(btn, &[(68.0, 791.2, 160.0, 18.0)]);
|
||||
assert_eq!(check_element_quality_dom(&d, btn, &BrowserConfig::default()), vec![]);
|
||||
|
||||
// The same control with the label actually against the edge: charged.
|
||||
d.set_text_lines(btn, &[(68.0, 780.2, 160.0, 18.0)]);
|
||||
let hits = check_element_quality_dom(&d, btn, &BrowserConfig::default());
|
||||
assert_eq!(hits.len(), 1, "{hits:?}");
|
||||
assert_eq!(
|
||||
hits[0].snippet,
|
||||
"0.5px of space above and below the text (need ≥4.5px for 15px text)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -847,6 +1035,78 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// REN-403, the wrapper half of the same rule. Crewline's framed table:
|
||||
/// the `<table>` fills the frame edge to edge, and every cell insets its
|
||||
/// own text by the padding the stylesheet gives it. Reading the cell's
|
||||
/// border box called all four sides flush.
|
||||
#[test]
|
||||
fn flush_reads_the_text_not_the_cell_that_holds_it() {
|
||||
let mut d = FakeDom::new();
|
||||
let (_h, body) = d.with_page();
|
||||
d.set_style(body, "backgroundColor", "rgb(255, 255, 255)");
|
||||
let frame = d.add(Some(body), "div");
|
||||
d.set_attr(frame, "class", "table-frame");
|
||||
d.set_rect(frame, 0.0, 0.0, 860.0, 300.0);
|
||||
d.set_styles(
|
||||
frame,
|
||||
&[
|
||||
("position", "static"),
|
||||
("borderTopWidth", "1px"),
|
||||
("borderRightWidth", "1px"),
|
||||
("borderBottomWidth", "1px"),
|
||||
("borderLeftWidth", "1px"),
|
||||
("borderTopColor", "rgb(220, 220, 220)"),
|
||||
("borderRightColor", "rgb(220, 220, 220)"),
|
||||
("borderBottomColor", "rgb(220, 220, 220)"),
|
||||
("borderLeftColor", "rgb(220, 220, 220)"),
|
||||
("outlineWidth", "0px"),
|
||||
("backgroundColor", "rgb(250, 250, 250)"),
|
||||
("paddingTop", "0px"),
|
||||
("paddingRight", "0px"),
|
||||
("paddingBottom", "0px"),
|
||||
("paddingLeft", "0px"),
|
||||
("fontSize", "15px"),
|
||||
],
|
||||
);
|
||||
let table = d.add(Some(frame), "table");
|
||||
d.set_rect(table, 0.0, 0.0, 860.0, 300.0);
|
||||
for (i, (x, y, w)) in [(0.0, 0.0, 430.0), (430.0, 0.0, 430.0), (0.0, 260.0, 430.0)]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let cell = d.add(Some(table), "td");
|
||||
d.add_text(cell, "Wednesday afternoon");
|
||||
d.set_rect(cell, x, y, w, 20.0);
|
||||
// Cell padding: 10px down, 16px across, which is where the text is.
|
||||
d.set_text_lines(cell, &[(x + 16.0, y + 10.0, w - 32.0, 17.0)]);
|
||||
let _ = i;
|
||||
}
|
||||
assert_eq!(check_element_quality_dom(&d, frame, &BrowserConfig::default()), vec![]);
|
||||
|
||||
// A cell that really does put its text on the frame line is charged.
|
||||
let tight = d.add(Some(table), "td");
|
||||
d.add_text(tight, "Wednesday afternoon");
|
||||
d.set_rect(tight, 0.0, 140.0, 860.0, 20.0);
|
||||
d.set_text_lines(tight, &[(1.0, 140.0, 858.0, 17.0)]);
|
||||
let hits = check_element_quality_dom(&d, frame, &BrowserConfig::default());
|
||||
assert_eq!(hits.len(), 1, "{hits:?}");
|
||||
assert_eq!(
|
||||
hits[0].snippet,
|
||||
"<div> \"table-frame\": children flush against border+bg on right/left (no inset)"
|
||||
);
|
||||
|
||||
// The same frame at 390px, where the table keeps its min-width and the
|
||||
// frame hides what does not fit: the right side is clipped, not snug,
|
||||
// and that is `clipped-overflow-container`'s business (REN-403).
|
||||
d.set_rect(table, 0.0, 0.0, 1400.0, 300.0);
|
||||
let hits = check_element_quality_dom(&d, frame, &BrowserConfig::default());
|
||||
assert_eq!(hits.len(), 1, "{hits:?}");
|
||||
assert_eq!(
|
||||
hits[0].snippet,
|
||||
"<div> \"table-frame\": children flush against border+bg on left (no inset)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typography_rules() {
|
||||
let mut d = FakeDom::new();
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
//! `checkRepeatedContainerTextDOM`) against the [`Dom`] probe. The pure
|
||||
//! gates live in `checks::rules` / `checks::text_rules`.
|
||||
|
||||
use super::dom::{tag_lower, Dom, ElId, ElStyle};
|
||||
use super::dom::{matches_or_false, tag_lower, Dom, ElId, ElStyle};
|
||||
use super::driver::DesignSystemConfig;
|
||||
use super::element_checks::{class_selector, is_rendered_for_browser_rule};
|
||||
use super::{BrowserFinding, ElFinding};
|
||||
use crate::checks::measures::resolve_length_px;
|
||||
use crate::checks::rules::{check_kicker_above_heading, KickerCandidate, RuleHit};
|
||||
use crate::checks::text_rules::{
|
||||
@@ -99,6 +101,17 @@ fn strip_edge_quotes_slice(text: &str, n: usize) -> String {
|
||||
|
||||
/// JS: checks.mjs#collectKickerCandidates(document, getComputedStyle, resolveLengthPx || 0)
|
||||
pub fn collect_kicker_candidates(dom: &dyn Dom) -> Vec<KickerCandidate> {
|
||||
collect_kicker_candidates_with_elements(dom)
|
||||
.into_iter()
|
||||
.map(|(_, c)| c)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The same walk, each candidate paired with the eyebrow element it came
|
||||
/// from. The finding is about that element and belongs on it: reported
|
||||
/// against the page it named `body`, and a charged row has to have something
|
||||
/// to point at (REN-406).
|
||||
pub fn collect_kicker_candidates_with_elements(dom: &dyn Dom) -> Vec<(ElId, KickerCandidate)> {
|
||||
let mut candidates = Vec::new();
|
||||
for heading in dom
|
||||
.query_all(None, "h1, h2, h3, h4, [role=\"heading\"]")
|
||||
@@ -164,18 +177,60 @@ pub fn collect_kicker_candidates(dom: &dyn Dom) -> Vec<KickerCandidate> {
|
||||
if heading_tag == "h1" && heading_font_size >= 48.0 && kicker_letter_spacing >= 1.6 {
|
||||
continue;
|
||||
}
|
||||
candidates.push(KickerCandidate {
|
||||
candidates.push((
|
||||
kicker,
|
||||
KickerCandidate {
|
||||
heading_tag,
|
||||
heading_text: strip_edge_quotes_slice(&heading_text, 60),
|
||||
kicker_text: slice_utf16_prefix(&kicker_text, 40),
|
||||
});
|
||||
},
|
||||
));
|
||||
}
|
||||
candidates
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#checkKickerAboveHeadingDOM()
|
||||
pub fn check_kicker_above_heading_dom(dom: &dyn Dom) -> Vec<RuleHit> {
|
||||
check_kicker_above_heading(&collect_kicker_candidates(dom))
|
||||
///
|
||||
/// Two things the page-level version could not do. The finding lands on the
|
||||
/// eyebrow it is about rather than on `body`. And an eyebrow the repository's
|
||||
/// own design document names — `.eyebrow`, written into DESIGN.md as the one
|
||||
/// place caps are allowed — is that repository's vocabulary, not slop: a
|
||||
/// pattern the author's contract declares by name is a component with rules,
|
||||
/// and charging it reviews the design system instead of the change (REN-406).
|
||||
pub fn check_kicker_above_heading_dom(
|
||||
dom: &dyn Dom,
|
||||
design_system: Option<&DesignSystemConfig>,
|
||||
) -> Vec<ElFinding> {
|
||||
let pairs: Vec<(ElId, KickerCandidate)> = collect_kicker_candidates_with_elements(dom)
|
||||
.into_iter()
|
||||
.filter(|(el, _)| !is_declared_component(dom, *el, design_system))
|
||||
.collect();
|
||||
let (els, candidates): (Vec<ElId>, Vec<KickerCandidate>) = pairs.into_iter().unzip();
|
||||
check_kicker_above_heading(&candidates)
|
||||
.into_iter()
|
||||
.zip(els)
|
||||
.map(|(hit, el)| ElFinding {
|
||||
el: Some(el),
|
||||
finding: BrowserFinding::new(hit.id, hit.snippet),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether the repository's design document declares this element by name.
|
||||
///
|
||||
/// The selectors come from the DESIGN.md the review already parses, through
|
||||
/// the same design-system config the colour and radius rules read.
|
||||
pub fn is_declared_component(
|
||||
dom: &dyn Dom,
|
||||
el: ElId,
|
||||
design_system: Option<&DesignSystemConfig>,
|
||||
) -> bool {
|
||||
let Some(ds) = design_system else {
|
||||
return false;
|
||||
};
|
||||
ds.declared_selectors
|
||||
.iter()
|
||||
.any(|sel| matches_or_false(dom, el, sel))
|
||||
}
|
||||
|
||||
/// JS: checks.mjs#collectNumberedSectionLabelCandidates(document, ...)
|
||||
@@ -447,13 +502,29 @@ mod tests {
|
||||
let h = d.add(Some(sec), "h2");
|
||||
d.add_text(h, "Everything you need");
|
||||
d.set_style(h, "fontSize", "32px");
|
||||
let hits = check_kicker_above_heading_dom(&d);
|
||||
let hits = check_kicker_above_heading_dom(&d, None);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].id, "kicker-above-heading");
|
||||
assert_eq!(hits[0].finding.type_, "kicker-above-heading");
|
||||
assert_eq!(
|
||||
hits[0].snippet,
|
||||
hits[0].finding.detail,
|
||||
"kicker \"Features\" above h2 \"Everything you need\""
|
||||
);
|
||||
// The finding names the eyebrow, not the page (REN-406).
|
||||
assert_eq!(hits[0].el, Some(kicker));
|
||||
|
||||
// An eyebrow the repository's DESIGN.md declares by name stands down.
|
||||
d.add_selector(kicker, ".eyebrow");
|
||||
let ds = DesignSystemConfig {
|
||||
declared_selectors: vec![".eyebrow".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(check_kicker_above_heading_dom(&d, Some(&ds)).is_empty());
|
||||
// A selector the document does not name leaves it charged.
|
||||
let other = DesignSystemConfig {
|
||||
declared_selectors: vec![".kicker".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(check_kicker_above_heading_dom(&d, Some(&other)).len(), 1);
|
||||
// A card context (heading inside <article> that also contains the
|
||||
// kicker) stands down.
|
||||
let art = d.add(Some(body), "article");
|
||||
@@ -463,7 +534,7 @@ mod tests {
|
||||
let h2 = d.add(Some(art), "h3");
|
||||
d.add_text(h2, "Card heading");
|
||||
d.set_style(h2, "fontSize", "24px");
|
||||
assert_eq!(check_kicker_above_heading_dom(&d).len(), 1);
|
||||
assert_eq!(check_kicker_above_heading_dom(&d, None).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//! `undefined` / `null` distinctions the source relies on.
|
||||
|
||||
use crate::color::{
|
||||
color_to_hex, contrast_ratio, get_hue, has_chroma, is_neutral_color, relative_luminance, Rgba,
|
||||
color_to_hex, contrast_ratio, get_hue, has_chroma, is_gray_ink, is_neutral_color,
|
||||
relative_luminance, Rgba,
|
||||
};
|
||||
use crate::constants::{
|
||||
BORDER_SAFE_TAGS, GENERIC_FONTS, KNOWN_SERIF_FONTS, SAFE_TAGS, WCAG_LARGE_BOLD_TEXT_PX,
|
||||
@@ -167,10 +168,12 @@ pub fn check_colors(opts: &ColorOpts) -> Vec<RuleHit> {
|
||||
}
|
||||
};
|
||||
if let Some(bgs) = bgs {
|
||||
let text_lum = relative_luminance(&text_color);
|
||||
let is_gray =
|
||||
!has_chroma(Some(&text_color), Some(20.0)) && text_lum > 0.05 && text_lum < 0.85;
|
||||
if is_gray && bgs.iter().all(|b| has_chroma(Some(b), Some(40.0))) {
|
||||
// Gray is low chroma at whatever lightness the ink sits at, and
|
||||
// the surface is a colour when it has chroma of its own. The old
|
||||
// pair of tests read relative luminance as if it were lightness,
|
||||
// which made every off-white under 0.85 gray and charged an
|
||||
// off-white nav on a teal masthead three times over (REN-404).
|
||||
if is_gray_ink(&text_color) && bgs.iter().all(|b| has_chroma(Some(b), Some(40.0))) {
|
||||
let bg_label = match opts.effective_bg {
|
||||
Some(bg) => color_to_hex(Some(&bg)),
|
||||
None => format!(
|
||||
@@ -1044,6 +1047,38 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// REN-404. The bench's masthead: `#e8edf2` nav links on `#123a36`. The
|
||||
/// ink is an off-white with a cool tint, not gray, and the pairing clears
|
||||
/// contrast; the old test called everything under 0.85 relative luminance
|
||||
/// gray and charged it three times over on a page with nothing wrong.
|
||||
#[test]
|
||||
fn off_white_on_a_colour_is_not_gray_ink() {
|
||||
let ink = |hex_r: f64, hex_g: f64, hex_b: f64| {
|
||||
check_colors(&ColorOpts {
|
||||
tag: "p".to_string(),
|
||||
font_size: 15.0,
|
||||
font_weight: 400.0,
|
||||
has_direct_text: true,
|
||||
text_color: Some(Rgba::new(hex_r, hex_g, hex_b, 1.0)),
|
||||
effective_bg: Some(Rgba::new(18.0, 58.0, 54.0, 1.0)),
|
||||
..Default::default()
|
||||
})
|
||||
.into_iter()
|
||||
.map(|h| h.id)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
// #e8edf2 on #123a36.
|
||||
assert_eq!(ink(232.0, 237.0, 242.0), Vec::<String>::new());
|
||||
// White, the other neutral ink a coloured surface carries.
|
||||
assert_eq!(ink(255.0, 255.0, 255.0), Vec::<String>::new());
|
||||
// #8a8f8c: the muddy middle, still charged, and the contrast check
|
||||
// beside it is untouched.
|
||||
assert_eq!(
|
||||
ink(138.0, 143.0, 140.0),
|
||||
vec!["gray-on-color".to_string(), "low-contrast".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heading_tags_and_card_like() {
|
||||
assert!(is_heading_tag("h4"));
|
||||
|
||||
@@ -25,6 +25,7 @@ use crate::jsp;
|
||||
use crate::util::{exists, js_string, re, read_json, read_text, ANY, WS};
|
||||
|
||||
const DESIGN_NAMES: &[&str] = &["DESIGN.md", "Design.md", "design.md"];
|
||||
|
||||
const FALLBACK_DIRS: &[&str] = &[".agents/context", "docs"];
|
||||
const PROJECT_ROOT_MARKERS: &[&str] = &[".git", "package.json", ".impeccable"];
|
||||
const COLOR_CHANNEL_TOLERANCE: f64 = 6.0;
|
||||
@@ -37,6 +38,11 @@ pub const STATIC_DESIGN_SKIP_TAGS: &[&str] = &[
|
||||
"head", "title", "meta", "link", "style", "script", "noscript", "template", "source",
|
||||
];
|
||||
|
||||
re!(DESIGN_BACKTICKED, "`([^`\n]{1,80})`".to_string());
|
||||
re!(
|
||||
DESIGN_CLASS_SELECTOR,
|
||||
"^\\.[A-Za-z_][A-Za-z0-9_-]*$".to_string()
|
||||
);
|
||||
re!(
|
||||
FONT_SIZE_LITERAL_RE,
|
||||
format!("^-?[{D}.]+(?:px|rem)$", D = "0-9")
|
||||
@@ -164,6 +170,33 @@ re!(LEADING_WS_RE, format!("^{WS}*"));
|
||||
|
||||
/// JS: design-system.mjs#parseFrontmatter. `None` when there is no
|
||||
/// `---` block; otherwise the parsed object (possibly empty).
|
||||
/// Class selectors the design document names as its own, in the order it
|
||||
/// names them.
|
||||
///
|
||||
/// A design document writes a component in backticks — "`No ALL CAPS outside
|
||||
/// the `.eyebrow` class`" — and that is the repository declaring a pattern by
|
||||
/// name. A rule that fires on one of those is reviewing the design system
|
||||
/// rather than the change, so the browser rules read this list and stand
|
||||
/// down (REN-406). Only a plain class selector counts: a backticked file
|
||||
/// name, property or hex is not a component.
|
||||
pub fn declared_component_selectors(design_md: &str) -> Vec<String> {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
for cap in DESIGN_BACKTICKED.captures_iter(design_md) {
|
||||
let token = js::trim(cap.get(1).map(|m| m.as_str()).unwrap_or(""));
|
||||
if !DESIGN_CLASS_SELECTOR.is_match(token) {
|
||||
continue;
|
||||
}
|
||||
let token = token.to_string();
|
||||
if !out.contains(&token) {
|
||||
out.push(token);
|
||||
}
|
||||
if out.len() >= 64 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn parse_frontmatter(md: &str) -> Option<Map<String, Value>> {
|
||||
let lines: Vec<&str> = CRLF_RE.split(md).collect();
|
||||
if js::trim(lines.first().copied().unwrap_or("")) != "---" {
|
||||
@@ -534,6 +567,10 @@ pub struct AllowedFontSize {
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct DesignSystem {
|
||||
pub present: bool,
|
||||
/// Class selectors the document names as its own (REN-406). Filled where
|
||||
/// the markdown itself is at hand; the allowlists come from frontmatter
|
||||
/// and sidecar, this comes from the prose.
|
||||
pub declared_selectors: Vec<String>,
|
||||
pub source_path: Option<String>,
|
||||
pub sidecar_path: Option<String>,
|
||||
pub md_newer_than_json: bool,
|
||||
@@ -877,13 +914,15 @@ pub fn load_design_system_for_cwd(cwd: &str) -> Option<DesignSystem> {
|
||||
let sidecar = sidecar_path.as_deref().and_then(read_json);
|
||||
let sidecar_stat = sidecar_path.as_deref().and_then(mtime_ms);
|
||||
let md_newer = matches!((md_stat, sidecar_stat), (Some(m), Some(s)) if m > s + 1000.0);
|
||||
Some(normalize_design_system(
|
||||
let mut ds = normalize_design_system(
|
||||
Some(&frontmatter),
|
||||
sidecar.as_ref(),
|
||||
Some(&md.path),
|
||||
sidecar_path.as_deref(),
|
||||
md_newer,
|
||||
))
|
||||
);
|
||||
ds.declared_selectors = declared_component_selectors(&text);
|
||||
Some(ds)
|
||||
}
|
||||
|
||||
/// JS `designSystemStartDir(targetPath, cwd)`.
|
||||
@@ -1934,6 +1973,23 @@ fn finding_ignore_or_value_only(item: &Finding) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// REN-406: the halfday design document's one rule about caps.
|
||||
#[test]
|
||||
fn declared_component_selectors_reads_the_document() {
|
||||
let md = "# Halfday design system\n\n- Plain British English, sentence case everywhere.\n No Title Case, no ALL CAPS outside the `.eyebrow` class.\n- `styles/tokens.css` is the only file with a raw colour, `--ink-900` or `#0f172a`.\n- **Button.** Four kinds and no more: `.btn-primary`, `.btn-secondary`,\n `.btn-ghost`, `.btn-danger`. And `.btn-primary` again.\n";
|
||||
assert_eq!(
|
||||
declared_component_selectors(md),
|
||||
vec![
|
||||
".eyebrow",
|
||||
".btn-primary",
|
||||
".btn-secondary",
|
||||
".btn-ghost",
|
||||
".btn-danger"
|
||||
]
|
||||
);
|
||||
assert!(declared_component_selectors("nothing to declare").is_empty());
|
||||
}
|
||||
|
||||
// ── #570 monorepo DESIGN.md inheritance ─────────────────────────────────
|
||||
// Mirrors tests/detect-cli-design-monorepo.test.mjs (public repo main,
|
||||
// 47e41195 + 5d7c1cce + e975bec4 + 91f2c7b4) at the findDesignRoot level.
|
||||
|
||||
@@ -168,6 +168,17 @@ pub trait Dom {
|
||||
/// of every non-blank direct text node (rects narrower/shorter than 1px
|
||||
/// dropped); `None` when there is none.
|
||||
fn direct_text_rect(&self, el: ElId) -> Option<Rect>;
|
||||
/// The same client rects, unmerged: one per line the text actually
|
||||
/// rendered on (`Range.getClientRects()` returns a rect per line box),
|
||||
/// in document order.
|
||||
///
|
||||
/// This is how a rule reads a line rather than the box that holds it. The
|
||||
/// default answers the union as a single rect, which is what a probe that
|
||||
/// cannot split a wrapped run has; a caller that needs the count divides
|
||||
/// the rect by the line box rather than assuming one line.
|
||||
fn direct_text_line_rects(&self, el: ElId) -> Vec<Rect> {
|
||||
self.direct_text_rect(el).into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
// ── shared helpers over the trait ─────────────────────────────────────────
|
||||
|
||||
@@ -35,6 +35,8 @@ pub struct FakeEl {
|
||||
pub hidden: bool,
|
||||
pub check_visibility: Option<bool>,
|
||||
pub direct_text_rect: Option<Rect>,
|
||||
/// The per-line rects of the direct text; empty falls back to the union.
|
||||
pub direct_text_line_rects: Vec<Rect>,
|
||||
/// Selectors (exact strings) this element matches beyond `*` and its tag.
|
||||
pub selectors: Vec<String>,
|
||||
/// `id` IDL property override (`None` = "not a string", falls back to attr).
|
||||
@@ -158,6 +160,28 @@ impl FakeDom {
|
||||
self.el_mut(id).rect = Rect::from_xywh(x, y, w, h);
|
||||
self
|
||||
}
|
||||
/// The union rect of `id`'s direct text, as `getClientRects()` would give it.
|
||||
pub fn set_text_rect(&mut self, id: ElId, x: f64, y: f64, w: f64, h: f64) -> &mut Self {
|
||||
self.el_mut(id).direct_text_rect = Some(Rect::from_xywh(x, y, w, h));
|
||||
self
|
||||
}
|
||||
/// The rects of `id`'s direct text, one per rendered line. The union is
|
||||
/// derived from them, so a test declares the lines and nothing else.
|
||||
pub fn set_text_lines(&mut self, id: ElId, lines: &[(f64, f64, f64, f64)]) -> &mut Self {
|
||||
let rects: Vec<Rect> = lines
|
||||
.iter()
|
||||
.map(|&(x, y, w, h)| Rect::from_xywh(x, y, w, h))
|
||||
.collect();
|
||||
if !rects.is_empty() {
|
||||
let left = rects.iter().map(|r| r.left).fold(f64::INFINITY, f64::min);
|
||||
let top = rects.iter().map(|r| r.top).fold(f64::INFINITY, f64::min);
|
||||
let right = rects.iter().map(|r| r.right).fold(f64::NEG_INFINITY, f64::max);
|
||||
let bottom = rects.iter().map(|r| r.bottom).fold(f64::NEG_INFINITY, f64::max);
|
||||
self.el_mut(id).direct_text_rect = Some(Rect::from_xywh(left, top, right - left, bottom - top));
|
||||
}
|
||||
self.el_mut(id).direct_text_line_rects = rects;
|
||||
self
|
||||
}
|
||||
pub fn add_text(&mut self, id: ElId, text: &str) -> &mut Self {
|
||||
self.el_mut(id)
|
||||
.child_nodes
|
||||
@@ -503,4 +527,12 @@ impl Dom for FakeDom {
|
||||
fn direct_text_rect(&self, el: ElId) -> Option<Rect> {
|
||||
self.els[el as usize].direct_text_rect
|
||||
}
|
||||
fn direct_text_line_rects(&self, el: ElId) -> Vec<Rect> {
|
||||
let lines = &self.els[el as usize].direct_text_line_rects;
|
||||
if lines.is_empty() {
|
||||
self.direct_text_rect(el).into_iter().collect()
|
||||
} else {
|
||||
lines.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +320,40 @@ pub fn has_chroma(c: Option<&Rgba>, threshold: Option<f64>) -> bool {
|
||||
(math_max3(c.r, c.g, c.b) - math_min3(c.r, c.g, c.b)) >= threshold
|
||||
}
|
||||
|
||||
/// A colour's lightness and saturation, HSL, both 0..1.
|
||||
///
|
||||
/// Saturation is chroma measured the same way at every lightness: the spread
|
||||
/// between the channels over the widest spread a colour of that lightness
|
||||
/// could have. The raw spread cannot say the same thing, because it shrinks
|
||||
/// towards white and towards black — `#e8edf2` spreads 10 of 255 and is a
|
||||
/// quarter of the way to fully saturated at its lightness, which is why it
|
||||
/// reads as a cool off-white and not as gray (REN-404).
|
||||
pub fn lightness_saturation(c: &Rgba) -> (f64, f64) {
|
||||
let max = math_max3(c.r, c.g, c.b) / 255.0;
|
||||
let min = math_min3(c.r, c.g, c.b) / 255.0;
|
||||
let l = (max + min) / 2.0;
|
||||
let d = max - min;
|
||||
let s = if d <= 0.0 || l <= 0.0 || l >= 1.0 {
|
||||
0.0
|
||||
} else {
|
||||
d / (1.0 - (2.0 * l - 1.0).abs())
|
||||
};
|
||||
(l, s)
|
||||
}
|
||||
|
||||
/// Whether a colour reads as gray ink.
|
||||
///
|
||||
/// Two things at once: little chroma for the lightness it sits at, and a
|
||||
/// lightness that is neither of the two neutral inks a coloured surface is
|
||||
/// meant to carry. Near-white and near-black on a colour are deliberate; the
|
||||
/// muddy middle is what this names. Relative luminance is not lightness and
|
||||
/// cannot stand in for it: `#e8edf2` measures 0.84 there, under the old 0.85
|
||||
/// ceiling, and 0.93 as lightness, which is where the eye puts it.
|
||||
pub fn is_gray_ink(c: &Rgba) -> bool {
|
||||
let (l, s) = lightness_saturation(c);
|
||||
s < 0.2 && l > 0.2 && l < 0.85
|
||||
}
|
||||
|
||||
/// JS `getHue(c)`.
|
||||
pub fn get_hue(c: Option<&Rgba>) -> f64 {
|
||||
let Some(c) = c else { return 0.0 };
|
||||
|
||||
@@ -86,7 +86,7 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
scopes: None,
|
||||
severity: None,
|
||||
name: "AI color palette",
|
||||
description: "Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.",
|
||||
description: "Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.",
|
||||
skill_section: Some("Color & Contrast"),
|
||||
skill_guideline: Some("AI color palette"),
|
||||
},
|
||||
@@ -416,7 +416,7 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
scopes: Some(&["type", "layout"]),
|
||||
severity: None,
|
||||
name: "Line length too long",
|
||||
description: "Text lines wider than ~80 characters are hard to read. The eye loses its place tracking back to the start of the next line. Add a max-width (65ch to 75ch) to text containers.",
|
||||
description: "Text lines wider than ~80 characters are hard to read. The eye loses its place tracking back to the start of the next line, so it is measured on the lines that rendered and charged when more than one of them runs long. Add a max-width (65ch to 75ch) to text containers.",
|
||||
skill_section: Some("Layout & Space"),
|
||||
skill_guideline: Some("wrap beyond ~80 characters"),
|
||||
},
|
||||
@@ -426,7 +426,7 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
scopes: Some(&["layout"]),
|
||||
severity: None,
|
||||
name: "Cramped padding",
|
||||
description: "Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.",
|
||||
description: "Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.",
|
||||
skill_section: Some("Layout & Space"),
|
||||
skill_guideline: Some("inside bordered or colored containers"),
|
||||
},
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -58,6 +58,7 @@ extern "C" {
|
||||
fn offset_height(el: u32) -> f64;
|
||||
fn check_visibility(el: u32) -> i32;
|
||||
fn direct_text_rect(el: u32) -> Vec<f64>;
|
||||
fn direct_text_line_rects(el: u32) -> Vec<f64>;
|
||||
}
|
||||
|
||||
fn opt(id: u32) -> Option<ElId> {
|
||||
@@ -340,4 +341,12 @@ impl Dom for JsDom {
|
||||
Some(to_rect(&v))
|
||||
}
|
||||
}
|
||||
/// The probe flattens the per-line rects into one array of eights, in the
|
||||
/// order `rect` uses; a tail shorter than a rect is ignored.
|
||||
fn direct_text_line_rects(&self, el: ElId) -> Vec<Rect> {
|
||||
direct_text_line_rects(el)
|
||||
.chunks_exact(8)
|
||||
.map(to_rect)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
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\": \"cramped-padding\",\n \"name\": \"Cramped padding\",\n \"description\": \"Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"<div> \\\"pass-split-container\\\": children flush against border on all sides (no inset)\"\n },\n {\n \"antipattern\": \"clipped-overflow-container\",\n \"name\": \"Positioned child clipped by overflow container\",\n \"description\": \"A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"div.box.flag-overflow-hidden clips a positioned child\"\n },\n {\n \"antipattern\": \"clipped-overflow-container\",\n \"name\": \"Positioned child clipped by overflow container\",\n \"description\": \"A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"div.box.flag-overflow-clip clips a positioned child\"\n },\n {\n \"antipattern\": \"clipped-overflow-container\",\n \"name\": \"Positioned child clipped by overflow container\",\n \"description\": \"A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"div.box.flag-overflow-negative clips a positioned child\"\n },\n {\n \"antipattern\": \"clipped-overflow-container\",\n \"name\": \"Positioned child clipped by overflow container\",\n \"description\": \"A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"div.box.flag-overflow-right clips a positioned child\"\n },\n {\n \"antipattern\": \"clipped-overflow-container\",\n \"name\": \"Positioned child clipped by overflow container\",\n \"description\": \"A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"div.box.flag-shadow-utility clips a positioned child\"\n },\n {\n \"antipattern\": \"clipped-overflow-container\",\n \"name\": \"Positioned child clipped by overflow container\",\n \"description\": \"A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"div.box.flag-overlay-surface clips a positioned child\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"cramped-padding\",\n \"name\": \"Cramped padding\",\n \"description\": \"Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"<div> \\\"pass-split-container\\\": children flush against border on all sides (no inset)\"\n },\n {\n \"antipattern\": \"clipped-overflow-container\",\n \"name\": \"Positioned child clipped by overflow container\",\n \"description\": \"A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"div.box.flag-overflow-hidden clips a positioned child\"\n },\n {\n \"antipattern\": \"clipped-overflow-container\",\n \"name\": \"Positioned child clipped by overflow container\",\n \"description\": \"A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"div.box.flag-overflow-clip clips a positioned child\"\n },\n {\n \"antipattern\": \"clipped-overflow-container\",\n \"name\": \"Positioned child clipped by overflow container\",\n \"description\": \"A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"div.box.flag-overflow-negative clips a positioned child\"\n },\n {\n \"antipattern\": \"clipped-overflow-container\",\n \"name\": \"Positioned child clipped by overflow container\",\n \"description\": \"A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"div.box.flag-overflow-right clips a positioned child\"\n },\n {\n \"antipattern\": \"clipped-overflow-container\",\n \"name\": \"Positioned child clipped by overflow container\",\n \"description\": \"A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"div.box.flag-shadow-utility clips a positioned child\"\n },\n {\n \"antipattern\": \"clipped-overflow-container\",\n \"name\": \"Positioned child clipped by overflow container\",\n \"description\": \"A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\",\n \"line\": 0,\n \"snippet\": \"div.box.flag-overlay-surface clips a positioned child\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"background-clip: text + gradient\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"background-clip: text + gradient\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"bg-clip-text + bg-gradient (Tailwind)\"\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/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"Purple/violet gradient (Tailwind)\"\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/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"Purple/violet accent colors detected\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"background-clip: text + gradient\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"bg-clip-text + bg-gradient (Tailwind)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"background-clip: text + gradient\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"background-clip: text + gradient\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"bg-clip-text + bg-gradient (Tailwind)\"\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. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"Purple/violet gradient (Tailwind)\"\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. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"Purple/violet accent colors detected\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"background-clip: text + gradient\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\",\n \"line\": 0,\n \"snippet\": \"bg-clip-text + bg-gradient (Tailwind)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"cramped-padding\",\n \"name\": \"Cramped padding\",\n \"description\": \"Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/edge-flush-cards.html\",\n \"line\": 0,\n \"snippet\": \"<div> \\\"scroller\\\": children flush against bg on all sides (no inset)\"\n },\n {\n \"antipattern\": \"cramped-padding\",\n \"name\": \"Cramped padding\",\n \"description\": \"Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/edge-flush-cards.html\",\n \"line\": 0,\n \"snippet\": \"<div> \\\"scroller\\\": children flush against bg on all sides (no inset)\"\n },\n {\n \"antipattern\": \"cramped-padding\",\n \"name\": \"Cramped padding\",\n \"description\": \"Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/edge-flush-cards.html\",\n \"line\": 0,\n \"snippet\": \"<div> \\\"scroller\\\": children flush against bg on all sides (no inset)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"cramped-padding\",\n \"name\": \"Cramped padding\",\n \"description\": \"Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/edge-flush-cards.html\",\n \"line\": 0,\n \"snippet\": \"<div> \\\"scroller\\\": children flush against bg on all sides (no inset)\"\n },\n {\n \"antipattern\": \"cramped-padding\",\n \"name\": \"Cramped padding\",\n \"description\": \"Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/edge-flush-cards.html\",\n \"line\": 0,\n \"snippet\": \"<div> \\\"scroller\\\": children flush against bg on all sides (no inset)\"\n },\n {\n \"antipattern\": \"cramped-padding\",\n \"name\": \"Cramped padding\",\n \"description\": \"Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/edge-flush-cards.html\",\n \"line\": 0,\n \"snippet\": \"<div> \\\"scroller\\\": children flush against bg on all sides (no inset)\"\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\": \"cramped-padding\",\n \"name\": \"Cramped padding\",\n \"description\": \"Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/heading-rhythm.html\",\n \"line\": 0,\n \"snippet\": \"<div> \\\"pass-band\\\": children flush against bg on right/left (no inset)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"cramped-padding\",\n \"name\": \"Cramped padding\",\n \"description\": \"Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/heading-rhythm.html\",\n \"line\": 0,\n \"snippet\": \"<div> \\\"pass-band\\\": children flush against bg on right/left (no inset)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -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\": \"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. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. 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\": \"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/jsx-should-flag.jsx\",\n \"line\": 8,\n \"snippet\": \"border-l-4\"\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/jsx-should-flag.jsx\",\n \"line\": 35,\n \"snippet\": \"borderLeft: '4px solid\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/jsx-should-flag.jsx\",\n \"line\": 22,\n \"snippet\": \"bg-clip-text + bg-gradient\"\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/jsx-should-flag.jsx\",\n \"line\": 10,\n \"snippet\": \"text-purple-500 on heading\"\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/jsx-should-flag.jsx\",\n \"line\": 22,\n \"snippet\": \"from-purple-400 gradient\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/jsx-should-flag.jsx\",\n \"line\": 50,\n \"snippet\": \"animate-bounce (Tailwind)\"\n },\n {\n \"antipattern\": \"layout-transition\",\n \"name\": \"Layout property animation\",\n \"description\": \"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 \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/jsx-should-flag.jsx\",\n \"line\": 51,\n \"snippet\": \"transition: width\"\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/jsx-should-flag.jsx\",\n \"line\": 8,\n \"snippet\": \"border-l-4\"\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/jsx-should-flag.jsx\",\n \"line\": 35,\n \"snippet\": \"borderLeft: '4px solid\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/jsx-should-flag.jsx\",\n \"line\": 22,\n \"snippet\": \"bg-clip-text + bg-gradient\"\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. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/jsx-should-flag.jsx\",\n \"line\": 10,\n \"snippet\": \"text-purple-500 on heading\"\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. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/jsx-should-flag.jsx\",\n \"line\": 22,\n \"snippet\": \"from-purple-400 gradient\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/jsx-should-flag.jsx\",\n \"line\": 50,\n \"snippet\": \"animate-bounce (Tailwind)\"\n },\n {\n \"antipattern\": \"layout-transition\",\n \"name\": \"Layout property animation\",\n \"description\": \"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 \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/jsx-should-flag.jsx\",\n \"line\": 51,\n \"snippet\": \"transition: width\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -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/multifile/Card.tsx\",\n \"line\": 11,\n \"snippet\": \"border-l-4\",\n \"importedBy\": [\n \"App.tsx\"\n ]\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/multifile/Card.tsx\",\n \"line\": 12,\n \"snippet\": \"text-purple-500 on heading\",\n \"importedBy\": [\n \"App.tsx\"\n ]\n },\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/multifile/styles.css\",\n \"line\": 3,\n \"snippet\": \"font-family: 'Inter\",\n \"importedBy\": [\n \"App.tsx\"\n ]\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/multifile/styles.css\",\n \"line\": 8,\n \"snippet\": \"animation: bounce\",\n \"importedBy\": [\n \"App.tsx\"\n ]\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/multifile/theme.sass\",\n \"line\": 4,\n \"snippet\": \"border-left: 4px solid $primary\"\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/multifile/theme.scss\",\n \"line\": 4,\n \"snippet\": \"border-left: 4px solid $primary\"\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/multifile/Card.tsx\",\n \"line\": 11,\n \"snippet\": \"border-l-4\",\n \"importedBy\": [\n \"App.tsx\"\n ]\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. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/multifile/Card.tsx\",\n \"line\": 12,\n \"snippet\": \"text-purple-500 on heading\",\n \"importedBy\": [\n \"App.tsx\"\n ]\n },\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/multifile/styles.css\",\n \"line\": 3,\n \"snippet\": \"font-family: 'Inter\",\n \"importedBy\": [\n \"App.tsx\"\n ]\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/multifile/styles.css\",\n \"line\": 8,\n \"snippet\": \"animation: bounce\",\n \"importedBy\": [\n \"App.tsx\"\n ]\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/multifile/theme.sass\",\n \"line\": 4,\n \"snippet\": \"border-left: 4px solid $primary\"\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/multifile/theme.scss\",\n \"line\": 4,\n \"snippet\": \"border-left: 4px solid $primary\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
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/svelte-should-flag.svelte\",\n \"line\": 6,\n \"snippet\": \"border-r-4\"\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/svelte-should-flag.svelte\",\n \"line\": 17,\n \"snippet\": \"border-right: 4px solid #8b5cf6\"\n },\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/svelte-should-flag.svelte\",\n \"line\": 19,\n \"snippet\": \"font-family: 'Roboto\"\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/svelte-should-flag.svelte\",\n \"line\": 7,\n \"snippet\": \"text-purple-500 on heading\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/svelte-should-flag.svelte\",\n \"line\": 10,\n \"snippet\": \"animate-bounce (Tailwind)\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/svelte-should-flag.svelte\",\n \"line\": 30,\n \"snippet\": \"animation: elastic\"\n },\n {\n \"antipattern\": \"layout-transition\",\n \"name\": \"Layout property animation\",\n \"description\": \"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 \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/svelte-should-flag.svelte\",\n \"line\": 31,\n \"snippet\": \"transition: height\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/svelte-should-flag.svelte\",\n \"line\": 26,\n \"snippet\": \"background-clip: text + gradient\"\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/svelte-should-flag.svelte\",\n \"line\": 6,\n \"snippet\": \"border-r-4\"\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/svelte-should-flag.svelte\",\n \"line\": 17,\n \"snippet\": \"border-right: 4px solid #8b5cf6\"\n },\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/svelte-should-flag.svelte\",\n \"line\": 19,\n \"snippet\": \"font-family: 'Roboto\"\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. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/svelte-should-flag.svelte\",\n \"line\": 7,\n \"snippet\": \"text-purple-500 on heading\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/svelte-should-flag.svelte\",\n \"line\": 10,\n \"snippet\": \"animate-bounce (Tailwind)\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/svelte-should-flag.svelte\",\n \"line\": 30,\n \"snippet\": \"animation: elastic\"\n },\n {\n \"antipattern\": \"layout-transition\",\n \"name\": \"Layout property animation\",\n \"description\": \"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 \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/svelte-should-flag.svelte\",\n \"line\": 31,\n \"snippet\": \"transition: height\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/svelte-should-flag.svelte\",\n \"line\": 26,\n \"snippet\": \"background-clip: text + gradient\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -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/vue-should-flag.vue\",\n \"line\": 2,\n \"snippet\": \"border-l-4\"\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/vue-should-flag.vue\",\n \"line\": 20,\n \"snippet\": \"border-left: 4px solid #6366f1\"\n },\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/vue-should-flag.vue\",\n \"line\": 22,\n \"snippet\": \"font-family: 'Inter\"\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/vue-should-flag.vue\",\n \"line\": 3,\n \"snippet\": \"text-purple-600 on heading\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/vue-should-flag.vue\",\n \"line\": 5,\n \"snippet\": \"animate-bounce (Tailwind)\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/vue-should-flag.vue\",\n \"line\": 33,\n \"snippet\": \"animation: bounce\"\n },\n {\n \"antipattern\": \"layout-transition\",\n \"name\": \"Layout property animation\",\n \"description\": \"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 \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/vue-should-flag.vue\",\n \"line\": 34,\n \"snippet\": \"transition: width\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/vue-should-flag.vue\",\n \"line\": 28,\n \"snippet\": \"background-clip: text + gradient\"\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/vue-should-flag.vue\",\n \"line\": 2,\n \"snippet\": \"border-l-4\"\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/vue-should-flag.vue\",\n \"line\": 20,\n \"snippet\": \"border-left: 4px solid #6366f1\"\n },\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/vue-should-flag.vue\",\n \"line\": 22,\n \"snippet\": \"font-family: 'Inter\"\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. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/vue-should-flag.vue\",\n \"line\": 3,\n \"snippet\": \"text-purple-600 on heading\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/vue-should-flag.vue\",\n \"line\": 5,\n \"snippet\": \"animate-bounce (Tailwind)\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/vue-should-flag.vue\",\n \"line\": 33,\n \"snippet\": \"animation: bounce\"\n },\n {\n \"antipattern\": \"layout-transition\",\n \"name\": \"Layout property animation\",\n \"description\": \"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 \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/vue-should-flag.vue\",\n \"line\": 34,\n \"snippet\": \"transition: width\"\n },\n {\n \"antipattern\": \"gradient-text\",\n \"name\": \"Gradient text\",\n \"description\": \"Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/vue-should-flag.vue\",\n \"line\": 28,\n \"snippet\": \"background-clip: text + gradient\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\n [cramped-padding] <div> \"pass-split-container\": children flush against border on all sides (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\n [clipped-overflow-container] div.box.flag-overflow-hidden clips a positioned child\n → A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\n [clipped-overflow-container] div.box.flag-overflow-clip clips a positioned child\n → A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\n [clipped-overflow-container] div.box.flag-overflow-negative clips a positioned child\n → A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\n [clipped-overflow-container] div.box.flag-overflow-right clips a positioned child\n → A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\n [clipped-overflow-container] div.box.flag-shadow-utility clips a positioned child\n → A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\n [clipped-overflow-container] div.box.flag-overlay-surface clips a positioned child\n → A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\n\n7 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/clipped-overflow-container.html\n [cramped-padding] <div> \"pass-split-container\": children flush against border on all sides (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\n [clipped-overflow-container] div.box.flag-overflow-hidden clips a positioned child\n → A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\n [clipped-overflow-container] div.box.flag-overflow-clip clips a positioned child\n → A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\n [clipped-overflow-container] div.box.flag-overflow-negative clips a positioned child\n → A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\n [clipped-overflow-container] div.box.flag-overflow-right clips a positioned child\n → A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\n [clipped-overflow-container] div.box.flag-shadow-utility clips a positioned child\n → A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\n [clipped-overflow-container] div.box.flag-overlay-surface clips a positioned child\n → A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.\n\n7 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\n [gradient-text] background-clip: text + gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n [gradient-text] background-clip: text + gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n [gradient-text] bg-clip-text + bg-gradient (Tailwind)\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n [ai-color-palette] Purple/violet gradient (Tailwind)\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\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 [gradient-text] background-clip: text + gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n [gradient-text] bg-clip-text + bg-gradient (Tailwind)\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n\n7 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/css-in-prose-should-flag.html\n [gradient-text] background-clip: text + gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n [gradient-text] background-clip: text + gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n [gradient-text] bg-clip-text + bg-gradient (Tailwind)\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n [ai-color-palette] Purple/violet gradient (Tailwind)\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\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. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\n [gradient-text] background-clip: text + gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n [gradient-text] bg-clip-text + bg-gradient (Tailwind)\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n\n7 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/edge-flush-cards.html\n [cramped-padding] <div> \"scroller\": children flush against bg on all sides (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\n [cramped-padding] <div> \"scroller\": children flush against bg on all sides (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\n [cramped-padding] <div> \"scroller\": children flush against bg on all sides (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\n\n3 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/edge-flush-cards.html\n [cramped-padding] <div> \"scroller\": children flush against bg on all sides (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\n [cramped-padding] <div> \"scroller\": children flush against bg on all sides (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\n [cramped-padding] <div> \"scroller\": children flush against bg on all sides (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\n\n3 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/flush-against-border.html\n [cramped-padding] <section> \"flag-frameworks\": children flush against border+bg on right/bottom/left (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\n [cramped-padding] <div> \"flag-card-borders\": children flush against border+bg on right/bottom/left (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\n [cramped-padding] <div> \"flag-bg-only\": children flush against bg on right/bottom/left (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\n [cramped-padding] <div> \"flag-outline-only\": children flush against outline on right/bottom/left (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\n [cramped-padding] <div> \"flag-asym-leftflush\": children flush against border+bg on left (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\n [cramped-padding] <section> \"pass-marquee-shell\": children flush against bg on all sides (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\n\n6 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/flush-against-border.html\n [cramped-padding] <section> \"flag-frameworks\": children flush against border+bg on right/bottom/left (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\n [cramped-padding] <div> \"flag-card-borders\": children flush against border+bg on right/bottom/left (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\n [cramped-padding] <div> \"flag-bg-only\": children flush against bg on right/bottom/left (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\n [cramped-padding] <div> \"flag-outline-only\": children flush against outline on right/bottom/left (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\n [cramped-padding] <div> \"flag-asym-leftflush\": children flush against border+bg on left (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\n [cramped-padding] <section> \"pass-marquee-shell\": children flush against bg on all sides (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\n\n6 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\nNext.js project detected (next.config.mjs).\nStart the dev server and scan via URL for best results:\n npx impeccable detect http://localhost:3000\n\n\n<REPO>/tests/fixtures/antipatterns/framework-next-tailwind/app/globals.css (imported by layout.tsx)\n line 19: [overused-font] font-family: '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\n<REPO>/tests/fixtures/antipatterns/framework-next-tailwind/app/page.tsx\n line 15: [gradient-text] bg-clip-text + bg-gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n line 41: [ai-color-palette] text-purple-500 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n line 15: [ai-color-palette] from-purple-400 gradient\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n\n<REPO>/tests/fixtures/antipatterns/framework-next-tailwind/components/FeatureCard.tsx (imported by page.tsx)\n line 9: [side-tab] border-l-4\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 line 11: [ai-color-palette] text-purple-600 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n line 10: [bounce-easing] animate-bounce (Tailwind)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n\n<REPO>/tests/fixtures/antipatterns/framework-next-tailwind/components/PricingCard.tsx (imported by page.tsx)\n line 18: [gradient-text] bg-clip-text + bg-gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n line 18: [ai-color-palette] from-violet-500 gradient\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n line 30: [ai-color-palette] from-purple-500 gradient\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n\n10 anti-patterns found.\n",
|
||||
"stderr": "\nNext.js project detected (next.config.mjs).\nStart the dev server and scan via URL for best results:\n npx impeccable detect http://localhost:3000\n\n\n<REPO>/tests/fixtures/antipatterns/framework-next-tailwind/app/globals.css (imported by layout.tsx)\n line 19: [overused-font] font-family: '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\n<REPO>/tests/fixtures/antipatterns/framework-next-tailwind/app/page.tsx\n line 15: [gradient-text] bg-clip-text + bg-gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n line 41: [ai-color-palette] text-purple-500 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\n line 15: [ai-color-palette] from-purple-400 gradient\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\n\n<REPO>/tests/fixtures/antipatterns/framework-next-tailwind/components/FeatureCard.tsx (imported by page.tsx)\n line 9: [side-tab] border-l-4\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 line 11: [ai-color-palette] text-purple-600 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\n line 10: [bounce-easing] animate-bounce (Tailwind)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n\n<REPO>/tests/fixtures/antipatterns/framework-next-tailwind/components/PricingCard.tsx (imported by page.tsx)\n line 18: [gradient-text] bg-clip-text + bg-gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n line 18: [ai-color-palette] from-violet-500 gradient\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\n line 30: [ai-color-palette] from-purple-500 gradient\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\n\n10 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/heading-rhythm.html\n [cramped-padding] <div> \"pass-band\": children flush against bg on right/left (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.\n\n1 anti-pattern found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/heading-rhythm.html\n [cramped-padding] <div> \"pass-band\": children flush against bg on right/left (no inset)\n → Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the space between the rendered text and the border box is too small for the font size, and (2) a wrapper whose children's text lands flush against a visible boundary (border, outline, or non-transparent background) with nothing to inset it. Add at least 8px (ideally 12–16px) of space inside bordered, outlined, or colored containers.\n\n1 anti-pattern found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -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 [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",
|
||||
"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. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. 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/jsx-should-flag.jsx\n line 8: [side-tab] border-l-4\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 line 35: [side-tab] borderLeft: '4px solid\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 line 22: [gradient-text] bg-clip-text + bg-gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n line 10: [ai-color-palette] text-purple-500 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n line 22: [ai-color-palette] from-purple-400 gradient\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n line 50: [bounce-easing] animate-bounce (Tailwind)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n line 51: [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\n7 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/jsx-should-flag.jsx\n line 8: [side-tab] border-l-4\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 line 35: [side-tab] borderLeft: '4px solid\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 line 22: [gradient-text] bg-clip-text + bg-gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n line 10: [ai-color-palette] text-purple-500 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\n line 22: [ai-color-palette] from-purple-400 gradient\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\n line 50: [bounce-easing] animate-bounce (Tailwind)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n line 51: [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\n7 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/multifile/Card.tsx (imported by App.tsx)\n line 11: [side-tab] border-l-4\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 line 12: [ai-color-palette] text-purple-500 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n\n<REPO>/tests/fixtures/antipatterns/multifile/styles.css (imported by App.tsx)\n line 3: [overused-font] font-family: '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 line 8: [bounce-easing] animation: bounce\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n\n<REPO>/tests/fixtures/antipatterns/multifile/theme.sass\n line 4: [side-tab] border-left: 4px solid $primary\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\n<REPO>/tests/fixtures/antipatterns/multifile/theme.scss\n line 4: [side-tab] border-left: 4px solid $primary\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\n6 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/multifile/Card.tsx (imported by App.tsx)\n line 11: [side-tab] border-l-4\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 line 12: [ai-color-palette] text-purple-500 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\n\n<REPO>/tests/fixtures/antipatterns/multifile/styles.css (imported by App.tsx)\n line 3: [overused-font] font-family: '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 line 8: [bounce-easing] animation: bounce\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n\n<REPO>/tests/fixtures/antipatterns/multifile/theme.sass\n line 4: [side-tab] border-left: 4px solid $primary\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\n<REPO>/tests/fixtures/antipatterns/multifile/theme.scss\n line 4: [side-tab] border-left: 4px solid $primary\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\n6 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/svelte-should-flag.svelte\n line 6: [side-tab] border-r-4\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 line 17: [side-tab] border-right: 4px solid #8b5cf6\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 line 19: [overused-font] font-family: 'Roboto\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 line 7: [ai-color-palette] text-purple-500 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n line 10: [bounce-easing] animate-bounce (Tailwind)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n line 30: [bounce-easing] animation: elastic\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n line 31: [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 line 26: [gradient-text] background-clip: text + gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n\n8 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/svelte-should-flag.svelte\n line 6: [side-tab] border-r-4\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 line 17: [side-tab] border-right: 4px solid #8b5cf6\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 line 19: [overused-font] font-family: 'Roboto\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 line 7: [ai-color-palette] text-purple-500 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\n line 10: [bounce-easing] animate-bounce (Tailwind)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n line 30: [bounce-easing] animation: elastic\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n line 31: [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 line 26: [gradient-text] background-clip: text + gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n\n8 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/vue-should-flag.vue\n line 2: [side-tab] border-l-4\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 line 20: [side-tab] border-left: 4px solid #6366f1\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 line 22: [overused-font] font-family: '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 line 3: [ai-color-palette] text-purple-600 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n line 5: [bounce-easing] animate-bounce (Tailwind)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n line 33: [bounce-easing] animation: bounce\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n line 34: [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 line 28: [gradient-text] background-clip: text + gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n\n8 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/vue-should-flag.vue\n line 2: [side-tab] border-l-4\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 line 20: [side-tab] border-left: 4px solid #6366f1\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 line 22: [overused-font] font-family: '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 line 3: [ai-color-palette] text-purple-600 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\n line 5: [bounce-easing] animate-bounce (Tailwind)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n line 33: [bounce-easing] animation: bounce\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n line 34: [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 line 28: [gradient-text] background-clip: text + gradient\n → Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.\n\n8 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
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/multifile/Card.tsx\",\n \"line\": 11,\n \"snippet\": \"border-l-4\",\n \"importedBy\": [\n \"App.tsx\"\n ]\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/multifile/Card.tsx\",\n \"line\": 12,\n \"snippet\": \"text-purple-500 on heading\",\n \"importedBy\": [\n \"App.tsx\"\n ]\n },\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/multifile/styles.css\",\n \"line\": 3,\n \"snippet\": \"font-family: 'Inter\",\n \"importedBy\": [\n \"App.tsx\"\n ]\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/multifile/styles.css\",\n \"line\": 8,\n \"snippet\": \"animation: bounce\",\n \"importedBy\": [\n \"App.tsx\"\n ]\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/multifile/theme.sass\",\n \"line\": 4,\n \"snippet\": \"border-left: 4px solid $primary\"\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/multifile/theme.scss\",\n \"line\": 4,\n \"snippet\": \"border-left: 4px solid $primary\"\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/multifile/Card.tsx\",\n \"line\": 11,\n \"snippet\": \"border-l-4\",\n \"importedBy\": [\n \"App.tsx\"\n ]\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. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/multifile/Card.tsx\",\n \"line\": 12,\n \"snippet\": \"text-purple-500 on heading\",\n \"importedBy\": [\n \"App.tsx\"\n ]\n },\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/multifile/styles.css\",\n \"line\": 3,\n \"snippet\": \"font-family: 'Inter\",\n \"importedBy\": [\n \"App.tsx\"\n ]\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/multifile/styles.css\",\n \"line\": 8,\n \"snippet\": \"animation: bounce\",\n \"importedBy\": [\n \"App.tsx\"\n ]\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/multifile/theme.sass\",\n \"line\": 4,\n \"snippet\": \"border-left: 4px solid $primary\"\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/multifile/theme.scss\",\n \"line\": 4,\n \"snippet\": \"border-left: 4px solid $primary\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/multifile/Card.tsx (imported by App.tsx)\n line 11: [side-tab] border-l-4\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 line 12: [ai-color-palette] text-purple-500 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n\n<REPO>/tests/fixtures/antipatterns/multifile/styles.css (imported by App.tsx)\n line 3: [overused-font] font-family: '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 line 8: [bounce-easing] animation: bounce\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n\n<REPO>/tests/fixtures/antipatterns/multifile/theme.sass\n line 4: [side-tab] border-left: 4px solid $primary\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\n<REPO>/tests/fixtures/antipatterns/multifile/theme.scss\n line 4: [side-tab] border-left: 4px solid $primary\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\n6 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/multifile/Card.tsx (imported by App.tsx)\n line 11: [side-tab] border-l-4\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 line 12: [ai-color-palette] text-purple-500 on heading\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. A gradient in one of those hues is the tell on its own; flat neon ink on a dark ground is charged once a second tell hue joins it. Choose a distinctive, intentional palette.\n\n<REPO>/tests/fixtures/antipatterns/multifile/styles.css (imported by App.tsx)\n line 3: [overused-font] font-family: '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 line 8: [bounce-easing] animation: bounce\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n\n<REPO>/tests/fixtures/antipatterns/multifile/theme.sass\n line 4: [side-tab] border-left: 4px solid $primary\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\n<REPO>/tests/fixtures/antipatterns/multifile/theme.scss\n line 4: [side-tab] border-left: 4px solid $primary\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\n6 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user