Update rules table with category information (#28651)

Summary
--

This PR updates our rules page with a big table inspired by [oxlint] and
[clippy]. I'm afraid this
might be a bit of a pain to review, as Codex added a ton of JS and CSS
to achieve our desired
functionality. I iterated with it a few times trying to shrink the diff
but without much luck.

Supersedes https://github.com/astral-sh/ruff/pull/28099

[clippy]: https://rust-lang.github.io/rust-clippy/master/
[oxlint]: https://oxc.rs/docs/guide/usage/linter/rules.html

Test Plan
--

Mostly local testing:


https://github.com/user-attachments/assets/fbf70dd0-1e43-467f-8d35-2767203aab3f
This commit is contained in:
Brent Westbrook authored and GitHub committed 2026-09-21 12:51:35 -04:00
1 parent 68073852bf
commit 67b3bd4122
6 files changed
+687 -150

No files matched your search

+119 -147
View File
@@ -2,19 +2,17 @@
//!
//! Used for <https://docs.astral.sh/ruff/rules/>.
use itertools::Itertools;
use ruff_linter::codes::RuleStatus;
use std::borrow::Cow;
use std::collections::{BTreeMap, HashSet};
use std::fmt::Write;
use itertools::Itertools;
use strum::IntoEnumIterator;
use ruff_linter::FixAvailability;
use ruff_linter::codes::{Category, RuleStatus};
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use ruff_linter::settings::LinterSettings;
use ruff_linter::settings::rule_table::RuleTable;
use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix;
use ruff_options_metadata::OptionsMetadata;
use ruff_workspace::options::Options;
const DEFAULT_SYMBOL: &str = "✅";
const FIX_SYMBOL: &str = "🛠️";
@@ -33,26 +31,26 @@ const SYMBOLS_CONTAINER: &str =
fn generate_table(
table_out: &mut String,
rules: impl IntoIterator<Item = Rule>,
linter: Option<&Linter>,
default_rules: &RuleTable,
) {
if linter.is_some() {
table_out.push_str("| Code { scope='col' } ");
}
table_out.push_str(
"| Name { scope='col' } | Message { scope='col' } | \
<span style='position: relative;'>\
<span class='sr-only'>Status/Fix/Default</span>\
</span> { scope='col' } |",
let table_start = table_out.len();
table_out.push('\n');
let default_categories = Category::default_categories().iter().join(" ");
let _ = writeln!(
table_out,
"| Code {{ scope='col' .rule-code }} \
| Rule {{ scope='col' .rule-identity }} \
| Category {{ scope='col' .rule-category data-default-categories='{default_categories}' }} \
| Linter {{ scope='col' .rule-linter }} \
| Status {{ scope='col' .rule-status aria-label='Status, fix availability, and default selection' }} |"
);
table_out.push_str("| ---- | ---- | -------- | ------ | -: |");
table_out.push('\n');
if linter.is_some() {
table_out.push_str("| ---- ");
}
table_out.push_str("| ---- | ------- | -: |");
table_out.push('\n');
let mut seen_anchors = HashSet::new();
let mut linters = BTreeMap::new();
for rule in rules {
let status_token = match rule.status() {
let status = rule.status();
let status_token = match status {
RuleStatus::Removed { since } => {
format!(
"<span aria-hidden='true' {SYMBOL_STYLE} title='Rule was removed in {since}'>{REMOVED_SYMBOL}</span><span class='sr-only'>Rule was removed in {since}</span>"
@@ -75,13 +73,15 @@ fn generate_table(
}
};
let fix_token = match rule.fixable() {
FixAvailability::Always | FixAvailability::Sometimes => {
let fix_token = if matches!(
rule.fixable(),
FixAvailability::Always | FixAvailability::Sometimes
) {
format!(
"<span aria-hidden='true' {SYMBOL_STYLE} title='Automatic fix available'>{FIX_SYMBOL}</span><span class='sr-only'>Automatic fix available</span>"
)
}
FixAvailability::None => format!("<span {SYMBOL_STYLE}></span>"),
} else {
format!("<span {SYMBOL_STYLE}></span>")
};
let default_token = if default_rules.enabled(rule) {
@@ -94,15 +94,7 @@ fn generate_table(
let rule_name = rule.name();
// If the message ends in a bracketed expression (like: "Use {replacement}"), escape the
// brackets. Otherwise, it'll be interpreted as an HTML attribute via the `attr_list`
// plugin. (Above, we'd convert to "Use {replacement\}".)
let message = rule.message_formats()[0];
let message = if let Some(prefix) = message.strip_suffix('}') {
Cow::Owned(format!("{prefix}\\}}"))
} else {
Cow::Borrowed(message)
};
// Start and end of style spans
let mut ss = "";
@@ -115,35 +107,114 @@ fn generate_table(
se = "</span>";
}
if let Some(linter) = linter {
let _ = write!(
table_out,
"| {ss}{prefix}{code}{se} {{ #{prefix}{code} }} ",
prefix = linter.common_prefix(),
code = linter.code_for_rule(rule).unwrap(),
let code = rule.noqa_code().map(|code| code.to_string());
let linter = code
.as_deref()
.and_then(|code| Linter::parse_code(code).map(|(linter, _)| linter));
let (linter_name, linter_slug, linter_label) = if let Some(linter) = &linter {
let prefixes = match linter.common_prefix() {
"" => linter
.upstream_categories()
.into_iter()
.flatten()
.map(|category| category.prefix)
.join(", "),
prefix => prefix.to_string(),
};
// The linter names for Ruff and NumPy are suffixed with "-specific rules."
let name = linter.name().trim_end_matches("-specific rules");
let slug = format!(
"{}-{}",
linter.name().to_lowercase().replace(' ', "-"),
prefixes.to_lowercase().replace(", ", "-"),
);
(name, slug, format!("{name} ({prefixes})"))
} else {
(
"—",
"rules-without-codes".to_string(),
"No originating linter".to_string(),
)
};
// Preserve links to the old linter headings on their first table row.
let linter_anchor = if seen_anchors.insert(linter_slug.clone()) {
linters.insert(
linter_label.to_lowercase(),
(linter_slug.clone(), linter_label.clone()),
);
format!("#{linter_slug}")
} else {
String::new()
};
if let Some(code) = code {
table_out.push_str("| ");
// The former subgroups have separate anchors in addition to each rule's code.
if let Some(linter) = linter
&& let Some(category) = rule.upstream_category(&linter)
{
let slug = format!(
"{}-{}{}",
category.category.to_lowercase().replace(' ', "-"),
linter.common_prefix().to_lowercase(),
category.prefix.to_lowercase(),
);
if seen_anchors.insert(slug.clone()) {
let _ = write!(table_out, "<span id='{slug}'></span>");
}
}
let _ = write!(table_out, "{ss}`{code}`{se} {{ #{code} .rule-code }} ");
} else {
let _ = write!(table_out, "| {ss}—{se} {{ .rule-code }} ");
}
// Message placeholders can be mistaken for an attribute list on the table cell.
// Keep the identity class on an HTML wrapper so messages retain their Markdown.
#[expect(clippy::or_fun_call)]
let _ = write!(
table_out,
"| {ss}{explanation}{se} | {ss}{message}{se} | <div {SYMBOLS_CONTAINER}>{status_token}{fix_token}{default_token}</div>|",
"| <span class='rule-identity'>{ss}{explanation}<br>{message}{se}</span> \
| {ss}{category}{se} {{ .rule-category }} \
| {ss}{linter_name}{se} {{ {linter_anchor} .rule-linter data-linter='{linter_slug}' }} \
| <div {SYMBOLS_CONTAINER}>{status_token}{fix_token}{default_token}</div> {{ .rule-status data-status='{status}' }} |",
category = rule.category(),
explanation = rule
.explanation()
.is_some()
.then_some(format_args!("[{rule_name}](rules/{rule_name}.md)"))
.unwrap_or(format_args!("{rule_name}")),
.then_some(format_args!("[`{rule_name}`](rules/{rule_name}.md)"))
.unwrap_or(format_args!("`{rule_name}`")),
);
table_out.push('\n');
}
table_out.push('\n');
// Reuse the table's linter labels and slugs for the filter options.
let category_options = Category::iter()
.map(|category| {
format!("<label><input type='checkbox' name='category' value='{category}' checked> {category}</label>")
})
.join("\n");
let linter_options = linters
.values()
.map(|(value, label)| {
format!("<label><input type='checkbox' name='linter' value='{value}' checked> {label}</label>")
})
.join("\n");
table_out.insert_str(
table_start,
&format!(
include_str!("../../../docs/.overrides/partials/rule-filters.html"),
category_options = category_options,
linter_options = linter_options,
),
);
}
pub(crate) fn generate() -> String {
// Generate the table string.
let mut table_out = String::new();
table_out.push_str("### Legend");
table_out.push_str("<details markdown=\"1\">\n<summary>Rule legend</summary>\n\n### Legend");
table_out.push('\n');
let _ = write!(
@@ -176,114 +247,15 @@ pub(crate) fn generate() -> String {
);
table_out.push_str("\n\n");
table_out.push_str("All rules not marked as preview, deprecated or removed are stable.");
table_out.push('\n');
table_out.push_str("\n\n</details>\n\n");
let default_rules = LinterSettings::default().rules;
for linter in Linter::iter() {
let codes_csv: String = match linter.common_prefix() {
"" => linter
.upstream_categories()
.unwrap()
.iter()
.map(|c| c.prefix)
.join(", "),
prefix => prefix.to_string(),
};
let _ = write!(&mut table_out, "### {} ({codes_csv})", linter.name());
table_out.push('\n');
table_out.push('\n');
if let Some(url) = linter.url() {
let host = url
.trim_start_matches("https://")
.split('/')
.next()
.unwrap();
let _ = write!(
table_out,
"For more, see [{}]({}) on {}.",
linter.name(),
url,
match host {
"pypi.org" => "PyPI",
"github.com" => "GitHub",
host => panic!(
"unexpected host in URL of {}, expected pypi.org or github.com but found \
{host}",
linter.name()
),
}
);
table_out.push('\n');
table_out.push('\n');
}
// The linter names for Ruff and NumPy are suffixed with "-specific rules."
let linter_name = linter.name().trim_end_matches("-specific rules");
// Several linter names are capitalized, but their settings are not.
let linter_name_lower = linter_name.to_lowercase();
if Options::metadata().has(&format!("lint.{linter_name_lower}")) {
let _ = write!(
table_out,
"For related settings, see [{linter_name}](settings.md#lint{linter_name_lower}).",
);
table_out.push('\n');
table_out.push('\n');
}
let rules_by_upstream_category = linter
.all_rules()
.map(|rule| (rule.upstream_category(&linter), rule))
.into_group_map();
let mut rules_by_upstream_category: Vec<_> = rules_by_upstream_category.iter().collect();
// Sort the upstream categories alphabetically by prefix.
rules_by_upstream_category.sort_by(|(a, _), (b, _)| {
a.as_ref()
.map(|category| category.prefix)
.unwrap_or_default()
.cmp(
b.as_ref()
.map(|category| category.prefix)
.unwrap_or_default(),
)
});
if rules_by_upstream_category.len() > 1 {
for (opt, rules) in rules_by_upstream_category {
if opt.is_some() {
let UpstreamCategoryAndPrefix { category, prefix } = opt.unwrap();
match codes_csv.as_str() {
"PL" => {
let _ = write!(table_out, "#### {category} ({codes_csv}{prefix})");
}
_ => {
let _ = write!(table_out, "#### {category} ({prefix})");
}
}
}
table_out.push('\n');
table_out.push('\n');
generate_table(&mut table_out, rules.clone(), Some(&linter), &default_rules);
}
} else {
generate_table(
&mut table_out,
linter.all_rules(),
Some(&linter),
&default_rules,
Linter::iter()
.flat_map(|linter| linter.all_rules())
.chain(Rule::iter().filter(|rule| rule.noqa_code().is_none())),
&LinterSettings::default().rules,
);
}
}
let mut codeless_rules = Rule::iter()
.filter(|rule| rule.noqa_code().is_none())
.peekable();
if codeless_rules.peek().is_some() {
table_out.push_str("### Rules without codes\n\n");
generate_table(&mut table_out, codeless_rules, None, &default_rules);
}
table_out
}
+14 -3
View File
@@ -109,9 +109,9 @@ impl serde::Serialize for NoqaCode {
/// ALL < category < linter group < linter prefix < rule
/// ```
///
/// The ordering of variants isn't currently used anywhere, but they should be kept in descending
/// order of severity, with error categories first, followed by warning, and then by off-by-default
/// categories.
/// Variants are ordered by descending severity, with error categories first, followed by warning,
/// and then by off-by-default categories. The rule documentation lists category filter options
/// in this order.
///
/// See our [rule categorization guidelines] for more information on assigning categories.
///
@@ -223,6 +223,17 @@ pub enum RuleStatus {
Removed { since: &'static str },
}
impl std::fmt::Display for RuleStatus {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
RuleStatus::Stable { .. } => "stable",
RuleStatus::Preview { .. } => "preview",
RuleStatus::Deprecated { .. } => "deprecated",
RuleStatus::Removed { .. } => "removed",
})
}
}
#[ruff_macros::map_codes]
pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleStatus, Rule)> {
#[expect(clippy::enum_glob_use)]
@@ -0,0 +1,59 @@
<form id="rule-filters" class="rule-filters" role="search" aria-label="Filter rules" hidden>
<div class="rule-filters__controls">
<label class="rule-filters__search">
Search rules
<input type="search" name="rule-search" placeholder="Code or name"
autocomplete="off" spellcheck="false">
</label>
<fieldset class="rule-filters__category">
<legend>Category</legend>
<div data-filter="category">
<button type="button" popovertarget="rule-category-options"><span>All</span><span aria-hidden="true">▾</span></button>
<div id="rule-category-options" class="rule-filters__options" popover>
<div class="rule-filters__actions">
<button type="button" data-preset="all">All</button>
<button type="button" data-preset="default">Default</button>
<button type="button" data-preset="none">None</button>
</div>
{category_options}
</div>
</div>
</fieldset>
<fieldset class="rule-filters__linter">
<legend>Linter</legend>
<div data-filter="linter">
<button type="button" popovertarget="rule-linter-options"><span>All</span><span aria-hidden="true">▾</span></button>
<div id="rule-linter-options" class="rule-filters__options" popover>
<div class="rule-filters__actions">
<button type="button" data-preset="all">All</button>
<button type="button" data-preset="none">None</button>
</div>
{linter_options}
</div>
</div>
</fieldset>
<fieldset class="rule-filters__status">
<legend>Status</legend>
<div data-filter="status">
<button type="button" popovertarget="rule-status-options"><span>All</span><span aria-hidden="true">▾</span></button>
<div id="rule-status-options" class="rule-filters__options" popover>
<div class="rule-filters__actions">
<button type="button" data-preset="all">All</button>
<button type="button" data-preset="none">None</button>
</div>
<label><input type="checkbox" name="status" value="stable" checked> Stable</label>
<label><input type="checkbox" name="status" value="preview" checked> Preview</label>
<label><input type="checkbox" name="status" value="deprecated" checked> Deprecated</label>
<label><input type="checkbox" name="status" value="removed" checked> Removed</label>
</div>
</div>
</fieldset>
<button type="reset">Clear filters</button>
</div>
<p class="rule-filters__count" role="status" aria-live="polite" aria-atomic="true"></p>
<p class="rule-filters__empty" hidden>No matching rules. Try another search or clear the filters.</p>
</form>
+299
View File
@@ -0,0 +1,299 @@
(() => {
let restoreFilters = () => {};
document$.subscribe(() => {
restoreFilters = () => {};
const form = document.getElementById("rule-filters");
if (!form || !("popover" in HTMLElement.prototype)) {
return;
}
const article = form.closest("article");
const table = article.querySelector(".rule-category")?.closest("table");
if (!table) {
return;
}
table.id = "rule-catalog";
const categoryMetadata =
table.tHead.querySelector(".rule-category").dataset;
const body = table.tBodies[0];
const rows = Array.from(body.rows, (element) => {
const [code, identity, category, linter, status] = element.cells;
return {
element,
text: `${code.textContent} ${identity.querySelector("code").textContent}`.toLowerCase(),
category: category.textContent.trim(),
linter: linter.dataset.linter,
status: status.dataset.status,
};
});
const search = form.elements.namedItem("rule-search");
const count = form.querySelector(".rule-filters__count");
const empty = form.querySelector(".rule-filters__empty");
const filters = [...form.querySelectorAll("[data-filter]")].map((group) => {
const name = group.dataset.filter;
const trigger = group.querySelector("[popovertarget]");
const panel = group.querySelector("[popover]");
const selection = trigger.firstElementChild;
const options = [...panel.querySelectorAll("input")];
const allValues = options.map((option) => option.value);
const isAll = () => options.every((option) => option.checked);
const updateSummary = () => {
const labels = options
.filter((option) => option.checked)
.map((option) => option.parentElement.textContent.trim());
selection.textContent =
labels.length === options.length
? "All"
: labels.length === 0
? "None"
: labels.length === 1
? labels[0]
: `${labels.length} selected`;
trigger.title =
isAll() || !labels.length ? selection.textContent : labels.join(", ");
trigger.setAttribute(
"aria-label",
`${group.closest("fieldset").querySelector("legend").textContent}: ${selection.textContent}`,
);
};
const setValues = (selected = allValues) => {
for (const option of options) {
option.checked = selected.includes(option.value);
}
};
const presets = {
all: allValues,
default: categoryMetadata.defaultCategories.split(" "),
none: [],
};
for (const button of panel.querySelectorAll("[data-preset]")) {
button.addEventListener("click", () => {
setValues(presets[button.dataset.preset]);
applyFilters();
});
}
const controls = [...panel.querySelectorAll("button, input")];
group.addEventListener("keydown", (event) => {
if (
!panel.matches(":popover-open") ||
(event.key !== "ArrowDown" && event.key !== "ArrowUp") ||
event.altKey ||
event.ctrlKey ||
event.metaKey
) {
return;
}
event.preventDefault();
const direction = event.key === "ArrowDown" ? 1 : -1;
const index = controls.indexOf(event.target);
const nextIndex =
index === -1
? direction === 1
? 0
: controls.length - 1
: Math.max(0, Math.min(controls.length - 1, index + direction));
controls[nextIndex]?.focus();
});
group.addEventListener("focusout", (event) => {
if (
panel.matches(":popover-open") &&
event.relatedTarget &&
!group.contains(event.relatedTarget)
) {
panel.hidePopover();
}
});
return {
name,
isAll,
setValues,
updateSummary,
};
});
function clearFilters() {
search.value = "";
for (const filter of filters) {
filter.setValues();
}
}
const container = table.closest(".md-typeset__scrollwrap") || table;
container.classList.add("rule-catalog-scroll");
container.setAttribute("role", "region");
container.setAttribute("aria-label", "Rules");
container.tabIndex = 0;
form.hidden = false;
for (const toc of document.querySelectorAll(".md-nav--secondary")) {
toc.hidden = true;
toc.parentElement.classList.add("rule-index-nav");
}
function filter() {
for (const filter of filters) {
filter.updateSummary();
}
const formData = new FormData(form);
const categories = formData.getAll("category");
const selectedLinters = formData.getAll("linter");
const statuses = formData.getAll("status");
const terms = search.value
.trim()
.toLowerCase()
.split(/\s+/)
.filter(Boolean);
let visible = 0;
for (const row of rows) {
const matches =
categories.includes(row.category) &&
selectedLinters.includes(row.linter) &&
statuses.includes(row.status) &&
terms.every((term) => row.text.includes(term));
row.element.hidden = !matches;
if (matches) {
visible++;
}
}
count.textContent =
visible === rows.length
? `${visible} rules`
: `${visible} of ${rows.length} rules`;
empty.hidden = visible !== 0;
container.hidden = visible === 0;
}
const fragmentLinks = [...document.querySelectorAll("a[href]")].filter(
(link) =>
link.hash &&
link.origin === location.origin &&
link.pathname === location.pathname,
);
function updateFragmentLinks() {
// Material resolves fragment links before filters can change the query.
for (const link of fragmentLinks) {
link.search = location.search;
}
}
function updateURL(clearFragment = false) {
const formData = new FormData(form);
const url = new URL(location.href);
if (clearFragment) {
url.hash = "";
}
if (search.value) {
url.searchParams.set(search.name, search.value);
} else {
url.searchParams.delete(search.name);
}
for (const filter of filters) {
url.searchParams.delete(filter.name);
if (filter.isAll()) {
continue;
}
const selected = formData.getAll(filter.name);
// An omitted parameter means All; preserve an explicit empty selection.
if (!selected.length) {
url.searchParams.set(filter.name, "none");
}
for (const value of selected) {
url.searchParams.append(filter.name, value);
}
}
history.replaceState(history.state, "", url);
updateFragmentLinks();
}
restoreFilters = () => {
if (!form.isConnected) {
return;
}
const url = new URL(location.href);
search.value = url.searchParams.get(search.name) || "";
for (const filter of filters) {
filter.setValues(
url.searchParams.has(filter.name)
? url.searchParams.getAll(filter.name)
: undefined,
);
}
let fragment;
try {
fragment = decodeURIComponent(url.hash.slice(1));
} catch {
fragment = "";
}
filter();
updateFragmentLinks();
const target = fragment && document.getElementById(fragment);
if (!target || !article.contains(target)) {
return;
}
const details = target.closest("details");
if (details) {
details.open = true;
}
if (target.closest("[hidden]")) {
clearFilters();
filter();
updateURL();
target.scrollIntoView();
}
};
function applyFilters() {
filter();
updateURL(true);
}
form.addEventListener("input", applyFilters);
form.addEventListener("submit", (event) => {
event.preventDefault();
applyFilters();
});
form.addEventListener("reset", (event) => {
event.preventDefault();
clearFilters();
applyFilters();
});
restoreFilters();
});
location$.subscribe(() => restoreFilters());
})();
+194
View File
@@ -0,0 +1,194 @@
.md-typeset .rule-filters {
margin: 1em 0;
padding: 0.75rem 0;
border-bottom: 1px solid var(--md-default-fg-color--lightest);
}
.rule-filters__controls {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 0.75rem;
}
.rule-filters__search,
.rule-filters fieldset {
display: flex;
flex-direction: column;
min-width: 0;
gap: 0.3rem;
font-size: 0.7rem;
font-weight: 600;
}
.rule-filters fieldset {
margin: 0;
padding: 0;
border: 0;
}
.rule-filters legend {
margin-bottom: 0.3rem;
}
.rule-filters [popovertarget] {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.4rem;
width: 100%;
anchor-name: var(--rule-filter-anchor);
}
.rule-filters [popovertarget] span:first-child {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.rule-filters__options {
width: 14rem;
max-width: calc(100vw - 2rem);
max-height: min(16rem, calc(50dvh - 2rem));
padding: 0.3rem;
overflow-y: auto;
border: 1px solid var(--md-default-fg-color--lightest);
border-radius: 0.2rem;
background: var(--md-default-bg-color);
color: var(--md-default-fg-color);
box-shadow: 0 0.2rem 0.5rem #0002;
position-anchor: var(--rule-filter-anchor);
}
/* Browsers without anchor positioning keep the popover centered. */
@supports (top: anchor(bottom)) {
.rule-filters__options {
inset: auto;
top: anchor(bottom);
left: anchor(left);
width: anchor-size(width);
margin: 0.25rem 0 0;
position-try-fallbacks: flip-block;
}
}
.rule-filters__options label {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.35rem;
font-weight: 400;
overflow-wrap: anywhere;
cursor: pointer;
}
.rule-filters__options label:hover,
.rule-filters__actions button:hover {
background: var(--md-code-bg-color);
}
.rule-filters__options input {
flex: none;
width: 0.8rem;
height: 0.8rem;
accent-color: var(--md-typeset-a-color);
}
.rule-filters__actions {
display: flex;
flex-direction: column;
margin-bottom: 0.3rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--md-default-fg-color--lightest);
}
.rule-filters .rule-filters__actions button {
width: 100%;
border: 0;
border-radius: 0;
text-align: center;
}
.rule-filters__search {
flex: 2 1 12rem;
}
.rule-filters__category {
--rule-filter-anchor: --rule-category;
flex: 1 1 8rem;
}
.rule-filters__linter {
--rule-filter-anchor: --rule-linter;
flex: 1 1 10rem;
}
.rule-filters__status {
--rule-filter-anchor: --rule-status;
flex: 1 1 7rem;
}
.rule-filters input[type="search"],
.rule-filters button {
min-width: 0;
min-height: 2.1rem;
padding: 0.4rem 0.6rem;
border: 1px solid var(--md-default-fg-color--lightest);
border-radius: 0.2rem;
background: var(--md-default-bg-color);
color: var(--md-default-fg-color);
font: inherit;
font-weight: 400;
}
.rule-filters input::placeholder {
color: var(--md-default-fg-color--light);
}
.rule-filters button {
font-size: 0.7rem;
cursor: pointer;
}
.rule-filters button:hover {
border-color: var(--md-typeset-a-color);
color: var(--md-typeset-a-color);
}
.rule-filters :is(input, button):focus-visible,
.rule-catalog-scroll:focus-visible {
outline: 2px solid var(--md-typeset-a-color);
outline-offset: 2px;
}
.rule-filters__actions button:focus-visible {
outline-offset: -2px;
}
.md-typeset .rule-filters__count {
margin: 0.5rem 0 0;
color: var(--md-default-fg-color--light);
font-size: 0.65rem;
}
#rule-catalog [hidden],
.rule-catalog-scroll[hidden],
.rule-filters [hidden],
.rule-index-nav > label[for="__toc"],
.md-nav--secondary[hidden] {
display: none !important;
}
.rule-index-nav > a.md-nav__link {
display: flex !important;
}
@media (max-width: 59.999em) {
.rule-filters__search {
flex-basis: 100%;
}
.rule-filters__linter {
flex-basis: 8rem;
}
}
+2
View File
@@ -104,8 +104,10 @@ plugins:
- faq.md
extra_css:
- stylesheets/extra.css
- stylesheets/rule-filters.css
extra_javascript:
- js/extra.js
- js/rule-filters.js
not_in_nav: |
/rules/*
/formatter/*