diff --git a/.agents/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.agents/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.agents/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.agents/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.agents/skills/impeccable/scripts/detector/rules/checks.mjs b/.agents/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.agents/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.agents/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.claude/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.claude/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.claude/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.claude/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.claude/skills/impeccable/scripts/detector/rules/checks.mjs b/.claude/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.claude/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.claude/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.cursor/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.cursor/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.cursor/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.cursor/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs b/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.gemini/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.gemini/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.gemini/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.gemini/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs b/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.github/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.github/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.github/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.github/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.github/skills/impeccable/scripts/detector/rules/checks.mjs b/.github/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.github/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.github/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.grok/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.grok/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.grok/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.grok/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.grok/skills/impeccable/scripts/detector/rules/checks.mjs b/.grok/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.grok/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.grok/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.kiro/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.kiro/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.kiro/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.kiro/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs b/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.opencode/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.opencode/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.opencode/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.opencode/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs b/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.pi/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.pi/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.pi/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.pi/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.pi/skills/impeccable/scripts/detector/rules/checks.mjs b/.pi/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.pi/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.pi/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.qoder/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.qoder/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.qoder/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.qoder/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs b/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.rovodev/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.rovodev/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.rovodev/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs b/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs b/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.trae/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.trae/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.trae/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.trae/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.trae/skills/impeccable/scripts/detector/rules/checks.mjs b/.trae/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.trae/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.trae/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/.vibe/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/.vibe/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/.vibe/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/.vibe/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/.vibe/skills/impeccable/scripts/detector/rules/checks.mjs b/.vibe/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/.vibe/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.vibe/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors, diff --git a/plugin/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs b/plugin/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs index c0f96f60f..53d7be48c 100644 --- a/plugin/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +++ b/plugin/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs'; -import { collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; +import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs'; // --------------------------------------------------------------------------- // jsdom CSS-variable border override map @@ -344,18 +344,29 @@ const STATIC_PROP_MAP = { 'overflow-y': 'overflowY', }; +// parseStaticColor tries parseAnyColor first, which already resolves every +// name in the shared CSS_NAMED_COLORS table. This fallback only carries the +// keywords parseAnyColor deliberately returns null for: the cascade needs +// `transparent` to read as an actual zero-alpha color. const STATIC_NAMED_COLORS = { - black: { r: 0, g: 0, b: 0, a: 1 }, - white: { r: 255, g: 255, b: 255, a: 1 }, transparent: { r: 0, g: 0, b: 0, a: 0 }, - gray: { r: 128, g: 128, b: 128, a: 1 }, - grey: { r: 128, g: 128, b: 128, a: 1 }, - silver: { r: 192, g: 192, b: 192, a: 1 }, - red: { r: 255, g: 0, b: 0, a: 1 }, - green: { r: 0, g: 128, b: 0, a: 1 }, - blue: { r: 0, g: 0, b: 255, a: 1 }, }; +// Named-color alternation for plucking a color token out of shorthand values +// (issue #359: a hardcoded 9-name list here silently dropped `purple`, +// `crimson`, `teal`, ... from border shorthands, so the side defaulted to +// neutral black and side-tab never fired on .html files). Derived from the +// same table parseAnyColor resolves against, so extraction and parsing can't +// drift apart. Longest-first so names containing other names as substrings +// (rebeccapurple) are matched whole. +const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)] + .sort((a, b) => b.length - a.length) + .join('|'); +const STATIC_COLOR_TOKEN_RE = new RegExp( + `(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`, + 'i' +); + function splitCssList(value) { const parts = []; let depth = 0, quote = '', start = 0; @@ -441,7 +452,7 @@ function extractStaticColor(value) { } return ''; } - const colorLike = raw.match(/(?:rgba?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)|lch\([^)]+\)|lab\([^)]+\)|hsla?\([^)]+\)|hwb\([^)]+\)|#[0-9a-f]{3,8}\b|\b(?:black|white|gray|grey|silver|red|green|blue|transparent)\b)/i); + const colorLike = raw.match(STATIC_COLOR_TOKEN_RE); if (!colorLike) return ''; return colorLike[0]; } diff --git a/plugin/skills/impeccable/scripts/detector/rules/checks.mjs b/plugin/skills/impeccable/scripts/detector/rules/checks.mjs index 418aa8f45..141a209a2 100644 --- a/plugin/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/plugin/skills/impeccable/scripts/detector/rules/checks.mjs @@ -5369,6 +5369,7 @@ function checkFirstViewportColumnOverflowDOM() { } export { + CSS_NAMED_COLORS, checkBorders, isEmojiOnlyText, checkColors,