Plan step 16. `navigation.js` takes its standard-rail threshold from
`from('expanded')` (840px) instead of the 64rem it had, and `navigation.css`'s
three 64rem queries follow, so the whole expanded class gets the in-layout
collapsible rail M3 asks for rather than one that opens over a scrim (N-06);
its two 40rem queries become 37.5rem, the compact/medium boundary the bar's own
container query already used (N-07). `search.js` and `datepicker.js` swap their
39.99rem media strings for `upTo('medium')`, so the full-screen search view and
the modal date picker end at 600px, not 640px.
The time picker's landscape layout stops keying on width at all: M3 swaps it on
orientation and viewport height, so it is now landscape plus a window too short
for the upright dial (35rem, the dialog's own height), and the two dial-shrink
rules key on height alone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
143 lines
4.9 KiB
JavaScript
143 lines
4.9 KiB
JavaScript
/**
|
|
* Navigation: the rail's state, shared by every rail and menu button on the page.
|
|
*
|
|
* `$store.rail.collapsed` is the visitor's choice for a collapsible rail, remembered in
|
|
* localStorage. <x-theme-script> has already applied it before the first paint as
|
|
* <html data-rail="expanded|collapsed">, which is what the stylesheet keys on (the
|
|
* `rail-collapsed:` variant); the store starts from that attribute and writes it back.
|
|
*
|
|
* `$store.rail.open` is the modal rail: on a window too narrow for an expanded rail, a menu
|
|
* button opens it over a scrim (`show()`), and Escape, the scrim or leaving the page closes it
|
|
* (`hide()`). It is never remembered.
|
|
*
|
|
* `materialNavigationRail` is one rail's view of the store for its `mode` — see
|
|
* resources/views/components/navigation-rail.blade.php.
|
|
*/
|
|
import { from } from './breakpoints.js'
|
|
|
|
/*
|
|
* The active indicator grows out of its centre when a page arrives through wire:navigate. The
|
|
* new page's indicator is new markup, so the only way to animate it is a starting style — and
|
|
* only while a navigation swaps the page in, or every full load would animate it too. The sheet
|
|
* is adopted as the navigation starts and dropped two frames after it ends; the transition itself
|
|
* is resources/css/components/navigation.css.
|
|
*/
|
|
const arriving = new CSSStyleSheet()
|
|
|
|
arriving.replaceSync(`@starting-style {
|
|
:is([data-navigation-bar-item], [data-navigation-rail-item])[data-active],
|
|
:is([data-navigation-bar-item], [data-navigation-rail-item])[data-active] :is([data-navigation-indicator], [data-navigation-pill]) {
|
|
background-size: 0% 100%;
|
|
}
|
|
}`)
|
|
|
|
document.addEventListener('livewire:navigating', () => {
|
|
if (!document.adoptedStyleSheets.includes(arriving)) {
|
|
document.adoptedStyleSheets = [...document.adoptedStyleSheets, arriving]
|
|
}
|
|
})
|
|
|
|
document.addEventListener('livewire:navigated', () => {
|
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
document.adoptedStyleSheets = document.adoptedStyleSheets.filter((sheet) => sheet !== arriving)
|
|
}))
|
|
})
|
|
|
|
document.addEventListener('alpine:init', () => {
|
|
const root = document.documentElement
|
|
|
|
window.Alpine.store('rail', {
|
|
collapsed: root.dataset.rail === 'collapsed',
|
|
open: false,
|
|
|
|
toggle() {
|
|
this.set(!this.collapsed)
|
|
},
|
|
|
|
collapse() {
|
|
this.set(true)
|
|
},
|
|
|
|
expand() {
|
|
this.set(false)
|
|
},
|
|
|
|
set(collapsed) {
|
|
this.collapsed = collapsed
|
|
root.dataset.rail = collapsed ? 'collapsed' : 'expanded'
|
|
|
|
try {
|
|
localStorage.setItem(root.dataset.railKey || 'material-rail', root.dataset.rail)
|
|
} catch {
|
|
// Blocked storage: the rail still toggles, it just will not remember.
|
|
}
|
|
},
|
|
|
|
show() {
|
|
this.open = true
|
|
},
|
|
|
|
hide() {
|
|
this.open = false
|
|
},
|
|
})
|
|
|
|
// A destination chosen in the modal rail leaves the page; the next one starts with it shut.
|
|
document.addEventListener('livewire:navigating', () => window.Alpine.store('rail').hide())
|
|
|
|
window.Alpine.data('materialNavigationRail', (mode) => ({
|
|
wide: mode === 'adaptive' ? from('expanded').matches : false,
|
|
query: null,
|
|
onWidth: null,
|
|
|
|
init() {
|
|
if (mode !== 'adaptive') {
|
|
return
|
|
}
|
|
|
|
// 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
|
|
|
|
if (event.matches) {
|
|
this.$store.rail.hide()
|
|
}
|
|
}
|
|
this.query.addEventListener('change', this.onWidth)
|
|
},
|
|
|
|
destroy() {
|
|
this.query?.removeEventListener('change', this.onWidth)
|
|
},
|
|
|
|
/** Whether this rail expands over a scrim rather than in the layout. */
|
|
get modal() {
|
|
return mode === 'modal' || (mode === 'adaptive' && !this.wide)
|
|
},
|
|
|
|
get open() {
|
|
return this.modal && this.$store.rail.open
|
|
},
|
|
|
|
get expanded() {
|
|
if (this.open || mode === 'expanded') {
|
|
return true
|
|
}
|
|
|
|
return (mode === 'collapsible' || (mode === 'adaptive' && this.wide)) && !this.$store.rail.collapsed
|
|
},
|
|
|
|
/** The rail's own menu button: open or close the modal, or collapse and expand in place. */
|
|
menu() {
|
|
if (this.modal) {
|
|
this.$store.rail.open ? this.$store.rail.hide() : this.$store.rail.show()
|
|
} else {
|
|
this.$store.rail.toggle()
|
|
}
|
|
},
|
|
}))
|
|
})
|