mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Emojis render as multicolor glyphs regardless of CSS \`color\`, so the text color is irrelevant for contrast calculations. The detector was flagging emoji icons as low-contrast whenever the surrounding bg/text colors were close (e.g. an emoji card with text-color set to match the bg). Adds an isEmojiOnlyText() helper that returns true when the direct text consists entirely of emoji characters (and zero-width joiners, variation selectors, skin-tone modifiers, regional indicators), and skips both gray-on-color and low-contrast checks when it's true. Same insight fixes a missed icon-tile-stack detection: many AI- generated cards use \`<div class="card-icon">⚡</div>\` where the tile contains the emoji directly as text, not an <svg>/<i> child. The detector now also recognizes these "inline emoji icon" tiles. Both fixes are TDD'd: new test cases in color.html (two emoji cards with matching text/bg colors) and icon-tile-stack.html (the inline emoji tile pattern). The test suite went from green → red → green across both rules. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
248 lines
9.2 KiB
HTML
248 lines
9.2 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Icon-Tile-Stack — Should Flag vs Should Pass</title>
|
||
<style>
|
||
/* ─── Layout convention: two-column fixture ─────────────────────────
|
||
Left column = patterns the rule SHOULD flag.
|
||
Right column = legitimate patterns the rule should NOT flag.
|
||
Each test case is annotated with a unique <h3> text so the test
|
||
can match snippets back to expectations.
|
||
──────────────────────────────────────────────────────────────── */
|
||
body { font-family: system-ui, sans-serif; margin: 0; padding: 24px; }
|
||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 960px; margin: 0 auto; }
|
||
.col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }
|
||
.case { margin-bottom: 24px; padding: 16px; border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; }
|
||
.case h3 { font-size: 18px; margin: 0 0 4px; color: #0f172a; }
|
||
.case p { font-size: 14px; margin: 0; color: #64748b; }
|
||
|
||
/* ── FLAG cases ── */
|
||
|
||
/* 1: pastel-gradient icon-tile (the canonical massive-icons.html shape) */
|
||
.icon-pastel {
|
||
width: 80px; height: 80px;
|
||
border-radius: 20px;
|
||
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||
display: flex; align-items: center; justify-content: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* 2: solid-color icon-tile, white svg inside (no gradient) */
|
||
.icon-solid {
|
||
width: 64px; height: 64px;
|
||
border-radius: 16px;
|
||
background: #3b82f6;
|
||
display: flex; align-items: center; justify-content: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* 3: white-bg outlined icon-tile (no chromatic background at all) */
|
||
.icon-white {
|
||
width: 72px; height: 72px;
|
||
border-radius: 18px;
|
||
background: #ffffff;
|
||
border: 1px solid #e2e8f0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* 4: larger square (still inside the 32–128 range) */
|
||
.icon-large {
|
||
width: 96px; height: 96px;
|
||
border-radius: 24px;
|
||
background: #f1f5f9;
|
||
display: flex; align-items: center; justify-content: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* 5: inline emoji icon — tile contains an emoji/symbol character directly,
|
||
not an <svg> or <i> child. This is the "card-icon" pattern from many
|
||
generated demos. */
|
||
.icon-emoji {
|
||
width: 48px; height: 48px;
|
||
border-radius: 12px;
|
||
background: linear-gradient(135deg, #8b5cf6, #a855f7);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 24px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* ── PASS cases ── */
|
||
|
||
/* a: round avatar above name (border-radius: 50% — circle, not rounded square) */
|
||
.avatar-round {
|
||
width: 64px; height: 64px;
|
||
border-radius: 50%;
|
||
background: #f59e0b;
|
||
display: flex; align-items: center; justify-content: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* b: wide thumbnail above article title (16:9, much wider than tall) */
|
||
.thumb-wide {
|
||
width: 200px; height: 112px;
|
||
border-radius: 8px;
|
||
background: #cbd5e1;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* c: inline icon next to heading (icon and h3 on the same row, NOT stacked) */
|
||
.inline-row { display: flex; align-items: center; gap: 12px; }
|
||
.inline-row .icon-inline {
|
||
width: 80px; height: 80px;
|
||
border-radius: 20px;
|
||
background: #ddd6fe;
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* d: nothing above the heading at all */
|
||
.no-icon h3 { margin-top: 0; }
|
||
|
||
/* e: icon-tile that's too small (under 32px) — out of size range */
|
||
.icon-tiny {
|
||
width: 24px; height: 24px;
|
||
border-radius: 6px;
|
||
background: #93c5fd;
|
||
display: flex; align-items: center; justify-content: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* f: oversized image card / hero image (over 128px) — out of size range */
|
||
.img-huge {
|
||
width: 200px; height: 200px;
|
||
border-radius: 16px;
|
||
background: #cbd5e1;
|
||
margin-bottom: 16px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="grid">
|
||
|
||
<!-- ════════════════════════════════════════════════════════════════
|
||
LEFT COLUMN: should flag
|
||
═══════════════════════════════════════════════════════════════ -->
|
||
<div class="col" data-col="flag">
|
||
<h2>Should flag</h2>
|
||
|
||
<div class="case">
|
||
<div class="icon-pastel">
|
||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="1.5">
|
||
<polyline points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
||
</svg>
|
||
</div>
|
||
<h3>Lightning Fast</h3>
|
||
<p>Built for speed and performance from the ground up.</p>
|
||
</div>
|
||
|
||
<div class="case">
|
||
<div class="icon-solid">
|
||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="1.5">
|
||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||
</svg>
|
||
</div>
|
||
<h3>Secure Storage</h3>
|
||
<p>Enterprise-grade security for your peace of mind.</p>
|
||
</div>
|
||
|
||
<div class="case">
|
||
<div class="icon-white">
|
||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#0f172a" stroke-width="1.5">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
<path d="M12 6v6l4 2"/>
|
||
</svg>
|
||
</div>
|
||
<h3>Easy Setup</h3>
|
||
<p>Get started in minutes with zero configuration.</p>
|
||
</div>
|
||
|
||
<div class="case">
|
||
<div class="icon-large">
|
||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#0f172a" stroke-width="1.5">
|
||
<line x1="18" y1="20" x2="18" y2="10"/>
|
||
<line x1="12" y1="20" x2="12" y2="4"/>
|
||
<line x1="6" y1="20" x2="6" y2="14"/>
|
||
</svg>
|
||
</div>
|
||
<h3>Powerful Analytics</h3>
|
||
<p>Insights that drive smarter business decisions.</p>
|
||
</div>
|
||
|
||
<div class="case">
|
||
<div class="icon-emoji">⚡</div>
|
||
<h3>Emoji Inline Icon</h3>
|
||
<p>The tile contains an emoji directly, no SVG child.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ════════════════════════════════════════════════════════════════
|
||
RIGHT COLUMN: should pass
|
||
═══════════════════════════════════════════════════════════════ -->
|
||
<div class="col" data-col="pass">
|
||
<h2>Should pass</h2>
|
||
|
||
<!-- a: round avatar (circle, not rounded square) -->
|
||
<div class="case">
|
||
<div class="avatar-round">
|
||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
|
||
<circle cx="12" cy="8" r="4"/>
|
||
<path d="M4 22v-2a8 8 0 0116 0v2"/>
|
||
</svg>
|
||
</div>
|
||
<h3>Sarah Chen</h3>
|
||
<p>Product designer based in Berlin.</p>
|
||
</div>
|
||
|
||
<!-- b: wide 16:9 thumbnail (aspect ratio out of range) -->
|
||
<div class="case">
|
||
<div class="thumb-wide"></div>
|
||
<h3>Article Headline</h3>
|
||
<p>A summary of an interesting article.</p>
|
||
</div>
|
||
|
||
<!-- c: side-by-side icon and heading (not stacked vertically) -->
|
||
<div class="case">
|
||
<div class="inline-row">
|
||
<div class="icon-inline">
|
||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#7c3aed" stroke-width="1.5">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<h3>Inline Side By Side</h3>
|
||
<p>Icon sits next to the heading, not above it.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- d: no icon above the heading at all -->
|
||
<div class="case no-icon">
|
||
<h3>Plain Heading No Icon</h3>
|
||
<p>Just a heading with body text, nothing above.</p>
|
||
</div>
|
||
|
||
<!-- e: icon-tile too small (24px — under 32px floor) -->
|
||
<div class="case">
|
||
<div class="icon-tiny">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1e40af" stroke-width="2">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
</svg>
|
||
</div>
|
||
<h3>Tiny Icon Above Me</h3>
|
||
<p>The icon container is smaller than 32px.</p>
|
||
</div>
|
||
|
||
<!-- f: huge image / hero (200px — over 128px ceiling) -->
|
||
<div class="case">
|
||
<div class="img-huge"></div>
|
||
<h3>Huge Hero Image</h3>
|
||
<p>The container is larger than the size cap.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script src="/js/detect-antipatterns-browser.js"></script>
|
||
</body>
|
||
</html>
|