mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-13 22:41:06 +08:00
fix(web): restore the ⋯ menu row styles removed as dead CSS
a894bdd deleted .action-menu-item/-label/-icon/-check/-description and the
.sheet-list-icon/-check/-description rules as unused, but ActionMenu builds
those names as `${base}-item` (base 'action-menu' or 'sheet-list'), which the
dead-class scan did not recognise. Every ⋯ dropdown and its bottom-sheet form
lost their row layout, hover, danger colour and check mark. Both files are
restored to their state before that commit.
The class-usage test now covers every stylesheet (not only media) and
understands built names: `${expr}-suffix` with a literal prefix, and
`prefix-${expr}`. Library markup (Notyf, DPlayer) is exempt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c20771e31
commit
20a85c33fe
4 files changed
+125
-26
No files matched your search
@@ -31,19 +31,88 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.action-menu .action-menu-item {
|
||||
appearance: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: start;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-menu .action-menu-item.is-danger {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.action-menu .action-menu-item:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.action-menu .action-menu-item:focus-visible {
|
||||
outline: 2px solid var(--color-brand-text);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.action-menu .action-menu-item[aria-checked='true'] {
|
||||
color: var(--color-brand-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-menu-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 1.125rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.action-menu .action-menu-item.is-danger .action-menu-icon {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.action-menu-label {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.action-menu-description {
|
||||
display: block;
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-meta);
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.action-menu-check {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.action-menu .action-menu-item:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--color-ink) 7%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
/* Two classes, so the media screens' generic `.media-screen button` rules lose. */
|
||||
/* Touch: a finger-sized row. */
|
||||
@media (pointer: coarse) {
|
||||
.action-menu .action-menu-item {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes action-menu-in {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
|
||||
@@ -220,13 +220,29 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sheet-list-description {
|
||||
display: block;
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-meta);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.sheet-list-item[aria-checked='true'] {
|
||||
color: var(--reader-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sheet-list-check {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.sheet-list-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 1.25rem;
|
||||
color: var(--reader-muted);
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.sheet-list-item:hover:not(:disabled) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* Every class a stylesheet names must be one the source can render.
|
||||
*
|
||||
* The media stylesheets grew by layering and replaced screens left their rules
|
||||
* behind (~300 lines of them), and a rule for a class nothing uses is where the next
|
||||
* change gets made, to no effect. The check has to know the two ways a class name is
|
||||
* *built* rather than written, because getting that wrong deletes live styles — which
|
||||
* is exactly what happened to the ⋯ menu rows (`${base}-item`, base 'action-menu' or
|
||||
* 'sheet-list'):
|
||||
*
|
||||
* - `${expr}-suffix`: any class ending in `-suffix` whose prefix the source spells
|
||||
* as a string literal;
|
||||
* - `prefix-${expr}`: any class starting with `prefix-`.
|
||||
*
|
||||
* Library markup (Notyf, DPlayer) is theirs, not ours. A class inside :not() is matched
|
||||
* by its absence, so it need not exist.
|
||||
*/
|
||||
const src = join(import.meta.dirname, '..', 'src');
|
||||
const files = (dir: string): string[] => readdirSync(dir, { withFileTypes: true }).flatMap(entry =>
|
||||
entry.isDirectory() ? files(join(dir, entry.name)) : [join(dir, entry.name)]);
|
||||
|
||||
it('styles only classes that the source renders', () => {
|
||||
const code = files(src).filter(file => /\.tsx?$/.test(file)).map(file => readFileSync(file, 'utf8')).join('\n');
|
||||
const builtSuffixes = new Set([...code.matchAll(/\$\{[\w.]+\}-([a-z][\w-]*)/g)].map(match => match[1]!));
|
||||
const builtPrefixes = new Set([...code.matchAll(/[`'" ]([a-z][\w-]*-)\$\{/g)].map(match => match[1]!));
|
||||
const literal = (text: string) => new RegExp(`['"\`]${text}['"\`]`).test(code);
|
||||
const used = (cls: string) => code.includes(cls)
|
||||
|| [...builtPrefixes].some(prefix => cls.startsWith(prefix))
|
||||
|| [...builtSuffixes].some(suffix => cls.endsWith('-' + suffix) && literal(cls.slice(0, -suffix.length - 1)));
|
||||
const unused = files(src).filter(file => file.endsWith('.css')).flatMap(file => {
|
||||
const css = readFileSync(file, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/:not\([^)]*\)/g, '');
|
||||
const names = new Set([...css.matchAll(/\.(-?[a-zA-Z_][\w-]*)/g)].map(match => match[1]!));
|
||||
return [...names].filter(cls => !/^(dplayer|notyf)/.test(cls) && !used(cls)).map(cls => `${relative(src, file)}: .${cls}`);
|
||||
});
|
||||
expect(unused).toEqual([]);
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* The media stylesheets grew by layering (media → presentation → v2 → manager), and
|
||||
* screens that were replaced left their rules behind: ~300 lines styled class names no
|
||||
* component renders any more. A rule for a class nothing uses is not harmless — it is
|
||||
* where the next change gets made, to no effect. Every class the media styles name must
|
||||
* appear somewhere in the source; DPlayer's own classes are its markup, not ours.
|
||||
*/
|
||||
const src = join(import.meta.dirname, '..', 'src');
|
||||
const files = (dir: string): string[] => readdirSync(dir, { withFileTypes: true }).flatMap(entry =>
|
||||
entry.isDirectory() ? files(join(dir, entry.name)) : [join(dir, entry.name)]);
|
||||
|
||||
it('styles only classes that the source renders (media stylesheets)', () => {
|
||||
const code = files(src).filter(file => /\.tsx?$/.test(file)).map(file => readFileSync(file, 'utf8')).join('\n');
|
||||
const styles = join(src, 'features', 'media', 'styles');
|
||||
const unused = readdirSync(styles).filter(name => name.endsWith('.css')).flatMap(name => {
|
||||
// A class inside :not() is matched by its absence, so it need not exist.
|
||||
const css = readFileSync(join(styles, name), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/:not\([^)]*\)/g, '');
|
||||
const names = new Set([...css.matchAll(/\.(-?[a-zA-Z_][\w-]*)/g)].map(match => match[1]!));
|
||||
return [...names].filter(cls => !cls.startsWith('dplayer') && !code.includes(cls)).map(cls => `${name}: .${cls}`);
|
||||
});
|
||||
expect(unused).toEqual([]);
|
||||
});
|
||||
Reference in new issue
Block a user