Plan step 23, containment.md § Missing ("Cards: the dragged state").
`data-dragged` on `<x-card>` takes the top of M3's card elevation scale
— Level4 (8dp) elevated, Level3 (6dp) filled and outlined — under the
16% dragged state layer from tokens/state.css. The row's hover and press
rules now exclude a dragged card, so the card a pointer is holding stays
at its dragged level. The application owns the attribute: nothing in the
browser tells a card it is being carried.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
109 lines
5.0 KiB
PHP
109 lines
5.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.
|
|
|
|
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-list-row` and one `data-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.
|
|
|
|
A row answers with M3's state layer and one step of elevation (elevated 1 → 2 hovered,
|
|
filled and outlined 0 → 1); its corner does not move, because M3 gives a card one shape.
|
|
Never a stretched link, and never a whole-card `<a>` around buttons.
|
|
|
|
`data-dragged` is M3's dragged card: the top of a card's elevation scale — 8dp elevated, 6dp
|
|
filled and outlined — under the 16% dragged state layer, and it holds while the pointer is
|
|
down rather than falling back to the press state. 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; resources/css/components/list.css draws it).
|
|
|
|
Do not pass a `bg-*` class to change its fill — it races the card's own in Tailwind's emit
|
|
order; use `variant`, or colour a wrapper inside. --}}
|
|
|
|
@props([
|
|
'title' => null,
|
|
'subtitle' => null,
|
|
'variant' => 'filled',
|
|
'separator' => false,
|
|
'actionable' => false,
|
|
])
|
|
|
|
@php
|
|
$variant = in_array($variant, ['filled', 'elevated', 'outlined'], true) ? $variant : 'filled';
|
|
$role = $actionable ? ($attributes->get('role') ?: 'button') : null;
|
|
$attributes = $actionable ? $attributes->except('role') : $attributes;
|
|
@endphp
|
|
|
|
<div
|
|
data-card="{{ $variant }}"
|
|
@if ($actionable)
|
|
data-list-row
|
|
data-list-actionable
|
|
tabindex="0"
|
|
role="{{ $role }}"
|
|
@if (filled($title)) aria-label="{{ $title }}" @endif
|
|
@endif
|
|
{{ $attributes->class([
|
|
'relative flex flex-col overflow-hidden rounded-corner-md text-on-surface',
|
|
'bg-surface-container-highest' => $variant === 'filled',
|
|
'bg-surface-container-low shadow-elevation-1' => $variant === 'elevated',
|
|
'border border-outline-variant bg-surface' => $variant === 'outlined',
|
|
]) }}
|
|
>
|
|
@isset($figure)
|
|
<div class="shrink-0 overflow-hidden [&>img]:w-full [&>img]:object-cover">{{ $figure }}</div>
|
|
@endisset
|
|
|
|
<div class="flex flex-1 flex-col gap-4 p-4">
|
|
@if ($title || $subtitle || isset($menu))
|
|
<div>
|
|
<div class="flex items-start gap-2">
|
|
<div class="min-w-0 flex-1">
|
|
@if ($title)
|
|
<h3 class="type-title-md">{{ $title }}</h3>
|
|
@endif
|
|
|
|
@if ($subtitle)
|
|
<p class="mt-0.5 type-body-md text-on-surface-variant">{{ $subtitle }}</p>
|
|
@endif
|
|
</div>
|
|
|
|
@isset($menu)
|
|
<div class="-me-2 -mt-2 flex shrink-0 items-center gap-1">{{ $menu }}</div>
|
|
@endisset
|
|
</div>
|
|
|
|
@if ($separator)
|
|
<x-livewire-material::divider class="mt-4" />
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
@if ($slot->isNotEmpty())
|
|
<div class="flex-1 type-body-md">{{ $slot }}</div>
|
|
@endif
|
|
|
|
@isset($actions)
|
|
<div class="flex flex-wrap items-center justify-end gap-2">{{ $actions }}</div>
|
|
@endisset
|
|
</div>
|
|
</div>
|