From c7539c867d77dbbe6c2b8c26f94ee2e6b2def483 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 17 Jun 2026 13:10:53 +0900 Subject: [PATCH] Fix live picker sizing and divider detection --- .../detector/engines/regex/detect-text.mjs | 8 +++-- .../skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ .../detector/engines/regex/detect-text.mjs | 8 +++-- .../skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ .../detector/engines/regex/detect-text.mjs | 8 +++-- .../skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ .../detector/engines/regex/detect-text.mjs | 8 +++-- .../skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ .../detector/engines/regex/detect-text.mjs | 8 +++-- .../skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ .../detector/engines/regex/detect-text.mjs | 8 +++-- .../skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ .../detector/engines/regex/detect-text.mjs | 8 +++-- .../skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ .../detector/engines/regex/detect-text.mjs | 8 +++-- .pi/skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ .../detector/engines/regex/detect-text.mjs | 8 +++-- .../skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ .../detector/engines/regex/detect-text.mjs | 8 +++-- .../skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ .../detector/engines/regex/detect-text.mjs | 8 +++-- .../skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ .../detector/engines/regex/detect-text.mjs | 8 +++-- .../skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ cli/engine/engines/regex/detect-text.mjs | 8 +++-- .../detector/engines/regex/detect-text.mjs | 8 +++-- .../skills/impeccable/scripts/live-browser.js | 30 +++++++++++++------ site/styles/live-mode.css | 11 +++++-- skill/scripts/live-browser.js | 30 +++++++++++++------ tests/detect-antipatterns-fixtures.test.mjs | 30 ++++++++++++++++++- tests/detect-antipatterns.test.js | 9 ++++-- .../antipatterns/modern-color-borders.html | 9 ++++++ tests/live-browser-regression.test.mjs | 20 +++++++++++++ 33 files changed, 437 insertions(+), 174 deletions(-) diff --git a/.agents/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.agents/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/.agents/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.agents/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/.claude/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.claude/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/.claude/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.claude/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/.cursor/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.cursor/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/.cursor/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.cursor/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/.gemini/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.gemini/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/.gemini/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.gemini/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/.github/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.github/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/.github/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.github/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/.github/skills/impeccable/scripts/live-browser.js +++ b/.github/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/.kiro/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.kiro/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/.kiro/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.kiro/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/.opencode/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.opencode/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/.opencode/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.opencode/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/.pi/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.pi/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/.pi/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.pi/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/.qoder/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.qoder/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/.qoder/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.qoder/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/.qoder/skills/impeccable/scripts/live-browser.js b/.qoder/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/.qoder/skills/impeccable/scripts/live-browser.js +++ b/.qoder/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/.rovodev/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.rovodev/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/.rovodev/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/.trae-cn/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.trae-cn/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/.trae/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.trae/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/.trae/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.trae/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/cli/engine/engines/regex/detect-text.mjs b/cli/engine/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/cli/engine/engines/regex/detect-text.mjs +++ b/cli/engine/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/plugin/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/plugin/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index 6cbd4935b..477b18fa1 100644 --- a/plugin/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/plugin/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,4 +1,5 @@ import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { isNeutralColor } from '../../shared/color.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { finding } from '../../findings.mjs'; @@ -37,10 +38,11 @@ function shouldRunPageAnalyzers(content, filePath) { } function isNeutralBorderColor(str) { - const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i); + const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; const c = m[1].toLowerCase(); if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true; + if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c); const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (hex) { const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; @@ -57,10 +59,10 @@ function isNeutralBorderColor(str) { const REGEX_MATCHERS = [ // --- Side-tab --- { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g, - test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; }, + test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; }, fmt: (m) => m[0] }, { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi, - test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; }, + test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; }, fmt: (m) => m[0].replace(/\s*;?\s*$/, '') }, { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi, test: (m, line) => !isSafeElement(line) && +m[1] >= 3, diff --git a/plugin/skills/impeccable/scripts/live-browser.js b/plugin/skills/impeccable/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/plugin/skills/impeccable/scripts/live-browser.js +++ b/plugin/skills/impeccable/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/site/styles/live-mode.css b/site/styles/live-mode.css index ae942d6f7..7ec40e170 100644 --- a/site/styles/live-mode.css +++ b/site/styles/live-mode.css @@ -292,14 +292,17 @@ display: flex; align-items: center; gap: 2px; - flex: 1 1 auto; - min-width: 0; + flex: 0 0 auto; padding: 4px 6px 4px 2px; } .live-demo-gbar-btn { position: relative; display: inline-flex; align-items: center; + justify-content: center; + box-sizing: border-box; + flex: 0 0 auto; + min-width: 30px; flex-shrink: 0; padding: 6px 8px; background: transparent; @@ -316,7 +319,9 @@ transition: background 0.15s ease, color 0.15s ease; } .live-demo-gbar-btn svg { - flex-shrink: 0; + width: 14px; + height: 14px; + flex: 0 0 14px; } .live-demo-gbar-btn-label { display: inline-block; diff --git a/skill/scripts/live-browser.js b/skill/scripts/live-browser.js index 1b8926333..3cbdaceb1 100644 --- a/skill/scripts/live-browser.js +++ b/skill/scripts/live-browser.js @@ -8289,14 +8289,23 @@ void main() { ].filter(Boolean); } - function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + function applyGlobalBarLabelState(expandInactive, forceCollapse = false) { globalBarModeToggles().forEach((toggle) => { - if (expanded && !pageChatExpanded) toggle._expandLabel?.(); - else if (toggle.dataset.active === 'true') toggle._expandLabel?.(); + if (forceCollapse) toggle._collapseLabel?.(true); + else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.(); else toggle._collapseLabel?.(); }); } + function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) { + const expandInactive = !!(expanded && !pageChatExpanded); + applyGlobalBarLabelState(expandInactive, pageChatExpanded); + + if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) { + applyGlobalBarLabelState(false); + } + } + function pageChatCollapsedWidthPx() { const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W); return Number.isFinite(parsed) ? parsed : 104; @@ -8305,7 +8314,7 @@ void main() { function pageChatExpandedWidth() { if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px'; const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx(); - const barWidth = globalBarEl.getBoundingClientRect().width || 0; + const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0); const nonChatWidth = Math.max(0, barWidth - currentChatWidth); const available = window.innerWidth - 16 - nonChatWidth; const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available)); @@ -9328,6 +9337,7 @@ void main() { zIndex: Z.bar + 5, display: 'flex', alignItems: 'stretch', gap: '0', + width: 'max-content', background: P.surface, border: '1px solid ' + P.border, borderRadius: '8px', @@ -9385,8 +9395,7 @@ void main() { const inner = el('div', { display: 'flex', alignItems: 'center', padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px', - minWidth: '0', - flex: '1 1 auto', + flex: '0 0 auto', }); inner.id = PREFIX + '-global-bar-inner'; globalBarEl.appendChild(inner); @@ -9395,7 +9404,10 @@ void main() { function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) { const b = el('button', { position: 'relative', - display: 'inline-flex', alignItems: 'center', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', + flex: '0 0 auto', + minWidth: '30px', padding: '6px 8px', borderRadius: '7px', border: 'none', background: 'transparent', color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500', @@ -9414,8 +9426,8 @@ void main() { if (!labelEl) return; labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)'; }; - const collapse = () => { - if (!labelEl || b.dataset.active === 'true') return; + const collapse = (force = false) => { + if (!labelEl || (!force && b.dataset.active === 'true')) return; labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)'; }; // Per-button hover only changes color (no layout). The label expand/ diff --git a/tests/detect-antipatterns-fixtures.test.mjs b/tests/detect-antipatterns-fixtures.test.mjs index ee8eeae5a..752342895 100644 --- a/tests/detect-antipatterns-fixtures.test.mjs +++ b/tests/detect-antipatterns-fixtures.test.mjs @@ -10,6 +10,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; import { detectHtml, + detectText, normalizeDesignSystem, } from '../cli/engine/detect-antipatterns.mjs'; @@ -205,7 +206,7 @@ describe('detectHtml — static HTML/CSS fixtures', () => { assert.equal(leftFindings.length, 11, `expected 11 border-left findings, got ${leftFindings.length}`); assert.equal(rightFindings.length, 1, `expected 1 border-right finding, got ${rightFindings.length}`); // PASS column must contribute zero border findings of either flavor. - // There are 13 pass cases: 6 structural neutrals plus 4 labels (plain + // There are 14 pass cases: 7 structural neutrals plus 4 labels (plain // inline form label, label with a neutral gray border, label in a form // row, and a label with a thin 1px colored left border), plus 3 var() // pass cases (neutral-resolving var, thin var, uniform all-sides var). @@ -218,6 +219,33 @@ describe('detectHtml — static HTML/CSS fixtures', () => { ); }); + it('modern-color-borders: regex fallback skips neutral 1px oklch dividers', () => { + const css = ` + .flag-side-tab { + border-radius: 8px; + border-left: 2px solid oklch(65% 0.12 250); + } + + .pass-context-divider { + border-radius: 8px; + border-right: 1px solid oklch(92% 0 0 / 0.12); + } + + .pass-neutral-side { + border-radius: 8px; + border-left: 3px solid oklch(80% 0 0); + } + `; + const f = detectText(css, path.join(FIXTURES, 'modern-color-borders-regex.css')); + const sideTabs = f.filter(r => r.antipattern === 'side-tab'); + assert.equal( + sideTabs.length, + 1, + `expected only the colored 2px side-tab to flag, got: ${sideTabs.map(r => r.snippet).join('; ')}` + ); + assert.match(sideTabs[0].snippet, /border-left: 2px solid oklch/); + }); + it('typography-should-flag: detects all three issues', async () => { const f = await detectHtml(path.join(FIXTURES, 'typography-should-flag.html')); assert.ok(f.some(r => r.antipattern === 'overused-font')); diff --git a/tests/detect-antipatterns.test.js b/tests/detect-antipatterns.test.js index 586ffc4d0..4eda7638a 100644 --- a/tests/detect-antipatterns.test.js +++ b/tests/detect-antipatterns.test.js @@ -136,11 +136,16 @@ describe('detectText — Tailwind side-tab', () => { expect(f.some(r => r.antipattern === 'side-tab')).toBe(true); }); - test('detects border-l-1 + rounded', () => { - const f = detectText('
', 'test.html'); + test('detects border-l-2 + rounded', () => { + const f = detectText('
', 'test.html'); expect(f.some(r => r.antipattern === 'side-tab')).toBe(true); }); + test('ignores border-l-1 + rounded', () => { + const f = detectText('
', 'test.html'); + expect(f.filter(r => r.antipattern === 'side-tab')).toHaveLength(0); + }); + test('ignores border-l-1 without rounded', () => { const f = detectText('
', 'test.html'); expect(f.filter(r => r.antipattern === 'side-tab')).toHaveLength(0); diff --git a/tests/fixtures/antipatterns/modern-color-borders.html b/tests/fixtures/antipatterns/modern-color-borders.html index f51747880..caced953a 100644 --- a/tests/fixtures/antipatterns/modern-color-borders.html +++ b/tests/fixtures/antipatterns/modern-color-borders.html @@ -264,6 +264,14 @@ border: 3px solid var(--brand); border-radius: 4px; } + + /* 14: neutral 1px oklch right divider — context-bar hairline, not a side-tab. */ + #pass-oklch-neutral-right-divider { + width: 400px; + background: #ffffff; + border-radius: 4px; + border-right: 1px solid oklch(92% 0 0 / 0.12); + } @@ -319,6 +327,7 @@

var() neutral

--line resolves to gray

var() thin

1px too thin to qualify

var() all sides

uniform, not a side-tab

+

oklch neutral right divider

1px neutral context-bar hairline

diff --git a/tests/live-browser-regression.test.mjs b/tests/live-browser-regression.test.mjs index d4ad8eebf..6d15346d3 100644 --- a/tests/live-browser-regression.test.mjs +++ b/tests/live-browser-regression.test.mjs @@ -175,6 +175,26 @@ describe('live-browser.js regression guards', () => { /maxWidth: 'calc\(100vw - 16px\)'[\s\S]{0,80}?boxSizing: 'border-box'/, 'global bar should be constrained to the viewport instead of clipping the exit control offscreen', ); + assert.match( + SOURCE, + /globalBarEl = el\('div', \{[\s\S]{0,360}?width: 'max-content'/, + 'fixed-position global bar must use max-content sizing before maxWidth clamps it, or narrow panes clip the exit button', + ); + assert.match( + SOURCE, + /const inner = el\('div', \{[\s\S]{0,220}?flex: '0 0 auto'/, + 'global bar inner controls must not flex-shrink and crop hover labels', + ); + assert.match( + SOURCE, + /function makeIconBtn[\s\S]{0,360}?flex: '0 0 auto'[\s\S]{0,80}?minWidth: '30px'/, + 'global bar icon buttons must keep stable hitboxes when Steer expands', + ); + assert.match( + SOURCE, + /applyGlobalBarLabelState\(expandInactive, pageChatExpanded\)/, + 'expanded Steer should force labels closed without shrinking the icons', + ); }); it('does not autofocus the steering chat while a page editable is focused', () => {