mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 15:46:30 +03:00
[codex] Improve detector false positive handling (#232)
* Improve detector false positive handling * Register docs integrity test * Fix clipped overflow decorative skip
This commit is contained in:
@@ -93,16 +93,57 @@ describe('detectUrl — browser-only fixtures', () => {
|
||||
// 6. 24px heading / 8px all sides — H fail (improvement over old 8px floor)
|
||||
// 7. 32px hero / 6px V / 16px H — V fail
|
||||
// 8. 14px <pre> / 2px all sides — both fail
|
||||
// Pass column has 12 cases (small pills, standard cards, code blocks,
|
||||
// Pass column has 13 cases (small pills, inline code, standard cards, code blocks,
|
||||
// buttons, inputs, big text with proportional padding) — none should fire.
|
||||
assert.equal(cramped.length, 8, `expected 8 cramped-padding findings, got ${cramped.length}`);
|
||||
});
|
||||
|
||||
it('cramped-padding wrapper: skips same-surface wrappers, full-bleed marquees, and inset inner text surfaces', async () => {
|
||||
const f = await detectUrl(`${baseUrl}/fixtures/antipatterns/flush-against-border.html`);
|
||||
const cramped = f.filter(r => r.antipattern === 'cramped-padding');
|
||||
const snippets = cramped.map(r => r.snippet || '').join('\n');
|
||||
|
||||
for (const cls of ['flag-frameworks', 'flag-card-borders', 'flag-bg-only', 'flag-outline-only', 'flag-asym-leftflush']) {
|
||||
assert.match(snippets, new RegExp(`"${cls}"`), `expected ".${cls}" to be flagged`);
|
||||
}
|
||||
for (const cls of ['pass-same-bg-child', 'pass-marquee-shell', 'pass-inner-text-surface']) {
|
||||
assert.doesNotMatch(snippets, new RegExp(`"${cls}"`), `".${cls}" should not be flagged`);
|
||||
}
|
||||
});
|
||||
|
||||
it('line-length: flag column triggers, pass column adds none', async () => {
|
||||
const f = await detectUrl(`${baseUrl}/fixtures/antipatterns/quality.html`);
|
||||
assert.equal(f.filter(r => r.antipattern === 'line-length').length, 1);
|
||||
});
|
||||
|
||||
it('clipped-overflow-container: utility-named popovers still flag when clipped', async () => {
|
||||
const f = await detectUrl(`${baseUrl}/fixtures/antipatterns/clipped-overflow-container.html`);
|
||||
const snippets = f
|
||||
.filter(r => r.antipattern === 'clipped-overflow-container')
|
||||
.map(r => r.snippet || '')
|
||||
.join('\n');
|
||||
|
||||
assert.match(snippets, /flag-shadow-utility/, 'shadow-lg utility surfaces must not be skipped as decorative');
|
||||
assert.match(snippets, /flag-overlay-surface/, 'overlay-named content surfaces must not be skipped as decorative');
|
||||
assert.doesNotMatch(snippets, /pass-contained-overlay/, 'aria-hidden decorative overlays should remain skipped');
|
||||
});
|
||||
|
||||
it('oversized-h1: requires the headline to dominate the viewport, not just be large', async () => {
|
||||
const f = await detectUrl(`${baseUrl}/fixtures/antipatterns/oversized-h1-browser.html`);
|
||||
const hits = f.filter(r => r.antipattern === 'oversized-h1');
|
||||
assert.equal(
|
||||
hits.length,
|
||||
1,
|
||||
`expected exactly one oversized-h1 finding, got ${hits.length}: ${hits.map(r => r.snippet).join('; ')}`,
|
||||
);
|
||||
assert.match(hits[0].snippet, /sprawls across the whole/i);
|
||||
assert.equal(
|
||||
hits.some(r => /missing design vocabulary/i.test(r.snippet || '')),
|
||||
false,
|
||||
'a large two-line homepage-style h1 must not flag unless it dominates the viewport',
|
||||
);
|
||||
});
|
||||
|
||||
it('typography side-by-side: element-level flag cases get regular overlays', async () => {
|
||||
const puppeteer = await import('puppeteer');
|
||||
const browser = await puppeteer.default.launch({
|
||||
@@ -174,6 +215,7 @@ describe('detectUrl — browser-only fixtures', () => {
|
||||
'pass-sr-only-legacy',
|
||||
'pass-sr-only-tiny-hidden',
|
||||
'pass-sr-only-clipped-wide',
|
||||
'pass-hidden-slide-overflow',
|
||||
]) {
|
||||
assert.ok(!flagged.has(cls), `".${cls}" should NOT be flagged as text-overflow`);
|
||||
}
|
||||
@@ -235,6 +277,11 @@ describe('detectUrl — browser-only fixtures', () => {
|
||||
false,
|
||||
'light image with dark text should keep enough contrast',
|
||||
);
|
||||
assert.equal(
|
||||
f.some(r => r.antipattern === 'low-contrast' && /Hidden mockup text/i.test(r.snippet || '')),
|
||||
false,
|
||||
'aria-hidden decorative mockups should not produce visual contrast findings',
|
||||
);
|
||||
assert.equal(
|
||||
f.some(r => r.antipattern === 'low-contrast' && /Should (?:flag|pass) after pixel sampling/i.test(r.snippet || '')),
|
||||
false,
|
||||
@@ -288,7 +335,9 @@ describe('detectUrl — browser-only fixtures', () => {
|
||||
});
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1280, height: 800 });
|
||||
// Keep three failing visual-contrast cards in the no-scroll viewport;
|
||||
// the offscreen cases are covered by the scrollOffscreen test above.
|
||||
await page.setViewport({ width: 1280, height: 1000 });
|
||||
await page.goto(`${baseUrl}/fixtures/antipatterns/visual-contrast.html`, { waitUntil: 'load' });
|
||||
const browserScript = fs.readFileSync(path.join(ROOT, 'cli/engine/detect-antipatterns-browser.js'), 'utf-8');
|
||||
await page.evaluate(() => { window.__IMPECCABLE_CONFIG__ = { autoScan: false }; });
|
||||
|
||||
@@ -407,6 +407,18 @@ describe('detectHtml — repeated-section-kickers', () => {
|
||||
'Figure Caption Label',
|
||||
'Normal Case Kicker',
|
||||
'Intentional Brand Label',
|
||||
'Garden Suite',
|
||||
'Sea Loft',
|
||||
'Cliff Suite',
|
||||
'/impeccabletypeset',
|
||||
'/impeccablelayout',
|
||||
'/impeccablecolorize',
|
||||
'/impeccablecraft',
|
||||
'/impeccableaudit',
|
||||
'/impeccablepolish',
|
||||
'Mockup Hero Variant One',
|
||||
'Mockup Hero Variant Two',
|
||||
'Mockup Hero Variant Three',
|
||||
];
|
||||
|
||||
it('repeated-section-kickers: flags only repeated section scaffolding', async () => {
|
||||
@@ -471,6 +483,10 @@ describe('detectHtml — cramped-padding (wrapper variant)', () => {
|
||||
'pass-bg-padded',
|
||||
'pass-outline-padded',
|
||||
'pass-image-only',
|
||||
'pass-margin-inset',
|
||||
'pass-inner-shell',
|
||||
'pass-same-bg-child',
|
||||
'pass-inner-text-surface',
|
||||
];
|
||||
|
||||
it('cramped-padding (wrapper): flags only the should-flag column', async () => {
|
||||
@@ -541,8 +557,26 @@ describe('detectHtml — clipped-overflow-container', () => {
|
||||
// (overflow hidden/clip) with an absolutely-positioned descendant clips
|
||||
// tooltips/menus that need to escape. Real scroll regions (auto/scroll),
|
||||
// visible overflow, and clipping containers without positioned children pass.
|
||||
const SHOULD_FLAG = ['flag-overflow-hidden', 'flag-overflow-clip'];
|
||||
const SHOULD_PASS = ['pass-hidden-no-abs', 'pass-visible-abs', 'pass-scroll-abs'];
|
||||
const SHOULD_FLAG = [
|
||||
'flag-overflow-hidden',
|
||||
'flag-overflow-clip',
|
||||
'flag-overflow-negative',
|
||||
'flag-overflow-right',
|
||||
'flag-shadow-utility',
|
||||
'flag-overlay-surface',
|
||||
];
|
||||
const SHOULD_PASS = [
|
||||
'pass-hidden-no-abs',
|
||||
'pass-visible-abs',
|
||||
'pass-scroll-abs',
|
||||
'pass-contained-abs',
|
||||
'pass-button-shine',
|
||||
'pass-crop-photo',
|
||||
'pass-contained-overlay',
|
||||
'pass-carousel-viewport',
|
||||
'pass-fisheye-list',
|
||||
'pass-split-container',
|
||||
];
|
||||
|
||||
it('clipped-overflow-container: flags only clipping ancestors with positioned children', async () => {
|
||||
const f = await detectHtml(path.join(FIXTURES, 'clipped-overflow-container.html'));
|
||||
|
||||
+61
-1
@@ -9,6 +9,26 @@
|
||||
.col { padding: 16px; }
|
||||
.box { position: relative; width: 200px; height: 120px; margin: 0 0 24px; padding: 16px; border: 1px solid #ddd; }
|
||||
.pop { position: absolute; top: 100%; left: 0; width: 220px; background: #222; color: #fff; padding: 8px; }
|
||||
.tip-negative { position: absolute; top: -34px; left: 16px; width: 160px; background: #222; color: #fff; padding: 8px; }
|
||||
.flyout { position: absolute; top: 28px; left: 100%; width: 180px; background: #222; color: #fff; padding: 8px; }
|
||||
.contained-badge { position: absolute; top: 10px; right: 10px; width: 36px; height: 24px; background: #e5f0ff; }
|
||||
.button-shine { position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,.6), transparent); }
|
||||
.crop-photo { position: absolute; inset: -18px; background: linear-gradient(135deg, #b7d7ff, #486aa3); }
|
||||
.inside-overlay { position: absolute; left: 12px; right: 12px; bottom: 12px; min-height: 28px; background: rgba(255,255,255,.86); }
|
||||
.pass-hidden-button { position: relative; overflow: hidden; width: 160px; height: 52px; border: 1px solid #ddd; background: #111; color: #fff; }
|
||||
.pass-carousel-viewport,
|
||||
.pass-fisheye-list,
|
||||
.pass-split-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
margin: 0 0 24px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.carousel-slide-next { position: absolute; top: 100%; left: 0; width: 100%; height: 100%; padding: 16px; background: #f5f5f5; }
|
||||
.fisheye-next-item { position: absolute; top: 100%; left: 0; width: 100%; padding: 8px; background: none; border: 0; text-align: left; }
|
||||
.split-after-panel { position: absolute; top: 0; left: 50%; width: 100%; height: 100%; background: #f5f5f5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -23,9 +43,25 @@
|
||||
<button>Tooltip host</button>
|
||||
<span class="pop">Tooltip clipped by overflow clip</span>
|
||||
</div>
|
||||
<div class="box flag-overflow-negative" style="overflow: hidden">
|
||||
<button>Help</button>
|
||||
<span class="tip-negative">Tooltip clipped above the host</span>
|
||||
</div>
|
||||
<div class="box flag-overflow-right" style="overflow: hidden">
|
||||
<button>Actions</button>
|
||||
<div class="flyout">Flyout menu clipped to the right of the host</div>
|
||||
</div>
|
||||
<div class="box flag-shadow-utility" style="overflow: hidden">
|
||||
<button>More</button>
|
||||
<div class="pop bg-white shadow-lg">Dropdown with utility classes still gets clipped.</div>
|
||||
</div>
|
||||
<div class="box flag-overlay-surface" style="overflow: clip">
|
||||
<button>Dialog</button>
|
||||
<div class="pop modal-overlay">Overlay-style dialog content still needs to escape.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PASS column: no clipping, no positioned child, or a real scroll region -->
|
||||
<!-- PASS column: no clipping, no positioned child, real scroll region, or contained decorative positioning -->
|
||||
<section class="col pass">
|
||||
<div class="box pass-hidden-no-abs" style="overflow: hidden">
|
||||
<p style="position: static">Hidden container with only static content.</p>
|
||||
@@ -38,6 +74,30 @@
|
||||
<button>Scroll host</button>
|
||||
<span class="pop">A genuine scroll region is allowed to contain positioned children.</span>
|
||||
</div>
|
||||
<div class="box pass-contained-abs" style="overflow: hidden">
|
||||
<p>Positioned decoration remains inside the clipping box.</p>
|
||||
<span class="contained-badge" aria-hidden="true"></span>
|
||||
</div>
|
||||
<button type="button" class="pass-hidden-button pass-button-shine">
|
||||
Save
|
||||
<span class="button-shine" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="box pass-crop-photo" style="overflow: hidden">
|
||||
<span class="crop-photo" aria-hidden="true"></span>
|
||||
</div>
|
||||
<div class="box pass-contained-overlay" style="overflow: clip">
|
||||
<p>Mockup frame with a contained absolute overlay.</p>
|
||||
<span class="inside-overlay" aria-hidden="true"></span>
|
||||
</div>
|
||||
<div class="pass-carousel-viewport" aria-roledescription="carousel">
|
||||
<div class="carousel-slide-next">A hidden next slide is clipped intentionally by the viewport.</div>
|
||||
</div>
|
||||
<div class="pass-fisheye-list">
|
||||
<button type="button" class="fisheye-next-item">Next command</button>
|
||||
</div>
|
||||
<div class="pass-split-container">
|
||||
<div class="split-after-panel">Before/after comparison panel clipped by the split frame.</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -66,6 +66,16 @@
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.pass-inline-code-chip {
|
||||
display: inline-block;
|
||||
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 2;
|
||||
padding: 0.3em 0.5em;
|
||||
background: #eef2f7;
|
||||
color: #0f172a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ── PASS: cards at current standards ── */
|
||||
.pass-card-min { padding: 8px; background: #f1f5f9; border-radius: 6px; font-size: 16px; line-height: 1.6; }
|
||||
@@ -157,6 +167,10 @@
|
||||
<span class="case-label">tag chip: 12px font, 4px / 10px padding</span>
|
||||
<span class="pass-chip">design system tag chip</span>
|
||||
</div>
|
||||
<div class="case">
|
||||
<span class="case-label">long inline code chip: compact inline code should not be treated like a padded card</span>
|
||||
<code class="pass-inline-code-chip">npx impeccable skills check</code>
|
||||
</div>
|
||||
|
||||
<h3>Cards at current standards</h3>
|
||||
<div class="case">
|
||||
|
||||
@@ -218,6 +218,83 @@
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, #cbd5e1, #94a3b8);
|
||||
}
|
||||
|
||||
/* ── PASS: parent has zero padding, but child margins create the visible inset ── */
|
||||
.pass-margin-inset {
|
||||
padding: 0;
|
||||
background: white;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.pass-margin-inset h4 { margin: 16px 18px 4px; font-size: 14px; color: #0f172a; }
|
||||
.pass-margin-inset p { margin: 0 18px 16px; font-size: 13px; color: #475569; }
|
||||
|
||||
/* ── PASS: an inner shell, not the parent, owns the inset spacing ── */
|
||||
.pass-inner-shell {
|
||||
padding: 0;
|
||||
background: white;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.pass-inner-shell .inner-shell {
|
||||
margin: 18px;
|
||||
}
|
||||
.pass-inner-shell h4 { margin: 0 0 4px; font-size: 14px; color: #0f172a; }
|
||||
.pass-inner-shell p { margin: 0; font-size: 13px; color: #475569; }
|
||||
|
||||
/* ── PASS: same background as parent, so the child draws no visible boundary ── */
|
||||
.pass-same-bg-parent {
|
||||
padding: 16px;
|
||||
background: #eef2f7;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.pass-same-bg-child {
|
||||
padding: 0;
|
||||
background: #eef2f7;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.pass-same-bg-child h4 { margin: 0; font-size: 14px; color: #0f172a; }
|
||||
.pass-same-bg-child p { margin: 4px 0 0; font-size: 13px; color: #475569; }
|
||||
|
||||
/* ── PASS: full-bleed marquee/surface, not a text card missing padding ── */
|
||||
.pass-marquee-shell {
|
||||
padding: 0;
|
||||
background: #172033;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
}
|
||||
.pass-marquee-track {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: max-content;
|
||||
}
|
||||
.pass-marquee-card {
|
||||
width: 220px;
|
||||
padding: 14px;
|
||||
background: #23304a;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
.pass-marquee-card p { margin: 0; font-size: 13px; }
|
||||
|
||||
/* ── PASS: outer scene owns background; inner text surface owns padding ── */
|
||||
.pass-inner-text-surface {
|
||||
padding: 0;
|
||||
background: #0f172a;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
}
|
||||
.pass-inner-text-surface-window {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-height: 92px;
|
||||
padding: 14px;
|
||||
background: #111827;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
.pass-inner-text-surface-window p { margin: 0; font-size: 13px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -346,6 +423,59 @@
|
||||
<div class="placeholder-img"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Child margins provide the inset</h3>
|
||||
<div class="case">
|
||||
<span class="case-label">card: parent padding 0, but text children have margins that create visible inset.</span>
|
||||
<div class="pass-margin-inset">
|
||||
<h4>Title inset by child margin</h4>
|
||||
<p>Body copy is not visually flush even though the parent has zero padding.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Inner shell provides the inset</h3>
|
||||
<div class="case">
|
||||
<span class="case-label">card: parent padding 0, but an inner shell is offset from every edge.</span>
|
||||
<div class="pass-inner-shell">
|
||||
<div class="inner-shell">
|
||||
<h4>Title inset by inner shell</h4>
|
||||
<p>The direct child creates the breathing room.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Same background as parent</h3>
|
||||
<div class="case">
|
||||
<span class="case-label">child padding 0, but it has the same background as the parent, so no child boundary is visible.</span>
|
||||
<div class="pass-same-bg-parent">
|
||||
<div class="pass-same-bg-child">
|
||||
<h4>Same-surface visual shell</h4>
|
||||
<p>The text belongs to the parent surface, not to a distinct zero-padding card.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Full-bleed marquee shell</h3>
|
||||
<div class="case">
|
||||
<span class="case-label">marquee shell clips animated cards intentionally; the cards, not the shell, own text padding.</span>
|
||||
<section class="pass-marquee-shell">
|
||||
<div class="pass-marquee-track">
|
||||
<article class="pass-marquee-card"><p>Quote card with its own comfortable padding.</p></article>
|
||||
<article class="pass-marquee-card"><p>Another moving card, also padded inside.</p></article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<h3>Inner text surface</h3>
|
||||
<div class="case">
|
||||
<span class="case-label">outer shell has a scene background; the inner window owns the actual text padding.</span>
|
||||
<div class="pass-inner-text-surface">
|
||||
<div class="pass-inner-text-surface-window">
|
||||
<p>Build step running</p>
|
||||
<p>Detector output belongs to the mock window.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/detect-antipatterns-browser.js"></script>
|
||||
|
||||
+6
@@ -27,6 +27,12 @@
|
||||
<div class="card pass-tight-shadow" style="border: 1px solid #e5e7eb; box-shadow: 0 1px 4px rgba(0,0,0,0.08)">
|
||||
Hairline border with a tight, purposeful shadow.
|
||||
</div>
|
||||
<div class="card pass-soft-border-wide-shadow" style="border: 1px solid rgba(148, 163, 184, 0.16); box-shadow: 0 18px 48px -16px rgba(15, 23, 42, 0.18)">
|
||||
A quiet low-contrast hairline with a broad but restrained ambient shadow.
|
||||
</div>
|
||||
<div class="card pass-oklch-soft-shadow" style="border: 1px solid oklch(78% 0 0 / 0.16); box-shadow: 0 16px 36px -12px oklch(2% 0.004 95 / 0.18)">
|
||||
OKLCH shadow hue values should not be mistaken for shadow blur.
|
||||
</div>
|
||||
<div class="stripes pass-plain-gradient" style="background: linear-gradient(180deg, #f3f4f6, #ffffff)"></div>
|
||||
<p class="pass-no-theater">We measured real outcomes for the people who use this every day.</p>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Oversized H1 browser fixture</title>
|
||||
<style>
|
||||
body { font-family: Georgia, serif; margin: 0; color: #1a1a1a; background: #fff; }
|
||||
main { display: grid; gap: 48px; padding: 32px; }
|
||||
h1 { margin: 0; line-height: 1.04; }
|
||||
.flag-dominating-h1 {
|
||||
max-width: 1040px;
|
||||
font-size: 104px;
|
||||
}
|
||||
.pass-contained-large-h1 {
|
||||
max-width: 1120px;
|
||||
font-size: 83px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1 class="flag-dominating-h1">A sweeping product headline that sprawls across the whole viewport and keeps going</h1>
|
||||
<h1 class="pass-contained-large-h1">The missing design vocabulary for agents.</h1>
|
||||
</main>
|
||||
<script src="/js/detect-antipatterns-browser.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+19
@@ -116,6 +116,17 @@
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.mock-terminal-title,
|
||||
.mock-terminal-meta,
|
||||
.mock-diff-line {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
color: #475569;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.mock-terminal-title { font-size: 11px; }
|
||||
.mock-terminal-meta { font-size: 10.8px; }
|
||||
.mock-diff-line { font-size: 11.84px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -220,6 +231,14 @@
|
||||
<h2 style="font-size: 18px; margin: 0 0 4px;">Second level</h2>
|
||||
<h3 style="font-size: 16px; margin: 0;">Third level</h3>
|
||||
</div>
|
||||
|
||||
<h3>Small mockup metadata</h3>
|
||||
<div class="case">
|
||||
<span class="case-label">terminal/diff metadata at tiny sizes</span>
|
||||
<span class="mock-terminal-title">/impeccable polish : scanning codebase</span>
|
||||
<div class="mock-terminal-meta">12 files inspected · 4 visual issues queued</div>
|
||||
<div class="mock-diff-line">+ Replace generic button label with action-specific copy</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/detect-antipatterns-browser.js"></script>
|
||||
|
||||
@@ -69,6 +69,54 @@
|
||||
.brand-system {
|
||||
min-height: 120px;
|
||||
}
|
||||
.card-grid,
|
||||
.command-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
.suite-card,
|
||||
.command-card {
|
||||
display: block;
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
border: 1px solid #d8cec0;
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
.taxonomy-label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.11em;
|
||||
text-transform: uppercase;
|
||||
color: #5b5046;
|
||||
}
|
||||
.suite-card h3,
|
||||
.command-card h3 {
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.command-carousel,
|
||||
.hidden-mockup {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
.command-spread,
|
||||
.mockup-hero {
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
border: 1px solid #d8cec0;
|
||||
background: #fff;
|
||||
}
|
||||
.command-spread h3,
|
||||
.mockup-hero h2 {
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -140,6 +188,75 @@
|
||||
<h2>"Intentional Brand Label"</h2>
|
||||
<p>Deliberate brand systems can opt out with an explicit marker.</p>
|
||||
</section>
|
||||
|
||||
<div class="card-grid" aria-label="Room cards">
|
||||
<article class="suite-card">
|
||||
<span class="taxonomy-label">Suite</span>
|
||||
<h3>"Garden Suite"</h3>
|
||||
<p>Repeated category labels inside cards are structured metadata, not page-section scaffolding.</p>
|
||||
</article>
|
||||
<article class="suite-card">
|
||||
<span class="taxonomy-label">Suite</span>
|
||||
<h3>"Sea Loft"</h3>
|
||||
<p>The label is useful because the user is scanning sibling cards.</p>
|
||||
</article>
|
||||
<article class="suite-card">
|
||||
<span class="taxonomy-label">Suite</span>
|
||||
<h3>"Cliff Suite"</h3>
|
||||
<p>Repeating it here does not create the generic section-kicker smell.</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="command-grid" aria-label="Command cards">
|
||||
<button type="button" class="command-card">
|
||||
<span class="taxonomy-label">Refine</span>
|
||||
<h3>"/impeccabletypeset"</h3>
|
||||
<p>Command category labels can repeat across many action cards.</p>
|
||||
</button>
|
||||
<button type="button" class="command-card">
|
||||
<span class="taxonomy-label">Refine</span>
|
||||
<h3>"/impeccablelayout"</h3>
|
||||
<p>The repeated taxonomy helps compare tools inside the same group.</p>
|
||||
</button>
|
||||
<button type="button" class="command-card">
|
||||
<span class="taxonomy-label">Refine</span>
|
||||
<h3>"/impeccablecolorize"</h3>
|
||||
<p>This is a card-grid label, not a page-level eyebrow before a section.</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="command-carousel" aria-label="Command carousel">
|
||||
<div class="command-spread" data-category="create">
|
||||
<span class="taxonomy-label">Create</span>
|
||||
<h3>"/impeccablecraft"</h3>
|
||||
<p>Carousel slides can repeat category labels as navigational taxonomy.</p>
|
||||
</div>
|
||||
<div class="command-spread" data-category="evaluate">
|
||||
<span class="taxonomy-label">Evaluate</span>
|
||||
<h3>"/impeccableaudit"</h3>
|
||||
<p>The slash command heading identifies an item, not a page section.</p>
|
||||
</div>
|
||||
<div class="command-spread" data-category="harden">
|
||||
<span class="taxonomy-label">Harden</span>
|
||||
<h3>"/impeccablepolish"</h3>
|
||||
<p>The label is a category chip for a selectable command slide.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden-mockup" aria-hidden="true">
|
||||
<div class="mockup-hero">
|
||||
<span class="taxonomy-label">Amalfi Coast</span>
|
||||
<h2>"Mockup Hero Variant One"</h2>
|
||||
</div>
|
||||
<div class="mockup-hero">
|
||||
<span class="taxonomy-label">Amalfi Coast</span>
|
||||
<h2>"Mockup Hero Variant Two"</h2>
|
||||
</div>
|
||||
<div class="mockup-hero">
|
||||
<span class="taxonomy-label">Amalfi Coast</span>
|
||||
<h2>"Mockup Hero Variant Three"</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
+12
@@ -29,6 +29,15 @@
|
||||
white-space: nowrap;
|
||||
clip-path: inset(50%);
|
||||
}
|
||||
.hidden-slide {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.hidden-slide .pass-hidden-slide-overflow {
|
||||
display: block;
|
||||
width: 120px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -49,6 +58,9 @@
|
||||
<span class="pass-sr-only-legacy sr-only sr-only-legacy-clip">A legacy clipped screen-reader-only label with long text that should not be flagged.</span>
|
||||
<span class="pass-sr-only-tiny-hidden sr-only sr-only-tiny-hidden">A tiny overflow-hidden screen-reader-only label without a clip declaration should not be flagged.</span>
|
||||
<span class="pass-sr-only-clipped-wide sr-only-clipped-wide">A fully clipped label with a normal-sized box and overflowing text should not be flagged.</span>
|
||||
<div class="hidden-slide">
|
||||
<span class="pass-hidden-slide-overflow">An inactive carousel slide can contain long text that overflows while hidden.</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -150,6 +150,10 @@
|
||||
</svg>
|
||||
<p style="color: rgb(35, 31, 27);">Dark text over a light SVG underlay should pass.</p>
|
||||
</article>
|
||||
|
||||
<article class="image-card light-image" aria-hidden="true">
|
||||
<p style="color: rgb(255, 255, 255);">Hidden mockup text on a light image should not be sampled.</p>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user