mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Fix live picker sizing and divider detection
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user