diff --git a/resources/css/components/navigation.css b/resources/css/components/navigation.css index 1ca4dc5a..e64b44d8 100644 --- a/resources/css/components/navigation.css +++ b/resources/css/components/navigation.css @@ -30,9 +30,16 @@ * * `rail-collapsed:` matches a rail, and everything in it, while it is drawn collapsed — whatever * made it so: its mode, the visitor's choice on (set before the first paint by - * ), or a window under `lg` for the adaptive rail. A rail item is written once + * ), or a window below `expanded` (840px) for the adaptive rail. A rail item is written once * and takes both shapes from it; so can anything an application puts in a rail * (``). + * + * The two widths the adaptive rail turns on are M3's window size classes, not Tailwind's screens + * (docs/reference/m3/foundations.md § Layout, foundations-supplement.md § Breakpoints): 37.5rem + * (600px) is the compact/medium boundary — below it the navigation bar carries the destinations and + * the rail takes no room in the layout; 52.5rem (840px) is `expanded`, where M3 asks for a standard + * rail in the layout rather than one that opens over a scrim. The bar's own item layout is a + * *container* query at the same 37.5rem, so a bar in a narrow column lays out by its own width. */ @custom-variant rail-collapsed { @@ -48,13 +55,13 @@ @slot; } - @media (width < 64rem) { + @media (width < 52.5rem) { &:where([data-navigation-rail='adaptive']:not([data-open]), [data-navigation-rail='adaptive']:not([data-open]) *) { @slot; } } - @media (width >= 64rem) { + @media (width >= 52.5rem) { &:where([data-rail='collapsed'] [data-navigation-rail='adaptive'], [data-rail='collapsed'] [data-navigation-rail='adaptive'] *) { @slot; } @@ -176,18 +183,18 @@ } /* A modal rail keeps its collapsed width in the layout while it is open over it, as Compose's - ModalWideNavigationRail does; the adaptive rail does below lg, and takes no room below sm. */ + ModalWideNavigationRail does; the adaptive rail does below `expanded`, and takes no room on a compact window. */ [data-navigation-rail='modal'] { width: 6rem; } - @media (width < 64rem) { + @media (width < 52.5rem) { [data-navigation-rail='adaptive'] { width: 6rem; } } - @media (width < 40rem) { + @media (width < 37.5rem) { [data-navigation-rail='adaptive'] { width: 0; } @@ -230,9 +237,9 @@ box-shadow: var(--md-sys-elevation-2); } - /* Below sm there is no collapsed rail to grow out of: the open rail slides in from the edge, + /* On a compact window there is no collapsed rail to grow out of: the open rail slides in from the edge, on emphasized decelerate rather than a spring, which would overshoot and open a gap. */ - @media (width < 40rem) { + @media (width < 37.5rem) { [data-navigation-rail='adaptive'] > [data-navigation-rail-panel] { position: fixed; inset-block: 0; diff --git a/resources/css/components/timepicker.css b/resources/css/components/timepicker.css index f08c012b..414a4b48 100644 --- a/resources/css/components/timepicker.css +++ b/resources/css/components/timepicker.css @@ -24,10 +24,18 @@ * on by default): two toggle buttons 4px apart, round and surface-container-lowest when off, a 12px * corner and primary-container with a bold label when on, not the outlined pair of the tokens. * - * In a landscape window the dial variant lies on its side, as Compose's HorizontalTimePicker does - * whenever the screen is wider than it is tall: the display with the period selector (216×38, 16px - * under it) on the start side, the dial 36px after it, the actions under both; and the dial shrinks - * to 238 or 200px when the window is short (ClockFaceSizeModifier). The input variant always stands. + * In a landscape window with no room for the upright dial the dial variant lies on its side, as + * Compose's HorizontalTimePicker does: the display with the period selector (216×38, 16px under it) + * on the start side, the dial 36px after it, the actions under both; and the dial shrinks to 238 or + * 200px when the window is shorter still (ClockFaceSizeModifier). The input variant always stands. + * + * The switch is orientation and viewport *height*, never a width breakpoint: M3 says the picker + * "swaps orientation/variant based on device orientation and viewport height" + * (docs/reference/m3/components-navigation-selection-inputs.md § Time pickers/Behaviour), and no + * window size class applies. 35rem is what the upright dial needs — 33rem of surface (3 padding, + * 2.25 title, 7.25 display, 17.5 dial, 3 actions) inside the dialog's `100dvh - 2rem` — so a + * landscape phone lies the dial down and a landscape tablet or desktop, which has the height, + * leaves it standing. * * The selector's angle is a registered property, so a single transition turns the line, carries the * handle round and moves the clip that inks the number under it, on the default spatial spring. The @@ -400,7 +408,7 @@ /* ---- Landscape ---------------------------------------------------------------------------- */ - @media (orientation: landscape) and (min-width: 37rem) { + @media (orientation: landscape) and (height < 35rem) { [data-timepicker-surface][data-mode="dial"] { grid-template-columns: 13.5rem auto; grid-template-areas: @@ -446,13 +454,13 @@ } } - @media (orientation: landscape) and (min-width: 37rem) and (max-height: 22.75rem) { + @media (orientation: landscape) and (max-height: 22.75rem) { [data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] { --dial: 14.875rem; } } - @media (orientation: landscape) and (min-width: 37rem) and (max-height: 21.625rem) { + @media (orientation: landscape) and (max-height: 21.625rem) { [data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] { --dial: 12.5rem; } diff --git a/resources/js/datepicker.js b/resources/js/datepicker.js index 232c7abb..84c57f86 100644 --- a/resources/js/datepicker.js +++ b/resources/js/datepicker.js @@ -4,7 +4,8 @@ * * The dialog is shown two ways. Docked, it is a `popover="manual"` placed under the field by CSS * anchor positioning; modal (the `modal` and `input` modes, and a docked picker on a compact - * window) it is opened with `showModal()`. Everything a person sees in it is drawn by Alpine from + * window — below `medium`, 600px, where M3 puts the picker in a dialog rather than under the + * field) it is opened with `showModal()`. Everything a person sees in it is drawn by Alpine from * the state below, and the dialog is `wire:ignore`, so a Livewire render never touches it while * it is open. * @@ -20,7 +21,7 @@ * Enter selects and confirms, Escape closes and hands focus back to the field. Focus never leaves * the `min`–`max` span, so a disabled day cannot be chosen from the keyboard either. */ -const COMPACT = '(max-width: 39.99rem)' +import { upTo } from './breakpoints.js' // CLDR's first day of the week by region, for an engine without `Intl.Locale#getWeekInfo`. const WEEK_STARTS_SUNDAY = 'AG AS BD BR BS BT BW BZ CA CN CO DM DO ET GT GU HK HN ID IL IN JM JP KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY SA SG SV TH TT TW UM US VE VI WS YE ZA ZW'.split(' ') @@ -222,7 +223,7 @@ document.addEventListener('alpine:init', () => { } }) - const query = window.matchMedia(COMPACT) + const query = upTo('medium') this.compact = query.matches query.addEventListener('change', (event) => (this.compact = event.matches)) diff --git a/resources/js/navigation.js b/resources/js/navigation.js index ffced60b..8987199b 100644 --- a/resources/js/navigation.js +++ b/resources/js/navigation.js @@ -13,7 +13,7 @@ * `materialNavigationRail` is one rail's view of the store for its `mode` — see * resources/views/components/navigation-rail.blade.php. */ -const WIDE = '(min-width: 64rem)' +import { from } from './breakpoints.js' /* * The active indicator grows out of its centre when a page arrives through wire:navigate. The @@ -86,7 +86,7 @@ document.addEventListener('alpine:init', () => { document.addEventListener('livewire:navigating', () => window.Alpine.store('rail').hide()) window.Alpine.data('materialNavigationRail', (mode) => ({ - wide: mode === 'adaptive' ? window.matchMedia(WIDE).matches : false, + wide: mode === 'adaptive' ? from('expanded').matches : false, query: null, onWidth: null, @@ -95,9 +95,10 @@ document.addEventListener('alpine:init', () => { return } - // From lg the adaptive rail is a standard, collapsible rail: a modal left open while - // the window widens is shut, or its focus trap would hold a page that has no scrim. - this.query = window.matchMedia(WIDE) + // From `expanded` (840px) the adaptive rail is a standard, collapsible rail — what M3 + // asks for at expanded and above. A modal left open while the window widens is shut, + // or its focus trap would hold a page that has no scrim. + this.query = from('expanded') this.onWidth = (event) => { this.wide = event.matches diff --git a/resources/js/search.js b/resources/js/search.js index 4d8aec73..56fe0603 100644 --- a/resources/js/search.js +++ b/resources/js/search.js @@ -4,11 +4,11 @@ * Focus or a press on the bar opens the view; Escape, the back arrow, a press outside, focus * leaving the search, or choosing a result closes it. ArrowDown from the input moves into the * results and the arrow keys walk them; ArrowUp past the first result returns to the input. On a - * compact window (below `sm`) the view is full screen and modal: focus stays in it and the page - * behind does not scroll. The results themselves are the caller's, rendered by Livewire into the - * view as the query changes. + * compact window (below `medium`, 600px) the view is full screen and modal: focus stays in it and + * the page behind does not scroll — M3 docks the view from medium upwards. The results themselves + * are the caller's, rendered by Livewire into the view as the query changes. */ -const COMPACT = '(max-width: 39.99rem)' +import { upTo } from './breakpoints.js' const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' @@ -26,7 +26,7 @@ document.addEventListener('alpine:init', () => { closedAt: -Infinity, init() { - const query = window.matchMedia(COMPACT) + const query = upTo('medium') this.compact = query.matches query.addEventListener('change', (event) => (this.compact = event.matches))