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>
121 lines
6.0 KiB
PHP
121 lines
6.0 KiB
PHP
{{-- An M3 card: content and actions about one subject.
|
|
|
|
`variant` is M3's three (FilledCardTokens, ElevatedCardTokens, OutlinedCardTokens, androidx
|
|
Compose Material 3, Apache-2.0): `filled` (surface-container-highest, the default),
|
|
`elevated` (surface-container-low at elevation 1) and `outlined` (surface, an outline-variant
|
|
edge); all with a medium corner. maryUI's props keep their meaning: `title`, `subtitle`,
|
|
`separator` (a divider under the header), and the `menu` (top-end, beside the title),
|
|
`figure` (full-bleed media on top) and `actions` (end-aligned, under the content) slots. The title
|
|
is an `<h3>` unless `heading` names the level the page's outline needs (`h2` … `h6`, or `div`/`p`
|
|
for a title that is not a heading), so a card straight under a page's `<h1>` skips no level.
|
|
|
|
M3 draws two kinds of card a person can press, and they differ in the keyboard: on a
|
|
**non-actionable card with actionable elements** Tab moves through each control inside before
|
|
the next card; on a **directly actionable card** Tab moves to the card, then to the next card
|
|
(docs/reference/m3/components-actions-communication-containment.md § Cards → Accessibility).
|
|
|
|
Both are a row: give the card `data-md-list-row` and one `data-md-list-open` control inside
|
|
(the title link or a button), and a press anywhere on it reaches that control while its other
|
|
buttons keep their own (resources/js/list-rows.js). That is the first kind — the opener is
|
|
the tab stop, the accessible name and the owner of the `href` or the `wire:click`.
|
|
|
|
`actionable` makes it the second: the card takes `tabindex="0"` and `role="button"` (pass
|
|
`role="link"` where it goes somewhere), is named by its `title`, and Enter or Space on it
|
|
reaches the opener. Put `tabindex="-1"` on that opener so the card is the one stop, and leave
|
|
the card's other actions as they are — they follow it in the tab order.
|
|
|
|
Every card renders the shared `md-state-layer` class and a row (`data-md-list-row`,
|
|
`actionable` or not) `md-focus-ring` too (foundation/interaction.css), which draw the tint and
|
|
the ring for a card focused directly; resources/css/components/card.css keeps the layer off a
|
|
card that is neither a row nor dragged, refines its opacity when the pointer or the keyboard is
|
|
on one of the card's *own* buttons instead (which should light only itself), and adds the
|
|
`:has([data-md-list-open]:focus-visible)` ring for the non-actionable row, whose focus never
|
|
lands on the card itself. Elevation is separate from the tint — M3 gives each variant its own
|
|
per-state box-shadow (elevated 1dp rest → 2dp hover → 1dp focus/pressed; filled and outlined
|
|
0dp rest → 1dp hover → 0dp focus/pressed) — so it is drawn directly on `[data-md-card]`, never
|
|
inside the shared class. Its corner does not move, because M3 gives a card one shape.
|
|
|
|
`data-md-dragged` is M3's dragged card, row or not: the top of a card's elevation scale — 8dp
|
|
elevated, 6dp filled and outlined — under the shared class's own 16% dragged tint (it already
|
|
reads `data-md-dragged`, foundation/interaction.css). Nothing in the browser tells a card it is
|
|
being carried, so the application sets the attribute when its drag starts and takes it off on
|
|
drop. M3 requires a single-pointer alternative beside any drag, so keep the same reorder or
|
|
delete actions in a menu on the card
|
|
(docs/reference/m3/components-actions-communication-containment.md § Cards → Specs,
|
|
Accessibility).
|
|
|
|
Do not pass a `bg-*` class to change its fill — the package's own rule for the variant sits in
|
|
`material.components`, and an application's unlayered CSS is the place to override it; use
|
|
`variant`, or colour a wrapper inside. --}}
|
|
|
|
@props([
|
|
'title' => null,
|
|
'subtitle' => null,
|
|
'variant' => 'filled',
|
|
'heading' => 'h3',
|
|
'separator' => false,
|
|
'actionable' => false,
|
|
])
|
|
|
|
@php
|
|
$variant = in_array($variant, ['filled', 'elevated', 'outlined'], true) ? $variant : 'filled';
|
|
$heading = in_array($heading, ['h2', 'h3', 'h4', 'h5', 'h6', 'div', 'p'], true) ? $heading : 'h3';
|
|
$role = $actionable ? ($attributes->get('role') ?: 'button') : null;
|
|
$isRow = $actionable || $attributes->has('data-md-list-row');
|
|
|
|
$attributes = $actionable ? $attributes->except('role') : $attributes;
|
|
@endphp
|
|
|
|
<div
|
|
{{ $attributes->merge([
|
|
'data-md-card' => $variant,
|
|
'data-md-list-row' => $actionable ? true : null,
|
|
'data-md-list-actionable' => $actionable ? true : null,
|
|
'tabindex' => $actionable ? '0' : null,
|
|
'role' => $role,
|
|
'aria-label' => $actionable && filled($title) ? $title : null,
|
|
])->class([
|
|
// Every card: the application sets `data-md-dragged` from script, after this renders.
|
|
'md-state-layer',
|
|
'md-focus-ring' => $isRow,
|
|
]) }}
|
|
>
|
|
@isset($figure)
|
|
<div data-md-card-figure>{{ $figure }}</div>
|
|
@endisset
|
|
|
|
<div data-md-card-body>
|
|
@if ($title || $subtitle || isset($menu))
|
|
<div data-md-card-header>
|
|
<div data-md-card-header-row>
|
|
<div data-md-card-heading>
|
|
@if ($title)
|
|
<{{ $heading }} data-md-card-title>{{ $title }}</{{ $heading }}>
|
|
@endif
|
|
|
|
@if ($subtitle)
|
|
<p data-md-card-subtitle>{{ $subtitle }}</p>
|
|
@endif
|
|
</div>
|
|
|
|
@isset($menu)
|
|
<div data-md-card-menu>{{ $menu }}</div>
|
|
@endisset
|
|
</div>
|
|
|
|
@if ($separator)
|
|
<x-livewire-material::divider />
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
@if ($slot->isNotEmpty())
|
|
<div data-md-card-content>{{ $slot }}</div>
|
|
@endif
|
|
|
|
@isset($actions)
|
|
<div data-md-card-actions>{{ $actions }}</div>
|
|
@endisset
|
|
</div>
|
|
</div>
|