@jsenv/navi 0.29.66 → 0.29.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21810,6 +21810,244 @@ const updateDocumentState = (value) => {
21810
21810
  documentStateSignal.value = value;
21811
21811
  };
21812
21812
 
21813
+ /**
21814
+ * The document's rendering, held for the one frame a view transition needs.
21815
+ *
21816
+ * The browser does not take the picture of the page being left when a
21817
+ * transition is ASKED for — it takes it at the next frame, just before running
21818
+ * the update callback. Preact renders sooner than that, in a microtask: so a
21819
+ * change nobody asked for (a tab pressed, the back button) has already reached
21820
+ * the DOM when the picture is taken, and the picture is of the page ARRIVING.
21821
+ * Both sides of the animation then show it, and one watches a page slide onto
21822
+ * itself.
21823
+ *
21824
+ * So what Preact has queued waits until the update callback, which is the
21825
+ * moment the API is built around — the change belongs inside it. The whole
21826
+ * document is held: it is about to be frozen under a picture anyway.
21827
+ *
21828
+ * ONE hold for the whole document, whoever animates. The hold is a wrapper
21829
+ * around Preact's `options.debounceRendering`, and two of them installed
21830
+ * independently restore each other in the wrong order when they let go — every
21831
+ * render queued in between is then handed to a wrapper nobody will ever
21832
+ * release. Everything that photographs a navigation (RouteTravel's box, a
21833
+ * route transition) must therefore hold through this module, never through a
21834
+ * wrapper of its own.
21835
+ */
21836
+
21837
+
21838
+ let renderingHold = null;
21839
+ const holdRendering = () => {
21840
+ if (renderingHold) {
21841
+ return renderingHold.release;
21842
+ }
21843
+ const debounceRenderingBefore = options.debounceRendering;
21844
+ const hold = {
21845
+ render: null,
21846
+ waiting: [],
21847
+ release: () => {
21848
+ // Only the hold that is still standing may be given back: a holder
21849
+ // releasing after another has taken over must not let go of what it
21850
+ // does not hold.
21851
+ if (renderingHold !== hold) {
21852
+ return;
21853
+ }
21854
+ renderingHold = null;
21855
+ options.debounceRendering = debounceRenderingBefore;
21856
+ const { render, waiting } = hold;
21857
+ hold.render = null;
21858
+ hold.waiting = [];
21859
+ if (render) {
21860
+ render();
21861
+ }
21862
+ for (const wait of waiting) {
21863
+ wait();
21864
+ }
21865
+ },
21866
+ };
21867
+ renderingHold = hold;
21868
+ options.debounceRendering = (render) => {
21869
+ hold.render = render;
21870
+ };
21871
+ return hold.release;
21872
+ };
21873
+
21874
+ // Anything else that must not happen before the picture is taken, and the
21875
+ // scroll is the other one: a page one arrives at starts at its top, and the
21876
+ // document put back to its top while the page being left is still on screen is
21877
+ // a page that has ALREADY jumped when the picture is taken. Worse, the browser
21878
+ // paints what the new offset shows and nothing else, so the picture keeps only
21879
+ // the band it had already painted — the page being left is then seen in
21880
+ // fragments, whatever the movement does afterwards.
21881
+ //
21882
+ // Run at once when nobody is photographing anything, which is the common case
21883
+ // and must stay free.
21884
+ const whenRenderingResumes = (callback) => {
21885
+ if (!renderingHold) {
21886
+ callback();
21887
+ return;
21888
+ }
21889
+ renderingHold.waiting.push(callback);
21890
+ };
21891
+
21892
+ // The hold a navigation takes on its way in — from before its first write,
21893
+ // because by the time a route announces that it matches, Preact has already
21894
+ // been told and the render is queued; a hold taken then is a hold taken too
21895
+ // late. Kept here until whoever animates the change takes it over, or the
21896
+ // navigation turns out to be one nobody animates.
21897
+ let routingRenderingHold = null;
21898
+ const holdRenderingForRouting = () => {
21899
+ routingRenderingHold = holdRendering();
21900
+ };
21901
+ // Nobody had a picture to take: a page held for a change it does not animate
21902
+ // is a page that stutters for nothing.
21903
+ const releaseRoutingRenderingHold = () => {
21904
+ const release = routingRenderingHold;
21905
+ routingRenderingHold = null;
21906
+ if (release) {
21907
+ release();
21908
+ }
21909
+ };
21910
+ // An animator takes the navigation's hold as its own — taking another would be
21911
+ // taking a hold on a page that is holding still — or takes a fresh one when
21912
+ // the change it animates is not a navigation.
21913
+ const takeoverRoutingRenderingHold = () => {
21914
+ const release = routingRenderingHold || holdRendering();
21915
+ routingRenderingHold = null;
21916
+ return release;
21917
+ };
21918
+
21919
+ /**
21920
+ * A container has put its page on screen — or as much of it as it can.
21921
+ *
21922
+ * A route matching is a signal changing, and the page it selects reaches the
21923
+ * DOM only once Preact has rendered — an unknown number of passes later, in an
21924
+ * unknown number of microtasks. Anyone who needs the page as it IS rather than
21925
+ * as it has been decided (a travel about to have its picture taken by the
21926
+ * browser, see route_travel.jsx) waits for this instead of counting.
21927
+ *
21928
+ * A page waiting on data is announced too, by the boundary showing its loading
21929
+ * state (see Loading in use_async_data.jsx): what the container could put on
21930
+ * screen is what the browser is about to take a picture of, and a page that
21931
+ * cannot render yet would otherwise be waited on until the transition dies of
21932
+ * it. It lives in a module of its own for that: the async layer says it as much
21933
+ * as the router does, and neither can import the other.
21934
+ */
21935
+ const [publishRouteRender, observeRouteRender] = createPubSub();
21936
+
21937
+ /**
21938
+ * Where a page was left, given back when one comes back to it.
21939
+ *
21940
+ * The browser does this on its own, and gets it wrong here for a reason that
21941
+ * has nothing to do with it: it puts the offset back at the instant the entry
21942
+ * changes, when the document still holds the page being LEFT. A position
21943
+ * further down than that page is tall is clamped to its bottom and lost — so
21944
+ * coming back to a long page from a short one lands short, and the deeper one
21945
+ * was, the more is missing.
21946
+ *
21947
+ * So the browser is told to stop (`scrollRestoration = "manual"`) and the
21948
+ * position is put back once the page one is coming back to is really there —
21949
+ * through the same wait as everything else that must not happen before the
21950
+ * picture of a transition is taken (see rendering_hold.js): restored after the
21951
+ * picture, the page arriving would be photographed at the top and seen jumping
21952
+ * from it.
21953
+ *
21954
+ * Kept per URL rather than per history entry: an entry has no name of its own
21955
+ * that survives a reload, and two entries on the same URL are the same place
21956
+ * to a reader. Kept in the session too, so a reload lands where the browser
21957
+ * would have landed — the flag above is a promise to do the whole job.
21958
+ *
21959
+ * What is NOT covered, and cannot be from here: a page whose height depends on
21960
+ * something still loading. Its content is not there at the moment it is put
21961
+ * back, so a position beyond what has arrived is clamped as before. Only the
21962
+ * page knows when it is whole.
21963
+ */
21964
+
21965
+
21966
+ const STORAGE_KEY = "navi_scroll_positions";
21967
+
21968
+ const positionByUrl = new Map();
21969
+ const readStoredPositions = () => {
21970
+ let stored;
21971
+ try {
21972
+ stored = window.sessionStorage.getItem(STORAGE_KEY);
21973
+ } catch {
21974
+ // A session storage that refuses to answer (a private window, a policy) is
21975
+ // not a reason to lose the positions of THIS session.
21976
+ return;
21977
+ }
21978
+ if (!stored) {
21979
+ return;
21980
+ }
21981
+ try {
21982
+ for (const [url, position] of Object.entries(JSON.parse(stored))) {
21983
+ positionByUrl.set(url, position);
21984
+ }
21985
+ } catch {
21986
+ // Something else wrote there, or it was truncated.
21987
+ }
21988
+ };
21989
+ const storePositions = () => {
21990
+ try {
21991
+ window.sessionStorage.setItem(
21992
+ STORAGE_KEY,
21993
+ JSON.stringify(Object.fromEntries(positionByUrl)),
21994
+ );
21995
+ } catch {
21996
+ // Full, or refused: the session is the only thing lost.
21997
+ }
21998
+ };
21999
+
22000
+ let installed = false;
22001
+ const installScrollRestoration = () => {
22002
+ if (installed) {
22003
+ return;
22004
+ }
22005
+ installed = true;
22006
+ if (!("scrollRestoration" in window.history)) {
22007
+ return;
22008
+ }
22009
+ window.history.scrollRestoration = "manual";
22010
+ readStoredPositions();
22011
+ // Read as it happens rather than when leaving: a traverse changes the url
22012
+ // before anything here is told, so a position read then would be read for
22013
+ // the wrong page.
22014
+ window.addEventListener(
22015
+ "scroll",
22016
+ () => {
22017
+ positionByUrl.set(window.location.href, {
22018
+ x: window.scrollX,
22019
+ y: window.scrollY,
22020
+ });
22021
+ },
22022
+ { passive: true },
22023
+ );
22024
+ window.addEventListener("pagehide", storePositions);
22025
+ // What a reload asks for, now that the browser has been told not to do it.
22026
+ // Once, and at the first render of a route: the position is only meaningful
22027
+ // once there is a page under it.
22028
+ const positionOnLoad = positionByUrl.get(window.location.href);
22029
+ if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
22030
+ const stopListening = observeRouteRender(() => {
22031
+ stopListening();
22032
+ scrollTo(positionOnLoad);
22033
+ });
22034
+ }
22035
+ };
22036
+
22037
+ // Nothing to put back is not the same as putting back the top: a page arrived
22038
+ // at for the first time is startAtTop's business, and this must not step on it.
22039
+ const restoreScrollPosition = (url) => {
22040
+ const position = positionByUrl.get(new URL(url, window.location.href).href);
22041
+ if (!position) {
22042
+ return;
22043
+ }
22044
+ scrollTo(position);
22045
+ };
22046
+
22047
+ const scrollTo = ({ x, y }) => {
22048
+ window.scrollTo({ top: y, left: x, behavior: "instant" });
22049
+ };
22050
+
21813
22051
  /**
21814
22052
  * A navigation is ABOUT to be applied — said before its very first write.
21815
22053
  *
@@ -21978,7 +22216,12 @@ const setupBrowserIntegrationViaHistory = ({
21978
22216
  state,
21979
22217
  });
21980
22218
  if (navigationType === "push") {
21981
- startAtTop(url);
22219
+ whenRenderingResumes(() => startAtTop(url));
22220
+ } else if (navigationType === "traverse") {
22221
+ // Where this entry was left. Waited for like the reset above, and for
22222
+ // the same two reasons: the page has to be there to be scrolled, and a
22223
+ // picture taken before it would be of a page at its top.
22224
+ whenRenderingResumes(() => restoreScrollPosition(url));
21982
22225
  }
21983
22226
  executeWithCleanup(
21984
22227
  () => allResult,
@@ -22056,6 +22299,11 @@ const setupBrowserIntegrationViaHistory = ({
22056
22299
  { capture: true },
22057
22300
  );
22058
22301
 
22302
+ // The browser's own scroll restoration is taken over here rather than left
22303
+ // to whoever navigates: it is a decision about the document, and the entry
22304
+ // being left must be recorded from the first pixel scrolled.
22305
+ installScrollRestoration();
22306
+
22059
22307
  window.addEventListener("popstate", (popstateEvent) => {
22060
22308
  const url = window.location.href;
22061
22309
  const state = popstateEvent.state;
@@ -22140,13 +22388,16 @@ const setupBrowserIntegrationViaHistory = ({
22140
22388
  // route_travel.jsx), and resetting there would throw the reader out of a page
22141
22389
  // they never left.
22142
22390
  //
22143
- // After the routes have been told, and that ordering is the whole subtlety:
22144
- // the routes changing is what sets a travel off, and a travel measures the box
22145
- // it is leaving as it stands. Reset before that and the picture of the page
22146
- // being left is taken at the top of a page the reader was not at the top of —
22147
- // it is then watched jumping back to its first line before it even begins to
22148
- // leave (see holdTravelGeometry in route_travel.jsx). After pushState too, so
22149
- // the entry being left keeps the offset it is at.
22391
+ // After the routes have been told, and after the picture of the page being
22392
+ // left has been taken that ordering is the whole subtlety. The routes
22393
+ // changing is what sets a movement off, and a movement measures the box it is
22394
+ // leaving as it stands; put the document back to its top any earlier and the
22395
+ // picture is of a page at its first line, which the reader was not at. The
22396
+ // browser paints what the new offset shows and nothing else, so what is kept
22397
+ // of the page being left is the band it had already painted, and the movement
22398
+ // carries a fragment (see rendering_hold.js, which is where the waiting
22399
+ // happens). After pushState too, so the entry being left keeps the offset it
22400
+ // is at.
22150
22401
  //
22151
22402
  // The document, because the document is the scrollport in the common case. An
22152
22403
  // app that scrolls an element of its own scrolls it itself.
@@ -36224,24 +36475,6 @@ const TYPE_CONVERTERS = {
36224
36475
  },
36225
36476
  };
36226
36477
 
36227
- /**
36228
- * A container has put its page on screen — or as much of it as it can.
36229
- *
36230
- * A route matching is a signal changing, and the page it selects reaches the
36231
- * DOM only once Preact has rendered — an unknown number of passes later, in an
36232
- * unknown number of microtasks. Anyone who needs the page as it IS rather than
36233
- * as it has been decided (a travel about to have its picture taken by the
36234
- * browser, see route_travel.jsx) waits for this instead of counting.
36235
- *
36236
- * A page waiting on data is announced too, by the boundary showing its loading
36237
- * state (see Loading in use_async_data.jsx): what the container could put on
36238
- * screen is what the browser is about to take a picture of, and a page that
36239
- * cannot render yet would otherwise be waited on until the transition dies of
36240
- * it. It lives in a module of its own for that: the async layer says it as much
36241
- * as the router does, and neither can import the other.
36242
- */
36243
- const [publishRouteRender, observeRouteRender] = createPubSub();
36244
-
36245
36478
  const promiseStateWeakMap = new WeakMap();
36246
36479
  const usePromiseAsyncData = (
36247
36480
  promise,
@@ -38317,86 +38550,90 @@ const RouteUI = ({
38317
38550
  };
38318
38551
 
38319
38552
  /**
38320
- * The document's rendering, held for the one frame a view transition needs.
38321
- *
38322
- * The browser does not take the picture of the page being left when a
38323
- * transition is ASKED for it takes it at the next frame, just before running
38324
- * the update callback. Preact renders sooner than that, in a microtask: so a
38325
- * change nobody asked for (a tab pressed, the back button) has already reached
38326
- * the DOM when the picture is taken, and the picture is of the page ARRIVING.
38327
- * Both sides of the animation then show it, and one watches a page slide onto
38328
- * itself.
38329
- *
38330
- * So what Preact has queued waits until the update callback, which is the
38331
- * moment the API is built around the change belongs inside it. The whole
38332
- * document is held: it is about to be frozen under a picture anyway.
38333
- *
38334
- * ONE hold for the whole document, whoever animates. The hold is a wrapper
38335
- * around Preact's `options.debounceRendering`, and two of them installed
38336
- * independently restore each other in the wrong order when they let go — every
38337
- * render queued in between is then handed to a wrapper nobody will ever
38338
- * release. Everything that photographs a navigation (RouteTravel's box, a
38339
- * route transition) must therefore hold through this module, never through a
38340
- * wrapper of its own.
38553
+ * The window two pages are seen through while one replaces the other, measured
38554
+ * once and published for the length of the movement.
38555
+ *
38556
+ * Both ways of moving from one route to another need the same six numbers, so
38557
+ * they are written under the same names and read by the same CSS formulas —
38558
+ * `route_travel.jsx` for a row a finger pushes, `route_transition.jsx` for a
38559
+ * relation between two pages. Only one of them ever plays at a time (there is
38560
+ * one view transition per document), which is what lets them share the names.
38561
+ *
38562
+ * What the numbers are for:
38563
+ *
38564
+ * - **the box the movement happens in**, in the WINDOW's coordinates. The
38565
+ * pictures of a transition are drawn in the top layer, above everything, so
38566
+ * no overflow of the document reaches them: where to cut them can only be
38567
+ * said from outside the document, and this is where that outside is known.
38568
+ * - **the height it is held at**, the taller of the two states. Left to the
38569
+ * browser the window's height animates from one to the other, and a window
38570
+ * that changes size under the pictures cuts the page being left from the
38571
+ * bottom, progressively. It does end up at the arriving height, and that is
38572
+ * right; what must not happen is the user watching it get there.
38573
+ * - **where the page being left WAS**, which is not where the window stands:
38574
+ * the two states are at the same place in the layout without being at the
38575
+ * same place in the window — one page is scrolled and the other is not.
38576
+ * Drawn at the window's own corner, the page being left would be seen
38577
+ * jumping to its top before it begins to leave.
38578
+ *
38579
+ * Only the measuring needs JS, and only for the one moment both states exist:
38580
+ * the page arriving is in the DOM and the transition has not started playing.
38581
+ * Everything DERIVED from these numbers — the band a fixed bar covers, how far
38582
+ * a page travels — is derived in CSS, so the application's own numbers (the
38583
+ * room its bars give back, see layout/safe_area.js) take part in it.
38341
38584
  */
38342
38585
 
38586
+ const WINDOW_TOP_PROPERTY = "--navi-transition-window-top";
38587
+ const WINDOW_LEFT_PROPERTY = "--navi-transition-window-left";
38588
+ const WINDOW_WIDTH_PROPERTY = "--navi-transition-window-width";
38589
+ const WINDOW_HEIGHT_PROPERTY = "--navi-transition-window-height";
38590
+ const WINDOW_OLD_TOP_PROPERTY = "--navi-transition-window-old-top";
38591
+ const WINDOW_OLD_LEFT_PROPERTY = "--navi-transition-window-old-left";
38592
+ const WINDOW_PROPERTIES = [
38593
+ WINDOW_TOP_PROPERTY,
38594
+ WINDOW_LEFT_PROPERTY,
38595
+ WINDOW_WIDTH_PROPERTY,
38596
+ WINDOW_HEIGHT_PROPERTY,
38597
+ WINDOW_OLD_TOP_PROPERTY,
38598
+ WINDOW_OLD_LEFT_PROPERTY,
38599
+ ];
38343
38600
 
38344
- let renderingHold = null;
38345
- const holdRendering = () => {
38346
- if (renderingHold) {
38347
- return renderingHold.release;
38348
- }
38349
- const debounceRenderingBefore = options.debounceRendering;
38350
- const hold = {
38351
- render: null,
38352
- release: () => {
38353
- // Only the hold that is still standing may be given back: a holder
38354
- // releasing after another has taken over must not let go of what it
38355
- // does not hold.
38356
- if (renderingHold !== hold) {
38357
- return;
38358
- }
38359
- renderingHold = null;
38360
- options.debounceRendering = debounceRenderingBefore;
38361
- const { render } = hold;
38362
- hold.render = null;
38363
- if (render) {
38364
- render();
38365
- }
38366
- },
38367
- };
38368
- renderingHold = hold;
38369
- options.debounceRendering = (render) => {
38370
- hold.render = render;
38371
- };
38372
- return hold.release;
38373
- };
38601
+ // Whose numbers are currently published. The window belongs to the movement
38602
+ // that measured it, and only that one may take it down: a movement ending
38603
+ // after another has replaced it must not wipe numbers the new one is standing
38604
+ // on.
38605
+ let windowOwner = null;
38374
38606
 
38375
- // The hold a navigation takes on its way in — from before its first write,
38376
- // because by the time a route announces that it matches, Preact has already
38377
- // been told and the render is queued; a hold taken then is a hold taken too
38378
- // late. Kept here until whoever animates the change takes it over, or the
38379
- // navigation turns out to be one nobody animates.
38380
- let routingRenderingHold = null;
38381
- const holdRenderingForRouting = () => {
38382
- routingRenderingHold = holdRendering();
38607
+ const holdTransitionWindow = (owner, element, rectBefore) => {
38608
+ const rectAfter = element.getBoundingClientRect();
38609
+ // It cannot be measured from one side alone: a page arriving shorter than
38610
+ // the one it replaces would cut the one leaving, a page arriving taller
38611
+ // would be cut itself.
38612
+ const height =
38613
+ rectBefore.height > rectAfter.height ? rectBefore.height : rectAfter.height;
38614
+ windowOwner = owner;
38615
+ const { style } = document.documentElement;
38616
+ style.setProperty(WINDOW_TOP_PROPERTY, `${rectAfter.top}px`);
38617
+ style.setProperty(WINDOW_LEFT_PROPERTY, `${rectAfter.left}px`);
38618
+ style.setProperty(WINDOW_WIDTH_PROPERTY, `${rectAfter.width}px`);
38619
+ style.setProperty(WINDOW_HEIGHT_PROPERTY, `${height}px`);
38620
+ style.setProperty(WINDOW_OLD_TOP_PROPERTY, `${rectBefore.top}px`);
38621
+ style.setProperty(WINDOW_OLD_LEFT_PROPERTY, `${rectBefore.left}px`);
38383
38622
  };
38384
- // Nobody had a picture to take: a page held for a change it does not animate
38385
- // is a page that stutters for nothing.
38386
- const releaseRoutingRenderingHold = () => {
38387
- const release = routingRenderingHold;
38388
- routingRenderingHold = null;
38389
- if (release) {
38390
- release();
38623
+
38624
+ // The live layout takes the box back. A discontinuity by construction — the
38625
+ // window stands at the held height, the box is at its own — and an invisible
38626
+ // one: the page arriving is fully in place, and the strip below it that the
38627
+ // window still covers shows the page leaving only while it is still on screen.
38628
+ const releaseTransitionWindow = (owner) => {
38629
+ if (owner !== windowOwner) {
38630
+ return;
38631
+ }
38632
+ windowOwner = null;
38633
+ const { style } = document.documentElement;
38634
+ for (const property of WINDOW_PROPERTIES) {
38635
+ style.removeProperty(property);
38391
38636
  }
38392
- };
38393
- // An animator takes the navigation's hold as its own — taking another would be
38394
- // taking a hold on a page that is holding still — or takes a fresh one when
38395
- // the change it animates is not a navigation.
38396
- const takeoverRoutingRenderingHold = () => {
38397
- const release = routingRenderingHold || holdRendering();
38398
- routingRenderingHold = null;
38399
- return release;
38400
38637
  };
38401
38638
 
38402
38639
  installImportMetaCssBuild(import.meta);/**
@@ -38473,14 +38710,158 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
38473
38710
  // or the marked area. The guard keeps the two exclusive — with an area marked,
38474
38711
  // the root pictures must NOT move (they carry the whole viewport, blank bands
38475
38712
  // included).
38476
- const movementsCSS = (name, guard) => /* css */`
38477
- :root${guard} {
38478
- &[${TRANSITION_TYPE_ATTRIBUTE}="slide-x"],
38479
- &[${TRANSITION_TYPE_ATTRIBUTE}="slide-y"],
38480
- &[${TRANSITION_TYPE_ATTRIBUTE}="cover-x"],
38481
- &[${TRANSITION_TYPE_ATTRIBUTE}="cover-y"] {
38482
- &::view-transition-old(${name}),
38483
- &::view-transition-new(${name}) {
38713
+
38714
+ const css$T = /* css */`
38715
+ /* The marked region is a picture of its own during every view transition of
38716
+ the document — which is what keeps it out of the root snapshot, where its
38717
+ place would otherwise be blank. */
38718
+ [data-navi-route-transition-area] {
38719
+ view-transition-name: navi-route-transition;
38720
+ }
38721
+
38722
+ /* Only while a transition of OURS is playing: everything below changes how
38723
+ the document animates, and the document belongs to the application the
38724
+ rest of the time. */
38725
+ :root[data-navi-route-transition] {
38726
+ /* With an area marked, the page AROUND it is not taken as a picture — so
38727
+ exactly one of the two names below exists at a time, and the movements
38728
+ can be written once for both. It is also what a fixed bar wants: a
38729
+ captured element is not painted where it stands and cannot be pointed
38730
+ at either, so a bar photographed with the document is dead for the
38731
+ length of every transition. Left live, it answers as it always did, and
38732
+ nothing shows through where the pages are — the two pictures cover the
38733
+ area's rectangle between them at every moment. Anything around that must
38734
+ ANIMATE rather than stand still gets a view-transition-name of its own,
38735
+ and the browser moves it on the same clock. */
38736
+ &[data-navi-route-transition-target="area"] {
38737
+ view-transition-name: none;
38738
+ }
38739
+
38740
+ &::view-transition-old(root),
38741
+ &::view-transition-new(root),
38742
+ &::view-transition-old(navi-route-transition),
38743
+ &::view-transition-new(navi-route-transition) {
38744
+ /* Written on the direction alone, so a relation with no type — the
38745
+ browser's cross-fade — answers to it like every other. */
38746
+ animation-duration: var(--navi-route-transition-duration, 300ms);
38747
+ }
38748
+
38749
+ /* The window the pages are seen through, when it is an area. Nothing here
38750
+ names \`root\`: the root picture IS the window when the whole document
38751
+ travels, already the size of the screen and already cut by it. */
38752
+ &::view-transition-group(navi-route-transition),
38753
+ &::view-transition-image-pair(navi-route-transition) {
38754
+ /* The pages are cut at the edge of the area they move in. Said HERE and
38755
+ nowhere else: these pictures are drawn in the top layer, so no
38756
+ overflow on any element of the document can reach them. */
38757
+ overflow: clip;
38758
+ }
38759
+ &::view-transition-group(navi-route-transition) {
38760
+ /* Held still for the whole transition, at the taller of the two states,
38761
+ and standing where the area stands (see transition_window.js). Held by
38762
+ dropping the group's animation rather than by winning against it with
38763
+ !important — which also drops its position animation, fine for an area
38764
+ that stays in the same place from one page to the next. */
38765
+ height: var(--navi-transition-window-height);
38766
+ animation-name: none;
38767
+
38768
+ /* Cut at the safe area, on top of being cut at the area's own box. The
38769
+ pictures are drawn in the top layer, so they cover a fixed bar as
38770
+ easily as anything else — and the area runs UNDER the bars by design:
38771
+ that is what a fixed bar is for, and what the room it gives back is
38772
+ for. An area taller than the screen therefore ends below the bottom
38773
+ bar, and a scrolled one starts above the top bar, so the movement
38774
+ would be watched painting over them for its whole length.
38775
+
38776
+ The band left free is the app's own safe area (see
38777
+ layout/safe_area.js) — every kind of furniture at once, not the bars
38778
+ alone, and read rather than asked for, so one that grows, shrinks or
38779
+ unmounts mid-transition is followed without anything being told. What
38780
+ the window cannot know is only where it itself stands, and that is the
38781
+ measured half. */
38782
+ --navi-route-transition-clip-top: max(
38783
+ 0px,
38784
+ var(--navi-safe-area-inset-top) - var(--navi-transition-window-top)
38785
+ );
38786
+ --navi-route-transition-clip-left: max(
38787
+ 0px,
38788
+ var(--navi-safe-area-inset-left) - var(--navi-transition-window-left)
38789
+ );
38790
+ --navi-route-transition-clip-bottom: max(
38791
+ 0px,
38792
+ var(--navi-transition-window-top) +
38793
+ var(--navi-transition-window-height) +
38794
+ var(--navi-safe-area-inset-bottom) - 100dvh
38795
+ );
38796
+ --navi-route-transition-clip-right: max(
38797
+ 0px,
38798
+ var(--navi-transition-window-left) +
38799
+ var(--navi-transition-window-width) +
38800
+ var(--navi-safe-area-inset-right) - 100dvw
38801
+ );
38802
+ clip-path: inset(
38803
+ var(--navi-route-transition-clip-top)
38804
+ var(--navi-route-transition-clip-right)
38805
+ var(--navi-route-transition-clip-bottom)
38806
+ var(--navi-route-transition-clip-left)
38807
+ );
38808
+
38809
+ /* How far a page travels: the WINDOW it is seen through, not its own
38810
+ size. A page is as tall as its content — several screens of it — and a
38811
+ movement measured on the picture would send it thousands of pixels
38812
+ away, off screen for most of the transition and flying past at the
38813
+ end. What one page crossing another means is one window's worth of
38814
+ movement, whatever the pages are made of. Inherited by the pictures,
38815
+ which is where it is used (see the keyframes). */
38816
+ --navi-route-transition-travel-x: calc(
38817
+ var(--navi-transition-window-width) - var(
38818
+ --navi-route-transition-clip-left
38819
+ ) - var(--navi-route-transition-clip-right)
38820
+ );
38821
+ --navi-route-transition-travel-y: calc(
38822
+ var(--navi-transition-window-height) - var(
38823
+ --navi-route-transition-clip-top
38824
+ ) - var(--navi-route-transition-clip-bottom)
38825
+ );
38826
+ }
38827
+ &::view-transition-old(navi-route-transition) {
38828
+ /* Where the area WAS on screen, which is not where the window stands
38829
+ (see transition_window.js). Offset here rather than by \`translate\`,
38830
+ which the movement itself uses. */
38831
+ top: calc(
38832
+ var(--navi-transition-window-old-top) - var(
38833
+ --navi-transition-window-top
38834
+ )
38835
+ );
38836
+ left: calc(
38837
+ var(--navi-transition-window-old-left) - var(
38838
+ --navi-transition-window-left
38839
+ )
38840
+ );
38841
+ }
38842
+
38843
+ /* ------------------------------------------------------------------
38844
+ The movements. One of \`root\` and \`navi-route-transition\` exists at a
38845
+ time (see the opt-out above), so each is written for both.
38846
+ ------------------------------------------------------------------ */
38847
+ &[data-navi-route-transition-type="slide-x"],
38848
+ &[data-navi-route-transition-type="slide-y"],
38849
+ &[data-navi-route-transition-type="cover-x"],
38850
+ &[data-navi-route-transition-type="cover-y"] {
38851
+ &::view-transition-old(root),
38852
+ &::view-transition-new(root),
38853
+ &::view-transition-old(navi-route-transition),
38854
+ &::view-transition-new(navi-route-transition) {
38855
+ width: auto;
38856
+ /* Each picture at the size it was taken at: a page is not resized by
38857
+ the page it crosses. The picture is as wide as the box the browser
38858
+ gives it — the arriving one's — so a page leaving a narrower box (a
38859
+ scrollbar appeared, a side panel closed) would be seen zooming over
38860
+ the length of the movement. Left to the untyped cross-fade, where
38861
+ scaling one picture into the other is the whole idea. */
38862
+ height: auto;
38863
+ object-fit: none;
38864
+ object-position: top left;
38484
38865
  /* The default cross-fade, dropped: two pages sliding past each other
38485
38866
  are two solid things, and seeing through one to the other says they
38486
38867
  are the same page changing its mind. */
@@ -38489,183 +38870,177 @@ const movementsCSS = (name, guard) => /* css */`
38489
38870
  animation-fill-mode: both;
38490
38871
  }
38491
38872
  }
38492
- &[${TRANSITION_TYPE_ATTRIBUTE}="slide-x"] {
38493
- &[${TRANSITION_ATTRIBUTE}="forward"] {
38494
- &::view-transition-old(${name}) {
38873
+
38874
+ &[data-navi-route-transition-type="slide-x"] {
38875
+ &[data-navi-route-transition="forward"] {
38876
+ &::view-transition-old(root),
38877
+ &::view-transition-old(navi-route-transition) {
38495
38878
  animation-name: navi-route-transition-leave-towards-start;
38496
38879
  }
38497
- &::view-transition-new(${name}) {
38880
+ &::view-transition-new(root),
38881
+ &::view-transition-new(navi-route-transition) {
38498
38882
  animation-name: navi-route-transition-enter-from-end;
38499
38883
  }
38500
38884
  }
38501
- &[${TRANSITION_ATTRIBUTE}="back"] {
38502
- &::view-transition-old(${name}) {
38885
+ &[data-navi-route-transition="back"] {
38886
+ &::view-transition-old(root),
38887
+ &::view-transition-old(navi-route-transition) {
38503
38888
  animation-name: navi-route-transition-leave-towards-end;
38504
38889
  }
38505
- &::view-transition-new(${name}) {
38890
+ &::view-transition-new(root),
38891
+ &::view-transition-new(navi-route-transition) {
38506
38892
  animation-name: navi-route-transition-enter-from-start;
38507
38893
  }
38508
38894
  }
38509
38895
  }
38896
+
38510
38897
  /* The same four movements, along the other axis: the start of a column is
38511
38898
  its top, so going forward there is the page rising and the next one
38512
38899
  coming up from below. */
38513
- &[${TRANSITION_TYPE_ATTRIBUTE}="slide-y"] {
38514
- &[${TRANSITION_ATTRIBUTE}="forward"] {
38515
- &::view-transition-old(${name}) {
38900
+ &[data-navi-route-transition-type="slide-y"] {
38901
+ &[data-navi-route-transition="forward"] {
38902
+ &::view-transition-old(root),
38903
+ &::view-transition-old(navi-route-transition) {
38516
38904
  animation-name: navi-route-transition-leave-towards-top;
38517
38905
  }
38518
- &::view-transition-new(${name}) {
38906
+ &::view-transition-new(root),
38907
+ &::view-transition-new(navi-route-transition) {
38519
38908
  animation-name: navi-route-transition-enter-from-bottom;
38520
38909
  }
38521
38910
  }
38522
- &[${TRANSITION_ATTRIBUTE}="back"] {
38523
- &::view-transition-old(${name}) {
38911
+ &[data-navi-route-transition="back"] {
38912
+ &::view-transition-old(root),
38913
+ &::view-transition-old(navi-route-transition) {
38524
38914
  animation-name: navi-route-transition-leave-towards-bottom;
38525
38915
  }
38526
- &::view-transition-new(${name}) {
38916
+ &::view-transition-new(root),
38917
+ &::view-transition-new(navi-route-transition) {
38527
38918
  animation-name: navi-route-transition-enter-from-top;
38528
38919
  }
38529
38920
  }
38530
38921
  }
38922
+
38531
38923
  /* One page over the other, the way a sheet covers a desk: the page
38532
38924
  arriving slides in ON TOP of one that does not move, and going back it
38533
38925
  slides off, uncovering it. The still page is animated all the same — to
38534
38926
  a keyframe that goes nowhere — because left to the browser it would
38535
38927
  fade. */
38536
- &[${TRANSITION_TYPE_ATTRIBUTE}="cover-x"] {
38537
- &[${TRANSITION_ATTRIBUTE}="forward"] {
38538
- &::view-transition-old(${name}) {
38928
+ &[data-navi-route-transition-type="cover-x"] {
38929
+ &[data-navi-route-transition="forward"] {
38930
+ &::view-transition-old(root),
38931
+ &::view-transition-old(navi-route-transition) {
38539
38932
  animation-name: navi-route-transition-still;
38540
38933
  }
38541
- &::view-transition-new(${name}) {
38934
+ &::view-transition-new(root),
38935
+ &::view-transition-new(navi-route-transition) {
38542
38936
  animation-name: navi-route-transition-enter-from-end;
38543
38937
  }
38544
38938
  }
38545
- &[${TRANSITION_ATTRIBUTE}="back"] {
38546
- &::view-transition-old(${name}) {
38939
+ &[data-navi-route-transition="back"] {
38940
+ &::view-transition-old(root),
38941
+ &::view-transition-old(navi-route-transition) {
38547
38942
  /* The page leaving is the cover: it must slide off ABOVE the one it
38548
- uncovers, against the browser's default of drawing the new page
38549
- on top. */
38943
+ uncovers, against the browser's default of drawing the new page on
38944
+ top. */
38550
38945
  z-index: 1;
38551
38946
  animation-name: navi-route-transition-leave-towards-end;
38552
38947
  }
38553
- &::view-transition-new(${name}) {
38948
+ &::view-transition-new(root),
38949
+ &::view-transition-new(navi-route-transition) {
38554
38950
  animation-name: navi-route-transition-still;
38555
38951
  }
38556
38952
  }
38557
38953
  }
38558
- &[${TRANSITION_TYPE_ATTRIBUTE}="cover-y"] {
38559
- &[${TRANSITION_ATTRIBUTE}="forward"] {
38560
- &::view-transition-old(${name}) {
38954
+ &[data-navi-route-transition-type="cover-y"] {
38955
+ &[data-navi-route-transition="forward"] {
38956
+ &::view-transition-old(root),
38957
+ &::view-transition-old(navi-route-transition) {
38561
38958
  animation-name: navi-route-transition-still;
38562
38959
  }
38563
- &::view-transition-new(${name}) {
38960
+ &::view-transition-new(root),
38961
+ &::view-transition-new(navi-route-transition) {
38564
38962
  animation-name: navi-route-transition-enter-from-bottom;
38565
38963
  }
38566
38964
  }
38567
- &[${TRANSITION_ATTRIBUTE}="back"] {
38568
- &::view-transition-old(${name}) {
38965
+ &[data-navi-route-transition="back"] {
38966
+ &::view-transition-old(root),
38967
+ &::view-transition-old(navi-route-transition) {
38569
38968
  z-index: 1;
38570
38969
  animation-name: navi-route-transition-leave-towards-bottom;
38571
38970
  }
38572
- &::view-transition-new(${name}) {
38971
+ &::view-transition-new(root),
38972
+ &::view-transition-new(navi-route-transition) {
38573
38973
  animation-name: navi-route-transition-still;
38574
38974
  }
38575
38975
  }
38576
38976
  }
38977
+
38577
38978
  /* Going deeper is coming closer: the page arriving lands from slightly too
38578
38979
  big, and going back it is the page leaving that grows away. The other
38579
38980
  side keeps the browser's own fade under it. */
38580
- &[${TRANSITION_TYPE_ATTRIBUTE}="zoom"] {
38581
- &::view-transition-old(${name}),
38582
- &::view-transition-new(${name}) {
38981
+ &[data-navi-route-transition-type="zoom"] {
38982
+ &::view-transition-old(root),
38983
+ &::view-transition-new(root),
38984
+ &::view-transition-old(navi-route-transition),
38985
+ &::view-transition-new(navi-route-transition) {
38583
38986
  animation-fill-mode: both;
38584
38987
  }
38585
- &[${TRANSITION_ATTRIBUTE}="forward"] {
38586
- &::view-transition-new(${name}) {
38988
+ &[data-navi-route-transition="forward"] {
38989
+ &::view-transition-new(root),
38990
+ &::view-transition-new(navi-route-transition) {
38587
38991
  animation-name: navi-route-transition-zoom-in;
38588
38992
  }
38589
38993
  }
38590
- &[${TRANSITION_ATTRIBUTE}="back"] {
38591
- &::view-transition-old(${name}) {
38994
+ &[data-navi-route-transition="back"] {
38995
+ &::view-transition-old(root),
38996
+ &::view-transition-old(navi-route-transition) {
38592
38997
  animation-name: navi-route-transition-zoom-out;
38593
38998
  }
38594
38999
  }
38595
39000
  }
38596
39001
  }
38597
- `;
38598
- const css$T = /* css */`
38599
- /* The marked region is a picture of its own during every view transition of
38600
- the document — which is what keeps it out of the root snapshot, where its
38601
- place would be blank. */
38602
- [${TRANSITION_AREA_ATTRIBUTE}] {
38603
- view-transition-name: ${AREA_NAME};
38604
- }
38605
-
38606
- /* Only while a transition of OURS is playing: everything below changes how
38607
- the document animates, and the document belongs to the application the
38608
- rest of the time. The duration is written here, on the direction alone, so
38609
- a relation with no type — the browser's cross-fade — answers to
38610
- --navi-route-transition-duration like every other. */
38611
- :root[${TRANSITION_ATTRIBUTE}] {
38612
- &::view-transition-old(root),
38613
- &::view-transition-new(root),
38614
- &::view-transition-old(${AREA_NAME}),
38615
- &::view-transition-new(${AREA_NAME}) {
38616
- animation-duration: var(${TRANSITION_DURATION_PROPERTY}, 300ms);
38617
- }
38618
-
38619
- /* The pages are cut at the edge of the area they move in: its pictures are
38620
- drawn in the top layer, above the bars, and a page sliding in would
38621
- otherwise be seen crossing them. */
38622
- &::view-transition-group(${AREA_NAME}),
38623
- &::view-transition-image-pair(${AREA_NAME}) {
38624
- overflow: clip;
38625
- }
38626
- }
38627
-
38628
- ${movementsCSS("root", `:not([${TRANSITION_TARGET_ATTRIBUTE}])`)}
38629
- ${movementsCSS(AREA_NAME, `[${TRANSITION_TARGET_ATTRIBUTE}="area"]`)}
38630
39002
 
39003
+ /* One window's worth of movement. The fallback is the picture's own size,
39004
+ which is what the window is when the whole document travels: the root
39005
+ picture IS the viewport. */
38631
39006
  @keyframes navi-route-transition-leave-towards-start {
38632
39007
  to {
38633
- translate: -100% 0;
39008
+ translate: calc(-1 * var(--navi-route-transition-travel-x, 100%)) 0;
38634
39009
  }
38635
39010
  }
38636
39011
  @keyframes navi-route-transition-enter-from-end {
38637
39012
  from {
38638
- translate: 100% 0;
39013
+ translate: var(--navi-route-transition-travel-x, 100%) 0;
38639
39014
  }
38640
39015
  }
38641
39016
  @keyframes navi-route-transition-leave-towards-end {
38642
39017
  to {
38643
- translate: 100% 0;
39018
+ translate: var(--navi-route-transition-travel-x, 100%) 0;
38644
39019
  }
38645
39020
  }
38646
39021
  @keyframes navi-route-transition-enter-from-start {
38647
39022
  from {
38648
- translate: -100% 0;
39023
+ translate: calc(-1 * var(--navi-route-transition-travel-x, 100%)) 0;
38649
39024
  }
38650
39025
  }
38651
39026
  @keyframes navi-route-transition-leave-towards-top {
38652
39027
  to {
38653
- translate: 0 -100%;
39028
+ translate: 0 calc(-1 * var(--navi-route-transition-travel-y, 100%));
38654
39029
  }
38655
39030
  }
38656
39031
  @keyframes navi-route-transition-enter-from-bottom {
38657
39032
  from {
38658
- translate: 0 100%;
39033
+ translate: 0 var(--navi-route-transition-travel-y, 100%);
38659
39034
  }
38660
39035
  }
38661
39036
  @keyframes navi-route-transition-leave-towards-bottom {
38662
39037
  to {
38663
- translate: 0 100%;
39038
+ translate: 0 var(--navi-route-transition-travel-y, 100%);
38664
39039
  }
38665
39040
  }
38666
39041
  @keyframes navi-route-transition-enter-from-top {
38667
39042
  from {
38668
- translate: 0 -100%;
39043
+ translate: 0 calc(-1 * var(--navi-route-transition-travel-y, 100%));
38669
39044
  }
38670
39045
  }
38671
39046
  @keyframes navi-route-transition-zoom-in {
@@ -38689,6 +39064,41 @@ const css$T = /* css */`
38689
39064
  }
38690
39065
  `;
38691
39066
 
39067
+ /**
39068
+ * The region the pages live in — where the movements play.
39069
+ *
39070
+ * Wrap the `<Route>` tree with it in an application that has fixed furniture
39071
+ * (a top bar, a tab bar): the movements then play on THIS element's pictures,
39072
+ * clipped at its bounds, and the bars never move. Without it the document
39073
+ * itself travels, which is right only when the pages are the whole viewport —
39074
+ * with bars around, the moving root picture drags a blank band across the
39075
+ * screen where they stand.
39076
+ *
39077
+ * It is a real box, and it must be: what is photographed and clipped IS its
39078
+ * rectangle. So `display: contents` cannot be used on it — an element with no
39079
+ * box is never captured, the movement plays on nothing and the browser aborts
39080
+ * the transition. Give it the layout the pages need instead — it is a `Box`,
39081
+ * so `flex`, `className`, `style` and the rest are there for that. An
39082
+ * application that already has an element holding its pages can mark that one
39083
+ * with `data-navi-route-transition-area` rather than nesting another.
39084
+ *
39085
+ * @type {import("ignore:preact").FunctionComponent<{ children?: any, [key: string]: any }>}
39086
+ */
39087
+ const RouteTransitionArea = ({
39088
+ children,
39089
+ ...rest
39090
+ }) => {
39091
+ import.meta.css = [css$T, "@jsenv/navi/src/nav/route_transition.jsx"];
39092
+ const props = {
39093
+ ...rest,
39094
+ [TRANSITION_AREA_ATTRIBUTE]: ""
39095
+ };
39096
+ return jsx(Box, {
39097
+ ...props,
39098
+ children: children
39099
+ });
39100
+ };
39101
+
38692
39102
  /**
38693
39103
  * Declare how a pair of routes moves against each other.
38694
39104
  *
@@ -38729,7 +39139,7 @@ const css$T = /* css */`
38729
39139
  * @returns {() => void} remove this relation.
38730
39140
  */
38731
39141
  const defineRouteTransition = (from, to, transition) => {
38732
- import.meta.css = [css$T, "@jsenv/navi/src/nav/route_transition.js"];
39142
+ import.meta.css = [css$T, "@jsenv/navi/src/nav/route_transition.jsx"];
38733
39143
  const {
38734
39144
  type,
38735
39145
  duration
@@ -38767,7 +39177,7 @@ const defineRouteTransition = (from, to, transition) => {
38767
39177
  * @returns {() => void} remove this default.
38768
39178
  */
38769
39179
  const defineRouteDefaultTransition = transition => {
38770
- import.meta.css = [css$T, "@jsenv/navi/src/nav/route_transition.js"];
39180
+ import.meta.css = [css$T, "@jsenv/navi/src/nav/route_transition.jsx"];
38771
39181
  const value = normalizeTransition(transition);
38772
39182
  defaultTransition = value;
38773
39183
  updateRoutingObservers();
@@ -38971,8 +39381,17 @@ const beginTransition = ({
38971
39381
  // Looked up per transition, not once: the area is the application's own
38972
39382
  // element and follows its lifecycle — a page layout without bars has none,
38973
39383
  // and the movement then plays on the document itself.
38974
- if (document.querySelector(`[${TRANSITION_AREA_ATTRIBUTE}]`)) {
39384
+ const areaElements = document.querySelectorAll(`[${TRANSITION_AREA_ATTRIBUTE}]`);
39385
+ if (areaElements.length > 1) {
39386
+ warnOnce("several-areas", `${areaElements.length} elements carry ${TRANSITION_AREA_ATTRIBUTE}. They all take the same view-transition-name, and a name belongs to one element at a time: the browser refuses EVERY view transition of the document while this holds. Mark the one element the pages live in.`);
39387
+ }
39388
+ const areaElement = areaElements.length > 0 ? areaElements[0] : null;
39389
+ // The area as it stands before anything moves: rendering is held, so this is
39390
+ // still the page being left (see holdAreaGeometry).
39391
+ let areaRectBefore = null;
39392
+ if (areaElement) {
38975
39393
  documentElement.setAttribute(TRANSITION_TARGET_ATTRIBUTE, "area");
39394
+ areaRectBefore = areaElement.getBoundingClientRect();
38976
39395
  }
38977
39396
  // A duration of this relation's own, worn for the length of the transition —
38978
39397
  // and whatever the application had written inline put back afterwards, not
@@ -38995,6 +39414,28 @@ const beginTransition = ({
38995
39414
  // been decided renders its page in between — a wait armed then waits for
38996
39415
  // something that has already happened.
38997
39416
  const renderWait = armRouteRenderWait$1();
39417
+ // What the browser ACTUALLY captured, read once the pictures exist: it is
39418
+ // the only place the two silent misconfigurations show. Both are about the
39419
+ // same thing — a movement playing on pictures that are not the pages.
39420
+ const viewTransitionReady = () => {
39421
+ const capturedNames = capturedViewTransitionNames();
39422
+ if (areaElements.length > 0) {
39423
+ if (!capturedNames.has(AREA_NAME)) {
39424
+ warnOnce("area-not-captured", `The element marked ${TRANSITION_AREA_ATTRIBUTE} was not captured, so the movement plays on nothing. An element is captured only if it generates a box: \`display: contents\` (or an element not rendered) cannot be the area — its rectangle is what gets photographed and clipped.`);
39425
+ }
39426
+ return;
39427
+ }
39428
+ for (const name of capturedNames) {
39429
+ if (name === "root") {
39430
+ continue;
39431
+ }
39432
+ // Something stands still while the whole document travels under it. The
39433
+ // root picture spans the viewport and has a HOLE where that thing was
39434
+ // captured, so what crosses the screen is a blank band.
39435
+ warnOnce("pages-travel-under-named-elements", `The movement plays on the whole document while "${name}" is captured on its own, so a blank band travels where it stands. Wrap the pages in <RouteTransitionArea> (or mark their element with ${TRANSITION_AREA_ATTRIBUTE}) so the movement plays on them rather than on the document.`);
39436
+ break;
39437
+ }
39438
+ };
38998
39439
  const viewTransition = startViewTransition$1(async () => {
38999
39440
  // The picture the browser is about to take must be of the page that was
39000
39441
  // asked for, and a route matching is not yet a page rendered. Whatever
@@ -39016,6 +39457,11 @@ const beginTransition = ({
39016
39457
  } finally {
39017
39458
  renderWait.stop();
39018
39459
  }
39460
+ // The page arriving is in the DOM and the transition has not started
39461
+ // playing: the one moment both states of the area can be known.
39462
+ if (areaElement) {
39463
+ holdTransitionWindow(transition, areaElement, areaRectBefore);
39464
+ }
39019
39465
  });
39020
39466
  const end = () => {
39021
39467
  // Whatever ends it — played out, skipped by another transition starting,
@@ -39030,14 +39476,50 @@ const beginTransition = ({
39030
39476
  documentElement.removeAttribute(TRANSITION_ATTRIBUTE);
39031
39477
  documentElement.removeAttribute(TRANSITION_TYPE_ATTRIBUTE);
39032
39478
  documentElement.removeAttribute(TRANSITION_TARGET_ATTRIBUTE);
39479
+ releaseTransitionWindow(transition);
39033
39480
  if (restoreDuration) {
39034
39481
  restoreDuration();
39035
39482
  }
39036
39483
  }
39037
39484
  };
39485
+ viewTransition.ready.then(viewTransitionReady, ignoreSkipped$1);
39038
39486
  viewTransition.finished.then(end, end);
39039
39487
  };
39040
39488
 
39489
+ // A transition skipped by another one starting is an outcome, not a failure.
39490
+ const ignoreSkipped$1 = () => {};
39491
+
39492
+ // The names the browser captured, read off the pictures themselves: what was
39493
+ // asked for in CSS and what was taken are not the same question (see
39494
+ // viewTransitionReady).
39495
+ const capturedViewTransitionNames = () => {
39496
+ const names = new Set();
39497
+ for (const animation of document.getAnimations()) {
39498
+ const pseudoElement = animation.effect?.pseudoElement;
39499
+ if (!pseudoElement || !pseudoElement.startsWith("::view-transition")) {
39500
+ continue;
39501
+ }
39502
+ const nameStart = pseudoElement.indexOf("(");
39503
+ if (nameStart === -1) {
39504
+ continue;
39505
+ }
39506
+ names.add(pseudoElement.slice(nameStart + 1, -1));
39507
+ }
39508
+ return names;
39509
+ };
39510
+
39511
+ // Said once per kind, whatever the number of navigations: a misconfiguration
39512
+ // is one fact about the application, and repeating it every time the user
39513
+ // moves would bury it.
39514
+ const warningsSaid = new Set();
39515
+ const warnOnce = (id, message) => {
39516
+ if (warningsSaid.has(id)) {
39517
+ return;
39518
+ }
39519
+ warningsSaid.add(id);
39520
+ console.warn(message);
39521
+ };
39522
+
39041
39523
  // A route matching is a signal changing; how many passes Preact takes to
39042
39524
  // answer it is its own business, and the render is the moment the picture can
39043
39525
  // be taken. Listening starts before the change, or a render landing while the
@@ -39159,19 +39641,6 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
39159
39641
  const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
39160
39642
  // The name the box wears while it travels, and only then (see nameForTravel).
39161
39643
  const TRAVEL_NAME = "navi-route-travel";
39162
- // Where the two boxes of a travel stand in the window, published for the
39163
- // length of it. Measurements only: what is DERIVED from them — where a picture
39164
- // goes, what a bar covers — is derived in the CSS below, so the app's own
39165
- // numbers (the room its fixed bars take) can take part in it. Only the
39166
- // measuring needs JS, and only for the one moment both boxes exist (see
39167
- // holdTravelGeometry).
39168
- const TRAVEL_TOP_PROPERTY = "--navi-route-travel-top";
39169
- const TRAVEL_LEFT_PROPERTY = "--navi-route-travel-left";
39170
- const TRAVEL_WIDTH_PROPERTY = "--navi-route-travel-width";
39171
- const TRAVEL_HEIGHT_PROPERTY = "--navi-route-travel-height";
39172
- const TRAVEL_OLD_TOP_PROPERTY = "--navi-route-travel-old-top";
39173
- const TRAVEL_OLD_LEFT_PROPERTY = "--navi-route-travel-old-left";
39174
- const TRAVEL_GEOMETRY_PROPERTIES = [TRAVEL_TOP_PROPERTY, TRAVEL_LEFT_PROPERTY, TRAVEL_WIDTH_PROPERTY, TRAVEL_HEIGHT_PROPERTY, TRAVEL_OLD_TOP_PROPERTY, TRAVEL_OLD_LEFT_PROPERTY];
39175
39644
  const css$S = /* css */`
39176
39645
  /* The name that makes the page inside this box a picture of its own during a
39177
39646
  transition — rather than part of the one big picture the document takes, so
@@ -39254,10 +39723,16 @@ const css$S = /* css */`
39254
39723
  would be seen jumping back to its top before it even begins to leave.
39255
39724
  Offset here rather than by \`translate\`, which the movement itself uses,
39256
39725
  and at its own size rather than the group's so that nothing is cut off
39257
- the far side of the shift (see holdTravelGeometry). */
39258
- top: calc(var(${TRAVEL_OLD_TOP_PROPERTY}) - var(${TRAVEL_TOP_PROPERTY}));
39726
+ the far side of the shift (see transition_window.js). */
39727
+ top: calc(
39728
+ var(--navi-transition-window-old-top) - var(
39729
+ --navi-transition-window-top
39730
+ )
39731
+ );
39259
39732
  left: calc(
39260
- var(${TRAVEL_OLD_LEFT_PROPERTY}) - var(${TRAVEL_LEFT_PROPERTY})
39733
+ var(--navi-transition-window-old-left) - var(
39734
+ --navi-transition-window-left
39735
+ )
39261
39736
  );
39262
39737
  width: auto;
39263
39738
  }
@@ -39272,7 +39747,7 @@ const css$S = /* css */`
39272
39747
  }
39273
39748
  &::view-transition-group(navi-route-travel) {
39274
39749
  /* The window the two pictures are seen through, held still for the whole
39275
- travel at the taller of the two boxes (see holdTravelGeometry): the group
39750
+ travel at the taller of the two boxes (see transition_window.js): the group
39276
39751
  is what CLIPS, and the browser animates its height from the box being
39277
39752
  left to the box arriving — so the window shrinks under the pictures and
39278
39753
  cuts the page leaving from the bottom, progressively. The box does end
@@ -39283,7 +39758,7 @@ const css$S = /* css */`
39283
39758
  winning against it with !important — which also drops its position
39284
39759
  animation, fine while a travel box stands in the same place from one
39285
39760
  route to the next. */
39286
- height: var(${TRAVEL_HEIGHT_PROPERTY});
39761
+ height: var(--navi-transition-window-height);
39287
39762
 
39288
39763
  /* Cut at the safe area, on top of being cut at the box. The pictures are
39289
39764
  drawn in the top layer, so they cover a fixed bar as easily as anything
@@ -39300,20 +39775,22 @@ const css$S = /* css */`
39300
39775
  only where it itself stands, and that is the measured half. */
39301
39776
  --navi-route-travel-clip-top: max(
39302
39777
  0px,
39303
- var(--navi-safe-area-inset-top) - var(${TRAVEL_TOP_PROPERTY})
39778
+ var(--navi-safe-area-inset-top) - var(--navi-transition-window-top)
39304
39779
  );
39305
39780
  --navi-route-travel-clip-left: max(
39306
39781
  0px,
39307
- var(--navi-safe-area-inset-left) - var(${TRAVEL_LEFT_PROPERTY})
39782
+ var(--navi-safe-area-inset-left) - var(--navi-transition-window-left)
39308
39783
  );
39309
39784
  --navi-route-travel-clip-bottom: max(
39310
39785
  0px,
39311
- var(${TRAVEL_TOP_PROPERTY}) + var(${TRAVEL_HEIGHT_PROPERTY}) +
39786
+ var(--navi-transition-window-top) +
39787
+ var(--navi-transition-window-height) +
39312
39788
  var(--navi-safe-area-inset-bottom) - 100dvh
39313
39789
  );
39314
39790
  --navi-route-travel-clip-right: max(
39315
39791
  0px,
39316
- var(${TRAVEL_LEFT_PROPERTY}) + var(${TRAVEL_WIDTH_PROPERTY}) +
39792
+ var(--navi-transition-window-left) +
39793
+ var(--navi-transition-window-width) +
39317
39794
  var(--navi-safe-area-inset-right) - 100dvw
39318
39795
  );
39319
39796
  clip-path: inset(
@@ -39635,7 +40112,7 @@ const RouteTravel = ({
39635
40112
  }
39636
40113
  pageAskedForRef.current = page;
39637
40114
  // The box as it stands before anything moves: rendering is held, so this is
39638
- // still the page being left (see holdTravelGeometry).
40115
+ // still the page being left (see transition_window.js).
39639
40116
  const rectBefore = elementRef.current.getBoundingClientRect();
39640
40117
  // The hold a navigation already took, if this travel is the answer to one:
39641
40118
  // taking another would be taking a hold on a page that is holding still.
@@ -39663,7 +40140,7 @@ const RouteTravel = ({
39663
40140
  }, renderWait);
39664
40141
  // The page arriving is in the DOM and the transition has not started
39665
40142
  // playing: the one moment both boxes can be known.
39666
- holdTravelGeometry(elementRef.current, rectBefore);
40143
+ holdTransitionWindow(travel, elementRef.current, rectBefore);
39667
40144
  });
39668
40145
  travel.viewTransition = viewTransition;
39669
40146
  if (scrub) {
@@ -39981,7 +40458,7 @@ const RouteTravel = ({
39981
40458
  document.documentElement.removeAttribute(TRAVEL_AXIS_ATTRIBUTE);
39982
40459
  document.documentElement.removeAttribute(DRAGGED_ATTRIBUTE);
39983
40460
  document.documentElement.removeAttribute(TURNED_ATTRIBUTE);
39984
- releaseTravelGeometry();
40461
+ releaseTransitionWindow(travel);
39985
40462
  }
39986
40463
  };
39987
40464
 
@@ -40364,44 +40841,6 @@ const releaseHold = travel => {
40364
40841
  document.documentElement.removeAttribute(HOLD_ATTRIBUTE);
40365
40842
  };
40366
40843
 
40367
- // The two boxes of a travel, measured at the one moment both exist: the
40368
- // arriving page is in the DOM and the transition has not started playing.
40369
- //
40370
- // The group stands at the ARRIVING box — its own animation is dropped, so it
40371
- // takes the geometry the browser declared for it and holds it for the whole
40372
- // travel. That is why both rectangles have to be published: a group that does
40373
- // not move says nothing about where the page being left was, and its rectangle
40374
- // in the window is the only thing CSS cannot work out on its own.
40375
- const holdTravelGeometry = (element, rectBefore) => {
40376
- const rectAfter = element.getBoundingClientRect();
40377
- // The height it is held at is the taller of the two boxes, so neither picture
40378
- // is ever cut. It cannot be measured from one side alone: a page arriving
40379
- // shorter than the one it replaces would cut the one leaving, a page arriving
40380
- // taller would be cut itself.
40381
- const height = rectBefore.height > rectAfter.height ? rectBefore.height : rectAfter.height;
40382
- const {
40383
- style
40384
- } = document.documentElement;
40385
- style.setProperty(TRAVEL_TOP_PROPERTY, `${rectAfter.top}px`);
40386
- style.setProperty(TRAVEL_LEFT_PROPERTY, `${rectAfter.left}px`);
40387
- style.setProperty(TRAVEL_WIDTH_PROPERTY, `${rectAfter.width}px`);
40388
- style.setProperty(TRAVEL_HEIGHT_PROPERTY, `${height}px`);
40389
- style.setProperty(TRAVEL_OLD_TOP_PROPERTY, `${rectBefore.top}px`);
40390
- style.setProperty(TRAVEL_OLD_LEFT_PROPERTY, `${rectBefore.left}px`);
40391
- };
40392
- // The live layout takes the box back. A discontinuity by construction — the
40393
- // group stands at the held height, the box is at the new one — and an invisible
40394
- // one: the page arriving is fully in place, and the strip below it that the
40395
- // group still covers shows the page leaving only while it is still on screen.
40396
- const releaseTravelGeometry = () => {
40397
- const {
40398
- style
40399
- } = document.documentElement;
40400
- for (const property of TRAVEL_GEOMETRY_PROPERTIES) {
40401
- style.removeProperty(property);
40402
- }
40403
- };
40404
-
40405
40844
  // The animations of the pictures, asked for again until there are some: they
40406
40845
  // come into existence with the transition, several frames after it was asked
40407
40846
  // for, and the gesture has already begun by then. Kept once found — the set
@@ -73738,5 +74177,5 @@ const UserSvg = () => jsx("svg", {
73738
74177
  })
73739
74178
  });
73740
74179
 
73741
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeSpin, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
74180
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeSpin, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
73742
74181
  //# sourceMappingURL=jsenv_navi.js.map