Every foundations, styles and components page of m3.material.io (238, from the sitemap) extracted into docs/reference/m3, five audit reports with 142 findings in docs/audits/m3-alignment, and the 2.0.0 plan in docs/plans/material-3-alignment.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
598 lines
46 KiB
Markdown
598 lines
46 KiB
Markdown
# Audit: containment
|
||
|
||
## Summary
|
||
|
||
The containment group is the most accurate part of the library I have measured: the card variants, the
|
||
dialog (28dp corner, 24dp padding, 560/280 width, surface-container-high, 32% scrim, headline-small), the
|
||
divider (1px outline-variant, 16dp inset), the list heights (56/72/88) and leading sizes (24/40/56), the
|
||
bottom sheet's 32×4 handle / 28dp top corner / 640px cap, and the carousel's ported Compose keylines all
|
||
match the published numbers or the androidx token files. The failures are concentrated in three places:
|
||
**accessibility of selection and disabling in lists** (colour-only selection with no `aria-selected`, a
|
||
`disabled` item whose link is still focusable and activatable), **the bottom-sheet drag handle** (a 32×4px
|
||
touch target where M3 requires 48dp, achieved by its 22dp padding), and **adaptive breakpoints** (the
|
||
list-detail pane opens at `xl`/1280 where M3 puts two panes from expanded/840). Beyond those, a handful of
|
||
measurable deviations: a cascade-layer bug that kills the state layer on segmented list rows, a basic
|
||
dialog whose headline and action row scroll away, side-sheet actions right-aligned where M3 says left,
|
||
carousel end padding of 0 where M3 says 16dp, a full-screen carousel that scrolls horizontally where M3's
|
||
scrolls vertically, and cards with no per-state elevation at all. Collapse, the error pages and the mail
|
||
theme are clean against M3's foundations and styles; the mail theme reproduces the typescale correctly in
|
||
px and the error pages use the roles and typescale properly.
|
||
|
||
## Findings
|
||
|
||
### C-01 · bottom-sheet · The drag handle is a 32×4px target where M3 requires 48dp
|
||
- Severity: must-fix
|
||
- M3 says: "drag handle has an accessible **48dp hit target**" and the specs table gives "Drag handle
|
||
padding top/bottom | 22dp" (reference-components-a.md § Bottom sheets → Specs; raw
|
||
`components_bottom-sheets_specs.md`). Confirmed in androidx: `SheetDefaults.kt` line 788,
|
||
`private val DragHandleVerticalPadding get() = 22.dp`, applied as
|
||
`modifier.padding(vertical = DragHandleVerticalPadding)` around the 32×4 handle → 4 + 22 + 22 = 48dp.
|
||
Foundations also require a ≥48×48 touch target for any interactive element.
|
||
- Library does: `resources/views/components/bottom-sheet.blade.php:60` — the `<button>` that is the handle
|
||
is `class="h-1 w-8 …"` (4×32px) with no padding of its own; the padding lives on the wrapper `<div>` at
|
||
line 59 (`py-4` = 16px, not 22px), and the wrapper is not the control. The clickable/focusable target is
|
||
therefore 32×4px, and the whole handle region is only 36px tall.
|
||
- Fix: move the padding onto the button: `class="h-1 w-8 box-content py-[22px] …"` (or wrap with
|
||
`py-[22px]` and give the button `before:absolute before:inset-x-0 before:-inset-y-[22px]`), and change
|
||
the wrapper at line 59 from `py-4` to `py-0`. Total sheet-top region becomes 48px, matching M3.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-02 · list-item · A `disabled` item's link stays keyboard-focusable and activatable
|
||
- Severity: must-fix
|
||
- M3 says: disabled list items carry `ItemDisabled*Opacity = 0.38` and a disabled state layer
|
||
(`ListTokens.kt:70,75`); foundations' states model treats disabled as "not interactive". A control that
|
||
looks disabled but still responds to Enter is an interaction bug.
|
||
- Library does: `resources/views/components/list-item.blade.php:50` adds only
|
||
`'pointer-events-none text-on-surface/38' => $disabled`. `pointer-events: none` blocks the pointer but
|
||
not the keyboard; the `<a href="…" data-list-open …>` at lines 73-78 is still rendered, still in the tab
|
||
order and still navigates on Enter. No `aria-disabled` is emitted, so a screen reader announces the item
|
||
as an ordinary link.
|
||
- Fix: in `list-item.blade.php`, when `$disabled` render the title as the `<p>` branch (skip the `<a>`
|
||
entirely, or add `tabindex="-1" aria-disabled="true"` and strip `href`), drop `data-list-row` when
|
||
disabled, and add `aria-disabled="true"` to the row `<div>` at line 41.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-03 · list-item · `selected` is colour-only and is never announced
|
||
- Severity: must-fix
|
||
- M3 says: "**Indicate selection with more than color** … don't rely on color as the only visual cue" and
|
||
"Use two visual cues to show a list item is selected, like a leading checkmark and filled color"
|
||
(raw `components_lists_accessibility.md:26-44`). Role mapping, same page: single-select and multi-select
|
||
lists on **Web** → container role **List box**, item role **Option**, state **Selected / Not-selected**.
|
||
- Library does: `list-item.blade.php:44` emits only `data-selected`; `resources/css/components/list.css:46-49`
|
||
paints `secondary-container` / `on-secondary-container`. There is no `aria-selected`, no `aria-current`,
|
||
no icon or checkmark, and the container stays `role="list"` / `role="listitem"`
|
||
(`list.blade.php:19`, `list-item.blade.php:41`), where `listitem` cannot carry a selected state at all.
|
||
- Fix: two parts. (a) Add a `selectable` (or `selection="single"|"multi"`) prop to `<x-list>` that switches
|
||
the container to `role="listbox"` and each item to `role="option" aria-selected="true|false"`; keep
|
||
`role="list"` as the default for non-selectable lists and use `aria-current="true"` there instead of
|
||
nothing. (b) Render a second cue when `selected` — e.g. a trailing `check` icon in
|
||
`on-secondary-container` — or document that callers must supply one (a `leading` checkbox/radio).
|
||
- Effort: M
|
||
- Breaks API? yes (a new prop; `role` on the container changes for selectable lists)
|
||
|
||
### C-04 · drawer · No close affordance by default, which M3 states as a requirement
|
||
- Severity: must-fix
|
||
- M3 says: "Material **requires** a close affordance (e.g. close icon button) to always be present —
|
||
without one, users can't predict the sheet's open/close flow or tell if it's transient or permanent"
|
||
(reference-components-a.md § Side sheets → Accessibility). Anatomy lists the close icon button for both
|
||
the standard and the modal side sheet.
|
||
- Library does: `resources/views/components/drawer.blade.php:33` — `'withCloseButton' => false`. With the
|
||
default, a side sheet renders with a headline and no close control at all; the only exits are Escape
|
||
(which `close-on-escape=false` removes) and the scrim (which `without-backdrop-close` removes). With both
|
||
of those off, the sheet is undismissable.
|
||
- Fix: flip the default to `'withCloseButton' => true` in `drawer.blade.php:33`, and make the close button
|
||
unconditional (ignore the prop) when `closeOnEscape` is false or `withoutBackdropClose` is set. Same for
|
||
`pane` mode, which has no scrim and, by default, no Escape.
|
||
- Effort: S
|
||
- Breaks API? yes (the default rendering of every existing `<x-drawer>` gains a close button)
|
||
|
||
### C-05 · carousel · Reduced motion still resizes items
|
||
- Severity: must-fix
|
||
- M3 says: "When reduced motion settings are turned on, the parallax effect should be removed and carousel
|
||
items should no longer expand as they come into view. **All items are the same size**" (quoted verbatim
|
||
in reference-styles.md § Motion → Accessibility requirements, from
|
||
`components/carousel/accessibility`; also reference-components-a.md § Carousel → Accessibility).
|
||
- Library does: `resources/js/carousel.js:966` sets `const pinned = state.reducedMotion.matches` and then
|
||
uses it only for the content pin (`const pin = pinned ? … : 0`, line 979). The mask itself
|
||
(`const inset = clamp((size - keyline.size) / 2, 0, size / 2)`, line 978) is still written to
|
||
`--material-carousel-inset` on every frame, so items keep growing and shrinking between keylines — the
|
||
exact behaviour M3 says to switch off. Only the parallax half of the rule is honoured.
|
||
- Fix: in `render()` (carousel.js ~line 966-995), when `state.reducedMotion.matches`, write `inset = 0`
|
||
(and `shift = 0`, `opacity = 1`) for every item so all items stay at `strategy.itemSize`; keep the
|
||
keyline maths for snap positions. M3's extra note for hero under reduced motion — "the small item shows
|
||
only partially" — then falls out of the scroll position rather than the mask.
|
||
- Effort: M
|
||
- Breaks API? no
|
||
|
||
### C-06 · modal · A scrolling dialog scrolls its headline and its action row away
|
||
- Severity: should-fix
|
||
- M3 says: "Scrolling: dialog content generally shouldn't scroll; if it must, **the title stays pinned at
|
||
top and buttons pinned at bottom**, and the dialog never scrolls with background content"
|
||
(reference-components-a.md § Dialogs → Behaviour and guidelines).
|
||
- Library does: `resources/views/components/modal.blade.php:66` puts `overflow-y-auto` on the outer box,
|
||
which contains the header block (lines 81-100), the body (line 102) and the actions (lines 105-112). The
|
||
body wrapper at line 80 is `min-h-0 flex-1` with no overflow of its own, so once the content exceeds
|
||
`max-h-[calc(100dvh-3rem)]` everything scrolls together. Only the `fullscreen` branch pins correctly
|
||
(`max-sm:overflow-y-auto` on the inner div, line 80).
|
||
- Fix: in `modal.blade.php`, remove `overflow-y-auto` from line 66 (keep `overflow-hidden`), give the
|
||
header block `shrink-0`, and move `overflow-y-auto` onto the `min-h-0 flex-1` wrapper at line 80 for all
|
||
cases (not just `max-sm:`). The actions already have `shrink-0`. Move `p-6` from the box to the three
|
||
regions so the pinned header/footer keep their 24dp padding.
|
||
- Effort: S
|
||
- Breaks API? no (`box-class` callers that relied on the outer scroll would change)
|
||
|
||
### C-07 · drawer · The list-detail pane opens at 1280px; M3 puts two panes from 840px
|
||
- Severity: should-fix
|
||
- M3 says: list-detail visible panes — "Compact (0–599): 1 pane; Medium (600–839): 1 (recommended) or 2;
|
||
**Expanded (840+): 2**; Large (1200–1599): 2; Extra-large (1600+): 2"
|
||
(reference-foundations-supplement.md § Canonical layout examples → List-detail).
|
||
- Library does: `drawer.blade.php:55` (`window.matchMedia('(min-width: 80rem)')`) and the `xl:` classes at
|
||
lines 64-65 and 100 — the pane appears only from 1280px. Between 840 and 1279px (the whole expanded class
|
||
and most of large) the detail still opens as a modal sheet over a scrim with the list inert, which is
|
||
M3's compact behaviour.
|
||
- Fix: change `80rem` to `52.5rem` (840px) in `drawer.blade.php:55` and swap the `xl:` prefixes for a
|
||
custom `expanded:` variant defined as `@custom-variant expanded (@media (min-width: 52.5rem))` in
|
||
`resources/css/tokens/theme.css`; update `SKILL.md:463` and the drawer header comment. If that is too
|
||
aggressive for narrow laptop layouts, make the threshold a prop (`pane-from`) with 840 as the default.
|
||
- Effort: M
|
||
- Breaks API? yes (pages laid out with `xl:flex xl:items-start xl:gap-6` around the drawer would need the
|
||
same breakpoint changed; that wrapper is documented in `SKILL.md:463`)
|
||
|
||
### C-08 · list.css · Segmented list rows lose their hover and press state layer to the cascade
|
||
- Severity: should-fix
|
||
- M3 says: list items show Hovered / Focused / Pressed / Dragged states (reference-components-a.md § Lists
|
||
→ Specs), and "Cursor: hover shows a visible cue that the item is interactive" (§ Accessibility). State
|
||
layer opacities: hover 8%, focus 10%, pressed 10% (reference-foundations, states).
|
||
- Library does: `resources/css/components/list.css:42-44` sets
|
||
`[data-list='segmented'] > [data-list-item] { background-color: var(--md-sys-color-surface-container); }`
|
||
**unlayered**, while the hover (line 22-24), focus (27-31) and press (33-35) rules live inside
|
||
`@layer components`. `resources/css/material.css` imports `list.css` without wrapping it, so the
|
||
unlayered declaration wins over every layered one regardless of the `:where()` specificity — a segmented
|
||
row gets no background state layer on hover or press. Only the corner morph (lines 93-100, unlayered)
|
||
and the focus outline (a different property) still show.
|
||
- Fix: move the `[data-list='segmented'] > [data-list-item]` background rule (lines 42-44, a duplicate of
|
||
the selector at 79-81) into `@layer components` alongside the state rules, or paint the state layer with
|
||
a `::before`/`background-image` instead of `background-color` so the two never collide.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-09 · bottom-sheet · Drag handle colour is on-surface-variant at 40%, not the role colour
|
||
- Severity: should-fix
|
||
- M3 says: "drag handle = On surface variant" (specs page colour roles, reference-components-a.md §
|
||
Bottom sheets). `SheetBottomTokens.kt:30` — `DockedDragHandleColor get() = ColorSchemeKeyTokens.OnSurfaceVariant`;
|
||
`SheetDefaults.kt:576` uses it undiluted (`color: Color = SheetBottomTokens.DockedDragHandleColor.value`),
|
||
with no opacity multiplier.
|
||
- Library does: `bottom-sheet.blade.php:60` — `bg-on-surface-variant/40`. The component's own header comment
|
||
(line 10) says "a 32×4px drag handle in on-surface-variant", so the code contradicts its own doc.
|
||
- Fix: `bg-on-surface-variant` in `bottom-sheet.blade.php:60`.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-10 · drawer · Bottom actions are right-aligned; the side-sheet spec says left
|
||
- Severity: should-fix
|
||
- M3 says: side sheet specs table (both standard and modal): "Bottom actions alignment (horizontal) |
|
||
**Left**"; "Bottom actions height 72dp; top padding 16dp; bottom padding 24dp"
|
||
(raw `components_side-sheets_specs.md:125` and `:199`).
|
||
- Library does: `drawer.blade.php:135` — `flex shrink-0 flex-wrap items-center justify-end gap-2 pt-6`
|
||
(right-aligned, 24px top padding, no bottom padding of its own beyond the sheet's `p-6`).
|
||
- Fix: `justify-start` in `drawer.blade.php:135`, and `pt-4 pb-0` inside a 72px-min row
|
||
(`min-h-18 pt-4`) to match 16/24/72. Note this deliberately differs from the dialog, whose actions are
|
||
trailing-aligned — M3 specifies them differently.
|
||
- Effort: S
|
||
- Breaks API? yes (visual position of every existing drawer action row)
|
||
|
||
### C-11 · carousel · The full-screen layout scrolls horizontally; M3's scrolls vertically
|
||
- Severity: should-fix
|
||
- M3 says: "The full-screen carousel layout shows one edge-to-edge large item at a time and **scrolls
|
||
vertically**"; "This layout works best with content that is taller than it is wide, and scrolls
|
||
vertically. It only works in **portrait orientation in compact and medium breakpoints**. Don't use this
|
||
layout in landscape orientation." (raw `components_carousel_guidelines.md:31,177-183`). Specs table:
|
||
full-screen padding 0dp all round, 16dp between elements, edge-to-edge with no item radius.
|
||
- Library does: `carousel.blade.php:127-131` renders every layout, full-screen included, as a horizontal
|
||
`overflow-x-auto` row; `carousel-item.blade.php:35` gives every item `rounded-corner-xl` and the 28px
|
||
clip-path. The header comment (line 22-23) describes full-screen as "one item the width of the carousel
|
||
at a time", i.e. horizontal by design.
|
||
- Fix: either (a) add a vertical mode for `layout="full-screen"` (`flex-col`, `overflow-y-auto`,
|
||
`snap-y snap-mandatory`, items `h-full w-full` with no corner radius, arrow keys Up/Down) and clamp it to
|
||
compact/medium widths, or (b) rename the layout so it does not claim to be M3's full-screen carousel and
|
||
say so in `SKILL.md:471`. Also drop the 28px corner and the 8px gap for this layout (M3: edge-to-edge,
|
||
16dp between elements).
|
||
- Effort: L
|
||
- Breaks API? yes if renamed; no for (a) if `layout="full-screen"` keeps its name
|
||
|
||
### C-12 · carousel · Leading/trailing padding defaults to 0 where M3 specifies 16dp
|
||
- Severity: should-fix
|
||
- M3 says: specs table — Multi-browse / Hero / Center-aligned hero: "Leading/trailing padding **16dp**,
|
||
Top/bottom padding **8dp**, Padding between elements 8dp"; Uncontained: "16dp (leading only)"
|
||
(reference-components-a.md § Carousel → Specs).
|
||
- Library does: `carousel.blade.php:55` — `'padding' => 0`; the scroller at lines 127-131 has no vertical
|
||
padding at all. Every showcase example (`resources/views/showcase/sections/carousel.blade.php`) therefore
|
||
renders with 0 end padding, so items sit flush against the container edge.
|
||
- Fix: default `'padding' => 16` in `carousel.blade.php:55` (and pass leading-only for `uncontained`, 0 for
|
||
`full-screen`); add `py-2` to the scroller class list at line 128. Update `SKILL.md:471` where it
|
||
documents "`padding` (px at the ends, 0)".
|
||
- Effort: S
|
||
- Breaks API? no (a default value changes; explicit `padding="0"` still works)
|
||
|
||
### C-13 · card · No elevation change on hover, focus, press or drag
|
||
- Severity: should-fix
|
||
- M3 says: per-state elevation from `ElevatedCardTokens.kt` / `FilledCardTokens.kt` / `OutlinedCardTokens.kt`
|
||
(reference-components-a.md § Cards → cross-check table): elevated 1dp rest → **3dp hover** → 1dp
|
||
focus/pressed → **8dp dragged**; filled 0 → **1dp hover** → 0 → **6dp dragged**; outlined 0 → **1dp
|
||
hover** → 0 → 6dp dragged. The specs page shows Hovered / Focused / Pressed / Dragged / Disabled for all
|
||
three variants (raw `components_cards_specs.md:78,128,180`).
|
||
- Library does: `card.blade.php:34-36` sets one elevation and never changes it
|
||
(`shadow-elevation-1` for elevated, none for filled/outlined). The only interactive response is in
|
||
`resources/css/components/list.css:52-72`, and that is a state layer plus a corner morph — no
|
||
`box-shadow` level change. There is no dragged state anywhere in the group.
|
||
- Fix: add hover/focus elevation to `card.blade.php` for interactive cards — e.g. in `list.css`
|
||
`[data-card][data-list-row]:hover { box-shadow: var(--md-sys-elevation-2), inset … }` for the elevated
|
||
variant and `var(--md-sys-elevation-1)` for filled/outlined. Needs a variant marker on the element
|
||
(`data-card="elevated|filled|outlined"` instead of the bare `data-card` at `card.blade.php:31`).
|
||
- Effort: M
|
||
- Breaks API? no (`data-card` gains a value; the bare attribute selector still matches)
|
||
|
||
### C-14 · list-item · Leading/trailing gap is 12px where M3 uses 16dp
|
||
- Severity: should-fix
|
||
- M3 says: `ListItem.kt` (androidx-main) lines 1269 and 1273 —
|
||
`internal val LeadingContentEndPadding = 16.dp`, `internal val TrailingContentStartPadding = 16.dp`;
|
||
`ListTokens.kt:171,338` — `ItemLeadingSpace = 16.dp`, `ItemTrailingSpace = 16.dp`.
|
||
- Library does: `list-item.blade.php:46` — `flex items-center gap-3 px-4` (12px gap). The container padding
|
||
(`px-4` = 16px) is correct; only the internal gaps are short.
|
||
- Fix: `gap-4` in `list-item.blade.php:46`.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-15 · list-item · Three-line items are middle-aligned; M3 top-aligns them
|
||
- Severity: should-fix
|
||
- M3 says: "Alignment: elements are middle-aligned by default; **top-aligned if the item is 88dp+ or has 3+
|
||
lines of text**" (reference-components-a.md § Lists → Specs, from the overview page). Compose confirms
|
||
the vertical padding also changes: `ListItem.kt:1259,1261` —
|
||
`ListItemVerticalPadding = 8.dp`, `ListItemThreeLineVerticalPadding = 12.dp`.
|
||
- Library does: `list-item.blade.php:46` — `items-center` for every case; padding is `py-2` (8px) for one
|
||
line and `py-2.5` (10px) for both two- and three-line items (lines 47-49), where M3 wants 8 and 12.
|
||
- Fix: in `list-item.blade.php:45-51`, add `'items-start' => $lines === 2` (alongside `items-center` for
|
||
the others) and change `'min-h-22 py-2.5' => $lines === 2` to `'min-h-22 py-3' => $lines === 2`; leave
|
||
`py-2` for `$lines === 0` and use `py-2` for `$lines === 1` too.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-16 · list · Dividers between items run edge to edge; the list token insets them 16dp
|
||
- Severity: should-fix
|
||
- M3 says: `ListTokens.kt:30,36` — `DividerLeadingSpace = 16.dp`, `DividerTrailingSpace = 16.dp` (top/bottom
|
||
space 0). Guidelines: inset dividers "separate related content within one section (e.g. emails in a
|
||
list); indented equally from both sides by default; pair with anchoring elements like icons/avatars"
|
||
(reference-components-a.md § Divider).
|
||
- Library does: `list.blade.php:25` — `'divide-y divide-outline-variant' => $dividers && ! $segmented`,
|
||
which draws a full-bleed 1px rule between items. `<x-divider>` has `inset` and `middle` props, but the
|
||
list never uses them and there is no way to ask for an inset list divider.
|
||
- Fix: in `list.blade.php`, render the divide with a 16px inset — e.g. add
|
||
`[&>[data-list-item]:not(:last-child)]:after` or keep `divide-y` and add `mx-4` to the rule via a small
|
||
CSS block in `resources/css/components/list.css`: `[data-list='plain'][data-dividers] > [data-list-item]:not(:last-child) { box-shadow: inset 0 -1px 0 … }` inset by 16px. Alternatively give `<x-list>` a
|
||
`dividers="full"|"inset"|"middle"` value instead of a boolean.
|
||
- Effort: M
|
||
- Breaks API? no if `dividers` stays boolean-compatible
|
||
|
||
### C-17 · modal · Full-screen dialog header is 64px where M3 specifies 56dp
|
||
- Severity: should-fix
|
||
- M3 says: full-screen dialog specs table — "Header height | **56dp**"; "Bottom action bar height | 56dp";
|
||
"Top/left/right padding 24dp" (reference-components-a.md § Dialogs → Specs).
|
||
- Library does: `modal.blade.php:71` — `flex h-16 shrink-0 items-center gap-1 px-1 sm:hidden` (64px), and
|
||
the bottom action bar at line 108 is `max-sm:px-6 max-sm:py-4` around a 40px button ≈ 72px.
|
||
- Fix: `h-14` (56px) at `modal.blade.php:71`; `max-sm:min-h-14 max-sm:py-2` at line 108. Keep `px-1` on the
|
||
bar so the close icon button's 48px target still reaches the 24dp text margin (M3 aligns the header
|
||
headline to 24dp from the edge; the icon button's own padding supplies it).
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-18 · carousel · Tab lands on the container, which M3 explicitly tells you not to do
|
||
- Severity: should-fix (two readings — see below)
|
||
- M3 says: "When navigating to a carousel using assistive technology, use **Tab** to place initial focus on
|
||
the **first carousel item**"; and the caption under the Don't image: "**Avoid focusing on the carousel
|
||
container**" (raw `components_carousel_accessibility.md:130,146`). Keyboard table: "Tab or Arrows — Moves
|
||
to the previous or next carousel item; Space or Enter — Activates the focused carousel item".
|
||
- Library does: `carousel.blade.php:123-126` gives the scroller `role="region"`,
|
||
`aria-roledescription="carousel"` and **`tabindex="0"`**; the items (`carousel-item.blade.php:23-32`) are
|
||
`role="group"` with no `tabindex`, so they are never focusable and Space/Enter cannot activate one. The
|
||
arrow keys only work while the container itself has focus (`carousel.js:1065-1068` returns early unless
|
||
`event.target === this.$refs.scroller`). The header comment (lines 37-43) says this is deliberate:
|
||
"WAI-ARIA's carousel pattern: the row is a focusable `region`".
|
||
- Both readings: the library's choice satisfies WCAG 2.1.1 for a scrollable region whose content may be
|
||
non-focusable (an `<img>`-only slide), which is the ARIA-APG "scrollable region" practice; M3's rule
|
||
assumes every item is itself an actionable target. They conflict; M3's is the stated rule here.
|
||
- Fix: give each `<x-carousel-item>` `tabindex="0"` and handle Arrow/Home/End/Space/Enter on the focused
|
||
item (roving tabindex), keeping the container out of the tab order — or, if the current pattern is kept,
|
||
record it in the "deliberate deviations" of `SKILL.md:471` with the M3 quote so a reviewer does not
|
||
re-litigate it.
|
||
- Effort: M
|
||
- Breaks API? no
|
||
|
||
### C-19 · bottom-sheet · Default height is 90dvh; M3 caps a modal sheet's initial position at 50%
|
||
- Severity: should-fix
|
||
- M3 says: "Modal: … Initial vertical position is **capped at 50% of screen height**; if content exceeds
|
||
that, it can be pulled to full screen and scrolled internally." Specs table: "Top margin 72dp; Top margin
|
||
(window width > 640dp) 56dp; Start/end margin (window width > 640dp) 56dp"
|
||
(reference-components-a.md § Bottom sheets).
|
||
- Library does: `bottom-sheet.blade.php:17` — `'height' => '90dvh'`, applied as
|
||
`max-h-(--sheet-max-height)` (line 55). There is one height and no preset-height cycling, so a sheet may
|
||
open at 90% of the viewport where M3 would open at 50% and let the user pull it up. The 56dp side margin
|
||
above 640px is also absent (`mx-auto … max-w-160`, line 55, with no horizontal margin).
|
||
- Fix: default `'height' => '50dvh'` with a `max-height` ceiling of `calc(100dvh - 72px)` in
|
||
`bottom-sheet.blade.php:17,55`, and add `sm:px-14` (56px) to the wrapper or `sm:max-w-[calc(100vw-7rem)]`.
|
||
A second preset height, cycled by the drag handle's click, would complete M3's "selecting the drag handle
|
||
toggles preset heights" rule; today the click only closes.
|
||
- Effort: M
|
||
- Breaks API? yes (default sheet height changes)
|
||
|
||
### C-20 · modal · No `role="alertdialog"` on a basic dialog
|
||
- Severity: should-fix (two readings)
|
||
- M3 says: "On web, basic dialogs should have the **alert dialog** role"; "Basic dialogs are known as alert
|
||
dialogs on web" (raw `components_dialogs_accessibility.md:130,134`).
|
||
- Library does: `modal.blade.php:41` uses a native `<dialog>` opened with `showModal()`
|
||
(line 51), which the browser maps to `role="dialog"` + `aria-modal="true"`. No `role` is set, and no
|
||
`aria-describedby` points at the supporting text (line 93).
|
||
- Both readings: ARIA-APG restricts `alertdialog` to dialogs that "interrupt … to communicate an important
|
||
message" and requires an `aria-describedby` message; applying it to every `<x-modal>` (including forms,
|
||
which the `fullscreen` variant is explicitly for) would over-announce. A middle path matches both.
|
||
- Fix: add an `alert` boolean prop to `modal.blade.php` that sets `role="alertdialog"` plus
|
||
`aria-describedby="{{ $id }}-body"`, and use it in the showcase's destructive-confirmation example
|
||
(`showcase/sections/containment.blade.php:73`). Add `aria-describedby` for the subtitle unconditionally.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-21 · list · Segmented items use `surface-container`; the token says `Surface`
|
||
- Severity: should-fix
|
||
- M3 says: `ListTokens.kt:201` — `ItemSegmentedContainerColor get() = ColorSchemeKeyTokens.Surface`
|
||
(`ItemContainerColor` is also `Surface`). `SegmentedGap = 2.0.dp` (line 353).
|
||
- Library does: `resources/css/components/list.css:43` and `:79-81` —
|
||
`background-color: var(--md-sys-color-surface-container)`. The 2px gap (`list.blade.php:24`,
|
||
`gap-0.5`) and the 4px/16px corner morph are right; only the fill is a tone off.
|
||
- Fix: `var(--md-sys-color-surface)` in `list.css:43`. Note this only reads as "segmented" when the page
|
||
behind it is a container tone; if the library prefers the stronger tone, say so in the file comment,
|
||
because the header currently claims "each item its own surface-container tile … (ListTokens)".
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-22 · list.css · A focused segmented row does not morph to the large corner
|
||
- Severity: nice-to-have
|
||
- M3 says: "Interaction-state expressive shapes: hovered = Medium (12dp); **focused**/pressed/dragged/
|
||
selected-any-state = Large (16dp)" (reference-components-a.md § Lists → Specs, cross-checked against
|
||
`ListTokens.kt` — `ItemPressedContainerExpressiveShape` = CornerLarge,
|
||
`ItemDraggedContainerExpressiveShape` = CornerLarge).
|
||
- Library does: `resources/css/components/list.css:93-100` covers `:hover` (→ md) and
|
||
`:is([data-selected], [data-list-row]:active)` (→ lg). `:focus-visible` is not in either selector, so a
|
||
keyboard-focused segmented row keeps the 4px corner.
|
||
- Fix: extend the selector at `list.css:99` to
|
||
`[data-list='segmented'] > [data-list-item]:is([data-selected], [data-list-row]:active, [data-list-row]:has([data-list-open]:focus-visible))`.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-23 · card · The corner morphs 12→16 on hover, which M3 cards do not do
|
||
- Severity: nice-to-have
|
||
- M3 says: cards have one shape — "Shape | 12dp corner radius" for all three variants, with no press or
|
||
hover shape listed in `ElevatedCardTokens.kt` / `FilledCardTokens.kt` / `OutlinedCardTokens.kt`
|
||
(reference-components-a.md § Cards → Specs). Shape morph is specified for buttons, FABs and list items,
|
||
not cards.
|
||
- Library does: `resources/css/components/list.css:59-62` —
|
||
`[data-card][data-list-row]:hover { border-radius: var(--md-sys-shape-corner-lg); … }`. The
|
||
`card.blade.php` header (lines 10-14) documents this: "It answers with a state layer and its corner
|
||
opening a step."
|
||
- Fix: either drop the `border-radius` line at `list.css:60` (keeping the state layer, which M3 does
|
||
require for a directly-actionable card) or keep it and note in the card header that this is an Expressive
|
||
extension M3 does not specify for cards. The state layer itself is correct and should stay.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-24 · collapse · `interpolate-size` is set but nothing animates the height
|
||
- Severity: nice-to-have
|
||
- M3 says: n/a — collapse is not an M3 component; foundations require only that motion respect reduced
|
||
motion, which the token system already does (`resources/css/tokens/motion.css:63-75`).
|
||
- Library does: `collapse.blade.php:49` adds `[interpolate-size:allow-keywords]` and the header comment
|
||
(lines 7-8) promises "where the browser supports animating `details` content (`interpolate-size`) — a
|
||
height that eases open". There is no `transition: height` and no `::details-content` rule anywhere in
|
||
`resources/css/` (grepped: `interpolate-size` and `details-content` appear only in this file), so the
|
||
content snaps open. `interpolate-size` on its own changes nothing.
|
||
- Fix: add to a component stylesheet:
|
||
`details.group\/collapse::details-content { block-size: 0; overflow: hidden; transition: block-size var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast), content-visibility var(--md-sys-motion-spatial-fast-duration) allow-discrete; }`
|
||
and `details[open].group\/collapse::details-content { block-size: auto; }` — or delete the
|
||
`interpolate-size` utility and the sentence in the header comment.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-25 · carousel-item · The overlay label is `text-white`, outside the role set
|
||
- Severity: nice-to-have
|
||
- M3 says: "Layering text/icons on images is **not recommended**; if necessary, add a translucent scrim or
|
||
a bounding shape behind the text/icon to guarantee accessible contrast" (reference-components-a.md §
|
||
Cards → Behaviour; the same rule is the reason the carousel's item content is art). Colour must come from
|
||
a scheme role.
|
||
- Library does: `carousel-item.blade.php:46` — `type-title-md text-white` over
|
||
`bg-linear-to-t from-scrim/60` (line 44). The scrim is right; `white` is a literal, not a role, so it
|
||
does not follow a scheme or a high-contrast profile.
|
||
- Fix: `text-inverse-on-surface` (which is near-white in a light scheme and dark in a dark one — check the
|
||
intent) or add an explicit `--md-sys-color-on-scrim` style token if a fixed light-on-dark is wanted for
|
||
both schemes; a literal `white` is defensible over a 60% black scrim but should be stated as such in the
|
||
file comment.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-26 · list-item · Leading icon stays 24px in a segmented (expressive) list
|
||
- Severity: nice-to-have
|
||
- M3 says: `ListTokens.kt:153,156,332,335` — `ItemLeadingIconExpressiveSize = 20.dp` /
|
||
`ItemLeadingIconSize = 24.dp`; `ItemTrailingIconExpressiveSize = 20.dp` / `ItemTrailingIconSize = 24.dp`.
|
||
The library's segmented list is explicitly the expressive variant (`list.blade.php:4-6`).
|
||
- Library does: `list-item.blade.php:64` and `:97` — `size-6` (24px) in both cases, regardless of the
|
||
parent list's mode.
|
||
- Fix: the item does not know its parent, so use CSS:
|
||
`[data-list='segmented'] > [data-list-item] svg { width: 20px; height: 20px }` in
|
||
`resources/css/components/list.css`, scoped to the leading/trailing icons only.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-27 · mail · The card corner is 24px, a value not on M3's shape scale
|
||
- Severity: nice-to-have
|
||
- M3 says: the corner scale is 0 / 4 / 8 / 12 / 16 / 20 / 28 / 32 / 48 (reference-styles.md § Shape; the
|
||
library's own `resources/css/tokens/shape.css:11-20` reproduces it). 24 is not a step.
|
||
- Library does: `resources/views/mail/theme.blade.php` `.inner-body { border-radius: 24px; }`; the panel
|
||
uses `16px` (on-scale) and `code` uses `4px` (on-scale).
|
||
- Fix: `border-radius: 28px` (extra-large) on `.inner-body`, matching the dialog/bottom-sheet surface tone
|
||
this card stands in for. Everything else in the theme is on-scale.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
### C-28 · mail · The header app name is title-large at weight 500, not 400
|
||
- Severity: nice-to-have
|
||
- M3 says: title-large = weight 400 (Regular), 22/28, tracking 0 — the library's own
|
||
`resources/css/tokens/type.css:39-41` has `--md-sys-typescale-title-lg` at regular and
|
||
`--md-sys-typescale-emphasized-title-lg` at medium.
|
||
- Library does: `resources/views/mail/theme.blade.php` `.header a { font-size: 22px; font-weight: 500; … }`,
|
||
while the section comment says "the app name in title-lg". `h2` right below it uses 22/28 at weight 400,
|
||
so the two disagree within the same file.
|
||
- Fix: either set `font-weight: 400` on `.header a`, or change the comment to say
|
||
title-large-**emphasized** — which is the more likely intent for a brand line and is a real M3 role.
|
||
- Effort: S
|
||
- Breaks API? no
|
||
|
||
## Deliberate deviations
|
||
|
||
- **`card.blade.php:16-17` — "Do not pass a `bg-*` class … it races the card's own in Tailwind's emit
|
||
order."** True and correctly explained; the CSS in `list.css` is unlayered for the same reason
|
||
(`list.css:10-11`). Holds up, and is the cause of C-08 (the same unlayered trick applied to the segmented
|
||
background, where it is not needed).
|
||
- **`card.blade.php:10-14` / `list-rows.js` — the `data-list-row` + `data-list-open` contract instead of a
|
||
stretched link or a wrapping `<a>`.** M3's accessibility page for cards says exactly this: "on a
|
||
**directly actionable card** Tab moves to the next card container; on a **non-actionable card with
|
||
actionable elements**, Tab moves through each actionable element inside before moving to the next card"
|
||
and forbids stacking an action on an already-actionable surface. The library implements the second case
|
||
with a single real control. Holds up, and is a better answer than M3 gives for the web.
|
||
- **`drawer.blade.php:9-11` — "it enters on emphasized decelerate rather than a spring — a sheet anchored
|
||
to the edge that overshot would open a gap."** M3 Expressive's motion scheme is springs for spatial
|
||
change, but `styles/motion` keeps the emphasized-decelerate curve for exactly this kind of entrance and
|
||
the reference records no rule against it. Holds up.
|
||
- **`modal.blade.php:24` — "It opens on the fast spatial spring and closes at once, as M3's do."** The
|
||
first half is fine; the second is not supported by Google's text, which says a dialog "appears via an
|
||
**enter/exit** transition" (reference-components-a.md § Dialogs → Behaviour). Native `<dialog>` makes an
|
||
exit transition awkward (`@starting-style` only covers entry), which is the real reason — worth saying so
|
||
instead. Folded into no finding of its own; it is a doc accuracy point.
|
||
- **`list.blade.php:8` — `role="list"` rather than `listbox`.** Correct for a plain list of links; it is
|
||
only wrong once `selected` is used, which is C-03.
|
||
- **`carousel.blade.php:37-43` — the WAI-ARIA carousel pattern rather than M3's "focus the first item".**
|
||
See C-18; a genuine standards conflict, but M3's Don't is explicit and is not acknowledged in the code.
|
||
- **`mail/theme.blade.php:9-26` — hexes only, light only, no `@media`, no elevation.** Every reason given
|
||
is correct (CssToInlineStyles' `doCleanup()` does strip `@media`; Outlook does drop alpha). Accepted by
|
||
the brief and by the constraints.
|
||
- **`collapse.blade.php:1-2` — "Not an M3 component; built on the native `<details>`."** Correct; M3 has
|
||
expandable list items and menu expansion but no standalone disclosure. Judged against foundations it is
|
||
sound: 48px summary (`min-h-12`), the 8/10% state layer, the 3px secondary focus ring, reduced motion via
|
||
the duration tokens, and the native `aria-expanded`.
|
||
|
||
## Aligned
|
||
|
||
- **Card**: filled `surface-container-highest`, elevated `surface-container-low` + elevation 1, outlined
|
||
`surface` + 1px `outline-variant` — all three match the specs page and the token files. `rounded-corner-md`
|
||
= 12dp; `p-4` = the 16dp left/right padding; `overflow-hidden` + a full-bleed `figure` slot is the media
|
||
anatomy. (Card typography is not specified by M3 at all — the specs page lists no type roles — so
|
||
`type-title-md` / `type-body-md` cannot be marked wrong.)
|
||
- **List heights and leading sizes**: `min-h-14` / `min-h-18` / `min-h-22` = 56 / 72 / 88dp exactly
|
||
(`ListTokens.kt:180,323,347`); avatar `size-10` = 40dp (`ItemLeadingAvatarSize`), image `size-14` = 56dp
|
||
(`ItemLeadingImageWidth`) with `rounded-corner-sm` = the expressive `CornerSmall`; icon `size-6` = 24dp
|
||
(baseline `ItemLeadingIconSize`); `px-4` = 16dp container padding.
|
||
- **List type roles**: title `type-body-lg` (`ItemLabelTextFont` = BodyLarge), description `type-body-md`
|
||
(`ItemSupportingTextFont` = BodyMedium), overline and trailing text `type-label-sm`
|
||
(`ItemOverlineFont` / `ItemTrailingSupportingTextFont` = LabelSmall), `line-clamp-2` on the description.
|
||
- **Selected colours**: `secondary-container` / `on-secondary-container` is right —
|
||
`ListTokens.kt:204,281,284` (`ItemSelectedContainerColor` = SecondaryContainer,
|
||
`ItemSelectedLabelTextColor` / `ItemSelectedLeadingIconColor` = OnSecondaryContainer). (The reference's
|
||
specs-page note about "Primary container" is contradicted by the token file; the library follows the
|
||
tokens.) Disabled content at 38% matches `ItemDisabled*Opacity = 0.38f`.
|
||
- **Segmented list geometry**: 2px gap = `SegmentedGap`; 4px item corner = `ItemContainerExpressiveShape`
|
||
(CornerExtraSmall); 16px at the list's ends and while pressed/selected = `ContainerShape` /
|
||
`ItemSelectedContainerExpressiveShape` (CornerLarge); 12px on hover = the specs page's Medium.
|
||
- **`data-list-row` state layer**: hover 8%, focus 10%, pressed 10% on `on-surface`, hover gated behind
|
||
`@media (hover: hover)`; the focus ring is 3px `secondary` (matching `ListTokens.kt:39`
|
||
`FocusIndicatorColor` = Secondary); the opener's own ring is suppressed so the row shows one indicator.
|
||
- **Divider**: 1px (`DividerTokens.Thickness`), `outline-variant`, `inset` = 16px start / 0 end,
|
||
`middle` = 16px both, a vertical variant, `role="separator"` with `aria-orientation`, and `decorative` to
|
||
hide it — a complete match to the specs table.
|
||
- **Dialog**: `surface-container-high` + `rounded-corner-xl` (28dp) + `shadow-elevation-3` =
|
||
`DialogTokens` exactly; `p-6` = 24dp all round; `max-w-[35rem]` / `min-w-70` = 560 / 280dp;
|
||
`backdrop:bg-scrim/32` = `ScrimTokens.ContainerOpacity = 0.32f`; `type-headline-sm` =
|
||
`DialogTokens.HeadlineFont`; `type-body-md` on-surface-variant = `SupportingTextFont`; a 24px
|
||
`text-secondary` hero icon that centres the headline (M3: "Alignment with icon: Center-aligned");
|
||
`gap-2` = the 8dp between buttons; `mt-4` = the 16dp title↔body and icon↔title gaps; `pt-6` = the 24dp
|
||
body↔actions gap; `justify-end` = trailing-edge actions, and the showcase orders Cancel before Delete
|
||
(`showcase/sections/containment.blade.php:75-76`), which is M3's "dismissive to the left of confirming".
|
||
`wire:ignore.self` + native `showModal()` gives top-layer, inert background, focus-in/focus-return and
|
||
Escape for free.
|
||
- **Bottom sheet**: `surface-container-low` (`DockedContainerColor`), `rounded-t-corner-xl` (28dp top,
|
||
`CornerExtraLargeTop`), `shadow-elevation-1` (`DockedModalContainerElevation` = Level1),
|
||
`max-w-160` = the 640dp max width, 32×4px handle geometry, 32% scrim, `x-trap.inert.noscroll`,
|
||
dismissal by scrim / Escape / downward drag, and a single-pointer alternative to the drag (the handle is
|
||
a real `<button>` with an accessible name and `role=button`, which is M3's "label only the drag handle").
|
||
- **Side sheet**: `surface-container-low`, a 16px corner on the inner edge only
|
||
(M3's "16dp corner radius for modal side sheets"), 400px default width = the specs max-width, `p-6` =
|
||
24dp start/end padding, end placement by default (M3: "usually the right"), `role="dialog"` (M3's stated
|
||
role), full height, independent vertical scroll and no horizontal scroll, `x-trap.inert.noscroll`, a
|
||
container query on the body so contents lay out by the sheet's width.
|
||
- **Carousel**: the Compose keyline maths is ported with attribution and a commit hash; small items clamp
|
||
to 40-56dp (`MIN_SMALL_ITEM_SIZE` / `MAX_SMALL_ITEM_SIZE`, carousel.js:64-65); 28px item corner
|
||
(`CarouselDefaults`); 8px between items (`gap-2` = the specs' "Padding between elements 8dp"); snap for
|
||
multi-browse / hero / full-screen and free scroll for uncontained, exactly M3's recommendation;
|
||
per-item "n of m" labels; controls placed **below** the row, never over it (M3's explicit Don't); RTL
|
||
mirroring; re-measure on resize and after a morph.
|
||
- **Collapse**: 48px summary, the shared state layer and focus ring, a chevron on the fast spatial spring,
|
||
reduced motion via zeroed duration tokens, native disclosure semantics, `wire:ignore.self` for morphs.
|
||
- **Error pages**: `bg-surface` / `text-on-surface`, `type-emphasized-display-lg` in `on-primary-container`
|
||
over a `primary-container` shape (a correct contrast pair), `type-headline-md`/`lg` for the headline,
|
||
`type-body-lg` in `on-surface-variant` for the message, a filled primary action with a text secondary
|
||
(M3's action hierarchy), 24px page gutters, and the decorative shape's rotation gated behind
|
||
`prefers-reduced-motion: no-preference`.
|
||
- **Mail theme**: the typescale is reproduced correctly in px — h1 24/32/400/0 (headline-small),
|
||
h2 22/28 (title-large), h3 16/24/500/0.15 (title-medium), p 16/24/400/0.5 (body-large),
|
||
`p.sub` and table cells 14/20/0.25 (body-medium), table head 14/20/500/0.1 (title-small),
|
||
footer 12/16/0.4 (body-small), the button 16/24/500/0.15 (title-medium, the Expressive medium button's
|
||
label) with a full corner. Roles are used properly throughout (`on-surface` for emphasis,
|
||
`on-surface-variant` for body, `outline-variant` for every rule, `surface-container-lowest` for the card
|
||
against `surface-container` for the page), and separation is by tone rather than shadow, as M3 does.
|
||
|
||
## Missing
|
||
|
||
- **Lists**: leading **video** slot (56×100dp small, 64×114dp large — `ListTokens.kt:129,132,177`); the
|
||
**expand/collapse** list-item interaction (M3: "items containing nested items can expand/collapse …
|
||
container-transform"); explicit **selection modes** (single-select / multi-select / single-action /
|
||
multi-action) with their role and keyboard mappings; **inset and middle dividers** between list items
|
||
(only a full-bleed `dividers` boolean exists); a **dragged** state (16% layer, elevation 4) for
|
||
reorderable lists.
|
||
- **Cards**: per-state **elevation** (C-13) and the **dragged** state; a first-class "directly actionable
|
||
card" that takes a `button`/`link` role — M3 says such a card gets one, and the `data-list-row` pattern
|
||
deliberately keeps the role on the inner opener instead.
|
||
- **Dialogs**: no divider pinned between a scrolling body and the header/actions (the `separator` prop
|
||
scrolls with the content); no 56dp edge-margin rule for custom-positioned dialogs on large screens; no
|
||
"discard unsaved changes" confirmation helper for the full-screen variant, which M3 requires of that
|
||
variant.
|
||
- **Bottom sheets**: **preset heights** and the handle's "cycle through heights on activation" behaviour
|
||
(M3 requires a non-drag alternative whenever more than one height exists); a peek/collapsed height for
|
||
the `standard` variant; the 56dp side margin above a 640dp window; the swap to a side sheet at expanded
|
||
widths that M3 recommends.
|
||
- **Side sheets**: a genuine **standard** (co-planar, non-modal, 0dp elevation) variant — `pane` is close
|
||
but is scoped to list-detail and starts at `xl`; the **back icon button** in the modal anatomy; a divider
|
||
above the action row; the 16dp "detached" inset M3 allows.
|
||
- **Carousel**: the **uncontained multi-aspect-ratio** layout (added November 2025 — items from 9:16 to
|
||
16:9); a **"Show all"** affordance opening a vertical list of every item, which M3 requires on
|
||
vertically-scrolling pages; a vertically-scrolling full-screen layout (C-11).
|
||
- **Divider**: the divider-with-text / subheader configuration (4dp gap to the supporting text, 8dp right
|
||
and bottom margins in the specs table).
|
||
|
||
## Breakpoint map
|
||
|
||
| Component | Library breakpoint used | M3 window size class it stands in for | Gap |
|
||
| --- | --- | --- | --- |
|
||
| `<x-modal fullscreen>` — full-screen below, basic above (`modal.blade.php:61,67,71,80,83,89,93,108`) | `max-sm` → < 640px | Compact (< 600dp): "full-screen dialogs are used only in compact breakpoints" | 40px too wide — a 600-639px window (small tablet portrait, split-screen) gets a full-screen dialog where M3 wants a basic one |
|
||
| `<x-drawer pane>` — pane vs modal sheet (`drawer.blade.php:55,64,65,100`; `xl:` = 1280px) | `xl` → ≥ 1280px | Expanded (≥ 840dp): list-detail shows **2 panes** from expanded through extra-large | 440px late — the entire expanded class (840-1199) and the bottom of large get the compact single-pane modal behaviour |
|
||
| `<x-drawer>` — fixed width and inner corner switch on (`drawer.blade.php:97-99`) | `sm` → ≥ 640px | Compact (< 600dp) is where a modal side sheet should be full-bleed; medium (600-839) onwards it has its fixed width | 40px too wide — a 600-639px window still gets a full-width sheet |
|
||
| `<x-bottom-sheet>` — 640px cap applied at every width (`bottom-sheet.blade.php:55`, `max-w-160`) | none (a `max-width`, not a breakpoint) | Specs: "Width: full width, up to max-width 640dp"; above a 640dp window also "Start/end margin 56dp" and "Top margin 56dp" | The 640 cap is right; the 56dp side/top margins above 640dp are missing entirely |
|
||
| `<x-modal>` — dialog gutters (`modal.blade.php:58`, `w-[calc(100vw-3rem)]`, `max-h-[calc(100dvh-3rem)]`) | none | Guidelines: custom-positioned dialogs on larger screens "must respect a 56dp margin from screen edges" | 24px used where 56dp is specified — only binding for custom-positioned dialogs, which the component does not offer, so informational |
|
||
| `<x-carousel controls>` (`carousel.blade.php:139`, `pointer-fine:`) | input-media query, not a width | M3 gives no breakpoint for carousel controls; it only says put them above or below the row | No gap — an input-capability query is a reasonable substitute and the placement rule is followed |
|
||
| `<x-carousel layout="full-screen">` (`carousel.blade.php:62`) | none — rendered at every width | Compact and medium only, portrait only ("Don't use this layout in landscape orientation") | Unbounded — nothing stops the layout above 840dp or in landscape |
|