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:
co-authored by
Claude Opus 5
parent
471d927e64
commit
247c596c3a
@@ -1,28 +1,12 @@
|
||||
{{-- An M3 Expressive menu: a list of actions that opens from a trigger.
|
||||
{{-- An M3 Expressive menu: a list of actions that opens from a trigger. Props, slots, `filter`
|
||||
and `sheet-at-compact` are documented in SKILL.md; this is the mechanics behind them.
|
||||
|
||||
<x-menu label="Share actions">
|
||||
<x-slot:trigger>
|
||||
<x-button icon="more_vert" tooltip="More" />
|
||||
</x-slot:trigger>
|
||||
|
||||
<x-menu-item label="Copy link" icon="content_copy" wire:click="copy" />
|
||||
<x-menu-separator />
|
||||
<x-menu-item label="Delete" icon="delete" wire:click="delete" />
|
||||
</x-menu>
|
||||
|
||||
The trigger's first button or link becomes the menu button (aria-haspopup, aria-expanded,
|
||||
aria-controls). The list is a `popover="auto"` in the top layer, placed by CSS anchor
|
||||
positioning at `position` (`bottom-start`, `bottom-end`, `top-start`, `top-end`) and flipping
|
||||
when there is no room — to the other side, the other end, or both, so a menu on a FAB in a
|
||||
corner of the window opens back across it; a click outside or Escape closes it. The keyboard
|
||||
is WAI-ARIA's menu button: Enter, Space or ArrowDown open on the first item, ArrowUp on the
|
||||
last; arrows, Home, End and typing a letter move between items; Tab closes; activating an item
|
||||
closes the menu unless the item says `keep-open`, and Escape returns focus to the trigger.
|
||||
|
||||
The anchor name is rendered on the wrapper around the trigger slot, the only element the
|
||||
server can name, and resources/js/menu.js moves it onto the menu button itself: a trigger
|
||||
that is `position: fixed` (`<x-button fab>` on a phone) leaves the wrapper behind as an empty
|
||||
box where the page put it, and the menu opened there.
|
||||
The list is a `popover="auto"` in the top layer, placed by CSS anchor positioning at
|
||||
`position`, flipping when there is no room. The anchor name is rendered on the wrapper around
|
||||
the trigger slot, the only element the server can name, and resources/js/menu.js moves it
|
||||
onto the menu button itself: a trigger that is `position: fixed` (`<x-button fab>` on a
|
||||
phone) leaves the wrapper behind as an empty box where the page put it, and the menu opened
|
||||
there.
|
||||
|
||||
The id and the anchor name are new with every render. The popover carries a `wire:key`, which
|
||||
a Livewire morph matches it by before the id, so a render of the component around an open
|
||||
@@ -32,54 +16,16 @@
|
||||
of the loop iteration around it, which would give every child component after the menu the
|
||||
same key.
|
||||
|
||||
A menu too long for the window scrolls, as M3 asks, rather than running off the edge of the
|
||||
top layer where nothing can reach it (ACT-04): 288px at most, and less on a short window. The
|
||||
arrow keys, Home, End and typeahead bring the item they move to into view, and a disabled
|
||||
item is among them: M3 keeps one reachable so a person can find out that it exists.
|
||||
|
||||
It opens by growing out of the corner nearest its trigger and fades as it goes, which is the
|
||||
transition M3 asks to tie a menu to what opened it (ACT-26).
|
||||
|
||||
`filter` is M3's menu as a filtering surface ("autocomplete"): a text field at the top of the
|
||||
list, which stays put while the list scrolls under it, narrowing the items to those whose
|
||||
label holds what has been typed — in the browser, over the items already rendered, so nothing
|
||||
is fetched and a `wire:click` stays where it was. `filter="Find a person"` names the field;
|
||||
bare `filter` calls it "Filter". The field, not the list, holds the focus, so a person can
|
||||
type and steer at once: the arrow keys, Home and End move a highlighted row and say which one
|
||||
through `aria-activedescendant`, and Enter chooses it — the APG combobox keyboard, the same
|
||||
one `<x-choices searchable>` uses. The list around it stays a `role="menu"` of its own inside
|
||||
the popover, because a text field is not a thing a menu may contain.
|
||||
|
||||
`sheet-at-compact` is M3's adaptive menu: "at compact breakpoints, consider swapping a menu
|
||||
for a bottom sheet (more room for items/longer labels); at medium/expanded breakpoints, menus
|
||||
work well in context" (docs/reference/m3/components-actions-communication-containment.md
|
||||
§ Menus → Behaviour; foundations.md § Layout gives compact the bottom sheet for supplemental
|
||||
selection and actions, and medium up the menu). Below `medium` (600px) the trigger opens the
|
||||
items in a modal `<x-bottom-sheet>` — M3's "alternative to inline menus … on mobile", closed
|
||||
by choosing an item, the scrim, a swipe down or Escape (§ Bottom sheets → Behaviour) — and
|
||||
from `medium` it opens the popover. The trigger says which: `aria-haspopup="dialog"` and the
|
||||
sheet's id while the window is compact, `menu` and the popover's otherwise, `aria-expanded`
|
||||
in both. The slot is written once and drawn twice, in the popover and in the sheet, so a
|
||||
Livewire render patches both copies and a chosen item shows chosen in either. The sheet is
|
||||
teleported to the end of <body>: a menu in a sticky app bar or a toolbar would otherwise cover
|
||||
the window only inside that bar's stacking context, under the navigation bar.
|
||||
|
||||
Inside the sheet the items keep their roles and the keyboard above — arrows, Home, End, a
|
||||
letter; Escape closes the sheet and Tab does too, and either returns focus to the trigger —
|
||||
and choosing one closes the sheet as it closes the popover. A submenu opens in place under
|
||||
its item instead of beside it (M3 calls submenus "best suited to large screens"), and a
|
||||
`filter` field stands at the top of the sheet. A window resized across 600px closes whichever
|
||||
is open rather than leaving the other shown. The sheet is M3's own container
|
||||
(surface-container-low) for a `vibrant` menu too, and is as tall as its items up to the
|
||||
half screen M3 caps a modal sheet's first position at, scrolling inside past that. The
|
||||
sheet's id is fixed and the lists are keyed, so a render keeps an open sheet open with its
|
||||
focus where it was (below). Since the items exist twice, an `id` of the caller's or a nested
|
||||
Livewire component among them would exist twice too: keep those out of a `sheet-at-compact`
|
||||
menu.
|
||||
For `sheet-at-compact`: the slot is written once and drawn twice, in the popover and in the
|
||||
sheet, so a Livewire render patches both copies and a chosen item shows chosen in either. The
|
||||
sheet is teleported to the end of `<body>`, so a menu in a sticky app bar or a toolbar still
|
||||
covers the whole window rather than only that bar's own stacking context. Since the items
|
||||
exist twice, an `id` of the caller's or a nested Livewire component among them would exist
|
||||
twice too: keep those out of a `sheet-at-compact` menu.
|
||||
|
||||
The container is Expressive's standard menu (surface-container-low, 16px corner, elevation
|
||||
2), or `vibrant` in tertiary-container — StandardMenuTokens and VibrantMenuTokens from
|
||||
androidx Compose Material 3 (Apache-2.0).
|
||||
androidx Compose Material 3 (Apache-2.0). A menu longer than the window scrolls at 288px.
|
||||
|
||||
Drawn by resources/css/components/menu.css, which also draws the dropdown a form's own lists
|
||||
wear — `<x-select>`'s exposed picker and `<x-choices searchable>`'s listbox — so a menu and a
|
||||
@@ -130,32 +76,7 @@
|
||||
x-on:click="activate($event)"
|
||||
>
|
||||
@if ($filtering)
|
||||
<div data-md-menu-filter>
|
||||
<x-livewire-material::icon name="search" size="20" />
|
||||
|
||||
<input
|
||||
type="text"
|
||||
role="combobox"
|
||||
autocomplete="off"
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="true"
|
||||
aria-controls="material-menu-{{ $key }}-list"
|
||||
aria-label="{{ $filterLabel }}"
|
||||
placeholder="{{ $filterLabel }}"
|
||||
x-on:input="refine()"
|
||||
x-on:keydown.stop="search($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div {{ new \Illuminate\View\ComponentAttributeBag(array_filter([
|
||||
'id' => "material-menu-{$key}-list",
|
||||
'role' => 'menu',
|
||||
'aria-label' => $label,
|
||||
], fn ($value): bool => filled($value))) }} data-md-menu-list>
|
||||
{{ $slot }}
|
||||
|
||||
<p data-md-menu-empty hidden>{{ __('Nothing matches') }}</p>
|
||||
</div>
|
||||
@include('livewire-material::partials.menu-filter', ['idSuffix' => '', 'wireKey' => null, 'delegated' => false])
|
||||
@else
|
||||
{{ $slot }}
|
||||
@endif
|
||||
@@ -190,31 +111,7 @@
|
||||
'wire:key' => 'material-menu-sheet-menu',
|
||||
'id' => "material-menu-{$key}-sheet-menu",
|
||||
]) }} data-md-menu-sheet>
|
||||
<div data-md-menu-filter>
|
||||
<x-livewire-material::icon name="search" size="20" />
|
||||
|
||||
<input
|
||||
type="text"
|
||||
role="combobox"
|
||||
autocomplete="off"
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="true"
|
||||
aria-controls="material-menu-{{ $key }}-sheet-menu-list"
|
||||
aria-label="{{ $filterLabel }}"
|
||||
placeholder="{{ $filterLabel }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div {{ new \Illuminate\View\ComponentAttributeBag(array_filter([
|
||||
'wire:key' => 'material-menu-sheet-list',
|
||||
'id' => "material-menu-{$key}-sheet-menu-list",
|
||||
'role' => 'menu',
|
||||
'aria-label' => $label,
|
||||
], fn ($value): bool => filled($value))) }} data-md-menu-list>
|
||||
{{ $slot }}
|
||||
|
||||
<p data-md-menu-empty hidden>{{ __('Nothing matches') }}</p>
|
||||
</div>
|
||||
@include('livewire-material::partials.menu-filter', ['idSuffix' => '-sheet-menu', 'wireKey' => 'material-menu-sheet-list', 'delegated' => true])
|
||||
</div>
|
||||
@else
|
||||
<div {{ new \Illuminate\View\ComponentAttributeBag(array_filter([
|
||||
|
||||
Reference in New Issue
Block a user