drop "no pure black/white" rule entirely

The rule was contested in the design world and causing more damage than
good — pushing every page into the tinted-near-white default which is
the cream/sand AI tell we already explicitly ban elsewhere. Vercel,
SVKMS, Brutalist sites, et al. use pure black/white successfully; the
skill shouldn't second-guess that.

Skill markdown deletions:
- SKILL.md Color: drop the "Never use #000 or #fff" bullet.
- color-and-contrast.md: drop the "Never Use Pure Gray or Pure Black"
  subsection, the "Never pure black" table-row prescription, and the
  "Avoid: Using pure black for large areas" bullet.
- colorize.md: drop the "NEVER use pure black or pure white for large
  areas" bullet.
- polish.md: drop the "Tinted neutrals: No pure gray or pure black"
  half of the bullet (the gray-on-color bullet survives).

Detector code (cli/engine):
- registry/antipatterns.mjs: remove the `pure-black-white` entry.
- rules/checks.mjs: remove the three `findings.push({ id:
  'pure-black-white', ... })` emit points (inline #000 bg, Tailwind
  bg-black class, plain-HTML scan path).
- engines/regex/detect-text.mjs: remove the two pure-black-white regex
  rules (CSS `background: #000…` + Tailwind `bg-black`).
- detect-antipatterns-browser.js: regenerated via
  scripts/build-browser-detector.js.

Tests:
- detect-antipatterns-fixtures.test.mjs: invert the assertion that
  pure-black-white fires; expect it to NOT fire post-v3.2. Drop the
  Tailwind bg-black-opacity edge-case test (no longer relevant).
- detect-antipatterns.test.js: drop the standalone "detects pure-
  black-white in styled-components" test and remove pure-black-white
  from the multi-detector assertions in PricingCard, globals.css, and
  GlobalStyle.tsx tests.

166 bun tests pass; 24 node fixture tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-05-20 10:26:01 -07:00
co-authored by Claude Opus 4.7
parent c9a2e79578
commit 7f28ee0e11
10 changed files with 12 additions and 106 deletions
-23
View File
@@ -249,15 +249,6 @@ const ANTIPATTERNS = [
},
// ── Quality: general design and accessibility issues ──
{
id: 'pure-black-white',
category: 'quality',
name: 'Pure black background',
description:
'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.',
skillSection: 'Color & Contrast',
skillGuideline: 'pure black (#000)',
},
{
id: 'gray-on-color',
category: 'quality',
@@ -527,11 +518,6 @@ function checkColors(opts) {
}
const findings = [];
// Pure black background (only solid or near-solid, not semi-transparent overlays)
if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) {
findings.push({ id: 'pure-black-white', snippet: '#000000 background' });
}
if (hasDirectText && textColor && !isEmojiOnly) {
// Run background-dependent checks against either a solid bg or, if the
// ancestor is a gradient, against every gradient stop (use the worst case).
@@ -587,9 +573,6 @@ function checkColors(opts) {
// Tailwind class checks
if (classList) {
const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' ');
if (/\bbg-black\b(?!\/)/.test(classStr)) {
findings.push({ id: 'pure-black-white', snippet: 'bg-black' });
}
const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/);
const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/);
@@ -905,12 +888,6 @@ function checkHtmlPatterns(html) {
// --- Color ---
// Pure black background
const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi;
if (pureBlackBgRe.test(html)) {
findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' });
}
// AI color palette: purple/violet
const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi;
if (purpleHexRe.test(html)) {
-8
View File
@@ -63,10 +63,6 @@ const REGEX_MATCHERS = [
{ id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Mona\+Sans|Geist)\b/gi,
test: () => true,
fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` },
// --- Pure black background ---
{ id: 'pure-black-white', regex: /background(?:-color)?\s*:\s*(#000000|#000|rgb\(0,\s*0,\s*0\))\b/gi,
test: () => true,
fmt: (m) => m[0] },
// --- Gradient text ---
{ id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi,
test: (m, line) => /gradient/i.test(line),
@@ -75,10 +71,6 @@ const REGEX_MATCHERS = [
{ id: 'gradient-text', regex: /\bbg-clip-text\b/g,
test: (m, line) => /\bbg-gradient-to-/i.test(line),
fmt: () => 'bg-clip-text + bg-gradient' },
// --- Tailwind pure black background ---
{ id: 'pure-black-white', regex: /\bbg-black\b/g,
test: () => true,
fmt: (m) => m[0] },
// --- Tailwind gray on colored bg ---
{ id: 'gray-on-color', regex: /\btext-(?:gray|slate|zinc|neutral|stone)-(\d+)\b/g,
test: (m, line) => /\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/.test(line),
-9
View File
@@ -147,15 +147,6 @@ const ANTIPATTERNS = [
},
// ── Quality: general design and accessibility issues ──
{
id: 'pure-black-white',
category: 'quality',
name: 'Pure black background',
description:
'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.',
skillSection: 'Color & Contrast',
skillGuideline: 'pure black (#000)',
},
{
id: 'gray-on-color',
category: 'quality',
-14
View File
@@ -78,11 +78,6 @@ function checkColors(opts) {
}
const findings = [];
// Pure black background (only solid or near-solid, not semi-transparent overlays)
if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) {
findings.push({ id: 'pure-black-white', snippet: '#000000 background' });
}
if (hasDirectText && textColor && !isEmojiOnly) {
// Run background-dependent checks against either a solid bg or, if the
// ancestor is a gradient, against every gradient stop (use the worst case).
@@ -138,9 +133,6 @@ function checkColors(opts) {
// Tailwind class checks
if (classList) {
const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' ');
if (/\bbg-black\b(?!\/)/.test(classStr)) {
findings.push({ id: 'pure-black-white', snippet: 'bg-black' });
}
const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/);
const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/);
@@ -456,12 +448,6 @@ function checkHtmlPatterns(html) {
// --- Color ---
// Pure black background
const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi;
if (pureBlackBgRe.test(html)) {
findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' });
}
// AI color palette: purple/violet
const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi;
if (purpleHexRe.test(html)) {