Install ReUI (MCP, skill, @reui registry, Tailwind v4, shadcn base-nova) and move the shared UI onto it: Button, Tag, Check, Input/Textarea, Segmented, Alert, Panel, Dialog, Tooltip, Menu and Select (Popover), Toast (sonner), the Table frame (DataGrid), the Dashboard KPI strip, the shnt Ops palette, Geist and the type scale. Design only: behaviour, texts and roles are unchanged. Tests that asserted the old DOM were updated; the tests now format numbers in English wherever they run. docs/reui-migration-plan.md records the plan and its decisions. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
91 lines
3.7 KiB
JavaScript
91 lines
3.7 KiB
JavaScript
/*
|
|
The same thing, written in two modules.
|
|
|
|
`css-dead.mjs` answers whether a rule is unused. This one answers something
|
|
different: whether the SAME rule is written more than once. That is the defect
|
|
that produces drift (two copies that start identical and stop being identical),
|
|
and no screen-by-screen review catches it, because on each screen, taken alone,
|
|
everything looks right.
|
|
|
|
Measured before fixing it, it found 27 groups, and out of those came, among
|
|
others:
|
|
|
|
· eleven `<textarea>` elements painting their own box by hand, with radius 6
|
|
instead of 8 and without the inset shadow every other field carries;
|
|
· the inline `<code>` in five modules and the intro paragraph in five, one of
|
|
them at 1.65 line-height against 1.6 in the other four;
|
|
· the `<details>` chevron hand-encoded as an SVG in a `data:` URI, in two
|
|
modules, with a different stroke width from every other icon;
|
|
· the count under a table sitting at four different distances.
|
|
|
|
node dev/css-duplicated.mjs
|
|
|
|
## How to read it
|
|
|
|
It groups by rule BODY, ignoring `composes`, and only shows bodies that appear in
|
|
more than one module. That leaves out what is already shared (a class that
|
|
composes from another repeats nothing) and leaves in three things that are NOT
|
|
defects, so look before you touch:
|
|
|
|
· **Applying the same token.** Two components with `background: var(--acc)`
|
|
duplicate nothing: the token IS the single source, and `composes` cannot even
|
|
reach a pseudo-class or an attribute selector, which is where nearly all of
|
|
these come from. Real example: the highlight on a menu option and the one on
|
|
a dropdown option.
|
|
· **Generic layout coincidences.** `display:flex; flex-direction:column;
|
|
gap:var(--s-9)` shows up in a column on About, another on the Dashboard and
|
|
the Login form. They are not the same thing; there are just few ways to stack
|
|
three elements.
|
|
· **The same responsive collapse.** `grid-template-columns: minmax(0,1fr)`
|
|
inside a media query means "one column when narrow", and every grid in the
|
|
console says it.
|
|
|
|
The minimum body length is there so the output is not drowned in single-property
|
|
rules, which are almost always one of the three cases above.
|
|
*/
|
|
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
|
|
const ROOT = join(import.meta.dirname, '..', 'src')
|
|
const MINIMUM = Number(process.env.MINIMO ?? 28)
|
|
|
|
function files(dir, acc = []) {
|
|
for (const e of readdirSync(dir)) {
|
|
const p = join(dir, e)
|
|
if (statSync(p).isDirectory()) files(p, acc)
|
|
else if (e.endsWith('.module.css')) acc.push(p)
|
|
}
|
|
return acc
|
|
}
|
|
|
|
const groups = new Map()
|
|
|
|
for (const mod of files(ROOT)) {
|
|
const css = readFileSync(mod, 'utf8').replaceAll(/\/\*[\s\S]*?\*\//g, '')
|
|
for (const m of css.matchAll(/([^{}]+)\{([^{}]+)\}/g)) {
|
|
const selector = m[1].trim().split('\n').at(-1).trim()
|
|
/* Normalised: no spaces and sorted, so two identical rules written with
|
|
different formatting, or with the properties in another order, match. */
|
|
const body = m[2]
|
|
.split(';')
|
|
.map((d) => d.trim().replaceAll(' ', ''))
|
|
.filter((d) => d && !d.startsWith('composes'))
|
|
.sort()
|
|
.join(' ')
|
|
if (body.length < MINIMUM) continue
|
|
if (!groups.has(body)) groups.set(body, [])
|
|
groups.get(body).push({ mod: mod.slice(ROOT.length + 1), selector })
|
|
}
|
|
}
|
|
|
|
const repeated = [...groups]
|
|
.filter(([, places]) => new Set(places.map((s) => s.mod)).size > 1)
|
|
.sort((a, b) => b[1].length - a[1].length)
|
|
|
|
for (const [body, places] of repeated) {
|
|
console.log(`\n[${places.length}] ${body}`)
|
|
for (const s of places) console.log(` ${s.mod} ${s.selector}`)
|
|
}
|
|
|
|
console.log(`\n${repeated.length} rule bodies written in more than one module`)
|