Files
livewire-material/resources/views/components/scaffold.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 247c596c3a 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>
2026-09-17 19:29:21 +02:00

128 lines
6.1 KiB
PHP

{{-- The scaffold: M3's structure of bars, rails and panes around the page, and the FAB, with
navigation that changes shape with the window (docs/reference/m3/foundations.md § Layout →
Scaffold). Before 2.0.0 it was `<x-app-shell>`. Props, slots and the breakpoint table are in
SKILL.md — this is the WHY behind the wiring, not the API.
`actions`' collapsed-column layout queries the rail's published `--md-navigation-rail-value`
rather than a breakpoint of its own, so `resources/css/layout/scaffold.css` stays in step with
the rail's conditions without repeating them.
`--md-layout-margin: 0px` on the content region (resources/css/layout/scaffold.css) is read by
layout/pane.css: a pane or canonical layout inside the scaffold draws no margin of its own
because it can see it is already in a margined region.
The snackbar host (`<x-toast />`) is part of the scaffold, not a separate element an app adds:
Compose raises the snackbar over its FAB, but here the host is one fixed element every page
shares, so the FAB (`fab` slot) is what moves above it instead (resources/css/layout/scaffold.css);
both clear the bottom bar through `--material-bottom-bar` on a compact window (the bar's 64px,
the safe-area inset and `--material-bottom-extra`, 0px unless the application docks something
above the bar).
Nothing application-specific belongs in here: an app's destinations and chrome come in through
the props and slots. The caller's `class` and `style` land on the root, `data-md-scaffold`
itself — the element `style="--material-banner: 4rem"` (SKILL.md) sets a custom property on. --}}
@props([
'destinations' => [],
'label' => null,
'railWidth' => '16rem',
'tallBar' => false,
'hideBarOnScroll' => false,
'hideRailWhenCollapsed' => false,
])
@php
$label ??= __('Main');
$current = \Livewire\Livewire::isLivewireRequest() ? \Livewire\Livewire::originalUrl() : request()->url();
$items = collect($destinations)
->filter(fn ($item): bool => is_array($item) && filled($item['title'] ?? null))
->map(fn (array $item): array => [
'title' => (string) $item['title'],
'icon' => $item['icon'] ?? null,
'url' => $item['url'] ?? null,
'active' => (bool) ($item['active'] ?? (filled($item['url'] ?? null) && rtrim(url($item['url']), '/') === rtrim($current, '/'))),
'badge' => $item['badge'] ?? null,
'badgeLabel' => filled($item['badgeLabel'] ?? null) ? (string) $item['badgeLabel'] : null,
'section' => filled($item['section'] ?? null) ? (string) $item['section'] : null,
'bar' => ($item['bar'] ?? true) !== false,
'navigate' => ($item['navigate'] ?? true) !== false,
])
->values();
$barItems = $items->where('bar', true)->values();
// Consecutive destinations under the same heading form one group, in the order given.
$groups = $items->chunkWhile(fn (array $item, int $key, $chunk): bool => $item['section'] === $chunk->last()['section']);
@endphp
<div data-md-scaffold {{ $attributes->only(['class', 'style']) }}>
<a href="#content" data-md-skip-link>{{ __('Skip to content') }}</a>
@isset($banner)
<div data-md-scaffold-banner>{{ $banner }}</div>
@endisset
<div data-md-scaffold-row>
<x-livewire-material::navigation-rail mode="adaptive" :label="$label" :width="$railWidth" :hide-when-collapsed="$hideRailWhenCollapsed">
@isset($brand)
<x-slot:brand>{{ $brand }}</x-slot:brand>
@endisset
@isset($railHeader)
<x-slot:header>{{ $railHeader }}</x-slot:header>
@endisset
@foreach ($groups as $group)
@if ($group->first()['section'] !== null)
<x-livewire-material::navigation-rail-section :label="$group->first()['section']">
@foreach ($group as $item)
<x-livewire-material::navigation-rail-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :badge-label="$item['badgeLabel']" :no-wire-navigate="! $item['navigate']" />
@endforeach
</x-livewire-material::navigation-rail-section>
@else
@foreach ($group as $item)
<x-livewire-material::navigation-rail-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :badge-label="$item['badgeLabel']" :no-wire-navigate="! $item['navigate']" />
@endforeach
@endif
@endforeach
@if (isset($railFooter) || isset($actions))
<x-slot:footer>
{{ $railFooter ?? '' }}
@isset($actions)
<div data-md-scaffold-actions>
{{ $actions }}
</div>
@endisset
</x-slot:footer>
@endif
</x-livewire-material::navigation-rail>
<div data-md-scaffold-content>
{{ $top ?? '' }}
@isset($fab)
<div data-md-scaffold-fab>{{ $fab }}</div>
@endisset
<main id="content" tabindex="-1" wire:transition.navigate>
{{ $slot }}
</main>
</div>
</div>
@if ($barItems->isNotEmpty())
<div data-md-scaffold-bar>
<x-livewire-material::navigation-bar :label="$label" :tall="$tallBar" :hide-on-scroll="$hideBarOnScroll">
@foreach ($barItems as $item)
<x-livewire-material::navigation-bar-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :badge-label="$item['badgeLabel']" :no-wire-navigate="! $item['navigate']" />
@endforeach
</x-livewire-material::navigation-bar>
</div>
@endif
<x-livewire-material::toast />
</div>