Plan step 36 (containment group): <x-modal>'s class lists move into resources/css/components/modal.css, keyed on data-md-modal (with data-md-fullscreen) and data-md-modal-box/-bar/-head/-title/-subtitle/ -body/-content/-actions. Sizes stay px (560/280/48px matching DialogTokens.kt/FullScreenDialogTokens.kt and the 56px full-screen header, C-17), spacing the measurement tokens, breakpoints a literal 600px range query. The phone-only header bar and the pinned head/actions keep their existing responsive behaviour (a subtitle or an icon keeps the head on screen below 600px; only the bar's own divider-eligibility needs a computed flag, since it alone can sit beside a head that is also on screen) — reproduced with :has() against data-md-icon/ data-md-modal-subtitle wherever that is self-contained, and with an explicit data-md-modal-divider attribute where it is not. The scroll-driven dividers rename data-dialog-head/-actions -> data-md-modal-head/-actions, data-overflow-top/-bottom -> data-md-overflow-top/-bottom (resources/js/dialog.js); the dialog-dividers Alpine directive keeps its name, since it isn't a data-md-* hook. resources/css/components/dialog.css (Tailwind-era) is replaced by modal.css, imported from the Containment block of components.css; its import leaves resources/css/tailwind.css. No other component renders <x-modal> or dialog.css's rules (checked: datepicker, timepicker and search draw their own dialog-shaped surfaces independently). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
272 lines
9.8 KiB
CSS
272 lines
9.8 KiB
CSS
/*
|
|
* `<x-modal>`: M3's basic and full-screen dialogs (DialogTokens.kt, FullScreenDialogTokens.kt,
|
|
* androidx Compose Material 3, Apache-2.0; docs/reference/m3/
|
|
* components-actions-communication-containment.md § Dialogs → Specs).
|
|
*
|
|
* [data-md-modal] the <dialog>; data-md-fullscreen while `fullscreen` holds
|
|
* [data-md-modal-box] surface-container-high, extra-large corner, elevation 3
|
|
* [data-md-modal-bar] the 56px phone-only header bar (fullscreen, below 600px)
|
|
* [data-md-modal-head] title/subtitle/icon, pinned
|
|
* [data-md-modal-body] the only part that scrolls; [data-md-modal-content] wraps the slot
|
|
* [data-md-modal-actions] pinned, trailing-aligned
|
|
*
|
|
* 560/280px width (min/max), 48px scrim margin, 28px corner and 24dp padding all match the specs
|
|
* page; a compact window's full-screen dialog swaps the box for the whole screen with a 56px bar
|
|
* (C-17: this used to be 64px) instead of the headline block. It opens on the fast spatial spring
|
|
* and closes at once — a native `<dialog>` makes an exit transition awkward, since
|
|
* `@starting-style` only ever supplies an entry.
|
|
*
|
|
* The dividers (§ Dialogs → Anatomy, C-06): a 1px outline-variant rule under the pinned head and
|
|
* over the pinned actions, each only while the body has more content hidden on its side.
|
|
* resources/js/dialog.js marks the `<dialog>` with data-md-overflow-top/-bottom as the body
|
|
* scrolls; `data-md-modal-divider` says an element takes part at all — the head and the actions
|
|
* always do while a body exists (structural: whichever follows or precedes it), the phone bar
|
|
* only when it is the row actually sitting above the body there (computed in the view, because a
|
|
* subtitle or an icon can keep the head on screen too, in which case the bar must not draw a rule
|
|
* the head already owns); `data-md-separator` (the `separator` prop) draws the rule whatever the
|
|
* scroll. The rule is a pseudo-element over the row's own edge, so showing it moves nothing.
|
|
*
|
|
* `[data-md-modal-body]:focus-visible` is Chrome's own focusable scroll container catching the
|
|
* dialog's first focus when nothing inside can take it — M3's 3px secondary indicator, drawn
|
|
* inside the edge because the box's rounded, overflow-hidden corner would clip one drawn outside.
|
|
*/
|
|
|
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
|
|
|
@import './icon.css';
|
|
@import './button.css';
|
|
|
|
@layer material.components {
|
|
[data-md-modal] {
|
|
margin: auto;
|
|
max-width: 560px;
|
|
min-width: 280px;
|
|
width: calc(100vw - 48px);
|
|
max-height: calc(100dvh - 48px);
|
|
overflow: visible;
|
|
padding: 0;
|
|
background-color: transparent;
|
|
color: var(--md-sys-color-on-surface);
|
|
opacity: 1;
|
|
scale: 1;
|
|
transition-property: opacity, scale;
|
|
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
|
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
|
}
|
|
|
|
@starting-style {
|
|
[data-md-modal] {
|
|
opacity: 0;
|
|
scale: 0.95;
|
|
}
|
|
}
|
|
|
|
[data-md-modal]::backdrop {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
|
|
}
|
|
|
|
@media (width < 600px) {
|
|
[data-md-modal][data-md-fullscreen] {
|
|
margin: 0;
|
|
width: 100%;
|
|
max-width: none;
|
|
min-width: 0;
|
|
height: 100dvh;
|
|
max-height: none;
|
|
}
|
|
}
|
|
|
|
[data-md-modal-box] {
|
|
display: flex;
|
|
max-height: inherit;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
border-radius: var(--md-sys-shape-corner-xl);
|
|
background-color: var(--md-sys-color-surface-container-high);
|
|
box-shadow: var(--md-sys-elevation-3);
|
|
}
|
|
|
|
@media (width < 600px) {
|
|
[data-md-modal][data-md-fullscreen] > [data-md-modal-box] {
|
|
height: 100%;
|
|
border-radius: var(--md-sys-shape-corner-none);
|
|
padding-top: var(--material-safe-top, env(safe-area-inset-top));
|
|
}
|
|
}
|
|
|
|
/* The phone-only header bar: M3's full-screen dialog header, 56dp (C-17). */
|
|
[data-md-modal-bar] {
|
|
display: flex;
|
|
height: var(--md-sys-measurement-space700);
|
|
flex-shrink: 0;
|
|
align-items: center;
|
|
gap: var(--md-sys-measurement-space50);
|
|
padding-inline: var(--md-sys-measurement-space50);
|
|
}
|
|
|
|
@media (width >= 600px) {
|
|
[data-md-modal-bar] {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
[data-md-modal-bar-title] {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
font: var(--md-sys-typescale-title-lg);
|
|
letter-spacing: var(--md-sys-typescale-title-lg-tracking);
|
|
}
|
|
|
|
[data-md-modal-head] {
|
|
flex-shrink: 0;
|
|
padding-inline: var(--md-sys-measurement-space300);
|
|
padding-top: var(--md-sys-measurement-space300);
|
|
}
|
|
|
|
[data-md-modal-head]:has(+ [data-md-modal-body]) {
|
|
padding-bottom: var(--md-sys-measurement-space100);
|
|
}
|
|
|
|
[data-md-modal-head]:has([data-md-icon]) {
|
|
text-align: center;
|
|
}
|
|
|
|
/* On a phone the bar already names a plain dialog; the head hides unless an icon or a
|
|
subtitle keeps it there (the bar cannot hold either). */
|
|
@media (width < 600px) {
|
|
[data-md-modal][data-md-fullscreen] [data-md-modal-head]:not(:has([data-md-icon])):not(:has([data-md-modal-subtitle])) {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
[data-md-modal-head] > [data-md-icon] {
|
|
margin-inline: auto;
|
|
margin-bottom: var(--md-sys-measurement-space200);
|
|
color: var(--md-sys-color-secondary);
|
|
}
|
|
|
|
[data-md-modal-title] {
|
|
font: var(--md-sys-typescale-headline-sm);
|
|
letter-spacing: var(--md-sys-typescale-headline-sm-tracking);
|
|
}
|
|
|
|
@media (width < 600px) {
|
|
[data-md-modal][data-md-fullscreen]:not(:has([data-md-modal-head] [data-md-icon])) [data-md-modal-title] {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
[data-md-modal-subtitle] {
|
|
color: var(--md-sys-color-on-surface-variant);
|
|
font: var(--md-sys-typescale-body-md);
|
|
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
|
}
|
|
|
|
[data-md-modal-title] ~ [data-md-modal-subtitle],
|
|
[data-md-modal-head] > [data-md-icon] ~ [data-md-modal-subtitle] {
|
|
margin-top: var(--md-sys-measurement-space200);
|
|
}
|
|
|
|
@media (width < 600px) {
|
|
[data-md-modal][data-md-fullscreen]:not(:has([data-md-modal-head] [data-md-icon])) [data-md-modal-subtitle] {
|
|
margin-top: 0;
|
|
}
|
|
}
|
|
|
|
[data-md-modal-body] {
|
|
min-height: 0;
|
|
flex: 1 1 0%;
|
|
overflow-y: auto;
|
|
padding-inline: var(--md-sys-measurement-space300);
|
|
padding-top: var(--md-sys-measurement-space300);
|
|
padding-bottom: var(--md-sys-measurement-space300);
|
|
color: var(--md-sys-color-on-surface-variant);
|
|
font: var(--md-sys-typescale-body-md);
|
|
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
|
outline: none;
|
|
}
|
|
|
|
[data-md-modal-body]:focus-visible {
|
|
outline: 3px solid var(--md-sys-color-secondary);
|
|
outline-offset: -3px;
|
|
}
|
|
|
|
[data-md-modal-head] ~ [data-md-modal-body] {
|
|
padding-top: var(--md-sys-measurement-space100);
|
|
}
|
|
|
|
@media (width < 600px) {
|
|
[data-md-modal][data-md-fullscreen] [data-md-modal-head]:not(:has([data-md-icon])):not(:has([data-md-modal-subtitle])) ~ [data-md-modal-body] {
|
|
padding-top: var(--md-sys-measurement-space200);
|
|
}
|
|
}
|
|
|
|
[data-md-modal-body]:has(~ [data-md-modal-actions]) {
|
|
padding-bottom: var(--md-sys-measurement-space100);
|
|
}
|
|
|
|
[data-md-modal-actions] {
|
|
display: flex;
|
|
flex-shrink: 0;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: var(--md-sys-measurement-space100);
|
|
padding-inline: var(--md-sys-measurement-space300);
|
|
padding-top: var(--md-sys-measurement-space300);
|
|
padding-bottom: var(--md-sys-measurement-space300);
|
|
}
|
|
|
|
[data-md-modal-body] ~ [data-md-modal-actions] {
|
|
padding-top: var(--md-sys-measurement-space200);
|
|
}
|
|
|
|
@media (width < 600px) {
|
|
[data-md-modal][data-md-fullscreen] [data-md-modal-actions] {
|
|
min-height: var(--md-sys-measurement-space700);
|
|
padding-top: var(--md-sys-measurement-space100);
|
|
padding-bottom: var(--md-sys-measurement-space100);
|
|
}
|
|
}
|
|
|
|
/* The pinned-edge dividers: a pseudo-element over the row's own padding, drawn only while
|
|
something is scrolled past it (or `separator` says always). */
|
|
[data-md-modal-head],
|
|
[data-md-modal-bar],
|
|
[data-md-modal-actions] {
|
|
position: relative;
|
|
}
|
|
|
|
[data-md-modal-head]::after,
|
|
[data-md-modal-bar]::after,
|
|
[data-md-modal-actions]::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset-inline: 0;
|
|
height: 1px;
|
|
background-color: var(--md-sys-color-outline-variant);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
[data-md-modal-head]::after,
|
|
[data-md-modal-bar]::after {
|
|
bottom: 0;
|
|
}
|
|
|
|
[data-md-modal-actions]::before {
|
|
top: 0;
|
|
}
|
|
|
|
dialog[data-md-overflow-top] > [data-md-modal-box] > [data-md-modal-head][data-md-modal-divider]::after,
|
|
dialog[data-md-overflow-top] > [data-md-modal-box] > [data-md-modal-bar][data-md-modal-divider]::after,
|
|
dialog[data-md-overflow-bottom] > [data-md-modal-box] > [data-md-modal-actions][data-md-modal-divider]::before,
|
|
[data-md-modal-head][data-md-modal-divider][data-md-separator]::after,
|
|
[data-md-modal-bar][data-md-modal-divider][data-md-separator]::after,
|
|
[data-md-modal-actions][data-md-modal-divider][data-md-separator]::before {
|
|
opacity: 1;
|
|
}
|
|
}
|