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>
103 lines
4.9 KiB
PHP
103 lines
4.9 KiB
PHP
{{-- M3's list-detail canonical layout: a list, and the detail of what is selected in it.
|
||
|
||
<x-list-detail wire:model.live="selectedId">
|
||
<x-slot:list>
|
||
<x-pane title="Inbox">
|
||
<x-list>… <x-list-item wire:click="$set('selectedId', {{ $message->id }})" …/> …</x-list>
|
||
</x-pane>
|
||
</x-slot:list>
|
||
<x-slot:detail>
|
||
<x-pane :title="$message?->subject" heading="h2" back>…</x-pane>
|
||
</x-slot:detail>
|
||
</x-list-detail>
|
||
|
||
Parent and child content: an inbox and a message, folders and a file, settings and a category
|
||
(docs/reference/m3/foundations-supplement.md § Canonical layouts → List-detail). Its
|
||
visible-panes table, row by row: compact (below 600px) shows 1 pane; medium (600–839) 1 or 2,
|
||
and this layout shows 1, M3's recommendation for anything but low-density content; expanded
|
||
(840+), large (1200–1599) and extra-large (1600+) show 2.
|
||
|
||
So below 840px the list shows until something is selected, then the detail replaces it with
|
||
a back button; from 840px the list is a fixed pane, 360px wide (412px from 1200px, M3's
|
||
fixed-pane widths at expanded and at large, § Breakpoints), the detail takes the rest, 24px
|
||
apart, and no back button shows. Resizing moves between the two as M3 describes ("rotating
|
||
from expanded to medium collapses two panes back to one"). A right-to-left document puts the
|
||
list on the right and turns the back arrow.
|
||
|
||
`selected` is what is selected, bound with `wire:model` (the Livewire property, whose value
|
||
the server also renders from, so the first paint shows the right pane) or `x-model`, or given
|
||
once as `selected`. Nothing selected is `null`, `false` or `''`. Inside both slots `selected`
|
||
is in Alpine scope, and `back()` clears it (a boolean to `false`, anything else to `null`).
|
||
A list item can select with `wire:click="$set('selectedId', 7)"` or `x-on:click="selected = 7"`.
|
||
M3 shows the list's selected state only where both panes show; give the item `aria-current`
|
||
for it, which also tells `back()` where to return focus.
|
||
|
||
The back button: `<x-pane back>` puts it in the detail pane's own app bar. Without one in the
|
||
detail slot, the layout draws its own row with it above the detail. Either way it is hidden
|
||
from 840px.
|
||
|
||
Focus (resources/js/layout.js): below 840px a selection moves focus to the detail pane, and
|
||
`back()` returns it to the list item it came from — or the item marked `aria-current` or
|
||
`aria-selected`, or the list — as a dialog returns focus to its trigger (WAI-ARIA APG). From
|
||
840px focus stays where it is, since both panes are on screen.
|
||
|
||
The layout keeps M3's margin (16px below medium, 24px from it) unless it sits inside something
|
||
that already does, such as `<x-scaffold>`'s content region. It takes `as`, `hide-below` and
|
||
`hide-from` like every layout component, and the caller's `class` and `style` land on it
|
||
untouched. Its own attributes belong to Alpine (`wire:ignore.self`), while both slots morph as
|
||
usual. Drawn by resources/css/layout/list-detail.css. --}}
|
||
|
||
@props([
|
||
'as' => null,
|
||
'selected' => null,
|
||
'hideBelow' => null,
|
||
'hideFrom' => null,
|
||
])
|
||
|
||
@php
|
||
$layout = \NoNameWeb\LivewireMaterial\Support\Layout::class;
|
||
$element = $layout::element($as);
|
||
$model = $attributes->wire('model')->value() ?: null;
|
||
$initial = \NoNameWeb\LivewireMaterial\Support\Field::bound($model, $selected);
|
||
|
||
$chosen = $initial !== null && $initial !== false && $initial !== '';
|
||
$wire = $attributes->wire('model');
|
||
$detailSlot = $detail ?? null;
|
||
$ownBack = $detailSlot !== null && str_contains((string) $detailSlot, 'data-md-list-detail-back');
|
||
|
||
$attributes = $attributes->whereDoesntStartWith('wire:model')->merge([
|
||
'data-md-list-detail' => true,
|
||
'data-md-selected' => $chosen ? true : null,
|
||
] + $layout::visibility($hideBelow, $hideFrom));
|
||
@endphp
|
||
|
||
<{{ $element }}
|
||
x-data="materialListDetail(@if ($model !== null) @entangle($wire) @else @js($initial) @endif)"
|
||
@if ($model === null) x-modelable="selected" @endif
|
||
x-bind:data-md-selected="hasSelection ? '' : null"
|
||
x-on:focusin="track($event)"
|
||
x-on:focusout="track($event)"
|
||
wire:ignore.self
|
||
{{ $attributes }}
|
||
>
|
||
<div
|
||
data-md-list-detail-pane="list"
|
||
tabindex="-1"
|
||
x-ref="list"
|
||
x-on:focusin="remember($event)"
|
||
x-on:click="remember($event)"
|
||
>{{ $list ?? '' }}</div>
|
||
|
||
<div data-md-list-detail-pane="detail" tabindex="-1" x-ref="detail">
|
||
@unless ($ownBack)
|
||
<div data-md-list-detail-back-row>
|
||
<span data-md-list-detail-back>
|
||
<x-livewire-material::button icon="arrow_back" :tooltip="__('Back')" x-on:click="back()" />
|
||
</span>
|
||
</div>
|
||
@endunless
|
||
|
||
{{ $detailSlot ?? '' }}
|
||
</div>
|
||
</{{ $element }}>
|