Fix live picker sizing and divider detection

This commit is contained in:
Paul Bakaus
2026-06-17 13:10:53 +09:00
parent 4f50db2bca
commit c7539c867d
33 changed files with 437 additions and 174 deletions
@@ -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,
@@ -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/
@@ -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,
@@ -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/
@@ -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,
@@ -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/
@@ -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,
@@ -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/
@@ -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,
@@ -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/
@@ -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,
@@ -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/
@@ -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,
@@ -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/
@@ -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,
+21 -9
View File
@@ -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/
@@ -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,
@@ -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/
@@ -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,
@@ -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/
@@ -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,
@@ -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/
@@ -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,
@@ -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/
+5 -3
View File
@@ -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,
@@ -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,
@@ -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/
+8 -3
View File
@@ -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;
+21 -9
View File
@@ -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/
+29 -1
View File
@@ -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'));
+7 -2
View File
@@ -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('<div class="border-l-1 border-blue-500 rounded-md">', 'test.html');
test('detects border-l-2 + rounded', () => {
const f = detectText('<div class="border-l-2 border-blue-500 rounded-md">', 'test.html');
expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
});
test('ignores border-l-1 + rounded', () => {
const f = detectText('<div class="border-l-1 border-blue-500 rounded-md">', 'test.html');
expect(f.filter(r => r.antipattern === 'side-tab')).toHaveLength(0);
});
test('ignores border-l-1 without rounded', () => {
const f = detectText('<div class="border-l-1 border-gray-300">', 'test.html');
expect(f.filter(r => r.antipattern === 'side-tab')).toHaveLength(0);
+9
View File
@@ -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);
}
</style>
</head>
<body>
@@ -319,6 +327,7 @@
<div class="case" id="pass-var-neutral"><h3>var() neutral</h3><p>--line resolves to gray</p></div>
<div class="case" id="pass-var-thin"><h3>var() thin</h3><p>1px too thin to qualify</p></div>
<div class="case" id="pass-var-allsides"><h3>var() all sides</h3><p>uniform, not a side-tab</p></div>
<div class="case" id="pass-oklch-neutral-right-divider"><h3>oklch neutral right divider</h3><p>1px neutral context-bar hairline</p></div>
</div>
</div>
<script src="/js/detect-antipatterns-browser.js"></script>
+20
View File
@@ -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', () => {