Cut duplicated and speculative code across the package

An over-engineering audit of the whole tree, applied in five reviewed
batches. Behaviour stays the same except where UPGRADE.md says otherwise.

PHP: the showcase and error-page stylesheets are prebuilt into
resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import
(first occurrence kept, the order an application's build gives), instead
of Stylesheets::bundle() inlining imports on every request; only the
import walk DesignGuard needs stays. SchemeStylesheet::withProfiles()
replaces three copies of the scheme-plus-profiles loop, material:scheme
leaves spec and contrast checks to the node script that already made
them, and the error page's scheme cache, the hashed view namespace, the
translations path with no lang/ folder and DesignGuard's 1.x-name hints
are gone.

JS: the androidx shape port progress.js and both bin scripts each carried
lives once in resources/js/shapes.js (the generated SVGs are unchanged);
util.js holds ringIndex(), ms(), reopenGuard() and remember(), which
were written out several times; listeners are released through
AbortController; tooltip.js's hoverPopover() serves the rich tooltip too.

CSS: every rule for an element inside the navigation rail queries
`--md-navigation-rail-value` instead of repeating the seven collapsed
conditions under five media branches; badge, alert, progress, slider and
button read one non-inheriting colour-role table (components/color.css);
the dialog chrome, the submenu's popover chrome, the chip's state layer
and touch target, and the visually-hidden inputs use the shared rules
they copied; foundation/tokens.css is folded into foundation.css.

Views: Support\Field and Support\Link replace the error-key, bound-value
and link-attribute blocks copied into the fields and link components;
the timepicker period group, the menu filter and the showcase head are
partials; the datepicker's steppers and entry fields are loops; component
docblocks no longer restate SKILL.md.

Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces
four per-group files, DesignGuardTest and the layout-component tests use
datasets, browser tests share one ready() helper, CSS parsing lives in
ComponentStylesheet alone. docs/audits and the finding IDs citing it are
removed, as are pestphp/pest-plugin-laravel, the unused composer scripts
and check:font; the lint job runs in the feature job, which now installs
node packages so the prebuilt-stylesheet staleness test runs in CI.

Feature suite 1177 passed, Chrome browser suite 299 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-17 19:29:21 +02:00
co-authored by Claude Opus 5
parent 471d927e64
commit 247c596c3a
233 changed files with 16635 additions and 10579 deletions
+88
View File
@@ -0,0 +1,88 @@
/**
* Small pieces shared by more than one component, in the pattern of breakpoints.js: a plain
* function or two, not a framework.
*/
/**
* The index an Arrow/Home/End key moves to from `index` (which may be -1, nothing chosen yet),
* wrapping around the ends; null when the key means nothing here. `vertical` picks Up/Down over
* Left/Right; `rtl` mirrors Left/Right (Up/Down never mirror). A caller that answers to both axes
* at once — a chip set's row, which also takes Up/Down — calls this twice and takes whichever
* answers.
*/
export function ringIndex(key, index, length, { rtl = false, vertical = false } = {}) {
if (key === 'Home') {
return 0
}
if (key === 'End') {
return length - 1
}
const forward = vertical ? 'ArrowDown' : rtl ? 'ArrowLeft' : 'ArrowRight'
const backward = vertical ? 'ArrowUp' : rtl ? 'ArrowRight' : 'ArrowLeft'
if (key === forward) {
return (index + 1 + length) % length
}
if (key === backward) {
return index < 0 ? length - 1 : (index - 1 + length) % length
}
return null
}
/** A CSS custom property's time value in milliseconds, whichever unit it is written in; null when unset. */
export function ms(element, token) {
const value = getComputedStyle(element).getPropertyValue(token).trim()
const number = parseFloat(value)
if (Number.isNaN(number)) {
return null
}
return value.endsWith('ms') ? number : number * 1000
}
/** Writes to localStorage, quietly: a blocked or full store still lets the page work, it just won't remember. */
export function remember(key, value) {
try {
localStorage.setItem(key, value)
} catch {
// Blocked storage: the page still works, it just will not remember.
}
}
const REOPEN_GUARD_MS = 250
/**
* A `popover="auto"` closes on the press that lands on its trigger, and the click that follows
* would open it again. A close this recent is taken as that press. It is timed from
* `beforetoggle`, which fires as the popover closes: `toggle` is queued, and arrives after that
* click. Only a close the browser made on its own (light dismiss) counts — `closing()` marks one
* this script makes as explicit, so a keyboard command that shuts it is never mistaken for that
* race, only the browser's own light dismiss racing the click it precedes is.
*/
export function reopenGuard() {
let closedAt = -Infinity
let closingExplicitly = false
return {
beforeToggle(event) {
if (event.newState === 'closed' && !closingExplicitly) {
closedAt = performance.now()
}
closingExplicitly = false
},
closing() {
closingExplicitly = true
},
canOpen() {
return performance.now() - closedAt > REOPEN_GUARD_MS
},
}
}