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>
89 lines
2.8 KiB
JavaScript
89 lines
2.8 KiB
JavaScript
/**
|
|
* 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
|
|
},
|
|
}
|
|
}
|