mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-20 10:06:54 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
809976638d |
@@ -626,7 +626,7 @@ if (IS_BROWSER) {
|
||||
if (currentStyle.filter && currentStyle.filter !== 'none') reasons.add('filter');
|
||||
if (currentStyle.backdropFilter && currentStyle.backdropFilter !== 'none') reasons.add('backdrop filter');
|
||||
|
||||
const solidBg = parseRgb(currentStyle.backgroundColor) || parseAnyColor(currentStyle.backgroundColor);
|
||||
const solidBg = parseRgb(currentStyle.backgroundColor);
|
||||
if (solidBg && solidBg.a >= 0.95 && (!bgImage || bgImage === 'none')) break;
|
||||
current = current.parentElement;
|
||||
}
|
||||
@@ -688,7 +688,7 @@ if (IS_BROWSER) {
|
||||
// starve the url()-backed texts this mode exists to sample.
|
||||
if (options.imageOnly && !reasons.includes('image background')) continue;
|
||||
|
||||
const textColor = parseRgb(style.color) || parseAnyColor(style.color);
|
||||
const textColor = parseRgb(style.color);
|
||||
const fontSize = parseFloat(style.fontSize) || 16;
|
||||
const fontWeight = parseInt(style.fontWeight) || 400;
|
||||
const isLargeText = fontSize >= WCAG_LARGE_TEXT_PX || (fontSize >= WCAG_LARGE_BOLD_TEXT_PX && fontWeight >= 700);
|
||||
@@ -985,7 +985,7 @@ if (IS_BROWSER) {
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
const bg = parseRgb(style.backgroundColor) || parseAnyColor(style.backgroundColor);
|
||||
const bg = parseRgb(style.backgroundColor);
|
||||
if (bg && bg.a > 0.05) return { status: 'sampled', color: bg, method: 'solid-background' };
|
||||
return { status: 'unresolved', reason: 'no readable background' };
|
||||
}
|
||||
@@ -1115,7 +1115,7 @@ if (IS_BROWSER) {
|
||||
}
|
||||
|
||||
const style = getComputedStyle(el);
|
||||
const textColor = parseRgb(style.color) || parseAnyColor(style.color) || candidate.textColor;
|
||||
const textColor = parseRgb(style.color) || candidate.textColor;
|
||||
if (!textColor) return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'unreadable text color' };
|
||||
|
||||
const rect = getDirectTextRect(el) || el.getBoundingClientRect();
|
||||
|
||||
@@ -3986,7 +3986,7 @@ function checkElementAIPaletteDOM(el) {
|
||||
}
|
||||
|
||||
// Check for neon text (vivid cyan/purple color on dark background)
|
||||
const textColor = parseRgb(style.color) || parseAnyColor(style.color);
|
||||
const textColor = parseRgb(style.color);
|
||||
if (textColor && hasChroma(textColor, 80)) {
|
||||
const hue = getHue(textColor);
|
||||
const isAIPalette = (hue >= 160 && hue <= 200) || (hue >= 260 && hue <= 310);
|
||||
@@ -7281,7 +7281,7 @@ if (IS_BROWSER) {
|
||||
if (currentStyle.filter && currentStyle.filter !== 'none') reasons.add('filter');
|
||||
if (currentStyle.backdropFilter && currentStyle.backdropFilter !== 'none') reasons.add('backdrop filter');
|
||||
|
||||
const solidBg = parseRgb(currentStyle.backgroundColor) || parseAnyColor(currentStyle.backgroundColor);
|
||||
const solidBg = parseRgb(currentStyle.backgroundColor);
|
||||
if (solidBg && solidBg.a >= 0.95 && (!bgImage || bgImage === 'none')) break;
|
||||
current = current.parentElement;
|
||||
}
|
||||
@@ -7343,7 +7343,7 @@ if (IS_BROWSER) {
|
||||
// starve the url()-backed texts this mode exists to sample.
|
||||
if (options.imageOnly && !reasons.includes('image background')) continue;
|
||||
|
||||
const textColor = parseRgb(style.color) || parseAnyColor(style.color);
|
||||
const textColor = parseRgb(style.color);
|
||||
const fontSize = parseFloat(style.fontSize) || 16;
|
||||
const fontWeight = parseInt(style.fontWeight) || 400;
|
||||
const isLargeText = fontSize >= WCAG_LARGE_TEXT_PX || (fontSize >= WCAG_LARGE_BOLD_TEXT_PX && fontWeight >= 700);
|
||||
@@ -7640,7 +7640,7 @@ if (IS_BROWSER) {
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
const bg = parseRgb(style.backgroundColor) || parseAnyColor(style.backgroundColor);
|
||||
const bg = parseRgb(style.backgroundColor);
|
||||
if (bg && bg.a > 0.05) return { status: 'sampled', color: bg, method: 'solid-background' };
|
||||
return { status: 'unresolved', reason: 'no readable background' };
|
||||
}
|
||||
@@ -7770,7 +7770,7 @@ if (IS_BROWSER) {
|
||||
}
|
||||
|
||||
const style = getComputedStyle(el);
|
||||
const textColor = parseRgb(style.color) || parseAnyColor(style.color) || candidate.textColor;
|
||||
const textColor = parseRgb(style.color) || candidate.textColor;
|
||||
if (!textColor) return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'unreadable text color' };
|
||||
|
||||
const rect = getDirectTextRect(el) || el.getBoundingClientRect();
|
||||
|
||||
@@ -2752,7 +2752,7 @@ function checkElementAIPaletteDOM(el) {
|
||||
}
|
||||
|
||||
// Check for neon text (vivid cyan/purple color on dark background)
|
||||
const textColor = parseRgb(style.color) || parseAnyColor(style.color);
|
||||
const textColor = parseRgb(style.color);
|
||||
if (textColor && hasChroma(textColor, 80)) {
|
||||
const hue = getHue(textColor);
|
||||
const isAIPalette = (hue >= 160 && hue <= 200) || (hue >= 260 && hue <= 310);
|
||||
|
||||
+14
-23
@@ -33,13 +33,8 @@ import {
|
||||
stampProductSchema,
|
||||
} from './lib/artifact-schema.mjs';
|
||||
import {
|
||||
checkBuildPathUnset,
|
||||
checkConfig,
|
||||
checkDesignSidecar,
|
||||
collectBootFindingGroups,
|
||||
checkNativePlatformEvidence,
|
||||
checkProduct,
|
||||
checkProjectRoots,
|
||||
checkSurfaceBriefs,
|
||||
designSidecarCandidatesFor,
|
||||
} from './lib/staleness.mjs';
|
||||
import {
|
||||
@@ -106,34 +101,30 @@ async function collect(cwd, targetOptions) {
|
||||
extractPlatform,
|
||||
readFile: safeRead,
|
||||
});
|
||||
const bootFindings = collectBootFindingGroups(ctx, {
|
||||
absDesignPath,
|
||||
sidecarCandidates,
|
||||
projectRootPatterns: readProjectRootPatterns(ctx.repoRoot),
|
||||
targetCandidates: workspaceCandidates,
|
||||
});
|
||||
|
||||
const findings = [
|
||||
...checkProduct(ctx.product, ctx.productPath || 'PRODUCT.md'),
|
||||
...(ctx.product
|
||||
? checkNativePlatformEvidence({
|
||||
projectRoot,
|
||||
platform: ctx.platform,
|
||||
product: ctx.product,
|
||||
productPath: ctx.productPath,
|
||||
})
|
||||
: []),
|
||||
...checkDesignSidecar({ designPath: absDesignPath, sidecarCandidates, projectRoot }),
|
||||
...bootFindings.product,
|
||||
...bootFindings.nativePlatform,
|
||||
...bootFindings.designSidecar,
|
||||
...checkDesignDrift({ designPath: absDesignPath, projectRoot }),
|
||||
...checkDesignCoverage({ design: ctx.design, designPath: ctx.designPath, parseDesignMd }),
|
||||
...checkConfig({ projectRoot, repoRoot: ctx.repoRoot }),
|
||||
...checkBuildPathUnset({ projectRoot, repoRoot: ctx.repoRoot, product: ctx.product }),
|
||||
...bootFindings.config,
|
||||
...bootFindings.buildPath,
|
||||
...checkDetectorIgnores({ projectRoot, knownRuleIds }),
|
||||
...checkSurfaceBriefs({ candidates: ctx.surfaceBriefCandidates, projectRoot }),
|
||||
...bootFindings.surfaceBriefs,
|
||||
...checkHookInstallation({
|
||||
projectRoot,
|
||||
repoRoot: ctx.repoRoot,
|
||||
providerId: IMPECCABLE_PROVIDER_ID,
|
||||
}),
|
||||
...checkLegacyLiveState({ projectRoot }),
|
||||
...checkProjectRoots({
|
||||
patterns: readProjectRootPatterns(ctx.repoRoot),
|
||||
candidates: workspaceCandidates,
|
||||
}),
|
||||
...bootFindings.projectRoots,
|
||||
...workspaceResult.findings,
|
||||
];
|
||||
|
||||
|
||||
@@ -488,41 +488,46 @@ export function describeWorkspaceContext(candidates = []) {
|
||||
// ─── Tier 1 orchestration ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Everything a boot can afford. `ctx` is the loadContext result; `extras`
|
||||
* carries values the caller already computed so nothing is recomputed here.
|
||||
* Everything a boot can afford, grouped by artifact so deeper reports can
|
||||
* interleave their own checks without rebuilding this policy. `ctx` is the
|
||||
* loadContext result; `extras` carries values the caller already computed so
|
||||
* nothing is recomputed here.
|
||||
*/
|
||||
export function collectBootFindings(ctx, extras = {}) {
|
||||
if (!ctx) return [];
|
||||
export function collectBootFindingGroups(ctx, extras = {}) {
|
||||
if (!ctx) return {};
|
||||
const projectRoot = ctx.projectRoot || process.cwd();
|
||||
const absProductPath = extras.absProductPath || null;
|
||||
const absDesignPath = extras.absDesignPath || null;
|
||||
|
||||
return [
|
||||
...checkProduct(ctx.product, ctx.productPath || 'PRODUCT.md'),
|
||||
return {
|
||||
product: checkProduct(ctx.product, ctx.productPath || 'PRODUCT.md'),
|
||||
// Only checked once a PRODUCT.md exists. Without one the boot already
|
||||
// emits NO_PRODUCT_MD and routes into init, which asks for the platform
|
||||
// directly; a second signal saying the same thing is noise.
|
||||
...(ctx.product
|
||||
nativePlatform: ctx.product
|
||||
? checkNativePlatformEvidence({
|
||||
projectRoot,
|
||||
platform: ctx.platform,
|
||||
product: ctx.product,
|
||||
productPath: ctx.productPath,
|
||||
})
|
||||
: []),
|
||||
...checkDesignSidecar({
|
||||
: [],
|
||||
designSidecar: checkDesignSidecar({
|
||||
designPath: absDesignPath,
|
||||
sidecarCandidates: extras.sidecarCandidates || [],
|
||||
projectRoot,
|
||||
}),
|
||||
...checkConfig({ projectRoot, repoRoot: ctx.repoRoot }),
|
||||
...checkBuildPathUnset({ projectRoot, repoRoot: ctx.repoRoot, product: ctx.product }),
|
||||
...checkSurfaceBriefs({ candidates: ctx.surfaceBriefCandidates, projectRoot }),
|
||||
...(extras.projectRootPatterns
|
||||
config: checkConfig({ projectRoot, repoRoot: ctx.repoRoot }),
|
||||
buildPath: checkBuildPathUnset({ projectRoot, repoRoot: ctx.repoRoot, product: ctx.product }),
|
||||
surfaceBriefs: checkSurfaceBriefs({ candidates: ctx.surfaceBriefCandidates, projectRoot }),
|
||||
projectRoots: extras.projectRootPatterns
|
||||
? checkProjectRoots({
|
||||
patterns: extras.projectRootPatterns,
|
||||
candidates: extras.targetCandidates || [],
|
||||
})
|
||||
: []),
|
||||
];
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function collectBootFindings(ctx, extras = {}) {
|
||||
return Object.values(collectBootFindingGroups(ctx, extras)).flat();
|
||||
}
|
||||
|
||||
@@ -221,19 +221,6 @@ describe('detectUrl — browser-only fixtures', () => {
|
||||
assert.equal(contrast.length, 3, `expected exactly the 3 flag-column cases, got ${contrast.length}:\n${snippets}`);
|
||||
});
|
||||
|
||||
it('ai-color-palette: oklch neon text flags the should-flag column only', async () => {
|
||||
const f = await detectUrl(`${baseUrl}/fixtures/antipatterns/oklch-neon-text.html`, { visualContrast: false });
|
||||
const neon = f.filter(r =>
|
||||
r.antipattern === 'ai-color-palette' && /neon text on dark background/i.test(r.snippet || '')
|
||||
);
|
||||
assert.equal(
|
||||
neon.length,
|
||||
1,
|
||||
`expected exactly 1 oklch neon-text finding, got ${neon.length}: ${JSON.stringify(f.map(r => r.snippet))}`,
|
||||
);
|
||||
assert.match(neon[0].snippet || '', /Cyan neon text on dark background/i);
|
||||
});
|
||||
|
||||
it('shadowed form.id: a <form> with <input name="id"> does not crash the scan (issue #407)', async () => {
|
||||
// HTMLFormElement named-property shadowing makes form.id / form.className
|
||||
// return the child input element, whose .startsWith throws. Every Shopify
|
||||
|
||||
@@ -634,6 +634,26 @@ describe('doctor CLI', () => {
|
||||
assert.equal(report.ruleRegistryAvailable, true);
|
||||
});
|
||||
|
||||
it('keeps boot and deep findings in their established artifact order', () => {
|
||||
write('PRODUCT.md', '# Product\n\n## Register\n\nbrand\n\n## Users\nDesigners.\n');
|
||||
write('DESIGN.md', '---\nname: Example\n---\n\n# Design System: Example\n');
|
||||
write('.impeccable/design.json', JSON.stringify({ schemaVersion: 1 }));
|
||||
write('.impeccable/config.json', JSON.stringify({ unknownSetting: true }));
|
||||
|
||||
const res = run(['--json']);
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
assert.deepEqual(
|
||||
JSON.parse(res.stdout).findings.map((entry) => entry.id),
|
||||
[
|
||||
'product-deprecated-register',
|
||||
'product-schema-legacy',
|
||||
'design-sidecar-schema-outdated',
|
||||
'design-md-coverage',
|
||||
'config-unknown-keys',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('applies only the automatic migrations under --fix', () => {
|
||||
write('PRODUCT.md', CURRENT_PRODUCT.replace('<!-- impeccable:product-schema 1 -->\n\n', ''));
|
||||
write('DESIGN.json', JSON.stringify({ schemaVersion: 2 }));
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OKLCH Neon Text Fixture</title>
|
||||
<style>
|
||||
:root {
|
||||
--neon: oklch(0.85 0.2 195);
|
||||
--muted: oklch(0.85 0.04 195);
|
||||
--paper: oklch(0.9 0 0);
|
||||
--ground: #050505;
|
||||
--light: #f5f5f5;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 32px;
|
||||
background: var(--ground);
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 24px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.column > h2 {
|
||||
margin: 0 0 2px;
|
||||
color: var(--paper);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
line-height: 1.4;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.neon-cyan { color: var(--neon); }
|
||||
.muted-cyan { color: var(--muted); }
|
||||
.oklch-paper { color: var(--paper); }
|
||||
|
||||
.light-shell {
|
||||
background: var(--light);
|
||||
padding: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="grid">
|
||||
<section class="column" data-col="flag">
|
||||
<h2>Should flag</h2>
|
||||
<p class="neon-cyan">Cyan neon token</p>
|
||||
</section>
|
||||
<section class="column" data-col="pass">
|
||||
<h2>Should pass</h2>
|
||||
<p class="oklch-paper">Achromatic oklch on dark should pass</p>
|
||||
<p class="muted-cyan">Muted cyan oklch on dark should pass</p>
|
||||
<div class="light-shell">
|
||||
<p class="neon-cyan">Cyan oklch on light ground should pass</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
+1
-2
@@ -9,7 +9,6 @@
|
||||
--paper: #f7f3ee;
|
||||
--ink: #171717;
|
||||
--muted: #566174;
|
||||
--flag-white: oklch(1 0 0);
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -111,7 +110,7 @@
|
||||
<h2>Should flag after pixel sampling</h2>
|
||||
|
||||
<article class="image-card light-image">
|
||||
<p style="color: var(--flag-white);">White text on light image should be sampled by pixel contrast.</p>
|
||||
<p style="color: rgb(255, 255, 255);">White text on light image should be sampled by pixel contrast.</p>
|
||||
</article>
|
||||
|
||||
<article class="image-card dark-image">
|
||||
|
||||
Reference in New Issue
Block a user