mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 09:06:53 +03:00
Refine product and visual work lifecycle
This commit is contained in:
@@ -312,17 +312,6 @@ const ANTIPATTERNS = [
|
||||
skillSection: 'Layout & Space',
|
||||
skillGuideline: 'numbered section markers',
|
||||
},
|
||||
{
|
||||
id: 'numbered-section-markers',
|
||||
category: 'slop',
|
||||
scopes: ['layout'],
|
||||
severity: 'advisory',
|
||||
name: 'Numbered section markers (01 / 02 / 03)',
|
||||
description:
|
||||
'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.',
|
||||
skillSection: 'Layout & Space',
|
||||
skillGuideline: 'numbered section markers',
|
||||
},
|
||||
{
|
||||
id: 'em-dash-overuse',
|
||||
category: 'slop',
|
||||
|
||||
@@ -280,22 +280,6 @@ const REGEX_ANALYZERS = [
|
||||
if (count === 0) return [];
|
||||
return [finding('marketing-buzzword', filePath, `${count} buzzword phrase${count === 1 ? '' : 's'}: "${firstSample}"`)];
|
||||
},
|
||||
// Numbered section markers (01 / 02 / 03 ...)
|
||||
(content, filePath) => {
|
||||
const text = stripHtmlToText(content);
|
||||
const re = /\b(0[1-9]|1[0-2])\b/g;
|
||||
const seen = new Set();
|
||||
let m;
|
||||
while ((m = re.exec(text)) !== null) seen.add(m[1]);
|
||||
if (seen.size < 3) return [];
|
||||
const sorted = [...seen].sort();
|
||||
let sequential = 0;
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
if (parseInt(sorted[i], 10) === parseInt(sorted[i - 1], 10) + 1) sequential++;
|
||||
}
|
||||
if (sequential < 2) return [];
|
||||
return [finding('numbered-section-markers', filePath, `Sequence: ${sorted.slice(0, 6).join(', ')}`)];
|
||||
},
|
||||
// Aphoristic cadence: manufactured-contrast + short-rebuttal
|
||||
(content, filePath) => {
|
||||
const text = stripHtmlToText(content);
|
||||
@@ -430,21 +414,20 @@ function runRegexMatchers(lines, filePath, lineOffset = 0, blockContext = null,
|
||||
}
|
||||
|
||||
/** Page-level analyzers that scan rendered text content (em-dash use,
|
||||
* buzzword phrases, numbered section markers, aphoristic cadence).
|
||||
* buzzword phrases, aphoristic cadence).
|
||||
* These are detector-agnostic — they work on any HTML/text source
|
||||
* and don't need a parsed DOM. Exported so detectHtml can call them
|
||||
* for `.html` files (which otherwise skip the regex engine). */
|
||||
const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
'em-dash-overuse',
|
||||
'marketing-buzzword',
|
||||
'numbered-section-markers',
|
||||
'aphoristic-cadence',
|
||||
];
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
// The 3 text-content analyzers are at indices 3-5 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
const analyzer = REGEX_ANALYZERS[3 + i];
|
||||
@@ -535,7 +518,6 @@ function detectText(content, filePath, options = {}) {
|
||||
'monotonous-spacing',
|
||||
'em-dash-overuse',
|
||||
'marketing-buzzword',
|
||||
'numbered-section-markers',
|
||||
'aphoristic-cadence',
|
||||
'dark-glow',
|
||||
];
|
||||
|
||||
@@ -210,17 +210,6 @@ const ANTIPATTERNS = [
|
||||
skillSection: 'Layout & Space',
|
||||
skillGuideline: 'numbered section markers',
|
||||
},
|
||||
{
|
||||
id: 'numbered-section-markers',
|
||||
category: 'slop',
|
||||
scopes: ['layout'],
|
||||
severity: 'advisory',
|
||||
name: 'Numbered section markers (01 / 02 / 03)',
|
||||
description:
|
||||
'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.',
|
||||
skillSection: 'Layout & Space',
|
||||
skillGuideline: 'numbered section markers',
|
||||
},
|
||||
{
|
||||
id: 'em-dash-overuse',
|
||||
category: 'slop',
|
||||
|
||||
+42
-10
@@ -352,27 +352,57 @@ function isAccentColor(cssColor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveHeroHeadingSizePx(value) {
|
||||
const input = String(value || '').trim().toLowerCase();
|
||||
if (!input) return 0;
|
||||
|
||||
const simpleLengthPx = (token) => {
|
||||
const match = /^(-?\d*\.?\d+)\s*(px|rem|em|%)?$/.exec(String(token || '').trim());
|
||||
if (!match) return null;
|
||||
const amount = Number(match[1]);
|
||||
if (!Number.isFinite(amount)) return null;
|
||||
if (match[2] === 'rem' || match[2] === 'em') return amount * 16;
|
||||
if (match[2] === '%') return amount * 0.16;
|
||||
return amount;
|
||||
};
|
||||
|
||||
const direct = simpleLengthPx(input);
|
||||
if (direct !== null) return direct;
|
||||
|
||||
// Static CSS engines cannot resolve viewport units, but clamp's min/max
|
||||
// bounds still tell us whether the heading can ever reach hero scale.
|
||||
const clamp = /^clamp\((.*)\)$/.exec(input);
|
||||
if (clamp) {
|
||||
const parts = clamp[1].split(',');
|
||||
if (parts.length === 3) {
|
||||
const bounds = [simpleLengthPx(parts[0]), simpleLengthPx(parts[2])]
|
||||
.filter((candidate) => candidate !== null);
|
||||
if (bounds.length > 0) return Math.max(...bounds);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Sibling-relationship rule. Anchor on a hero-scale h1, look at the
|
||||
// previousElementSibling, and gate on EITHER the classic tracked-
|
||||
// uppercase eyebrow OR the modern accent-colored bold eyebrow.
|
||||
function checkHeroEyebrow(opts) {
|
||||
const {
|
||||
headingTag, headingText, headingFontSize,
|
||||
headingInApplicationContext,
|
||||
siblingTag, siblingText, siblingTextTransform,
|
||||
siblingFontSize, siblingLetterSpacing,
|
||||
siblingFontWeight, siblingColor,
|
||||
siblingHasAccentDashPseudo,
|
||||
} = opts;
|
||||
if (headingTag !== 'h1') return [];
|
||||
// We previously gated on headingFontSize >= 48 to anchor "hero scale".
|
||||
// But modern hero h1s use clamp() / vw / var(--text-*), none of which
|
||||
// jsdom can resolve — the computed value comes back as "2em" or
|
||||
// "var(--text-9xl)" and parseFloat returns 2 or NaN. The gate fails
|
||||
// on virtually every Tailwind v4 / framework build. The other gates
|
||||
// (sibling text 2-60 chars, font-size ≤ 14px, accent-bold OR
|
||||
// tracked-caps) are tight enough to avoid false positives on non-
|
||||
// hero h1s — a tiny tan label directly above any h1 is the
|
||||
// antipattern regardless of how big the h1 ends up.
|
||||
// This is specifically a marketing-hero cliché, not a ban on compact
|
||||
// context labels in product UI (for example, a station name inside a tab
|
||||
// panel). Browser-computed sizes are reliable; the static adapter also
|
||||
// resolves ordinary px/rem/em and clamp() bounds before reaching here.
|
||||
if (headingInApplicationContext) return [];
|
||||
if (!(headingFontSize >= 48)) return [];
|
||||
if (!siblingTag) return [];
|
||||
// An h2 above an h1 is a different anti-pattern (heading hierarchy / dual
|
||||
// headings) — never an eyebrow.
|
||||
@@ -1938,6 +1968,7 @@ function checkElementHeroEyebrowDOM(el) {
|
||||
headingTag: tag,
|
||||
headingText: el.textContent || '',
|
||||
headingFontSize: parseFloat(headStyle.fontSize) || 0,
|
||||
headingInApplicationContext: !!el.closest('[role="tabpanel"], [role="dialog"], [role="application"], dialog'),
|
||||
siblingTag: sibling.tagName.toLowerCase(),
|
||||
siblingText: sibling.textContent || '',
|
||||
siblingTextTransform: sibStyle.textTransform || '',
|
||||
@@ -3367,7 +3398,8 @@ function checkElementHeroEyebrow(el, style, tag, window, customPropMap) {
|
||||
return checkHeroEyebrow({
|
||||
headingTag: tag,
|
||||
headingText: el.textContent || '',
|
||||
headingFontSize: parseFloat(headingFontSizeRaw) || 0,
|
||||
headingFontSize: resolveHeroHeadingSizePx(headingFontSizeRaw),
|
||||
headingInApplicationContext: !!el.closest?.('[role="tabpanel"], [role="dialog"], [role="application"], dialog'),
|
||||
siblingTag: sibling.tagName.toLowerCase(),
|
||||
siblingText: sibling.textContent || '',
|
||||
siblingTextTransform: sibStyle.textTransform || '',
|
||||
|
||||
Reference in New Issue
Block a user