@@ -19,6 +19,14 @@ linking to them, and turning them into tabs.
1919- [ Tabs that are not routes] ( #tabs-that-are-not-routes )
2020 - [ A ` SlideContainer ` in the URL: a position that is not a place one came from] ( #a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from )
2121 - [ A state whose values ARE places: ` history: "push" ` ] ( #a-state-whose-values-are-places-history-push )
22+ - [ A layer over the screen: what its address may say] ( #a-layer-over-the-screen-what-its-address-may-say )
23+ - [ Why a layer is a routing question at all] ( #why-a-layer-is-a-routing-question-at-all )
24+ - [ ` /me/settings ` names a place the reader is not at] ( #mesettings-names-a-place-the-reader-is-not-at )
25+ - [ It is not "use a search param"] ( #it-is-not-use-a-search-param )
26+ - [ What the search param buys: the way back is in the address] ( #what-the-search-param-buys-the-way-back-is-in-the-address )
27+ - [ The wiring] ( #the-wiring )
28+ - [ Places inside the layer] ( #places-inside-the-layer )
29+ - [ What it costs] ( #what-it-costs )
2230
2331## The rule that decides everything else: the position belongs in the URL
2432
@@ -747,3 +755,162 @@ not one. What travels is the box, and the address is a label on where the box
747755stands. A position several parts of the app must react to is still a route.
748756
749757Demo: [ ../src/layout/demos/8_slide_container_demo.html] ( ../src/layout/demos/8_slide_container_demo.html ) .
758+
759+ ## A layer over the screen: what its address may say
760+
761+ Some things are opened from everywhere and drawn OVER whatever the reader was
762+ looking at: the settings behind a gear in the top bar, a photo, a help sheet.
763+ Closing one puts the reader back exactly where they were — not on a page the app
764+ picked, on the one they had. And the address has to hold it, for the same reasons
765+ every other position does: a reload lands on it, a link opens on it, the back
766+ button closes it.
767+
768+ ### Why a layer is a routing question at all
769+
770+ Layout should not decide routing. But routing decides a great deal of what is on
771+ screen: which page is mounted, whose data loads, which bar entry lights up, which
772+ movement plays between two screens. An address is not a label — it is read by
773+ everything — so a layer, which is a pure fact of layout, cannot be given just any
774+ address.
775+
776+ The rule that falls out of it, and the whole of this section:
777+
778+ > The URL may say what is drawn OVER the screen. It must never name a place the
779+ > reader is not at.
780+
781+ ### ` /me/settings ` names a place the reader is not at
782+
783+ Settings opened from anywhere and drawn over anything are not inside ` /me ` , even
784+ when they conceptually belong to "me". Written ` /me/settings ` , the router
785+ believes otherwise, and everything that reads the router inherits the belief:
786+
787+ - the bar entry for the "me" section lights up while the settings cover a game;
788+ - ` /me ` matches, so the screen the reader was on is REPLACED. The covering is a
789+ fiction that lasts as long as the animation, and the page beneath is gone;
790+ - a route transition plays a crossing between two pages that never crossed, and
791+ its back half plays on every way out — the sheet lifts to reveal a page that
792+ was never underneath;
793+ - and it is wrong on the way out too: closing does not return to ` /me ` , it
794+ returns to wherever the reader was, which the address never said.
795+
796+ These do not get fixed one at a time. ` currentExcept ` stops one link from
797+ claiming "you are here"; a ` "none" ` relation silences one crossing; a hand-rolled
798+ ` active ` replaces one reading. Each patches a reader of the lie without removing
799+ it, and the next reader arrives quietly wrong.
800+
801+ ### It is not "use a search param"
802+
803+ ` /settings ` at the top level does not claim to be inside ` /me ` — it is honest
804+ about not being there, and every symptom above goes with it. What it still is,
805+ though, is a PAGE: it names a screen, so the screen beneath is gone, and closing
806+ has nowhere written to return to. ` whatever/settings ` is the same answer in a
807+ longer word.
808+
809+ So the rule is not about search params. It is about not claiming a position. A
810+ path can be honest; what a path cannot do is carry what is underneath.
811+
812+ ### What the search param buys: the way back is in the address
813+
814+ ` /places?settings ` says two things at once — which screen the reader is on, and
815+ what is drawn over it. That is what makes closing exact from a cold start: a
816+ reload, a link someone sent, a new tab. The layer's address CONTAINS the address
817+ of what it covers, so "close" needs no memory at all.
818+
819+ ` /settings ` has to remember instead. In practice the browser often does — after a
820+ reload the session's history is still there, so a back-press lands somewhere —
821+ but that is the BROWSER's memory of this tab, not the address. A link someone
822+ else opens has none of it, and neither does a bookmark. Only the address travels,
823+ which is what makes the param shape the right model rather than the convenient
824+ one.
825+
826+ ### The wiring
827+
828+ ``` js
829+ const settingsSignal = stateSignal (false , {
830+ id: " settings" ,
831+ type: " boolean" ,
832+ weak: true ,
833+ history: " push" ,
834+ });
835+ export const ROOT_ROUTE = route (" /" , {
836+ searchParams: { settings: settingsSignal },
837+ });
838+ ```
839+
840+ ``` jsx
841+ < Button command= " --navi-toggle" commandFor= " settings_panel" > ⚙< / Button>
842+ …
843+ < SidePanel id= " settings_panel" signal= {settingsSignal} side= " top" expandY / >
844+ ```
845+
846+ Every piece of it answers something:
847+
848+ - ** declared on the ROOT route** , so the layer opens over every screen. Declared
849+ on one route it would only exist there, and a door in the furniture is on every
850+ screen ([ Search params] ( #search-params ) );
851+ - ** ` weak ` ** , so a link built to a page never inherits a layer that happens to be
852+ open. A layer qualifies one visit; it is not part of anyone's address;
853+ - ** ` history: "push" ` ** , so the layer is a place one came from and the back button
854+ closes it. The closing itself is never an entry of its own — see
855+ [ popup_open.md] ( ./popup_open.md ) , which owns what a ` signal ` bound to a URL
856+ writes on open, on close, and on cancel;
857+ - ** the panel is rendered outside the page area** , next to the router rather than
858+ inside it. It is drawn over the pages and the fixed bars alike, and it stays
859+ mounted while closed so it has somewhere to animate from;
860+ - ** ` expandY ` ** , because a sheet is content-tall by default and a layer covers
861+ the screen. It is also what any tab row inside it must NOT be given: ` expand `
862+ is both axes, and in the panel's own column that makes the row eat the height
863+ and push everything below it off screen;
864+ - ** no ` defineRouteTransition ` for it.** Nothing crosses: the page beneath does
865+ not change, so there is no pair of pages and no movement between them. A layer
866+ has an entrance of its own, and route transitions are for pages replacing pages
867+ ([ route_transitions.md] ( ./route_transitions.md ) ).
868+
869+ ### Places inside the layer
870+
871+ A layer big enough to have tabs holds them the way a wizard does — a search param
872+ of its own, ` oneOf ` its pages, replacing rather than pushing, so the back button
873+ closes the layer instead of stepping back one tab. Everything in
874+ [ A ` SlideContainer ` in the URL] ( #a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from )
875+ applies unchanged, and a layer is exactly the case that section names for
876+ ` SlideContainer ` over ` RouteTravel ` : a popup lives in the browser's top layer,
877+ where two pages cannot stand side by side.
878+
879+ ``` js
880+ const settingsTabSignal = stateSignal (" account" , {
881+ id: " settings_tab" ,
882+ oneOf: [" account" , " alerts" , " advanced" ],
883+ weak: true ,
884+ });
885+ ```
886+
887+ The address then grows and shrinks with what the reader does: ` /me ` closed,
888+ ` /me?settings ` open on the tab it opens on, ` /me?settings&settings_tab=alerts `
889+ one tab further, and back to ` /me?settings ` returning to the first.
890+
891+ Two params, because they answer two questions — is the layer there, and which of
892+ its pages is shown. One param whose value is "which page of the layer, or none"
893+ (` ?settings=alerts ` ) is arguably the shape the address wants, and it is not
894+ expressible today: a popup says its open state as a boolean, so it would
895+ overwrite the tab on every open.
896+
897+ ### What it costs
898+
899+ This is a compromise, and it is worth stating rather than discovering:
900+
901+ - ** the page beneath stays mounted and alive under an opaque layer.** Its actions
902+ keep running, refreshing, retrying, for something nobody can see. Nothing tells
903+ a route "you are covered", and nothing should be inferred from a layer being
904+ open — the reader is still on that page, and will be back on it in a moment;
905+ - ** the router knows nothing about layers.** ` <Route> ` renders pages; the layer is
906+ drawn beside them by the application. The address is honest about it, but "over"
907+ is not a routing concept, and no ` <Route> ` shape expresses it;
908+ - ** the app really is on the page underneath** , which is right for ` aria-current ` ,
909+ for the bar, for a link built while the layer is open — and is exactly wrong for
910+ anything wanting "the settings are what is current". If something needs to
911+ answer that, it reads the layer's own state, not the router.
912+
913+ Demo:
914+ [ ../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html] ( ../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html )
915+ — a bar holding both doors side by side: notifications, a page that replaces the
916+ screen with a route transition, and settings, a layer that covers it.
0 commit comments