mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Harden CSS specificity lint
This commit is contained in:
parent
cf7b717517
commit
7e1f86bd6e
4 changed files with 95 additions and 18 deletions
67
scripts/stylelint-no-specificity-where.mjs
Normal file
67
scripts/stylelint-no-specificity-where.mjs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import stylelint from "stylelint";
|
||||
|
||||
const ruleName = "codex-panel/no-specificity-where";
|
||||
|
||||
const messages = stylelint.utils.ruleMessages(ruleName, {
|
||||
rejected: (selector) => `Do not hide class, id, or attribute selectors inside :where(): "${selector}"`,
|
||||
});
|
||||
|
||||
function rule(primary) {
|
||||
return (root, result) => {
|
||||
if (primary === false) return;
|
||||
|
||||
root.walkRules((ruleNode) => {
|
||||
const selector = ruleNode.selector;
|
||||
if (!selector.includes(":where(")) return;
|
||||
|
||||
for (const range of whereRanges(selector)) {
|
||||
const body = selector.slice(range.bodyStart, range.bodyEnd);
|
||||
if (!/[.#[]/.test(body)) continue;
|
||||
|
||||
stylelint.utils.report({
|
||||
message: messages.rejected,
|
||||
messageArgs: [selector.slice(range.start, range.end)],
|
||||
node: ruleNode,
|
||||
result,
|
||||
ruleName,
|
||||
index: range.start,
|
||||
endIndex: range.end,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function whereRanges(selector) {
|
||||
const ranges = [];
|
||||
let searchFrom = 0;
|
||||
|
||||
while (searchFrom < selector.length) {
|
||||
const start = selector.indexOf(":where(", searchFrom);
|
||||
if (start === -1) break;
|
||||
|
||||
const bodyStart = start + ":where(".length;
|
||||
let depth = 1;
|
||||
let index = bodyStart;
|
||||
|
||||
for (; index < selector.length && depth > 0; index += 1) {
|
||||
const char = selector[index];
|
||||
if (char === "(") depth += 1;
|
||||
if (char === ")") depth -= 1;
|
||||
}
|
||||
|
||||
if (depth === 0) {
|
||||
ranges.push({ start, bodyStart, bodyEnd: index - 1, end: index });
|
||||
searchFrom = index;
|
||||
} else {
|
||||
searchFrom = bodyStart;
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
rule.ruleName = ruleName;
|
||||
rule.messages = messages;
|
||||
|
||||
export default stylelint.createPlugin(ruleName, rule);
|
||||
|
|
@ -1,6 +1,15 @@
|
|||
import noSpecificityWhere from "./scripts/stylelint-no-specificity-where.mjs";
|
||||
|
||||
export default {
|
||||
extends: ["stylelint-config-standard"],
|
||||
plugins: [noSpecificityWhere],
|
||||
rules: {
|
||||
"codex-panel/no-specificity-where": [
|
||||
true,
|
||||
{
|
||||
severity: "warning",
|
||||
},
|
||||
],
|
||||
"color-named": [
|
||||
"never",
|
||||
{
|
||||
|
|
|
|||
26
styles.css
26
styles.css
|
|
@ -373,7 +373,7 @@
|
|||
stroke: currentcolor;
|
||||
}
|
||||
|
||||
.codex-panel-ui__toolbar-control:not(.is-active):focus:not(:hover):not(:focus-visible) {
|
||||
.codex-panel-ui__toolbar-control:where(:focus:not(:hover):not(:focus-visible)) {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: var(--icon-color);
|
||||
|
|
@ -619,24 +619,20 @@
|
|||
font-size: var(--codex-panel-toolbar-meta-size);
|
||||
}
|
||||
|
||||
.codex-panel__toolbar-panel-item:hover:not(:where(:disabled, .is-disabled)) {
|
||||
.codex-panel__toolbar-panel-item:hover:not(:disabled):not(.is-disabled) {
|
||||
color: var(--nav-item-color-hover, var(--text-normal));
|
||||
background: var(--background-modifier-hover);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.codex-panel__toolbar-panel-item:hover:not(:where(:disabled, .is-disabled)) .codex-panel__toolbar-panel-label,
|
||||
.codex-panel__toolbar-panel-item:focus:not(:where(:disabled, .is-disabled)) .codex-panel__toolbar-panel-label,
|
||||
.codex-panel__toolbar-panel-item:focus-visible:not(:where(:disabled, .is-disabled)) .codex-panel__toolbar-panel-label,
|
||||
.codex-panel__toolbar-panel-item:active:not(:where(:disabled, .is-disabled)) .codex-panel__toolbar-panel-label {
|
||||
.codex-panel__toolbar-panel-item:where(:hover, :focus, :focus-visible, :active):not(:disabled):not(.is-disabled)
|
||||
.codex-panel__toolbar-panel-label {
|
||||
color: var(--nav-item-color-hover, var(--text-normal));
|
||||
}
|
||||
|
||||
.codex-panel__toolbar-panel-item:hover:not(:where(:disabled, .is-disabled)) .codex-panel__toolbar-panel-meta,
|
||||
.codex-panel__toolbar-panel-item:focus:not(:where(:disabled, .is-disabled)) .codex-panel__toolbar-panel-meta,
|
||||
.codex-panel__toolbar-panel-item:focus-visible:not(:where(:disabled, .is-disabled)) .codex-panel__toolbar-panel-meta,
|
||||
.codex-panel__toolbar-panel-item:active:not(:where(:disabled, .is-disabled)) .codex-panel__toolbar-panel-meta {
|
||||
.codex-panel__toolbar-panel-item:where(:hover, :focus, :focus-visible, :active):not(:disabled):not(.is-disabled)
|
||||
.codex-panel__toolbar-panel-meta {
|
||||
color: var(--nav-item-color-hover, var(--text-normal));
|
||||
}
|
||||
|
||||
|
|
@ -1242,7 +1238,7 @@
|
|||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
.codex-panel__thread:hover:not(:where(:disabled, .is-disabled)),
|
||||
.codex-panel__thread:hover:not(:disabled):not(.is-disabled),
|
||||
.codex-panel__thread.is-checked {
|
||||
background: transparent;
|
||||
}
|
||||
|
|
@ -1653,11 +1649,11 @@
|
|||
animation: codex-panel-reasoning-dot 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.codex-panel__reasoning:where(.is-active) .codex-panel__reasoning-dots span:nth-child(2) {
|
||||
.codex-panel__reasoning.is-active .codex-panel__reasoning-dots span:where(:nth-child(2)) {
|
||||
animation-delay: 0.18s;
|
||||
}
|
||||
|
||||
.codex-panel__reasoning:where(.is-active) .codex-panel__reasoning-dots span:nth-child(3) {
|
||||
.codex-panel__reasoning.is-active .codex-panel__reasoning-dots span:where(:nth-child(3)) {
|
||||
animation-delay: 0.36s;
|
||||
}
|
||||
|
||||
|
|
@ -2093,11 +2089,11 @@
|
|||
animation: codex-panel-reasoning-dot 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.codex-panel-selection-rewrite__status:where(.is-active) .codex-panel-selection-rewrite__status-dots span:nth-child(2) {
|
||||
.codex-panel-selection-rewrite__status.is-active .codex-panel-selection-rewrite__status-dots span:where(:nth-child(2)) {
|
||||
animation-delay: 0.18s;
|
||||
}
|
||||
|
||||
.codex-panel-selection-rewrite__status:where(.is-active) .codex-panel-selection-rewrite__status-dots span:nth-child(3) {
|
||||
.codex-panel-selection-rewrite__status.is-active .codex-panel-selection-rewrite__status-dots span:where(:nth-child(3)) {
|
||||
animation-delay: 0.36s;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,14 +17,19 @@ describe("panel CSS token scope", () => {
|
|||
});
|
||||
|
||||
describe("chat toolbar CSS", () => {
|
||||
it("does not let mouse-focus reset override active toolbar controls", () => {
|
||||
it("keeps mouse-focus reset less specific than active toolbar controls", () => {
|
||||
const toolbarMouseFocus =
|
||||
/\.codex-panel-ui__toolbar-control:not\(\.is-active\):focus:not\(:hover\):not\(:focus-visible\) \{(?<body>[^}]+)\}/.exec(styles)
|
||||
?.groups?.["body"] ?? "";
|
||||
/\.codex-panel-ui__toolbar-control:where\(:focus:not\(:hover\):not\(:focus-visible\)\) \{(?<body>[^}]+)\}/.exec(styles)?.groups?.[
|
||||
"body"
|
||||
] ?? "";
|
||||
|
||||
expect(toolbarMouseFocus).toContain("background: transparent");
|
||||
expect(toolbarMouseFocus).toContain("color: var(--icon-color)");
|
||||
});
|
||||
|
||||
it("keeps class selectors out of zero-specificity :where selectors", () => {
|
||||
expect(styles).not.toMatch(/:where\([^)]*[.#[]/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("threads view CSS", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue