Files
livewire-material/resources/js/navigation.js
T
Andreas Reinhold / reiniandClaude Fable 5.1 17723d2a76 Ask breakpoints.js, not Tailwind's screens, for a window class
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
2026-09-14 04:06:35 +02:00

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()
}
},
}))
})