mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 18:47:02 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
502f0f28b5 |
+11
-41
@@ -46,35 +46,6 @@ function __rectArray(r) {
|
|||||||
return [r.x, r.y, r.width, r.height, r.top, r.right, r.bottom, r.left];
|
return [r.x, r.y, r.width, r.height, r.top, r.right, r.bottom, r.left];
|
||||||
}
|
}
|
||||||
|
|
||||||
// The client rects of the non-blank text nodes under `node`, in document
|
|
||||||
// order. `deep` walks element children too: one line of prose is one line
|
|
||||||
// box however the markup splits it, and an inline <strong>, an <a> or a
|
|
||||||
// framework marker in the middle of a sentence is a separate text node whose
|
|
||||||
// rects belong to the same line. Nothing is merged here — the rects travel as
|
|
||||||
// the page gave them and the consumer groups them into lines (see
|
|
||||||
// merge_text_rects_into_lines in crates/foundation/src/browser/dom.rs).
|
|
||||||
function __collectTextRects(node, deep, out) {
|
|
||||||
for (const child of node.childNodes) {
|
|
||||||
if (child.nodeType === 3) {
|
|
||||||
if (!(child.textContent || '').trim()) continue;
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(child);
|
|
||||||
for (const rect of range.getClientRects()) {
|
|
||||||
if (rect.width >= 1 && rect.height >= 1) out.push(rect);
|
|
||||||
}
|
|
||||||
range.detach?.();
|
|
||||||
} else if (deep && child.nodeType === 1) {
|
|
||||||
__collectTextRects(child, true, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The element's own direct text, unmerged: what the union rect is built from.
|
|
||||||
function __directTextRects(el) {
|
|
||||||
return __collectTextRects(__el(el), false, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const __impeccableDom = {
|
const __impeccableDom = {
|
||||||
document_element() { return __intern(document.documentElement); },
|
document_element() { return __intern(document.documentElement); },
|
||||||
body() { return __intern(document.body); },
|
body() { return __intern(document.body); },
|
||||||
@@ -210,7 +181,17 @@ const __impeccableDom = {
|
|||||||
// getDirectTextRect(el) from the JS driver: union of the client rects of
|
// getDirectTextRect(el) from the JS driver: union of the client rects of
|
||||||
// the element's non-blank direct text nodes.
|
// the element's non-blank direct text nodes.
|
||||||
direct_text_rect(el) {
|
direct_text_rect(el) {
|
||||||
const rects = __directTextRects(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?.();
|
||||||
|
}
|
||||||
if (rects.length === 0) return [];
|
if (rects.length === 0) return [];
|
||||||
const left = Math.min(...rects.map(r => r.left));
|
const left = Math.min(...rects.map(r => r.left));
|
||||||
const top = Math.min(...rects.map(r => r.top));
|
const top = Math.min(...rects.map(r => r.top));
|
||||||
@@ -218,15 +199,4 @@ const __impeccableDom = {
|
|||||||
const bottom = Math.max(...rects.map(r => r.bottom));
|
const bottom = Math.max(...rects.map(r => r.bottom));
|
||||||
return [left, top, right - left, bottom - top, top, right, bottom, left];
|
return [left, top, right - left, bottom - top, top, right, bottom, left];
|
||||||
},
|
},
|
||||||
// Every rect of the element's rendered text, descendants included, flattened
|
|
||||||
// into eights. The scope is the element's whole text_content, which is the
|
|
||||||
// text a caller counts characters from; the caller merges the rects that
|
|
||||||
// share a row into the line they rendered on.
|
|
||||||
text_rects(el) {
|
|
||||||
const out = [];
|
|
||||||
for (const r of __collectTextRects(__el(el), true, [])) {
|
|
||||||
out.push(r.left, r.top, r.width, r.height, r.top, r.right, r.bottom, r.left);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -69,26 +69,19 @@ const __SNAP_DEFAULT_MAX_BYTES = 48 * 1024 * 1024;
|
|||||||
function __snapRect4(r) { return [r.x, r.y, r.width, r.height]; }
|
function __snapRect4(r) { return [r.x, r.y, r.width, r.height]; }
|
||||||
function __snapNum(v) { return typeof v === 'number' ? v : null; }
|
function __snapNum(v) { return typeof v === 'number' ? v : null; }
|
||||||
|
|
||||||
// The client rects of `node`'s own non-blank text nodes (same walk as
|
// getDirectTextRect(el): union of the client rects of the element's
|
||||||
// 10-probe.js#__collectTextRects with `deep` off). Each element records only
|
// non-blank direct text nodes (same measure as 10-probe.js).
|
||||||
// its own, so a line that rendered is recorded exactly once in a snapshot.
|
function __snapDirectTextRect(node) {
|
||||||
function __snapTextRects(node, out) {
|
const rects = [];
|
||||||
for (const child of node.childNodes) {
|
for (const child of node.childNodes) {
|
||||||
if (child.nodeType !== 3) continue;
|
if (child.nodeType !== 3 || !(child.textContent || '').trim()) continue;
|
||||||
if (!(child.textContent || '').trim()) continue;
|
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.selectNodeContents(child);
|
range.selectNodeContents(child);
|
||||||
for (const rect of range.getClientRects()) {
|
for (const rect of range.getClientRects()) {
|
||||||
if (rect.width >= 1 && rect.height >= 1) out.push(rect);
|
if (rect.width >= 1 && rect.height >= 1) rects.push(rect);
|
||||||
}
|
}
|
||||||
range.detach?.();
|
range.detach?.();
|
||||||
}
|
}
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// getDirectTextRect(el) over rects already collected: their union, as
|
|
||||||
// 10-probe.js#direct_text_rect builds it.
|
|
||||||
function __snapDirectTextRectOf(rects) {
|
|
||||||
if (rects.length === 0) return null;
|
if (rects.length === 0) return null;
|
||||||
const left = Math.min(...rects.map(r => r.left));
|
const left = Math.min(...rects.map(r => r.left));
|
||||||
const top = Math.min(...rects.map(r => r.top));
|
const top = Math.min(...rects.map(r => r.top));
|
||||||
@@ -637,17 +630,8 @@ const __impeccableSnapshot = {
|
|||||||
rec.v = typeof el.checkVisibility === 'function'
|
rec.v = typeof el.checkVisibility === 'function'
|
||||||
? (el.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true }) ? 1 : 0)
|
? (el.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true }) ? 1 : 0)
|
||||||
: -1;
|
: -1;
|
||||||
// The element's OWN text rects, unmerged (`dl`), and their union
|
const dtr = __snapDirectTextRect(el);
|
||||||
// (`d`, the long-standing field). Own and not the subtree's: every
|
|
||||||
// ancestor would otherwise carry a copy of every line under it, which
|
|
||||||
// on a deep text-heavy page multiplies the snapshot by its depth and
|
|
||||||
// can push it past the byte cap. The consumer walks the tree and
|
|
||||||
// assembles an element's lines from the rects its descendants each
|
|
||||||
// recorded once (`SnapshotDom::text_line_rects`).
|
|
||||||
const own = __snapTextRects(el, []);
|
|
||||||
const dtr = __snapDirectTextRectOf(own);
|
|
||||||
if (dtr) rec.d = dtr;
|
if (dtr) rec.d = dtr;
|
||||||
if (own.length) rec.dl = own.map(r => [r.x, r.y, r.width, r.height]);
|
|
||||||
if (el.isContentEditable) rec.e = true;
|
if (el.isContentEditable) rec.e = true;
|
||||||
if (el.hidden) rec.h = true;
|
if (el.hidden) rec.h = true;
|
||||||
if (typeof el.id !== 'string') rec.i = true;
|
if (typeof el.id !== 'string') rec.i = true;
|
||||||
@@ -677,7 +661,6 @@ const __impeccableSnapshot = {
|
|||||||
}
|
}
|
||||||
const snapshot = {
|
const snapshot = {
|
||||||
v: 1,
|
v: 1,
|
||||||
textLines: true,
|
|
||||||
hostname: location.hostname,
|
hostname: location.hostname,
|
||||||
quirks: document.compatMode === 'BackCompat',
|
quirks: document.compatMode === 'BackCompat',
|
||||||
innerWidth: window.innerWidth,
|
innerWidth: window.innerWidth,
|
||||||
|
|||||||
@@ -224,7 +224,6 @@ pub fn serialize_design_system_for_browser(ds: Option<&DesignSystem>) -> Value {
|
|||||||
"hasRadii": ds.has_radii,
|
"hasRadii": ds.has_radii,
|
||||||
"allowedRadii": radii,
|
"allowedRadii": radii,
|
||||||
"hasPillRadius": ds.has_pill_radius,
|
"hasPillRadius": ds.has_pill_radius,
|
||||||
"declaredSelectors": ds.declared_selectors,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,56 +77,31 @@ fn legacy_kebab(value: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn normalized_kebab(value: &str) -> Option<String> {
|
fn normalized_kebab(value: &str) -> Option<String> {
|
||||||
let lower = value.to_lowercase();
|
let normalized = value
|
||||||
// replace runs of / \ . with '-'
|
.to_lowercase()
|
||||||
let mut s = String::with_capacity(lower.len());
|
.split(|c: char| !c.is_ascii_lowercase() && !c.is_ascii_digit())
|
||||||
let mut in_sep = false;
|
.filter(|part| !part.is_empty())
|
||||||
for c in lower.chars() {
|
.collect::<Vec<_>>()
|
||||||
if c == '/' || c == '\\' || c == '.' {
|
.join("-");
|
||||||
if !in_sep {
|
(!normalized.is_empty()).then_some(normalized)
|
||||||
s.push('-');
|
|
||||||
in_sep = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
in_sep = false;
|
|
||||||
s.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// replace runs of [^a-z0-9-] with '-'
|
|
||||||
let mut t = String::with_capacity(s.len());
|
|
||||||
let mut in_bad = false;
|
|
||||||
for c in s.chars() {
|
|
||||||
if c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' {
|
|
||||||
in_bad = false;
|
|
||||||
t.push(c);
|
|
||||||
} else if !in_bad {
|
|
||||||
t.push('-');
|
|
||||||
in_bad = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// collapse -+
|
|
||||||
let mut u = String::with_capacity(t.len());
|
|
||||||
let mut in_dash = false;
|
|
||||||
for c in t.chars() {
|
|
||||||
if c == '-' {
|
|
||||||
if !in_dash {
|
|
||||||
u.push('-');
|
|
||||||
in_dash = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
in_dash = false;
|
|
||||||
u.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// strip leading/trailing '-' (JS: /^-|-$/g -> one at each end; after collapse there is at most one)
|
|
||||||
let u = u.strip_prefix('-').unwrap_or(&u).to_string();
|
|
||||||
let u = u.strip_suffix('-').unwrap_or(&u).to_string();
|
|
||||||
(!u.is_empty()).then_some(u)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{kebab, legacy_kebab, SLUG_MAX};
|
use super::{kebab, legacy_kebab, normalized_kebab, SLUG_MAX};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalization_preserves_ascii_runs_after_unicode_lowercasing() {
|
||||||
|
for (input, expected) in [
|
||||||
|
("", None),
|
||||||
|
(" /\\.--_!?\n🦀 ", None),
|
||||||
|
("--Button./\\ _Primary...99--", Some("button-primary-99")),
|
||||||
|
("A🦀B café", Some("a-b-caf")),
|
||||||
|
("İSTANBUL KELVIN", Some("i-stanbul-kelvin")),
|
||||||
|
] {
|
||||||
|
assert_eq!(normalized_kebab(input).as_deref(), expected, "{input:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn truncated_slugs_keep_distinct_full_inputs_distinct() {
|
fn truncated_slugs_keep_distinct_full_inputs_distinct() {
|
||||||
|
|||||||
@@ -78,10 +78,6 @@ pub struct DesignSeen {
|
|||||||
/// `None` when `!raw?.present`.
|
/// `None` when `!raw?.present`.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct DesignSystemConfig {
|
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 has_fonts: bool,
|
||||||
pub allowed_fonts: Vec<String>,
|
pub allowed_fonts: Vec<String>,
|
||||||
pub has_colors: bool,
|
pub has_colors: bool,
|
||||||
@@ -221,15 +217,8 @@ pub fn browser_design_system_config(config: &BrowserConfig) -> Option<DesignSyst
|
|||||||
.map(js_number)
|
.map(js_number)
|
||||||
.filter(|px| px.is_finite())
|
.filter(|px| px.is_finite())
|
||||||
.collect();
|
.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)));
|
let is_true = |k: &str| matches!(obj.get(k), Some(serde_json::Value::Bool(true)));
|
||||||
Some(DesignSystemConfig {
|
Some(DesignSystemConfig {
|
||||||
declared_selectors,
|
|
||||||
has_fonts: is_true("hasFonts") && !allowed_fonts.is_empty(),
|
has_fonts: is_true("hasFonts") && !allowed_fonts.is_empty(),
|
||||||
allowed_fonts,
|
allowed_fonts,
|
||||||
has_colors: is_true("hasColors") && !allowed_colors.is_empty(),
|
has_colors: is_true("hasColors") && !allowed_colors.is_empty(),
|
||||||
@@ -1329,11 +1318,6 @@ 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 rule_ok = |id: &str| disabled.is_empty() || !disabled.iter().any(|d| d == id);
|
||||||
let design_system = browser_design_system_config(config);
|
let design_system = browser_design_system_config(config);
|
||||||
let mut design_seen = DesignSeen::default();
|
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 body = dom.body();
|
||||||
let root = dom.document_element();
|
let root = dom.document_element();
|
||||||
// JS `document.body` may be null on a bare document; every
|
// JS `document.body` may be null on a bare document; every
|
||||||
@@ -1369,19 +1353,7 @@ 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_colors_dom(dom, el)));
|
||||||
findings.extend(hits(ec::check_element_motion_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_glow_dom(dom, el)));
|
||||||
let palette = ec::check_element_ai_palette_dom(dom, el);
|
findings.extend(hits(ec::check_element_ai_palette_dom(dom, el)));
|
||||||
// An ignored subtree gets no vote in the page-wide reading. A cyan
|
|
||||||
// tell inside `data-impeccable-ignore="ai-color-palette"` would
|
|
||||||
// otherwise open the two-hue gate and charge neon ink somewhere else
|
|
||||||
// on the page that nobody waived — ignored content changing the
|
|
||||||
// result for content that was not ignored.
|
|
||||||
if !scoped_ignore_active(dom, el, "ai-color-palette") {
|
|
||||||
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_radial_spotlight_dom(dom, el)));
|
||||||
findings.extend(hits(ec::check_element_icon_tile_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)));
|
findings.extend(hits(ec::check_element_italic_serif_dom(dom, el)));
|
||||||
@@ -1418,17 +1390,6 @@ 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 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();
|
let list: Vec<BrowserFinding> = list.into_iter().filter(|f| rule_ok(&f.type_)).collect();
|
||||||
if !list.is_empty() {
|
if !list.is_empty() {
|
||||||
@@ -1437,6 +1398,17 @@ 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>| {
|
let el_pass = |groups: &mut Vec<FindingGroup>, list: Vec<super::ElFinding>| {
|
||||||
for f in list {
|
for f in list {
|
||||||
if !rule_ok(&f.finding.type_) {
|
if !rule_ok(&f.finding.type_) {
|
||||||
@@ -1451,18 +1423,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));
|
|
||||||
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_layout(dom));
|
||||||
el_pass(&mut groups, pc::check_heading_rhythm_dom(dom));
|
el_pass(&mut groups, pc::check_heading_rhythm_dom(dom));
|
||||||
el_pass(&mut groups, pc::check_edge_flush_cards_dom(dom));
|
el_pass(&mut groups, pc::check_edge_flush_cards_dom(dom));
|
||||||
@@ -1678,57 +1638,6 @@ mod tests {
|
|||||||
assert!(!is_likely_hashed_class("abcdefg"));
|
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]
|
#[test]
|
||||||
fn skip_scan_empties_the_collect_pass() {
|
fn skip_scan_empties_the_collect_pass() {
|
||||||
// JS: index.mjs#skipScanActive() — an ignoreFiles-waived page answers
|
// JS: index.mjs#skipScanActive() — an ignoreFiles-waived page answers
|
||||||
@@ -1938,55 +1847,6 @@ mod tests {
|
|||||||
assert_eq!(html_pattern_query(".a::before,"), Some(".a".to_string()));
|
assert_eq!(html_pattern_query(".a::before,"), Some(".a".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An ignored subtree does not get to open the page-wide palette gate.
|
|
||||||
/// `ai-color-palette` holds neon ink until a second tell hue turns up
|
|
||||||
/// somewhere on the page; a cyan tell inside a
|
|
||||||
/// `data-impeccable-ignore="ai-color-palette"` subtree used to count
|
|
||||||
/// toward that, so waiving one component charged an unrelated one.
|
|
||||||
#[test]
|
|
||||||
fn ignored_colors_do_not_contribute_tell_hues() {
|
|
||||||
let build = |ignore: bool| {
|
|
||||||
let mut d = FakeDom::new();
|
|
||||||
let (_h, body) = d.with_page();
|
|
||||||
d.set_style(body, "backgroundColor", "rgb(5, 6, 10)");
|
|
||||||
|
|
||||||
// The waived component: cyan neon ink on near-black.
|
|
||||||
let demo = d.add(Some(body), "div");
|
|
||||||
d.set_style(demo, "backgroundColor", "rgb(5, 6, 10)");
|
|
||||||
if ignore {
|
|
||||||
d.set_attr(demo, "data-impeccable-ignore", "ai-color-palette");
|
|
||||||
}
|
|
||||||
let cyan = d.add(Some(demo), "span");
|
|
||||||
d.add_text(cyan, "Terminal output");
|
|
||||||
d.set_style(cyan, "color", "rgb(34, 238, 238)");
|
|
||||||
d.set_style(cyan, "backgroundColor", "rgba(0, 0, 0, 0)");
|
|
||||||
d.el_mut(cyan).check_visibility = Some(true);
|
|
||||||
|
|
||||||
// Somewhere else on the page, and waived by nobody.
|
|
||||||
let card = d.add(Some(body), "div");
|
|
||||||
d.set_style(card, "backgroundColor", "rgb(5, 6, 10)");
|
|
||||||
let purple = d.add(Some(card), "span");
|
|
||||||
d.add_text(purple, "Upgrade");
|
|
||||||
d.set_style(purple, "color", "rgb(180, 60, 245)");
|
|
||||||
d.set_style(purple, "backgroundColor", "rgba(0, 0, 0, 0)");
|
|
||||||
d.el_mut(purple).check_visibility = Some(true);
|
|
||||||
d
|
|
||||||
};
|
|
||||||
let charged = |d: &FakeDom| -> Vec<String> {
|
|
||||||
collect_browser_findings(d, &BrowserConfig::default())
|
|
||||||
.groups
|
|
||||||
.iter()
|
|
||||||
.flat_map(|g| g.findings.iter().map(|f| f.type_.clone()))
|
|
||||||
.filter(|t| t == "ai-color-palette")
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
// Two tell hues, neither waived: the palette is the page's.
|
|
||||||
assert_eq!(charged(&build(false)).len(), 2);
|
|
||||||
// The cyan half waived: one tell hue is an accent, and the purple ink
|
|
||||||
// outside the ignored subtree is not charged either.
|
|
||||||
assert!(charged(&build(true)).is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scoped_ignore_and_visual_merge() {
|
fn scoped_ignore_and_visual_merge() {
|
||||||
let mut d = FakeDom::new();
|
let mut d = FakeDom::new();
|
||||||
|
|||||||
@@ -716,67 +716,24 @@ 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)
|
/// JS: checks.mjs#checkElementAIPaletteDOM(el)
|
||||||
///
|
pub fn check_element_ai_palette_dom(dom: &dyn Dom, el: ElId) -> Vec<RuleHit> {
|
||||||
/// One element's reading. The gradient half answers on its own; the ink half
|
let mut findings = Vec::new();
|
||||||
/// 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");
|
let bg_image = dom.style(el, "backgroundImage");
|
||||||
for c in parse_gradient_colors(Some(&bg_image)) {
|
for c in parse_gradient_colors(Some(&bg_image)) {
|
||||||
if has_chroma(Some(&c), Some(50.0)) {
|
if has_chroma(Some(&c), Some(50.0)) {
|
||||||
if let Some(tell) = TellHue::of(get_hue(Some(&c))) {
|
let hue = get_hue(Some(&c));
|
||||||
reading.tells.push(tell);
|
if hue >= 260.0 && hue <= 310.0 {
|
||||||
reading.hits.push(RuleHit::new(
|
findings.push(RuleHit::new(
|
||||||
"ai-color-palette",
|
"ai-color-palette",
|
||||||
match tell {
|
"Purple/violet gradient background".to_string(),
|
||||||
TellHue::Purple => "Purple/violet gradient background".to_string(),
|
));
|
||||||
TellHue::Cyan => "Cyan gradient background".to_string(),
|
break;
|
||||||
},
|
}
|
||||||
|
if hue >= 160.0 && hue <= 200.0 {
|
||||||
|
findings.push(RuleHit::new(
|
||||||
|
"ai-color-palette",
|
||||||
|
"Cyan gradient background".to_string(),
|
||||||
));
|
));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -785,7 +742,10 @@ pub fn check_element_ai_palette_dom(dom: &dyn Dom, el: ElId) -> AiPaletteReading
|
|||||||
let text_color = parse_rgb_or_any(&dom.style(el, "color"));
|
let text_color = parse_rgb_or_any(&dom.style(el, "color"));
|
||||||
if let Some(tc) = text_color {
|
if let Some(tc) = text_color {
|
||||||
if has_chroma(Some(&tc), Some(80.0)) {
|
if has_chroma(Some(&tc), Some(80.0)) {
|
||||||
if let Some(tell) = TellHue::of(get_hue(Some(&tc))) {
|
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 {
|
||||||
let parent = dom.parent(el);
|
let parent = dom.parent(el);
|
||||||
let parent_bg_info = match parent {
|
let parent_bg_info = match parent {
|
||||||
Some(p) => resolve_background_info(dom, p),
|
Some(p) => resolve_background_info(dom, p),
|
||||||
@@ -800,17 +760,21 @@ pub fn check_element_ai_palette_dom(dom: &dyn Dom, el: ElId) -> AiPaletteReading
|
|||||||
}
|
}
|
||||||
if let Some(bg) = effective_bg {
|
if let Some(bg) = effective_bg {
|
||||||
if relative_luminance(&bg) < 0.1 {
|
if relative_luminance(&bg) < 0.1 {
|
||||||
reading.tells.push(tell);
|
let label = if hue >= 260.0 {
|
||||||
reading.ink = Some(RuleHit::new(
|
"Purple/violet"
|
||||||
|
} else {
|
||||||
|
"Cyan"
|
||||||
|
};
|
||||||
|
findings.push(RuleHit::new(
|
||||||
"ai-color-palette",
|
"ai-color-palette",
|
||||||
format!("{} neon text on dark background", tell.label()),
|
format!("{label} neon text on dark background"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reading
|
findings
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── radial spotlight ──────────────────────────────────────────────────────
|
// ── radial spotlight ──────────────────────────────────────────────────────
|
||||||
@@ -1570,11 +1534,9 @@ mod tests {
|
|||||||
"linear-gradient(rgb(168, 85, 247), rgb(59, 130, 246))",
|
"linear-gradient(rgb(168, 85, 247), rgb(59, 130, 246))",
|
||||||
);
|
);
|
||||||
d.set_style(hero, "color", "rgb(0, 0, 0)");
|
d.set_style(hero, "color", "rgb(0, 0, 0)");
|
||||||
let reading = check_element_ai_palette_dom(&d, hero);
|
let hits = check_element_ai_palette_dom(&d, hero);
|
||||||
assert_eq!(reading.hits.len(), 1);
|
assert_eq!(hits.len(), 1);
|
||||||
assert_eq!(reading.hits[0].snippet, "Purple/violet gradient background");
|
assert_eq!(hits[0].snippet, "Purple/violet gradient background");
|
||||||
assert!(reading.ink.is_none());
|
|
||||||
assert_eq!(reading.tells, vec![TellHue::Purple]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -92,30 +92,7 @@ pub fn has_meaningful_direct_text(dom: &dyn Dom, el: ElId) -> bool {
|
|||||||
has_direct_text_longer_than(dom, el, 4)
|
has_direct_text_longer_than(dom, el, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The width of every line the element's text rendered on, or `None` when
|
|
||||||
/// the DOM cannot say where the lines are.
|
|
||||||
///
|
|
||||||
/// `Dom::text_line_rects` has already merged the fragments of a line back
|
|
||||||
/// together, so each rect here is one line box and nothing is divided by
|
|
||||||
/// anything: a leading tighter than the glyph box used to turn one rect into
|
|
||||||
/// two identical "lines" and charge a single long line twice.
|
|
||||||
fn rendered_line_widths(dom: &dyn Dom, el: ElId) -> Option<Vec<f64>> {
|
|
||||||
Some(
|
|
||||||
dom.text_line_rects(el)?
|
|
||||||
.into_iter()
|
|
||||||
.filter(|r| r.width > 0.0 && r.height > 0.0)
|
|
||||||
.map(|r| r.width)
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// JS: checks.mjs#textDescendantsFlushSides(el, rect) → [top, right, bottom, left]
|
/// 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] {
|
pub fn text_descendants_flush_sides(dom: &dyn Dom, el: ElId, rect: &Rect) -> [bool; 4] {
|
||||||
let mut flush = [false; 4];
|
let mut flush = [false; 4];
|
||||||
const TEXT_EDGE_THRESHOLD: f64 = 4.0;
|
const TEXT_EDGE_THRESHOLD: f64 = 4.0;
|
||||||
@@ -125,7 +102,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) {
|
if !TEXT_EDGE_TAGS.contains(&tag_name.as_str()) || !has_meaningful_direct_text(dom, node) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let nr = dom.direct_text_rect(node).unwrap_or_else(|| dom.rect(node));
|
let nr = dom.rect(node);
|
||||||
if nr.width <= 0.0 || nr.height <= 0.0 {
|
if nr.width <= 0.0 || nr.height <= 0.0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -268,55 +245,21 @@ pub fn check_quality(dom: &dyn Dom, q: &QualityInput) -> Vec<RuleHit> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Line length too long ---
|
// --- 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 `text_line_rects`, one rect per line box with the fragments of a
|
|
||||||
// line merged back together, 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.
|
|
||||||
//
|
|
||||||
// The rects cover the element's whole rendered text, descendants and all,
|
|
||||||
// which is the same text `text_len` counts: measuring the direct text
|
|
||||||
// alone and then charging it with the characters of an inline `<strong>`
|
|
||||||
// inflated every paragraph that had one.
|
|
||||||
//
|
|
||||||
// A DOM that cannot say where the lines are gets no finding. The union of
|
|
||||||
// a long first line and a short tail is the same union as two even lines,
|
|
||||||
// so there is nothing in it to read a line off, and a rule that guesses
|
|
||||||
// there is charging noise.
|
|
||||||
//
|
|
||||||
// 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
|
if has_direct_text
|
||||||
&& QUALITY_TEXT_TAGS.contains(&tag)
|
&& QUALITY_TEXT_TAGS.contains(&tag)
|
||||||
&& rect.width > 0.0
|
&& rect.width > 0.0
|
||||||
&& (text_len as f64) > line_max
|
&& (text_len as f64) > line_max
|
||||||
{
|
{
|
||||||
if let Some(widths) = rendered_line_widths(dom, el) {
|
let chars_per_line = rect.width / (font_size * 0.5);
|
||||||
let total: f64 = widths.iter().sum();
|
if chars_per_line > line_max + 5.0 {
|
||||||
if total > 0.0 {
|
findings.push(RuleHit::new(
|
||||||
let over = line_max + 5.0;
|
"line-length",
|
||||||
let chars = |w: f64| (text_len as f64) * w / total;
|
format!(
|
||||||
let long = widths.iter().filter(|w| chars(**w) > over).count();
|
"~{} chars/line (aim for <{})",
|
||||||
if long >= 2 {
|
number_to_string(math_round(chars_per_line)),
|
||||||
let longest = widths.iter().copied().fold(0.0, js::math_max);
|
number_to_string(line_max)
|
||||||
findings.push(RuleHit::new(
|
),
|
||||||
"line-length",
|
));
|
||||||
format!(
|
|
||||||
"~{} 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)
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,43 +274,31 @@ pub fn check_quality(dom: &dyn Dom, q: &QualityInput) -> Vec<RuleHit> {
|
|||||||
];
|
];
|
||||||
let border_count = borders.iter().filter(|w| **w > 0.0).count();
|
let border_count = borders.iter().filter(|w| **w > 0.0).count();
|
||||||
let has_bg = has_visible_background_boundary(dom, el);
|
let has_bg = has_visible_background_boundary(dom, el);
|
||||||
// The space the reader sees, not the space the stylesheet declares:
|
if border_count >= 2 || has_bg {
|
||||||
// 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 v_pads: Vec<f64> = Vec::new();
|
||||||
let mut h_pads: Vec<f64> = Vec::new();
|
let mut h_pads: Vec<f64> = Vec::new();
|
||||||
if has_bg || borders[0] > 0.0 {
|
if has_bg || borders[0] > 0.0 {
|
||||||
v_pads.push(text_rect.top - (rect.top + borders[0]));
|
v_pads.push(spx("paddingTop"));
|
||||||
}
|
}
|
||||||
if has_bg || borders[2] > 0.0 {
|
if has_bg || borders[2] > 0.0 {
|
||||||
v_pads.push((rect.bottom - borders[2]) - text_rect.bottom);
|
v_pads.push(spx("paddingBottom"));
|
||||||
}
|
}
|
||||||
if has_bg || borders[3] > 0.0 {
|
if has_bg || borders[3] > 0.0 {
|
||||||
h_pads.push(text_rect.left - (rect.left + borders[3]));
|
h_pads.push(spx("paddingLeft"));
|
||||||
}
|
}
|
||||||
if has_bg || borders[1] > 0.0 {
|
if has_bg || borders[1] > 0.0 {
|
||||||
h_pads.push((rect.right - borders[1]) - text_rect.right);
|
h_pads.push(spx("paddingRight"));
|
||||||
}
|
}
|
||||||
let v_min = v_pads.iter().copied().fold(f64::INFINITY, js::math_min);
|
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 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 v_thresh = js::math_max(4.0, font_size * 0.3);
|
||||||
let h_thresh = js::math_max(8.0, font_size * 0.5);
|
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 {
|
if v_min < v_thresh {
|
||||||
findings.push(RuleHit::new(
|
findings.push(RuleHit::new(
|
||||||
"cramped-padding",
|
"cramped-padding",
|
||||||
format!(
|
format!(
|
||||||
"{}px of space above and below the text (need ≥{}px for {}px text)",
|
"{}px vertical padding (need ≥{}px for {}px text)",
|
||||||
px(v_min),
|
number_to_string(v_min),
|
||||||
to_fixed(v_thresh, 1),
|
to_fixed(v_thresh, 1),
|
||||||
number_to_string(font_size)
|
number_to_string(font_size)
|
||||||
),
|
),
|
||||||
@@ -376,8 +307,8 @@ pub fn check_quality(dom: &dyn Dom, q: &QualityInput) -> Vec<RuleHit> {
|
|||||||
findings.push(RuleHit::new(
|
findings.push(RuleHit::new(
|
||||||
"cramped-padding",
|
"cramped-padding",
|
||||||
format!(
|
format!(
|
||||||
"{}px of space beside the text (need ≥{}px for {}px text)",
|
"{}px horizontal padding (need ≥{}px for {}px text)",
|
||||||
px(h_min),
|
number_to_string(h_min),
|
||||||
to_fixed(h_thresh, 1),
|
to_fixed(h_thresh, 1),
|
||||||
number_to_string(font_size)
|
number_to_string(font_size)
|
||||||
),
|
),
|
||||||
@@ -449,13 +380,6 @@ pub fn check_quality(dom: &dyn Dom, q: &QualityInput) -> Vec<RuleHit> {
|
|||||||
];
|
];
|
||||||
const PAD_THRESHOLD: f64 = 2.0;
|
const PAD_THRESHOLD: f64 = 2.0;
|
||||||
const CHILD_INSULATE_THRESHOLD: f64 = 4.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];
|
let mut children_insulate = [false; 4];
|
||||||
for &child in &children {
|
for &child in &children {
|
||||||
let child_pad = [
|
let child_pad = [
|
||||||
@@ -472,18 +396,6 @@ pub fn check_quality(dom: &dyn Dom, q: &QualityInput) -> Vec<RuleHit> {
|
|||||||
];
|
];
|
||||||
let cr = dom.rect(child);
|
let cr = dom.rect(child);
|
||||||
if cr.width > 0.0 && cr.height > 0.0 {
|
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 {
|
if cr.top - rect.top >= CHILD_INSULATE_THRESHOLD {
|
||||||
children_insulate[0] = true;
|
children_insulate[0] = true;
|
||||||
}
|
}
|
||||||
@@ -516,12 +428,7 @@ pub fn check_quality(dom: &dyn Dom, q: &QualityInput) -> Vec<RuleHit> {
|
|||||||
for s in 0..4 {
|
for s in 0..4 {
|
||||||
let bg_bounds_side = bg_visible && !(full_bleed_bg_band && (s == 1 || s == 3));
|
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;
|
let side_bounded = border_visible[s] || outline_visible || bg_bounds_side;
|
||||||
if side_bounded
|
if side_bounded && pad[s] <= PAD_THRESHOLD && !children_insulate[s] && text_flush[s] {
|
||||||
&& pad[s] <= PAD_THRESHOLD
|
|
||||||
&& !children_insulate[s]
|
|
||||||
&& !children_overflow[s]
|
|
||||||
&& text_flush[s]
|
|
||||||
{
|
|
||||||
flush_sides.push(side_names[s]);
|
flush_sides.push(side_names[s]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -859,176 +766,22 @@ mod tests {
|
|||||||
fn line_length_and_viewport_edge() {
|
fn line_length_and_viewport_edge() {
|
||||||
let mut d = FakeDom::new();
|
let mut d = FakeDom::new();
|
||||||
let (_h, body) = d.with_page();
|
let (_h, body) = d.with_page();
|
||||||
let long = "x".repeat(240);
|
let long = "x".repeat(120);
|
||||||
let p = text_el(&mut d, body, "p", &long, "16px");
|
let p = text_el(&mut d, body, "p", &long, "16px");
|
||||||
d.set_rect(p, 0.0, 100.0, 1200.0, 72.0);
|
d.set_rect(p, 0.0, 100.0, 1200.0, 40.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 hits = check_element_quality_dom(&d, p, &BrowserConfig::default());
|
||||||
let ids: Vec<&str> = hits.iter().map(|h| h.id.as_str()).collect();
|
let ids: Vec<&str> = hits.iter().map(|h| h.id.as_str()).collect();
|
||||||
assert!(ids.contains(&"line-length"), "{ids:?}");
|
assert!(ids.contains(&"line-length"), "{ids:?}");
|
||||||
assert_eq!(hits[0].snippet, "~103 chars on 2 of 3 rendered lines (aim for <80)");
|
assert_eq!(hits[0].snippet, "~150 chars/line (aim for <80)");
|
||||||
assert!(ids.contains(&"body-text-viewport-edge"));
|
assert!(ids.contains(&"body-text-viewport-edge"));
|
||||||
let edge = hits.iter().find(|h| h.id == "body-text-viewport-edge").unwrap();
|
let edge = hits.iter().find(|h| h.id == "body-text-viewport-edge").unwrap();
|
||||||
assert_eq!(edge.snippet, "<p> with 240-char body bleeds to viewport edge (left 0px)");
|
assert_eq!(edge.snippet, "<p> with 120-char body bleeds to viewport edge (left 0px)");
|
||||||
// narrower, inset paragraph: neither fires
|
// narrower, inset paragraph: neither fires
|
||||||
d.set_rect(p, 40.0, 100.0, 600.0, 72.0);
|
d.set_rect(p, 40.0, 100.0, 600.0, 40.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());
|
let hits = check_element_quality_dom(&d, p, &BrowserConfig::default());
|
||||||
assert!(hits.is_empty(), "{hits:?}");
|
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)");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A line box split across text nodes is still one line. An inline
|
|
||||||
/// `<strong>` or `<a>` in the middle of a sentence is its own text node,
|
|
||||||
/// so `getClientRects()` hands back a rect per fragment; counting each
|
|
||||||
/// fragment as a line divided the paragraph's characters among them and
|
|
||||||
/// hid a genuinely long column.
|
|
||||||
#[test]
|
|
||||||
fn a_line_split_across_fragments_is_one_line() {
|
|
||||||
let mut d = FakeDom::new();
|
|
||||||
let (_h, body) = d.with_page();
|
|
||||||
// 300 characters over three rendered lines, each interrupted mid-line
|
|
||||||
// by an inline element and so measured in two pieces.
|
|
||||||
let p = text_el(&mut d, body, "p", &"w".repeat(300), "16px");
|
|
||||||
d.set_rect(p, 0.0, 100.0, 1020.0, 72.0);
|
|
||||||
d.set_text_lines(
|
|
||||||
p,
|
|
||||||
&[
|
|
||||||
(0.0, 100.0, 520.0, 19.0),
|
|
||||||
(520.0, 100.0, 480.0, 19.0),
|
|
||||||
(0.0, 124.0, 510.0, 19.0),
|
|
||||||
(510.0, 124.0, 490.0, 19.0),
|
|
||||||
(0.0, 148.0, 505.0, 19.0),
|
|
||||||
(505.0, 148.0, 495.0, 19.0),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
let hits = check_element_quality_dom(&d, p, &BrowserConfig::default());
|
|
||||||
let line = hits.iter().find(|h| h.id == "line-length").expect("charged");
|
|
||||||
// Three lines of 1000px, not six of ~500: six would have put 50
|
|
||||||
// characters on each and charged nothing at all.
|
|
||||||
assert_eq!(line.snippet, "~100 chars on 3 of 3 rendered lines (aim for <80)");
|
|
||||||
|
|
||||||
// The same merge the other way: one long line in two fragments plus a
|
|
||||||
// short tail is two lines, and one long line is a sentence that
|
|
||||||
// wrapped once.
|
|
||||||
let q = text_el(&mut d, body, "p", &"w".repeat(190), "16px");
|
|
||||||
d.set_rect(q, 0.0, 300.0, 1020.0, 48.0);
|
|
||||||
d.set_text_lines(
|
|
||||||
q,
|
|
||||||
&[
|
|
||||||
(0.0, 300.0, 600.0, 19.0),
|
|
||||||
(600.0, 300.0, 400.0, 19.0),
|
|
||||||
(0.0, 324.0, 120.0, 19.0),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
let hits = check_element_quality_dom(&d, q, &BrowserConfig::default());
|
|
||||||
assert!(!hits.iter().any(|h| h.id == "line-length"), "{hits:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Two columns that happen to sit on the same rows are two flows, not one
|
|
||||||
/// page-wide line. Fragments join a row only when they run on from it —
|
|
||||||
/// a gap no wider than the row's own line box — so a gutter keeps them
|
|
||||||
/// apart and the characters stay where the reader sees them.
|
|
||||||
#[test]
|
|
||||||
fn columns_on_the_same_rows_are_separate_lines() {
|
|
||||||
let mut d = FakeDom::new();
|
|
||||||
let (_h, body) = d.with_page();
|
|
||||||
let p = text_el(&mut d, body, "p", &"w".repeat(240), "16px");
|
|
||||||
d.set_rect(p, 0.0, 100.0, 1020.0, 72.0);
|
|
||||||
// Two 300px columns with a 100px gutter, three rows each.
|
|
||||||
d.set_text_lines(
|
|
||||||
p,
|
|
||||||
&[
|
|
||||||
(0.0, 100.0, 300.0, 19.0),
|
|
||||||
(400.0, 100.0, 300.0, 19.0),
|
|
||||||
(0.0, 124.0, 300.0, 19.0),
|
|
||||||
(400.0, 124.0, 300.0, 19.0),
|
|
||||||
(0.0, 148.0, 300.0, 19.0),
|
|
||||||
(400.0, 148.0, 300.0, 19.0),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
let hits = check_element_quality_dom(&d, p, &BrowserConfig::default());
|
|
||||||
// Six short lines of 40 characters each, not three of 700px.
|
|
||||||
assert!(!hits.iter().any(|h| h.id == "line-length"), "{hits:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A rect that is already one line is one line, whatever the leading is.
|
|
||||||
/// Dividing every rect by the line box turned a paragraph whose leading
|
|
||||||
/// is tighter than its glyph box into two copies of the same line, and
|
|
||||||
/// two copies of one long line satisfied "at least two long lines".
|
|
||||||
#[test]
|
|
||||||
fn tight_leading_does_not_double_count_a_line() {
|
|
||||||
let mut d = FakeDom::new();
|
|
||||||
let (_h, body) = d.with_page();
|
|
||||||
let p = text_el(&mut d, body, "p", &"w".repeat(200), "15px");
|
|
||||||
// 10px of leading under an 19px glyph box: `round(19 / 10)` is 2.
|
|
||||||
d.set_style(p, "lineHeight", "10px");
|
|
||||||
d.set_rect(p, 0.0, 100.0, 1020.0, 19.0);
|
|
||||||
d.set_text_lines(p, &[(0.0, 100.0, 1000.0, 19.0)]);
|
|
||||||
let hits = check_element_quality_dom(&d, p, &BrowserConfig::default());
|
|
||||||
assert!(!hits.iter().any(|h| h.id == "line-length"), "{hits:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A DOM that kept only the union of its text rects cannot say where the
|
|
||||||
/// lines are, and the rule stands down rather than inventing them. A
|
|
||||||
/// snapshot captured before the lines were recorded is that DOM: the
|
|
||||||
/// union of a long first line and a short tail is the same union as two
|
|
||||||
/// even lines, so any width read off it is a width nothing rendered.
|
|
||||||
#[test]
|
|
||||||
fn a_dom_without_lines_does_not_charge_line_length() {
|
|
||||||
let mut d = FakeDom::new();
|
|
||||||
let (_h, body) = d.with_page();
|
|
||||||
let p = text_el(&mut d, body, "p", &"w".repeat(300), "16px");
|
|
||||||
d.set_rect(p, 0.0, 100.0, 1020.0, 72.0);
|
|
||||||
// The same paragraph the merge test charges, measured once.
|
|
||||||
d.set_text_rect(p, 0.0, 100.0, 1000.0, 67.0);
|
|
||||||
let hits = check_element_quality_dom(&d, p, &BrowserConfig::default());
|
|
||||||
assert!(!hits.iter().any(|h| h.id == "line-length"), "{hits:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cramped_padding_vertical() {
|
fn cramped_padding_vertical() {
|
||||||
let mut d = FakeDom::new();
|
let mut d = FakeDom::new();
|
||||||
@@ -1050,54 +803,9 @@ mod tests {
|
|||||||
("paddingRight", "12px"),
|
("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());
|
let hits = check_element_quality_dom(&d, p, &BrowserConfig::default());
|
||||||
assert_eq!(hits.len(), 1, "{hits:?}");
|
assert_eq!(hits.len(), 1, "{hits:?}");
|
||||||
assert_eq!(
|
assert_eq!(hits[0].snippet, "2px vertical padding (need ≥4.8px for 16px text)");
|
||||||
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]
|
#[test]
|
||||||
@@ -1139,78 +847,6 @@ 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]
|
#[test]
|
||||||
fn typography_rules() {
|
fn typography_rules() {
|
||||||
let mut d = FakeDom::new();
|
let mut d = FakeDom::new();
|
||||||
|
|||||||
@@ -5,10 +5,8 @@
|
|||||||
//! `checkRepeatedContainerTextDOM`) against the [`Dom`] probe. The pure
|
//! `checkRepeatedContainerTextDOM`) against the [`Dom`] probe. The pure
|
||||||
//! gates live in `checks::rules` / `checks::text_rules`.
|
//! gates live in `checks::rules` / `checks::text_rules`.
|
||||||
|
|
||||||
use super::dom::{matches_or_false, tag_lower, Dom, ElId, ElStyle};
|
use super::dom::{tag_lower, Dom, ElId, ElStyle};
|
||||||
use super::driver::DesignSystemConfig;
|
|
||||||
use super::element_checks::{class_selector, is_rendered_for_browser_rule};
|
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::measures::resolve_length_px;
|
||||||
use crate::checks::rules::{check_kicker_above_heading, KickerCandidate, RuleHit};
|
use crate::checks::rules::{check_kicker_above_heading, KickerCandidate, RuleHit};
|
||||||
use crate::checks::text_rules::{
|
use crate::checks::text_rules::{
|
||||||
@@ -101,17 +99,6 @@ fn strip_edge_quotes_slice(text: &str, n: usize) -> String {
|
|||||||
|
|
||||||
/// JS: checks.mjs#collectKickerCandidates(document, getComputedStyle, resolveLengthPx || 0)
|
/// JS: checks.mjs#collectKickerCandidates(document, getComputedStyle, resolveLengthPx || 0)
|
||||||
pub fn collect_kicker_candidates(dom: &dyn Dom) -> Vec<KickerCandidate> {
|
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();
|
let mut candidates = Vec::new();
|
||||||
for heading in dom
|
for heading in dom
|
||||||
.query_all(None, "h1, h2, h3, h4, [role=\"heading\"]")
|
.query_all(None, "h1, h2, h3, h4, [role=\"heading\"]")
|
||||||
@@ -177,60 +164,18 @@ pub fn collect_kicker_candidates_with_elements(dom: &dyn Dom) -> Vec<(ElId, Kick
|
|||||||
if heading_tag == "h1" && heading_font_size >= 48.0 && kicker_letter_spacing >= 1.6 {
|
if heading_tag == "h1" && heading_font_size >= 48.0 && kicker_letter_spacing >= 1.6 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
candidates.push((
|
candidates.push(KickerCandidate {
|
||||||
kicker,
|
heading_tag,
|
||||||
KickerCandidate {
|
heading_text: strip_edge_quotes_slice(&heading_text, 60),
|
||||||
heading_tag,
|
kicker_text: slice_utf16_prefix(&kicker_text, 40),
|
||||||
heading_text: strip_edge_quotes_slice(&heading_text, 60),
|
});
|
||||||
kicker_text: slice_utf16_prefix(&kicker_text, 40),
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
candidates
|
candidates
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JS: checks.mjs#checkKickerAboveHeadingDOM()
|
/// JS: checks.mjs#checkKickerAboveHeadingDOM()
|
||||||
///
|
pub fn check_kicker_above_heading_dom(dom: &dyn Dom) -> Vec<RuleHit> {
|
||||||
/// Two things the page-level version could not do. The finding lands on the
|
check_kicker_above_heading(&collect_kicker_candidates(dom))
|
||||||
/// 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, ...)
|
/// JS: checks.mjs#collectNumberedSectionLabelCandidates(document, ...)
|
||||||
@@ -502,29 +447,13 @@ mod tests {
|
|||||||
let h = d.add(Some(sec), "h2");
|
let h = d.add(Some(sec), "h2");
|
||||||
d.add_text(h, "Everything you need");
|
d.add_text(h, "Everything you need");
|
||||||
d.set_style(h, "fontSize", "32px");
|
d.set_style(h, "fontSize", "32px");
|
||||||
let hits = check_kicker_above_heading_dom(&d, None);
|
let hits = check_kicker_above_heading_dom(&d);
|
||||||
assert_eq!(hits.len(), 1);
|
assert_eq!(hits.len(), 1);
|
||||||
assert_eq!(hits[0].finding.type_, "kicker-above-heading");
|
assert_eq!(hits[0].id, "kicker-above-heading");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
hits[0].finding.detail,
|
hits[0].snippet,
|
||||||
"kicker \"Features\" above h2 \"Everything you need\""
|
"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
|
// A card context (heading inside <article> that also contains the
|
||||||
// kicker) stands down.
|
// kicker) stands down.
|
||||||
let art = d.add(Some(body), "article");
|
let art = d.add(Some(body), "article");
|
||||||
@@ -534,7 +463,7 @@ mod tests {
|
|||||||
let h2 = d.add(Some(art), "h3");
|
let h2 = d.add(Some(art), "h3");
|
||||||
d.add_text(h2, "Card heading");
|
d.add_text(h2, "Card heading");
|
||||||
d.set_style(h2, "fontSize", "24px");
|
d.set_style(h2, "fontSize", "24px");
|
||||||
assert_eq!(check_kicker_above_heading_dom(&d, None).len(), 1);
|
assert_eq!(check_kicker_above_heading_dom(&d).len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
//! `undefined` / `null` distinctions the source relies on.
|
//! `undefined` / `null` distinctions the source relies on.
|
||||||
|
|
||||||
use crate::color::{
|
use crate::color::{
|
||||||
color_to_hex, composite_color_over, contrast_ratio, get_hue, has_chroma, is_gray_ink,
|
color_to_hex, composite_color_over, contrast_ratio, get_hue, has_chroma, is_neutral_color,
|
||||||
is_neutral_color, relative_luminance, Rgba,
|
relative_luminance, Rgba,
|
||||||
};
|
};
|
||||||
use crate::constants::{
|
use crate::constants::{
|
||||||
BORDER_SAFE_TAGS, GENERIC_FONTS, KNOWN_SERIF_FONTS, SAFE_TAGS, WCAG_LARGE_BOLD_TEXT_PX,
|
BORDER_SAFE_TAGS, GENERIC_FONTS, KNOWN_SERIF_FONTS, SAFE_TAGS, WCAG_LARGE_BOLD_TEXT_PX,
|
||||||
@@ -231,12 +231,9 @@ fn contrast_findings(opts: &ColorOpts, text_color: &Rgba) -> Vec<RuleHit> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut findings = Vec::new();
|
let mut findings = Vec::new();
|
||||||
// Gray is low chroma at whatever lightness the ink sits at, and the
|
let text_lum = relative_luminance(text_color);
|
||||||
// surface is a colour when it has chroma of its own. The old pair of
|
let is_gray = !has_chroma(Some(text_color), Some(20.0)) && text_lum > 0.05 && text_lum < 0.85;
|
||||||
// tests read relative luminance as if it were lightness, which made every
|
if is_gray && bgs.iter().all(|b| has_chroma(Some(b), Some(40.0))) {
|
||||||
// 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 {
|
let bg_label = match opts.effective_bg {
|
||||||
Some(bg) => color_to_hex(Some(&bg)),
|
Some(bg) => color_to_hex(Some(&bg)),
|
||||||
None => format!(
|
None => format!(
|
||||||
@@ -1096,38 +1093,6 @@ 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]
|
#[test]
|
||||||
fn placeholder_colors_ignore_host_class_heuristics() {
|
fn placeholder_colors_ignore_host_class_heuristics() {
|
||||||
let opts = ColorOpts {
|
let opts = ColorOpts {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ use crate::jsp;
|
|||||||
use crate::util::{exists, js_string, re, read_json, read_text, ANY, WS};
|
use crate::util::{exists, js_string, re, read_json, read_text, ANY, WS};
|
||||||
|
|
||||||
const DESIGN_NAMES: &[&str] = &["DESIGN.md", "Design.md", "design.md"];
|
const DESIGN_NAMES: &[&str] = &["DESIGN.md", "Design.md", "design.md"];
|
||||||
|
|
||||||
const FALLBACK_DIRS: &[&str] = &[".agents/context", "docs"];
|
const FALLBACK_DIRS: &[&str] = &[".agents/context", "docs"];
|
||||||
const PROJECT_ROOT_MARKERS: &[&str] = &[".git", "package.json", ".impeccable"];
|
const PROJECT_ROOT_MARKERS: &[&str] = &[".git", "package.json", ".impeccable"];
|
||||||
const COLOR_CHANNEL_TOLERANCE: f64 = 6.0;
|
const COLOR_CHANNEL_TOLERANCE: f64 = 6.0;
|
||||||
@@ -38,58 +37,6 @@ pub const STATIC_DESIGN_SKIP_TAGS: &[&str] = &[
|
|||||||
"head", "title", "meta", "link", "style", "script", "noscript", "template", "source",
|
"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()
|
|
||||||
);
|
|
||||||
// A design document's prose, cut where one statement stops and the next
|
|
||||||
// starts: punctuation, a line break, and the phrases that turn a sentence
|
|
||||||
// around. `instead of` / `rather than` open a clause about what the document
|
|
||||||
// is steering *away* from; the reversals (`outside`, `except`, ...) open one
|
|
||||||
// about what it is steering *toward*, which is what lets "no ALL CAPS outside
|
|
||||||
// the `.eyebrow` class" declare `.eyebrow`.
|
|
||||||
re!(
|
|
||||||
DESIGN_CLAUSE_SPLIT,
|
|
||||||
r"(?i)[.!?;:,()\[\]\n]|\u{2014}|\u{2013}|\binstead of\b|\brather than\b|\bas opposed to\b|\boutside\b|\bexcept\b|\bother than\b|\bunless\b|\bbesides\b|\bapart from\b|\bbeyond\b".to_string()
|
|
||||||
);
|
|
||||||
re!(
|
|
||||||
DESIGN_NEGATING_BOUNDARY,
|
|
||||||
r"(?i)^(?:instead of|rather than|as opposed to)$".to_string()
|
|
||||||
);
|
|
||||||
// A directive: it condemns what comes after it, and nothing before it.
|
|
||||||
// "Use `.kicker` and never `.tagline`" sanctions the first and forbids the
|
|
||||||
// second, and a rule that read the whole clause would lose both.
|
|
||||||
re!(
|
|
||||||
DESIGN_DIRECTIVE_NEGATIVE,
|
|
||||||
r"(?i)\b(?:no|not|never|nor|none|avoid\w*|don'?t|do not|doesn'?t|does not|drop|remove\w*|stop|skip)\b".to_string()
|
|
||||||
);
|
|
||||||
// A state: it describes whatever its clause is about, wherever in the clause
|
|
||||||
// the name sits. "`.card-old` is deprecated" names the class first.
|
|
||||||
re!(
|
|
||||||
DESIGN_STATE_NEGATIVE,
|
|
||||||
r"(?i)\b(?:deprecat\w*|obsolete|legacy|forbidden|banned|disallow\w*|discourag\w*|retired|unsupported|wrong|bad|anti-?pattern\w*|no longer|not allowed|not permitted|not supported|not used)\b".to_string()
|
|
||||||
);
|
|
||||||
// Headings that introduce a section of counter-examples.
|
|
||||||
re!(
|
|
||||||
DESIGN_NEGATIVE_HEADING,
|
|
||||||
r"(?i)\b(?:don'?ts?|do not|avoid|never|not to|anti-?patterns?|deprecat\w*|forbidden|banned|legacy|obsolete|retired|unsupported|removed|discourag\w*|disallow\w*|mistakes?|wrong|bad)\b".to_string()
|
|
||||||
);
|
|
||||||
// A heading that names both sides — "Do and Don't", "Dos and Don'ts",
|
|
||||||
// "Do / Do not" — introduces a section of both, so the subsections under it
|
|
||||||
// say which is which and the heading itself condemns nothing. The two sides
|
|
||||||
// have to be *joined* by something that pairs them: "Don't do this" and "What
|
|
||||||
// we don't do" also put a `do` beside a `don't`, and they mean only the one
|
|
||||||
// thing.
|
|
||||||
re!(DESIGN_BOTH_SIDES_HEADING, {
|
|
||||||
// One joiner or several: "Do's, and Don'ts" and "Do and/or Don't" pair
|
|
||||||
// the two sides with a comma plus a conjunction and with a conjunction
|
|
||||||
// plus a slash.
|
|
||||||
let joiner = format!(r"(?:{WS}*(?:and|or|&|/|\||\+|,|vs\.?|versus)){{1,4}}{WS}*", WS = WS);
|
|
||||||
let affirmative = r"\bdo'?s?\b";
|
|
||||||
let negative = r"\b(?:do ?n[o']?ts?|do not)\b";
|
|
||||||
format!("(?i)(?:{affirmative}{joiner}{negative}|{negative}{joiner}{affirmative})")
|
|
||||||
});
|
|
||||||
re!(
|
re!(
|
||||||
FONT_SIZE_LITERAL_RE,
|
FONT_SIZE_LITERAL_RE,
|
||||||
format!("^-?[{D}.]+(?:px|rem)$", D = "0-9")
|
format!("^-?[{D}.]+(?:px|rem)$", D = "0-9")
|
||||||
@@ -217,129 +164,6 @@ re!(LEADING_WS_RE, format!("^{WS}*"));
|
|||||||
|
|
||||||
/// JS: design-system.mjs#parseFrontmatter. `None` when there is no
|
/// JS: design-system.mjs#parseFrontmatter. `None` when there is no
|
||||||
/// `---` block; otherwise the parsed object (possibly empty).
|
/// `---` 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.
|
|
||||||
///
|
|
||||||
/// Not every class a document names is a class it sanctions. A document also
|
|
||||||
/// writes down what it does *not* want — "Avoid `.eyebrow`", "`.card-old` is
|
|
||||||
/// deprecated", a "Don't" section of counter-examples — and exempting those
|
|
||||||
/// would silence exactly the misuse the document was written to forbid
|
|
||||||
/// (REN-406 follow-up). So each occurrence is read in the document's own
|
|
||||||
/// structure: the heading chain above it, and the clause it sits in. A class
|
|
||||||
/// condemned anywhere in the document is declared nowhere — a document that
|
|
||||||
/// says "deprecated" about a class has said enough.
|
|
||||||
pub fn declared_component_selectors(design_md: &str) -> Vec<String> {
|
|
||||||
// The prose is cut into clauses on punctuation, and a code span is full
|
|
||||||
// of punctuation that is not prose: `.btn-primary` carries a full stop,
|
|
||||||
// `rgb(0, 0, 0)` a pair of brackets and a comma. Masking every span to a
|
|
||||||
// run of letters of the same byte length keeps every offset where it was.
|
|
||||||
let masked = mask_code_spans(design_md);
|
|
||||||
let mut declared: Vec<String> = Vec::new();
|
|
||||||
let mut condemned: Vec<String> = Vec::new();
|
|
||||||
for cap in DESIGN_BACKTICKED.captures_iter(design_md) {
|
|
||||||
let span = cap.get(0).expect("whole match");
|
|
||||||
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();
|
|
||||||
let list = if design_condemns(&masked, span.start(), span.end()) {
|
|
||||||
&mut condemned
|
|
||||||
} else {
|
|
||||||
&mut declared
|
|
||||||
};
|
|
||||||
if !list.contains(&token) {
|
|
||||||
list.push(token);
|
|
||||||
}
|
|
||||||
if declared.len() + condemned.len() >= 128 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
declared.retain(|t| !condemned.contains(t));
|
|
||||||
declared.truncate(64);
|
|
||||||
declared
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every code span replaced by a run of `a` of the same byte length, so the
|
|
||||||
/// prose around it can be scanned for sentence structure without a class
|
|
||||||
/// selector's own dot ending a sentence. ASCII in, same length out, so byte
|
|
||||||
/// offsets into the original still address the same characters.
|
|
||||||
fn mask_code_spans(design_md: &str) -> String {
|
|
||||||
let mut masked = design_md.as_bytes().to_vec();
|
|
||||||
for span in DESIGN_BACKTICKED.find_iter(design_md) {
|
|
||||||
for b in &mut masked[span.start()..span.end()] {
|
|
||||||
*b = b'a';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String::from_utf8(masked).unwrap_or_else(|_| design_md.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the document speaks against the code span at `[start, end)`:
|
|
||||||
/// either it sits under a heading that introduces counter-examples, or its
|
|
||||||
/// own clause carries a word that condemns what the clause names.
|
|
||||||
fn design_condemns(masked: &str, start: usize, end: usize) -> bool {
|
|
||||||
under_negative_heading(masked, start) || clause_condemns(masked, start, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The heading chain above `start`, by level: a subsection of a "Don't"
|
|
||||||
/// section is still inside it.
|
|
||||||
fn under_negative_heading(masked: &str, start: usize) -> bool {
|
|
||||||
let mut chain: Vec<(usize, &str)> = Vec::new();
|
|
||||||
for line in masked[..start].split('\n') {
|
|
||||||
let line = line.trim_start();
|
|
||||||
let level = line.bytes().take_while(|b| *b == b'#').count();
|
|
||||||
if level == 0 || level > 6 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let text = &line[level..];
|
|
||||||
if !text.is_empty() && !text.starts_with(' ') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
chain.retain(|(l, _)| *l < level);
|
|
||||||
chain.push((level, text));
|
|
||||||
}
|
|
||||||
chain.iter().any(|(_, text)| {
|
|
||||||
// "Dos and Don'ts" heads a section of both, and the subsections
|
|
||||||
// under it are what say which is which.
|
|
||||||
DESIGN_NEGATIVE_HEADING.is_match(text) && !DESIGN_BOTH_SIDES_HEADING.is_match(text)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The clause the span sits in — from the boundary before it to the boundary
|
|
||||||
/// after it — and whether that clause condemns what it names.
|
|
||||||
///
|
|
||||||
/// Where the negative word sits decides what it governs. A *state* ("`.x` is
|
|
||||||
/// deprecated") describes whatever the clause is about, so it condemns the
|
|
||||||
/// class wherever in the clause the name appears. A *directive* ("never use
|
|
||||||
/// `.x`") condemns what follows it and nothing before it, which is what keeps
|
|
||||||
/// "Use `.kicker` and never `.tagline`" from losing `.kicker`. A clause
|
|
||||||
/// opened by "instead of" or "rather than" is condemned by the boundary
|
|
||||||
/// itself, whatever words follow.
|
|
||||||
fn clause_condemns(masked: &str, start: usize, end: usize) -> bool {
|
|
||||||
let mut clause_start = 0usize;
|
|
||||||
let mut opened_by_negation = false;
|
|
||||||
for boundary in DESIGN_CLAUSE_SPLIT.find_iter(&masked[..start]) {
|
|
||||||
clause_start = boundary.end();
|
|
||||||
opened_by_negation = DESIGN_NEGATING_BOUNDARY.is_match(js::trim(boundary.as_str()));
|
|
||||||
}
|
|
||||||
if opened_by_negation {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
let clause_end = DESIGN_CLAUSE_SPLIT
|
|
||||||
.find(&masked[end..])
|
|
||||||
.map(|m| end + m.start())
|
|
||||||
.unwrap_or(masked.len());
|
|
||||||
DESIGN_STATE_NEGATIVE.is_match(&masked[clause_start..clause_end])
|
|
||||||
|| DESIGN_DIRECTIVE_NEGATIVE.is_match(&masked[clause_start..start])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_frontmatter(md: &str) -> Option<Map<String, Value>> {
|
pub fn parse_frontmatter(md: &str) -> Option<Map<String, Value>> {
|
||||||
let lines: Vec<&str> = CRLF_RE.split(md).collect();
|
let lines: Vec<&str> = CRLF_RE.split(md).collect();
|
||||||
if js::trim(lines.first().copied().unwrap_or("")) != "---" {
|
if js::trim(lines.first().copied().unwrap_or("")) != "---" {
|
||||||
@@ -710,10 +534,6 @@ pub struct AllowedFontSize {
|
|||||||
#[derive(Debug, Clone, PartialEq, Default)]
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
pub struct DesignSystem {
|
pub struct DesignSystem {
|
||||||
pub present: bool,
|
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 source_path: Option<String>,
|
||||||
pub sidecar_path: Option<String>,
|
pub sidecar_path: Option<String>,
|
||||||
pub md_newer_than_json: bool,
|
pub md_newer_than_json: bool,
|
||||||
@@ -1057,15 +877,13 @@ pub fn load_design_system_for_cwd(cwd: &str) -> Option<DesignSystem> {
|
|||||||
let sidecar = sidecar_path.as_deref().and_then(read_json);
|
let sidecar = sidecar_path.as_deref().and_then(read_json);
|
||||||
let sidecar_stat = sidecar_path.as_deref().and_then(mtime_ms);
|
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);
|
let md_newer = matches!((md_stat, sidecar_stat), (Some(m), Some(s)) if m > s + 1000.0);
|
||||||
let mut ds = normalize_design_system(
|
Some(normalize_design_system(
|
||||||
Some(&frontmatter),
|
Some(&frontmatter),
|
||||||
sidecar.as_ref(),
|
sidecar.as_ref(),
|
||||||
Some(&md.path),
|
Some(&md.path),
|
||||||
sidecar_path.as_deref(),
|
sidecar_path.as_deref(),
|
||||||
md_newer,
|
md_newer,
|
||||||
);
|
))
|
||||||
ds.declared_selectors = declared_component_selectors(&text);
|
|
||||||
Some(ds)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JS `designSystemStartDir(targetPath, cwd)`.
|
/// JS `designSystemStartDir(targetPath, cwd)`.
|
||||||
@@ -2116,81 +1934,6 @@ fn finding_ignore_or_value_only(item: &Finding) -> String {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A document also writes down what it does not want, and exempting those
|
|
||||||
/// classes silences exactly the misuse the document forbids.
|
|
||||||
#[test]
|
|
||||||
fn a_negative_example_declares_nothing() {
|
|
||||||
// Condemned in its own clause, by several spellings.
|
|
||||||
let md = "- Avoid `.eyebrow`; it shouts.\n- `.card-old` is deprecated.\n - Use `.kicker` instead of `.eyebrow-legacy`.\n - Every section label is a `.kicker`, not a `.tagline`.\n";
|
|
||||||
assert_eq!(declared_component_selectors(md), vec![".kicker"]);
|
|
||||||
|
|
||||||
// A section of counter-examples, and its subsections with it.
|
|
||||||
let md = "## Components\n\n- The label above a heading is `.kicker`.\n\n ## Don't\n\n### Labels\n\n- `.eyebrow` anywhere.\n";
|
|
||||||
assert_eq!(declared_component_selectors(md), vec![".kicker"]);
|
|
||||||
|
|
||||||
// Condemned once is condemned: a class the document calls deprecated
|
|
||||||
// is not rescued by a list that also names it.
|
|
||||||
let md = "- Buttons: `.btn-primary`, `.btn-old`.\n- `.btn-old` is deprecated.\n";
|
|
||||||
assert_eq!(declared_component_selectors(md), vec![".btn-primary"]);
|
|
||||||
|
|
||||||
// A directive governs what follows it, not the whole clause: a
|
|
||||||
// sentence that sanctions one class and forbids another says both.
|
|
||||||
let md = "- Use `.kicker` and never `.tagline`.\n\
|
|
||||||
- Every label is a `.kicker`, not a `.tagline`.\n";
|
|
||||||
assert_eq!(declared_component_selectors(md), vec![".kicker"]);
|
|
||||||
|
|
||||||
// Headings that retire a set, in the words a document uses for it.
|
|
||||||
let md = "## Retired components\n\n- `.tagline`\n\n\
|
|
||||||
## Unsupported patterns\n\n- `.marquee-row`\n\n\
|
|
||||||
## Components\n\n- `.kicker`\n";
|
|
||||||
assert_eq!(declared_component_selectors(md), vec![".kicker"]);
|
|
||||||
|
|
||||||
// A heading that names both sides heads a section of both, and its
|
|
||||||
// subsections are what say which is which.
|
|
||||||
let md = "## Dos and Don'ts\n\n### Do\n\n- Label a section with `.kicker`.\n\n\
|
|
||||||
### Don't\n\n- Reach for `.eyebrow`.\n";
|
|
||||||
assert_eq!(declared_component_selectors(md), vec![".kicker"]);
|
|
||||||
|
|
||||||
// Compound separators pair the two sides just as well.
|
|
||||||
for heading in ["Do's, and Don'ts", "Do and/or Don't", "Don'ts / Dos"] {
|
|
||||||
let md = format!("## {heading}\n\n### Do\n\n- Use `.kicker`.\n");
|
|
||||||
assert_eq!(declared_component_selectors(&md), vec![".kicker"], "{heading}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// A heading that only puts a `do` beside a `don't` is not a section
|
|
||||||
// of both, and still condemns what it names.
|
|
||||||
let md = "## Don't do this\n\n- `.eyebrow` above a heading.\n\n\
|
|
||||||
## What we don't do\n\n- `.tagline`\n\n\
|
|
||||||
## Components\n\n- `.kicker`\n";
|
|
||||||
assert_eq!(declared_component_selectors(md), vec![".kicker"]);
|
|
||||||
|
|
||||||
// What the negative words govern is their own clause. "No ALL CAPS
|
|
||||||
// outside the `.eyebrow` class" declares `.eyebrow`, and "four kinds
|
|
||||||
// and no more" declares all four.
|
|
||||||
let md = "- No Title Case, no ALL CAPS outside the `.eyebrow` class.\n - **Button.** Four kinds and no more: `.btn-primary` (one per\n surface), `.btn-secondary`, `.btn-ghost`, `.btn-danger`.\n";
|
|
||||||
assert_eq!(
|
|
||||||
declared_component_selectors(md),
|
|
||||||
vec![".eyebrow", ".btn-primary", ".btn-secondary", ".btn-ghost", ".btn-danger"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── #570 monorepo DESIGN.md inheritance ─────────────────────────────────
|
// ── #570 monorepo DESIGN.md inheritance ─────────────────────────────────
|
||||||
// Mirrors tests/detect-cli-design-monorepo.test.mjs (public repo main,
|
// Mirrors tests/detect-cli-design-monorepo.test.mjs (public repo main,
|
||||||
// 47e41195 + 5d7c1cce + e975bec4 + 91f2c7b4) at the findDesignRoot level.
|
// 47e41195 + 5d7c1cce + e975bec4 + 91f2c7b4) at the findDesignRoot level.
|
||||||
@@ -2526,5 +2269,3 @@ mod tests {
|
|||||||
assert_eq!(js_string(&parse_scalar("007")), "7");
|
assert_eq!(js_string(&parse_scalar("007")), "7");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -168,98 +168,6 @@ pub trait Dom {
|
|||||||
/// of every non-blank direct text node (rects narrower/shorter than 1px
|
/// of every non-blank direct text node (rects narrower/shorter than 1px
|
||||||
/// dropped); `None` when there is none.
|
/// dropped); `None` when there is none.
|
||||||
fn direct_text_rect(&self, el: ElId) -> Option<Rect>;
|
fn direct_text_rect(&self, el: ElId) -> Option<Rect>;
|
||||||
/// The rows the element's rendered text occupies: one rect per line box,
|
|
||||||
/// top to bottom. `None` when this DOM cannot say where the lines are.
|
|
||||||
///
|
|
||||||
/// This is how a rule reads a line rather than the box that holds it, and
|
|
||||||
/// a line here is the whole line the reader sees. `getClientRects()` on a
|
|
||||||
/// text node gives a rect per line box, but a line box is routinely split
|
|
||||||
/// across several text nodes — an inline `<strong>` in the middle of a
|
|
||||||
/// sentence, a framework marker, an HTML comment — so the rects are
|
|
||||||
/// collected over the element's whole rendered text (descendants
|
|
||||||
/// included, which is the text `text_content` counts) and the ones that
|
|
||||||
/// share a row are merged back into the one line they came from. Without
|
|
||||||
/// that merge each fragment is a "line" and one wrapped sentence is
|
|
||||||
/// charged as several.
|
|
||||||
///
|
|
||||||
/// `None` is the honest answer from a DOM that only kept the union of
|
|
||||||
/// those rects (a page snapshot captured before the lines were recorded).
|
|
||||||
/// A caller stands down there; it never divides a union by a line height
|
|
||||||
/// and calls the pieces lines, because the union of a long first line and
|
|
||||||
/// a short tail says nothing about either.
|
|
||||||
fn text_line_rects(&self, _el: ElId) -> Option<Vec<Rect>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The rects of one element's rendered text, merged into the lines they
|
|
||||||
/// rendered on.
|
|
||||||
///
|
|
||||||
/// Two rects are the same line when they share a row *and* run on from each
|
|
||||||
/// other. Sharing a row is a vertical band overlapping the band the row
|
|
||||||
/// started with by more than half the shorter height — that is what makes an
|
|
||||||
/// inline `<strong>`, a superscript and the text around them one line.
|
|
||||||
/// Running on is a horizontal gap no wider than the row's own line box: the
|
|
||||||
/// fragments of a wrapped line are contiguous, while two columns of text that
|
|
||||||
/// happen to sit on the same rows are separated by a gutter, and unioning
|
|
||||||
/// those would invent a page-wide line neither column ever rendered. An
|
|
||||||
/// inline image wider than the leading splits its line in two by the same
|
|
||||||
/// test, which understates a line rather than overstating it — the direction
|
|
||||||
/// this rule should err in.
|
|
||||||
///
|
|
||||||
/// Rects arrive in whatever order a DOM walked the text (an element's own
|
|
||||||
/// text and its descendants' are interleaved on the page but not in the
|
|
||||||
/// walk), so they are sorted top then left first and each rect joins the
|
|
||||||
/// newest row still level with it.
|
|
||||||
pub fn merge_text_rects_into_lines(rects: Vec<Rect>) -> Vec<Rect> {
|
|
||||||
let mut rects: Vec<Rect> = rects
|
|
||||||
.into_iter()
|
|
||||||
.filter(|r| r.width > 0.0 && r.height > 0.0 && r.all_finite())
|
|
||||||
.collect();
|
|
||||||
rects.sort_by(|a, b| {
|
|
||||||
a.top
|
|
||||||
.partial_cmp(&b.top)
|
|
||||||
.unwrap_or(std::cmp::Ordering::Equal)
|
|
||||||
.then(a.left.partial_cmp(&b.left).unwrap_or(std::cmp::Ordering::Equal))
|
|
||||||
});
|
|
||||||
let mut lines: Vec<Rect> = Vec::new();
|
|
||||||
// The band of the rect each row started with. Membership is tested
|
|
||||||
// against that rather than against the row as it grows, so an
|
|
||||||
// inline-block taller than the leading does not swallow the line beneath.
|
|
||||||
let mut bands: Vec<(f64, f64)> = Vec::new();
|
|
||||||
for r in rects {
|
|
||||||
let mut joined = false;
|
|
||||||
for i in (0..lines.len()).rev() {
|
|
||||||
let (band_top, band_bottom) = bands[i];
|
|
||||||
// Sorted by top: once a row sits entirely above this rect, every
|
|
||||||
// row before it does too.
|
|
||||||
if band_bottom <= r.top {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let overlap = band_bottom.min(r.bottom) - band_top.max(r.top);
|
|
||||||
let shorter = (band_bottom - band_top).min(r.height);
|
|
||||||
if shorter <= 0.0 || overlap <= shorter / 2.0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let line = lines[i];
|
|
||||||
let gap = (r.left - line.right).max(line.left - r.right);
|
|
||||||
if gap > band_bottom - band_top {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let left = line.left.min(r.left);
|
|
||||||
let top = line.top.min(r.top);
|
|
||||||
let right = line.right.max(r.right);
|
|
||||||
let bottom = line.bottom.max(r.bottom);
|
|
||||||
lines[i] = Rect::from_xywh(left, top, right - left, bottom - top);
|
|
||||||
joined = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if !joined {
|
|
||||||
bands.push((r.top, r.bottom));
|
|
||||||
lines.push(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── shared helpers over the trait ─────────────────────────────────────────
|
// ── shared helpers over the trait ─────────────────────────────────────────
|
||||||
|
|||||||
@@ -35,9 +35,6 @@ pub struct FakeEl {
|
|||||||
pub hidden: bool,
|
pub hidden: bool,
|
||||||
pub check_visibility: Option<bool>,
|
pub check_visibility: Option<bool>,
|
||||||
pub direct_text_rect: Option<Rect>,
|
pub direct_text_rect: Option<Rect>,
|
||||||
/// The rects of the element's rendered text. `None` is a DOM that cannot
|
|
||||||
/// say where the lines are, the way a snapshot without them cannot.
|
|
||||||
pub text_line_rects: Option<Vec<Rect>>,
|
|
||||||
/// Selectors (exact strings) this element matches beyond `*` and its tag.
|
/// Selectors (exact strings) this element matches beyond `*` and its tag.
|
||||||
pub selectors: Vec<String>,
|
pub selectors: Vec<String>,
|
||||||
/// `id` IDL property override (`None` = "not a string", falls back to attr).
|
/// `id` IDL property override (`None` = "not a string", falls back to attr).
|
||||||
@@ -161,31 +158,6 @@ impl FakeDom {
|
|||||||
self.el_mut(id).rect = Rect::from_xywh(x, y, w, h);
|
self.el_mut(id).rect = Rect::from_xywh(x, y, w, h);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
/// The union rect of `id`'s direct text, and nothing about its lines:
|
|
||||||
/// a DOM that measured the text once, the way a page snapshot captured
|
|
||||||
/// before the lines were recorded did.
|
|
||||||
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 rendered text. The union is derived from them, so a
|
|
||||||
/// test declares what rendered and nothing else. Fragments that share a
|
|
||||||
/// row merge into one line on the way out, exactly as a live page's do.
|
|
||||||
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).text_line_rects = Some(rects);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub fn add_text(&mut self, id: ElId, text: &str) -> &mut Self {
|
pub fn add_text(&mut self, id: ElId, text: &str) -> &mut Self {
|
||||||
self.el_mut(id)
|
self.el_mut(id)
|
||||||
.child_nodes
|
.child_nodes
|
||||||
@@ -531,8 +503,4 @@ impl Dom for FakeDom {
|
|||||||
fn direct_text_rect(&self, el: ElId) -> Option<Rect> {
|
fn direct_text_rect(&self, el: ElId) -> Option<Rect> {
|
||||||
self.els[el as usize].direct_text_rect
|
self.els[el as usize].direct_text_rect
|
||||||
}
|
}
|
||||||
fn text_line_rects(&self, el: ElId) -> Option<Vec<Rect>> {
|
|
||||||
let rects = self.els[el as usize].text_line_rects.clone()?;
|
|
||||||
Some(super::dom::merge_text_rects_into_lines(rects))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -273,16 +273,6 @@ pub struct SnapNode {
|
|||||||
/// `getDirectTextRect` as `[x, y, width, height]`.
|
/// `getDirectTextRect` as `[x, y, width, height]`.
|
||||||
#[serde(rename = "d", default)]
|
#[serde(rename = "d", default)]
|
||||||
pub direct_text_rect: Option<[f64; 4]>,
|
pub direct_text_rect: Option<[f64; 4]>,
|
||||||
/// The client rects of the element's OWN direct text, unmerged, each
|
|
||||||
/// `[x, y, width, height]` — `direct_text_rect` before it was merged into
|
|
||||||
/// a union. Own and not the subtree's: an element's rendered lines are
|
|
||||||
/// assembled from these across its descendants
|
|
||||||
/// ([`SnapshotDom::text_line_rects`]), so a line that rendered is
|
|
||||||
/// recorded once rather than once per ancestor. Empty is "no rendered
|
|
||||||
/// text", not "not recorded" — [`Snapshot::text_lines`] is what says a
|
|
||||||
/// capture recorded them at all.
|
|
||||||
#[serde(rename = "dl", default, skip_serializing_if = "Vec::is_empty")]
|
|
||||||
pub text_rects: Vec<[f64; 4]>,
|
|
||||||
#[serde(rename = "e", default)]
|
#[serde(rename = "e", default)]
|
||||||
pub content_editable: bool,
|
pub content_editable: bool,
|
||||||
#[serde(rename = "h", default)]
|
#[serde(rename = "h", default)]
|
||||||
@@ -384,12 +374,6 @@ pub struct Snapshot {
|
|||||||
pub body: Option<u32>,
|
pub body: Option<u32>,
|
||||||
#[serde(rename = "bodyInnerText", default)]
|
#[serde(rename = "bodyInnerText", default)]
|
||||||
pub body_inner_text: Option<String>,
|
pub body_inner_text: Option<String>,
|
||||||
/// Whether this capture recorded the rects of each element's rendered
|
|
||||||
/// text (`SnapNode::text_rects`). False in captures older than that
|
|
||||||
/// change, where the union in `d` is all there is: a rule that needs the
|
|
||||||
/// lines stands down rather than inventing them from the union.
|
|
||||||
#[serde(rename = "textLines", default)]
|
|
||||||
pub text_lines: bool,
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub hits: Vec<HitTest>,
|
pub hits: Vec<HitTest>,
|
||||||
/// Derived on load: column index per style property name.
|
/// Derived on load: column index per style property name.
|
||||||
@@ -927,29 +911,6 @@ impl Dom for SnapshotDom {
|
|||||||
fn direct_text_rect(&self, el: ElId) -> Option<Rect> {
|
fn direct_text_rect(&self, el: ElId) -> Option<Rect> {
|
||||||
self.snap.node(el).direct_text_rect.as_ref().map(rect4)
|
self.snap.node(el).direct_text_rect.as_ref().map(rect4)
|
||||||
}
|
}
|
||||||
/// Assembled from the per-element rects the capture recorded: the
|
|
||||||
/// element's own, then each descendant's, in document order, merged into
|
|
||||||
/// the lines they rendered as.
|
|
||||||
///
|
|
||||||
/// `None` on a capture that never recorded them. The union in `d` is not
|
|
||||||
/// an answer here: a paragraph with one long line and a short tail has
|
|
||||||
/// the same union as one with two even lines, so dividing it by a line
|
|
||||||
/// height would invent widths that nothing on the page rendered.
|
|
||||||
fn text_line_rects(&self, el: ElId) -> Option<Vec<Rect>> {
|
|
||||||
if !self.snap.text_lines {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
fn walk(snap: &Snapshot, el: ElId, out: &mut Vec<Rect>) {
|
|
||||||
let node = snap.node(el);
|
|
||||||
out.extend(node.text_rects.iter().map(rect4));
|
|
||||||
for child in &node.children {
|
|
||||||
walk(snap, *child, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut rects = Vec::new();
|
|
||||||
walk(&self.snap, el, &mut rects);
|
|
||||||
Some(super::dom::merge_text_rects_into_lines(rects))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `undefined` read into a wasm f64 is NaN (`offsetWidth` on an SVG
|
/// `undefined` read into a wasm f64 is NaN (`offsetWidth` on an SVG
|
||||||
@@ -1126,46 +1087,6 @@ mod tests {
|
|||||||
assert!(!d.has_needs());
|
assert!(!d.has_needs());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A capture that recorded the text rects hands over the lines; one that
|
|
||||||
/// did not says so, and the caller stands down rather than reading lines
|
|
||||||
/// out of a union.
|
|
||||||
#[test]
|
|
||||||
fn text_lines_come_from_the_capture_or_not_at_all() {
|
|
||||||
const OLD: &str = r#"{
|
|
||||||
"v": 1, "documentElement": 1, "body": 2,
|
|
||||||
"els": [
|
|
||||||
{"t":"HTML","c":[2]},
|
|
||||||
{"t":"BODY","p":1,"c":[3]},
|
|
||||||
{"t":"P","p":2,"c":["hello"],"d":[0,100,1000,43]}
|
|
||||||
]
|
|
||||||
}"#;
|
|
||||||
assert_eq!(snap(OLD).text_line_rects(3), None);
|
|
||||||
|
|
||||||
// Each element records only its own text rects; an element's lines
|
|
||||||
// are assembled from its own plus its descendants'. Here the `<p>`
|
|
||||||
// wraps once and an inline `<b>` sits in the middle of its first
|
|
||||||
// line, so the first line arrives in three pieces from two elements.
|
|
||||||
const NEW: &str = r#"{
|
|
||||||
"v": 1, "textLines": true, "documentElement": 1, "body": 2,
|
|
||||||
"els": [
|
|
||||||
{"t":"HTML","c":[2]},
|
|
||||||
{"t":"BODY","p":1,"c":[3]},
|
|
||||||
{"t":"P","p":2,"c":["hello ",4," there"],"d":[0,100,1000,43],
|
|
||||||
"dl":[[0,100,600,19],[700,100,300,19],[0,124,120,19]]},
|
|
||||||
{"t":"B","p":3,"c":["bold"],"d":[600,100,100,19],"dl":[[600,100,100,19]]}
|
|
||||||
]
|
|
||||||
}"#;
|
|
||||||
let lines = snap(NEW).text_line_rects(3).expect("lines");
|
|
||||||
assert_eq!(lines.len(), 2);
|
|
||||||
assert_eq!((lines[0].left, lines[0].width), (0.0, 1000.0));
|
|
||||||
assert_eq!((lines[1].top, lines[1].width), (124.0, 120.0));
|
|
||||||
// The `<b>` on its own is the one line it rendered.
|
|
||||||
assert_eq!(snap(NEW).text_line_rects(4).expect("lines").len(), 1);
|
|
||||||
// An element the capture found no rendered text under is not
|
|
||||||
// "unknown" — it is an element with no lines.
|
|
||||||
assert_eq!(snap(NEW).text_line_rects(1).map(|l| l.len()), Some(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn css_escape_matches_spec() {
|
fn css_escape_matches_spec() {
|
||||||
assert_eq!(css_escape("foo"), "foo");
|
assert_eq!(css_escape("foo"), "foo");
|
||||||
|
|||||||
@@ -320,40 +320,6 @@ 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
|
(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)`.
|
/// JS `getHue(c)`.
|
||||||
pub fn get_hue(c: Option<&Rgba>) -> f64 {
|
pub fn get_hue(c: Option<&Rgba>) -> f64 {
|
||||||
let Some(c) = c else { return 0.0 };
|
let Some(c) = c else { return 0.0 };
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
|||||||
scopes: None,
|
scopes: None,
|
||||||
severity: None,
|
severity: None,
|
||||||
name: "AI color palette",
|
name: "AI color 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.",
|
description: "Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.",
|
||||||
skill_section: Some("Color & Contrast"),
|
skill_section: Some("Color & Contrast"),
|
||||||
skill_guideline: Some("AI color palette"),
|
skill_guideline: Some("AI color palette"),
|
||||||
},
|
},
|
||||||
@@ -416,7 +416,7 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
|||||||
scopes: Some(&["type", "layout"]),
|
scopes: Some(&["type", "layout"]),
|
||||||
severity: None,
|
severity: None,
|
||||||
name: "Line length too long",
|
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, 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.",
|
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.",
|
||||||
skill_section: Some("Layout & Space"),
|
skill_section: Some("Layout & Space"),
|
||||||
skill_guideline: Some("wrap beyond ~80 characters"),
|
skill_guideline: Some("wrap beyond ~80 characters"),
|
||||||
},
|
},
|
||||||
@@ -426,7 +426,7 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
|||||||
scopes: Some(&["layout"]),
|
scopes: Some(&["layout"]),
|
||||||
severity: None,
|
severity: None,
|
||||||
name: "Cramped padding",
|
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 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.",
|
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.",
|
||||||
skill_section: Some("Layout & Space"),
|
skill_section: Some("Layout & Space"),
|
||||||
skill_guideline: Some("inside bordered or colored containers"),
|
skill_guideline: Some("inside bordered or colored containers"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"id": "ai-color-palette",
|
"id": "ai-color-palette",
|
||||||
"name": "AI color palette",
|
"name": "AI color palette",
|
||||||
"category": "slop",
|
"category": "slop",
|
||||||
"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."
|
"description": "Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cream-palette",
|
"id": "cream-palette",
|
||||||
@@ -231,13 +231,13 @@
|
|||||||
"id": "line-length",
|
"id": "line-length",
|
||||||
"name": "Line length too long",
|
"name": "Line length too long",
|
||||||
"category": "quality",
|
"category": "quality",
|
||||||
"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."
|
"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."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cramped-padding",
|
"id": "cramped-padding",
|
||||||
"name": "Cramped padding",
|
"name": "Cramped padding",
|
||||||
"category": "quality",
|
"category": "quality",
|
||||||
"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."
|
"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."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "body-text-viewport-edge",
|
"id": "body-text-viewport-edge",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,9 +5,7 @@
|
|||||||
//! (`u32::MAX`, or a first element of `u32::MAX` in arrays) because the probe
|
//! (`u32::MAX`, or a first element of `u32::MAX` in arrays) because the probe
|
||||||
//! catches the DOM's SyntaxError and cannot throw across the boundary.
|
//! catches the DOM's SyntaxError and cannot throw across the boundary.
|
||||||
|
|
||||||
use impeccable_core::browser::dom::{
|
use impeccable_core::browser::dom::{Dom, ElId, KeyframeFrame, Rect, SelectorError};
|
||||||
merge_text_rects_into_lines, Dom, ElId, KeyframeFrame, Rect, SelectorError,
|
|
||||||
};
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
@@ -60,7 +58,6 @@ extern "C" {
|
|||||||
fn offset_height(el: u32) -> f64;
|
fn offset_height(el: u32) -> f64;
|
||||||
fn check_visibility(el: u32) -> i32;
|
fn check_visibility(el: u32) -> i32;
|
||||||
fn direct_text_rect(el: u32) -> Vec<f64>;
|
fn direct_text_rect(el: u32) -> Vec<f64>;
|
||||||
fn text_rects(el: u32) -> Vec<f64>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn opt(id: u32) -> Option<ElId> {
|
fn opt(id: u32) -> Option<ElId> {
|
||||||
@@ -343,14 +340,4 @@ impl Dom for JsDom {
|
|||||||
Some(to_rect(&v))
|
Some(to_rect(&v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// A live page can always say where its lines are. The probe flattens the
|
|
||||||
/// rects of every text node under the element into one array of eights, in
|
|
||||||
/// the order `rect` uses (a tail shorter than a rect is ignored), and the
|
|
||||||
/// fragments that share a row are merged back into the line they came
|
|
||||||
/// from here.
|
|
||||||
fn text_line_rects(&self, el: ElId) -> Option<Vec<Rect>> {
|
|
||||||
Some(merge_text_rects_into_lines(
|
|
||||||
text_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 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",
|
"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",
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"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. 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",
|
"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",
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"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 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",
|
"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",
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"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 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",
|
"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",
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"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. 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",
|
"stdout": "[\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"80x80px icon tile above h3 \\\"Lightning Fast\\\"\"\n },\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"64x64px icon tile above h3 \\\"Secure Storage\\\"\"\n },\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"72x72px icon tile above h3 \\\"Easy Setup\\\"\"\n },\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"96x96px icon tile above h3 \\\"Powerful Analytics\\\"\"\n },\n {\n \"antipattern\": \"icon-tile-stack\",\n \"name\": \"Icon tile stacked above heading\",\n \"description\": \"A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"48x48px icon tile above h3 \\\"Emoji Inline Icon\\\"\"\n },\n {\n \"antipattern\": \"ai-color-palette\",\n \"name\": \"AI color palette\",\n \"description\": \"Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"Purple/violet accent colors detected\"\n },\n {\n \"antipattern\": \"marketing-buzzword\",\n \"name\": \"Marketing buzzword\",\n \"description\": \"Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\",\n \"line\": 0,\n \"snippet\": \"1 buzzword phrase: \\\"ure Storage Enterprise-grade security fo\\\"\"\n }\n]\n",
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"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. 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",
|
"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",
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"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. 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",
|
"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",
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"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. 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",
|
"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",
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"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. 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",
|
"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",
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"stdout": "",
|
"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 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",
|
"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",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
"files": {}
|
"files": {}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"stdout": "",
|
"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. 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",
|
"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",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
"files": {}
|
"files": {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"stdout": "",
|
"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 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",
|
"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",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
"files": {}
|
"files": {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"stdout": "",
|
"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 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",
|
"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",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
"files": {}
|
"files": {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"stdout": "",
|
"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. 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",
|
"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",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
"files": {}
|
"files": {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"stdout": "",
|
"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 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",
|
"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",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
"files": {}
|
"files": {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"stdout": "",
|
"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. 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",
|
"stderr": "\n<REPO>/tests/fixtures/antipatterns/icon-tile-stack.html\n [icon-tile-stack] 80x80px icon tile above h3 \"Lightning Fast\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [icon-tile-stack] 64x64px icon tile above h3 \"Secure Storage\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [icon-tile-stack] 72x72px icon tile above h3 \"Easy Setup\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [icon-tile-stack] 96x96px icon tile above h3 \"Powerful Analytics\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [icon-tile-stack] 48x48px icon tile above h3 \"Emoji Inline Icon\"\n → A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.\n [ai-color-palette] Purple/violet accent colors detected\n → Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.\n [marketing-buzzword] 1 buzzword phrase: \"ure Storage Enterprise-grade security fo\"\n → Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.\n\n7 anti-patterns found.\n",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
"files": {}
|
"files": {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"stdout": "",
|
"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. 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",
|
"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",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
"files": {}
|
"files": {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"stdout": "",
|
"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. 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",
|
"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",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
"files": {}
|
"files": {}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"stdout": "",
|
"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. 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",
|
"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",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
"files": {}
|
"files": {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"stdout": "",
|
"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. 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",
|
"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",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
"files": {}
|
"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. 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",
|
"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",
|
||||||
"stderr": "",
|
"stderr": "",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"stdout": "",
|
"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. 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",
|
"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",
|
||||||
"exit": 2,
|
"exit": 2,
|
||||||
"signal": null,
|
"signal": null,
|
||||||
"files": {}
|
"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